Socket 是网络协议栈暴露给编程人员的 API,相比复杂的计算机网络协议,API 对关键操作和配置数据进行了抽象,简化了程序编程。
本文讲述的 socket 内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解 socket 编程。
poll() 遵循 POSIX.1 - 2008
ppoll() 遵循 Linux
标准 c 库,libc, -lc
int listen(int sockfd, int backlog);
listen() 标记 sockfd 指定的 socket 为被动(passive)socket,也就是说 socket 通过 accept() 来接收进来的连接请求。
sockfd 参数是一个指向 SOCK_STREAM 或者 SOCK_SEQPACKET 类型的文件描述符。
backlog 参数定义了在 sockfd 上可以排队的最大长度,如果一个连接请求到达时队列已满,那么客户端会收到一个 ECONNREFUSED 错误,或者如果底层协议支持重传,那么该请求会被忽略从而导致客户端连接重试可能会成功。
成功时,返回值是 0。
发生错误时,返回 -1,并设置errno 来指示错误类型。
错误值定义如下:
EADDRINUSE | 另一个 socket 已经监听了同样的端口 |
EADDRINUSE | (网络 domain socket)sockfd 指向的 socket没有绑定到一个地址,尝试绑定到临时端口时,临时端口用尽了 |
EBADF | sockfd 不是一个有效的文件描述符 |
ENOTSOCK | sockfd 文件描述符没有指向一个 socket |
EOPNOTSUPP | socket 不是支持 listen() 操作的socket |
为了接收连接,需要进行以下步骤:
(1)通过 socket() 接口创建 socket。
(2)通过 bing() 将 socket 绑定到本地地址,这样其他 socket 就可以通过 connect 连接它。
(3)根据意愿,可以通过 listen() 接口来接收连接,并设置连接队列的上限值。
从 Linux 2.2 后,TCP socket 的 backlog 参数就发生了变化,它表示的是已经建立连接等待接收(accept)的队列长度,而不是未完成连接的队列长度。未完成的连接队列长度的上限可以通过 /proc/sys/net/ipv4/tcp_max_syn_backlog 设置。当开启了同步 cookie 功能时,这个设置将被忽略,即没有本地最大长度限制。参考 tcp(7) 获得更多信息。
如果 backlog 大于 /proc/sys/net/core/somaxconn,那么默认复制这个值到 somaxconn。Linux 5.4 以后,这个默认值为 4096,而在早期的版本中,默认值是 128。Linux 2.4.25 之前,这个值更是硬编码为 128,不可改变。
accept() 遵循 POSIX.1 - 2008
accept4() 遵循 Linux
标准 c 库,libc, -lc
- int accept(int sockfd, struct sockaddr *_Nullable restrict addr,
- socklen_t *_Nullable restrict addrlen);
-
- int accept4(int sockfd, struct sockaddr *_Nullable restrict addr,
- socklen_t *_Nullable restrict addrlen, int flags);
accept() 系统调用用作基于连接的 socket 上(SOCK_STREAM,SOCK_SEQPACKET)。它会从监听 socket(sockfd) 的等待连接队列里拿出第一个连接请求,然后创建一个新的连接了的 socket,并返回指向该 socket 的新的文件描述符。新创建的 socket 并不处于监听状态,原始 socket(sockfd) 不受该调用影响。
sockfd 参数是一个通过 socket() 接口创建的 socket,通过 bind() 绑定到了本地地址上,使用 listen() 监听着它的连接情况。
addr 参数是一个指向 sockaddr 结构的指针,这个结构由通信层填充的对端 socket 地址。返回的 addr 地址格式取决于具体的地址家族(可以参考 socket() 和相关协议 man 页面)。当 addr 是 NULL 时,不会向里填充任何东西。在这种情况下,addrlen 也没有用,也应该是 NULL。
addrlen 参数是一个输入输出参数,调用者必须使用 addr 结构的大小来初始化它,并返回对端地址的实际大小。
如果提供的 buffer 太小,那么返回的地址将会被截断。这种情况下,addrlen 会返回一个比提供值大的数值。
如果当前队列上没有等待着的连接,并且 socket 没有被设置为非阻塞,那么 accept() 将会一直阻塞直到有连接到达。而如果 socket 被设置为非阻塞,那么 accept() 将会报告 EAGAIN 或者 EWOULDBLOCK 错误码。
为了获得新到连接通知,我们可以使用 select()、poll()、epoll 接口。当有新连接尝试发生时,我们会收到可读的事件,然后我们可以调用 accept() 来获取对应连接的 socket。
也可以设置当 socket 上有动静时发送 SIGIO 信号,参考 socket(7)。
如果 flags 为 0,那么 accept4() 等同于 accept()。flags 可以是以下标志的位或:
SOCK_NONBLOCK
设置新文件描述符的文件状态标记为 O_NONBLOCK,这样就不用再调用 fcntl() 来实现同样的效果了。
SOCK_CLOEXEC
设置新文件描述符的 FD_CLOEXEC 标志,可以查看 open(2) 说明来看这个标志的意义。
成功时,这个系统调用返回一个接收 socket 的文件描述符(非负整数)。
发生错误时,返回 -1,并设置errno 来指示错误类型,addrlen 不会改变。
Linux 的 accept() 以及 accept4() 接口会将既存的网络错误传递到新创建的 socket 上。这个行为和 BSD socket 实现是不一样的。为了实现可靠的操作,我们需要处理相应协议的网络错误,把它们当作 EAGAIN 重试处理。在 TCP/IP 的场景下,会有 ENETDOWN/EPROTO/ENOPROTOOPT/EHOSTDOWN/ENONET/EHOSTUNREACH/EOPNOTSUPP/ENETUNREACH 等网络错误。
错误值定义如下:
EAGAIN/EWOULDBLOCK | socket 设置为非阻塞,并且当前没有连接等待接收。POSIX.1-2001 和 POSIX.1-2008 都允许返回随便哪个错误码,并且并不要求这两个值相同,所以移植程序应该对每个都进行处理。 |
EBADF | sockfd 不是一个打开的文件描述符 |
ECONNABORTED | 连接已经终止 |
EFAULT | addr 参数不是用户地址空间可写的地址 |
EINTR | 系统调用在有效的连接到达前被信号打断 |
EINVAL | socket 没有处在监听连接状态,或者 addrlen 不合法 |
EINVAL | (accept4()) flags 的值不合法 |
EMFILE | 文件描述符数达到进程最大限制 |
ENFILE | 系统文件描述符数达到系统最大限制 |
ENOBUFS/ENOMEM | 没有足够的内存。这通常说的不是系统内存,而是内存分配受到 socket 缓冲区限制而无法分配 |
ENOTSOCK | sockfd 文件描述符不是一个 socket |
EOPNOTSUPP | socket 不是 SOCK_STREAM 类型 |
EPERM | 防火墙规则禁止连接 |
EPROTO | 协议错误 |
此外,新 socket 协议的网络错误也会返回,Linux 内核还可能返回一些其他错误:ENOSR/ESOCKTNOSUPPORT/EPROTONOSUPPORT/ETIMEDOUT。ERESTARTSYS 也可能在 trace 过程中返回。
Linux 上,accept() 新返回的 socket 不会从监听 socket 上集成发文件状态标志,比如 O_NONBLOCK 和 O_ASYNC。这个行为和 BSD 实现是不一样。一个可移植的程序不应该对这些进行假设,显示的设置这些标志。
我们收到 SIGIO 信号后或者 select()/poll/epoll 返回一个可读事件后,并不一定真的有连接存在,因为很可能在 accept() 调用之前这个连接因为网络被异步网络错误或者其他线程移除。一旦这种情况发送,系统调用就会一直阻塞到下一个连接到达。为了保证 accept() 永不阻塞,sockfd 指定的 socket 必须设置 O_NONBLOCK 标志。
对于有些需要显示确认的协议,比如 DECnet,accept() 只是将下一个连接请求从从队列里拿出来而不做确认。确认是通过对文件描述符 read 或者 write 完成。目前只有 DECnet 在 Linux 上有类似语义。
socklen_t 类型
在原始的 BSD 实现中,accept() 第三个参数被声明为 int *。POSIX.1g 草稿标准想将其改为 size_t *C,后来 POSIX 标准和 glibc 2.x 把它定义为 socklen_t *。
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <sys/socket.h>
- #include <sys/un.h>
- #include <unistd.h>
-
- #define MY_SOCK_PATH "/somepath"
- #define LISTEN_BACKLOG 50
-
- #define handle_error(msg) \
- do { perror(msg); exit(EXIT_FAILURE); } while (0)
-
- int
- main(void)
- {
- int sfd, cfd;
- socklen_t peer_addr_size;
- struct sockaddr_un my_addr, peer_addr;
-
- sfd = socket(AF_UNIX, SOCK_STREAM, 0);
- if (sfd == -1)
- handle_error("socket");
-
- memset(&my_addr, 0, sizeof(my_addr));
- my_addr.sun_family = AF_UNIX;
- strncpy(my_addr.sun_path, MY_SOCK_PATH,
- sizeof(my_addr.sun_path) - 1);
-
- if (bind(sfd, (struct sockaddr *) &my_addr,
- sizeof(my_addr)) == -1)
- handle_error("bind");
-
- if (listen(sfd, LISTEN_BACKLOG) == -1)
- handle_error("listen");
-
- /* Now we can accept incoming connections one
- at a time using accept(2). */
-
- peer_addr_size = sizeof(peer_addr);
- cfd = accept(sfd, (struct sockaddr *) &peer_addr,
- &peer_addr_size);
- if (cfd == -1)
- handle_error("accept");
-
- /* Code to deal with incoming connection(s)... */
-
- if (close(sfd) == -1)
- handle_error("close");
-
- if (unlink(MY_SOCK_PATH) == -1)
- handle_error("unlink");
- }