返回

揭秘 Socket 的存储与阻塞唤醒机制,为你打开网络编程新世界

后端

网络编程的基础:Socket

Socket 的定义

在网络编程中,Socket 是一个抽象的概念,它代表应用程序与网络之间的通信端点。在 Linux 系统中,Socket 被存储在内核中,并以文件符的形式提供给应用程序。这意味着应用程序可以通过对 Socket 进行读写操作来与网络进行通信。

Socket 的存储结构

Socket 在内核中以文件符的形式存储。文件描述符是一个整数,它唯一地标识一个文件或设备。应用程序可以通过调用 socket() 函数创建 Socket,并得到一个与该 Socket 关联的文件描述符。

Socket 的存储结构包括以下几个部分:

  • Socket 类型:Socket 类型指定了 Socket 的通信方式。常见的 Socket 类型有 SOCK_STREAMSOCK_DGRAM
  • Socket 地址:Socket 地址指定了 Socket 的通信端点。Socket 地址可以是 IPv4 地址、IPv6 地址或 UNIX 域套接字地址。
  • Socket 状态:Socket 状态指定了 Socket 的当前状态。常见的 Socket 状态有 LISTENESTABLISHEDCLOSE_WAIT 等。
  • Socket 选项:Socket 选项可以修改 Socket 的行为。常见的 Socket 选项有 SO_REUSEADDRSO_LINGER 等。

阻塞与唤醒机制

当 Socket 接收数据时,数据会存储在 Socket 的接收缓冲区中。应用程序可以通过调用 read() 函数从接收缓冲区中读取数据。如果接收缓冲区中没有数据,应用程序将被阻塞,直到数据到达为止。

为了避免应用程序长时间被阻塞,内核提供了 select()poll() 等系统调用,允许应用程序在多个 Socket 上进行轮询,从而提高应用程序的并发性。当某个 Socket 上有数据到达时,内核会唤醒应用程序,应用程序就可以对该 Socket 进行读写操作。

select()poll() 系统调用的工作原理如下:

  1. 应用程序调用 select()poll() 函数,并指定要轮询的 Socket 集合。
  2. 内核将应用程序挂起,直到以下情况之一发生:
    • 有数据到达了某个 Socket。
    • 有信号发送给了应用程序。
    • 超时时间到了。
  3. 内核唤醒应用程序,并返回已就绪的 Socket 集合。
  4. 应用程序可以对已就绪的 Socket 进行读写操作。

代码示例

以下是一个使用 select() 系统调用实现多路复用服务器的代码示例:

#include <sys/socket.h>
#include <sys/select.h>
#include <stdio.h>

int main() {
    // 创建一个监听套接字
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) {
        perror("socket");
        return -1;
    }

    // 绑定监听套接字到一个地址和端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    addr.sin_addr.s_addr = INADDR_ANY;
    if (bind(listenfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("bind");
        return -1;
    }

    // 监听套接字
    if (listen(listenfd, 10) == -1) {
        perror("listen");
        return -1;
    }

    // 设置文件描述符集合
    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(listenfd, &rfds);

    // 循环轮询文件描述符集合
    while (1) {
        // 复制文件描述符集合
        fd_set readfds = rfds;

        // 轮询文件描述符集合
        int ret = select(listenfd + 1, &readfds, NULL, NULL, NULL);
        if (ret == -1) {
            perror("select");
            break;
        }

        // 处理已就绪的套接字
        if (FD_ISSET(listenfd, &readfds)) {
            // 接受新连接
            int connfd = accept(listenfd, NULL, NULL);
            if (connfd == -1) {
                perror("accept");
                continue;
            }

            // 将新的连接添加到文件描述符集合中
            FD_SET(connfd, &rfds);
        }

        for (int i = 0; i < FD_SETSIZE; i++) {
            if (FD_ISSET(i, &readfds)) {
                // 从已就绪的套接字中读取数据
                char buf[1024];
                int n = read(i, buf, sizeof(buf));
                if (n == -1) {
                    perror("read");
                    FD_CLR(i, &rfds);
                } else if (n == 0) {
                    // 客户端断开连接
                    FD_CLR(i, &rfds);
                } else {
                    // 处理数据
                    printf("Received data: %s", buf);

                    // 向客户端写数据
                    n = write(i, buf, n);
                    if (n == -1) {
                        perror("write");
                        FD_CLR(i, &rfds);
                    }
                }
            }
        }
    }

    // 关闭监听套接字
    close(listenfd);

    return 0;
}

常见问题解答

  1. 什么是 Socket?
    Socket 是应用程序与网络之间的通信端点,应用程序可以通过 Socket 与网络进行通信。

  2. Socket 是如何存储的?
    Socket 在内核中以文件描述符的形式存储,文件描述符是唯一标识 Socket 的整数。

  3. 应用程序如何使用 Socket?
    应用程序可以通过调用 socket() 函数创建 Socket,并得到一个与该 Socket 关联的文件描述符。应用程序可以对 Socket 进行读写操作来与网络进行通信。

  4. 当应用程序读取 Socket 时会发生什么?
    当应用程序读取 Socket 时,数据会从 Socket 的接收缓冲区中读取。如果接收缓冲区中没有数据,应用程序将被阻塞,直到数据到达为止。

  5. 如何避免应用程序被阻塞?
    为了避免应用程序被阻塞,可以使用 select()poll() 等系统调用对多个 Socket 进行轮询。当某个 Socket 上有数据到达时,内核会唤醒应用程序。