【四万字】网络编程接口 Socket API 解读大全

Socket 是网络协议栈暴露给编程人员的 API,相比复杂的计算机网络协议,API 对关键操作和配置数据进行了抽象,简化了程序编程。

本文讲述的 socket 内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解 socket 编程。

socket()

遵循 POSIX.1 - 2001、POSIX.1-2008、4.4BSD

1.库

cpp 复制代码
标准 c 库,libc, -lc

2.头文件

cpp 复制代码
<sys/socket.h>

3.接口定义

cpp 复制代码
int socket(int domain, int type, int protocol);

4.接口描述

socket() 创建一个通信端点并返回一个指向该端点的文件描述符。返回的文件描述符号是当前进程没有打开的号最小的文件描述符。

5.参数

  • domain

domain 参数指定了一个通信域,它选择了用于通信的协议家族,这些协议家族在<sys/socket.h> 中定义,当前 Linux 内核能够认识的格式包括:

|--------------|-------------------------------------------------------|
| 名称 | 目的 |
| AF_UNIX | 本地通信 |
| AF_LOCAL | 和 AF_UNIX 同意 |
| AF_INET | IPv4 网络协议 |
| AF_AX25 | 业余无线电 AX.25 协议 |
| AF_IPX | Novell 分组交换协议 |
| AF_APPLETALK | Appletalk 协议 |
| AF_X25 | X25 分组交换网络 |
| AF_INET6 | IPv6 网络协议 |
| AF_DECnet | DECnet 协议 socket |
| AF_KEY | 密钥管理协议 |
| AF_NETLINK | 内核用户接口设备 |
| AF_PACKET | 底层 packet 接口 |
| AF_RDS | 可靠的数据报套接字协议 |
| AF_PPPOX | 通用 PPP 传输层,用于设置 L2 层隧道(L2TP、PPPoE) |
| AF_LLC | 逻辑链路控制协议(IEEE 802.2 LLC) |
| AF_IB | InfiniBand 本地访问 |
| AF_MPLS | 多协议标记切换 |
| AF_CAN | 控制器局域网汽车总线协议 |
| AF_TIPC | 集群域内套接字 |
| AF_BLUETOOTH | 蓝牙底层套接字协议 |
| AF_ALG | 内核密码学 API 接口 |
| AF_VSOCK | VSOCK 原来用于 VMWARE VSockets,hypervisor 和 guest 之间的通信协议 |
| AF_KCM | 内核连接多路复用器接口 |
| AF_XDP | 快速数据路径接口 |

更多关于地址家族的信息可以从 address_families(7) 中查看。

socket 有一个指定的 type 类型,定义了双方通信语义,目前定义的类型有:

SOCK_STREAM

提供有序、可靠、双向、面向连接的字节流,可以支持带歪数据传输机制。

SOCK_DGRAM

支持数据报文(无连接、不可靠定长消息)。

SOCK_SEQPACKET

提供了有序、可靠、双向、面向连接的数据传输,传输的内容不是字节流,而是固定长度的数据报文。数据报消费者每次通过 read 系统调用读取整个数据报文。

SOCK_RAW

提供原始网络协议访问。

SOCK_RDM

提供可靠的数据报层,但是并不保证有序。

SOCK_PACKET

已经过时了,新应用不应该使用,参考 packet(7)。

一些协议类型并不是被所有协议家族支持的。

Linux 2.6.27 后,type 类型具有另外一个目的:除了指定 socket 类型,还包含了下面数值的位或值,来控制 socket 的行为:

SOCK_NONBLOCK

在打开新文件描述符指向的文件时设置文件状态标记 O_NONBLOCK,这就不需要额外使用 fcntl() 来进行设置。

SOCK_CLOEXEC

设置新文件描述符的 FD_CLOEXEC 标记,可以参考 open() 来看为什么需要设置整个参数

protocol 指定了 socket 使用的具体协议,通常一个指定的协议家族、协议类型中只有一种协议,这时 protocol 可以指定为 0。然而,也可能存在多个协议,这种情况下就必须指定协议,而协议号是根据实际的通信域的不同而不同的。参考 protocol(5)。参考 getprotoent(3) 来查看如何将协议号映射到协议名字符串上。

SOCK_STREAM socket 是全双工字节流,它并没有保留记录边界。流套接字必须处于连接状态来进行数据的发送和接收。连接到其他套接字是通过 connect(2) 系统调用实现的。一旦连接上了,数据就可以通过 read(2) 和 write(2) 调用来进行传输,或者使用 send(2) 和 recv(2) 变体调用。会话结束后,应该使用 close(2) 来关闭。带外数据可以根据 send(2) 和 recv(2) 的描述来进行收发。

实现 SOCK_STREAM 的通信协议需要保证数据不能丢失或者重复。对于缓存到底层协议中在规定时间内无法传输完成的数据来讲,该连接会被视为死掉了。当套接字协议开启了 SO_KEEPALIVE 保活机制,协议会使用协议自己定义的方式来检查对端是否还活着。当我们在一个破损了的 pipe 上发送接收数据,那么就会收到 SIGPIPE信号,这会导致没有处理这个信号的本地进程直接退出。

SOCK_QEQPACKET 套接字使用同样的系统调用 SOCK_STREAM,唯一的不同 read(2) 调用返回指定请求数量的数据,接收数据包中剩余的数据将会被丢弃。同时,发过来的数据报文的所有边界都保留着。

SOCK_DGRAM 和 SOCK_RAW 套接字允许使用 sendto(2) 来发送数据报文给对端。数据报文通常使用 recvfrom(2) 来接收,这个接口会返回下一个数据报文以及发送者的地址。

SOCK_PACKET 是一个过时的直接从对端接收原始数据报文的套接字类型,应该使用 packet(7) 来替代。

我们可以使用 fcntl(2) 的 F_SETOWN 操作来指定进程或者进程组来接收带外数据到达信号 SIGURG 和连接异常中断信号 SIGPIPE。这个操作也可以用来接收 SIGIO 异步 I/O 通知事件。使用 F_SETOWN 等效于 ioctl() 调用的 FIOSETOWN 或者 SIOCSPGRP。

当网络给协议模块发送了错误指示信号时(比如 IP 层的 ICMP 消息),那么错误标记将会设置到套接字上,在套接字的下一次操作发生时,会将挂起的错误以错误码的形式返回。对于一些协议而言,也可以通过开启套接字特定的错误队列来获得关于错误的详细信息,可以参考 ip(7) 中的 IP_RECVERR。

对于套接字的操作是由套接字层面的选项来控制的,这些选项定义在 <sys/socket.h> 中。函数 setsockopt(2) 和 getsockopt(2) 用来设置和获取对于的选项。

带外数据传输指的是 TCP 在紧急情况下通过调整报文在发送/接收缓冲区的位置以及数据包中添加紧急标记的逻辑。

6.返回值

发生错误时返回 -1,设置 errno 指示错误码,否则返回一个新创建的整型文件描述符。

可能的错误码包括:

|-------------------|-------------------|
| 错误码 | 含义 |
| EACCES | 没有权限创建对应的 socket |
| EAFNOSUPPORT | 实现不支持指定的 AF_ 地址家族 |
| EINVAL | 未知的协议或者地址家族不可用 |
| EINVAL | type 参数不合法 |
| EMFILE | 进程文件描述符到达最大限制 |
| ENFILE | 系统文件描述符到达上限 |
| ENOBUFS or ENOMEM | 内存不足 |
| EPROTONOSUPPORT | domain 不支持指定的协议类型 |


bind

遵循 POSIX.1-2008

1.库

cpp 复制代码
标准 c 库,libc, -lc

2.头文件

cpp 复制代码
<sys/socket.h>

3.接口定义

cpp 复制代码
 int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);

4.接口描述

通过 socket() 接口创建 socket 后,socket 只存在于名字空间中,并没有实际的地址分配给它。bind 接口将 addr 指定的 IP 地址分配给由文件描述符 sockfd 指定的 socket。addrlen 指定了 addr 指针指向的地址结构的字节长度。以前我们将这个操作给 socket 分配名字。

通常在 TCP_STREAM socket 接收连接前需要将一个本地地址通过 bind 分配给 socket。

名字绑定规则随着地址家族的不同而不同。

addr 的数据结构也是随着地址家族的变化而变化的。sockaddr 结构的定义类似:

cpp 复制代码
 struct sockaddr {
      sa_family_t sa_family;
      char        sa_data[14];
 }

这个结构定义主要是为了防止编译器报错,主要是将各种地址结构做一个强制转换。

5. 返回值

发生错误时返回 -1,设置 errno 指示错误码,否则返回一个新创建的整型文件描述符。

可能的错误码包括:

|---------------|---------------------------------------------------------------|
| 错误码 | 含义 |
| EACCES | 地址是保护地址,并且用户不是超级用户 |
| EADDRINUSE | 指定的地址已经使用 |
| EADDRINUSE | 对于 domain socket,端口号在地址结构体中 指定为 0,但在尝试 bind 到临时端口时,临时端口没有空闲的了 |
| EBADF | sockfd 不是可用的文件描述符 |
| EINVAL | socket 已经绑定到了一个地址 |
| EINVAL | addrlen 错误,或者 addr 不是一个可用的 domain 地址 |
| ENOTSOCK | 文件描述符没有指向任何 socket |
| UNIX domain(AF_UNIX) 特定的错误码 ||
| EACCESS | 在路径前缀下无搜索权限 |
| EADDRNOTAVAIL | 请求的接口不存在或者不是本地的接口 |
| EFAULT | addr 指向了用户无法访问的地址空间 |
| ELOOP | 解析地址时遇到了太多的符号链接 |
| ENAMETOOLONG | 地址太长 |
| ENOENT | 指定路径不存在 |
| ENOMEM | 内核内存不足 |
| ENOTDIR | 路径前缀不是一个目录 |
| EROFS | socket inode 位于只读文件系统中 |

6.示例代码

cpp 复制代码
       #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");
       }

listen

poll() 遵循 POSIX.1 - 2008

ppoll() 遵循 Linux

1.库

cpp 复制代码
标准 c 库,libc, -lc

2.头文件

cpp 复制代码
<sys/socket.h>

3.接口定义

cpp 复制代码
       int listen(int sockfd, int backlog);

4.接口描述

listen() 标记 sockfd 指定的 socket 为被动(passive)socket,也就是说 socket 通过 accept() 来接收进来的连接请求。

sockfd 参数是一个指向 SOCK_STREAM 或者 SOCK_SEQPACKET 类型的文件描述符。

backlog 参数定义了在 sockfd 上可以排队的最大长度,如果一个连接请求到达时队列已满,那么客户端会收到一个 ECONNREFUSED 错误,或者如果底层协议支持重传,那么该请求会被忽略从而导致客户端连接重试可能会成功。

5.返回值

成功时,返回值是 0。

发生错误时,返回 -1,并设置errno 来指示错误类型。

错误值定义如下:

|------------|-----------------------------------------------------------------|
| EADDRINUSE | 另一个 socket 已经监听了同样的端口 |
| EADDRINUSE | (网络 domain socket)sockfd 指向的 socket没有绑定到一个地址,尝试绑定到临时端口时,临时端口用尽了 |
| EBADF | sockfd 不是一个有效的文件描述符 |
| ENOTSOCK | sockfd 文件描述符没有指向一个 socket |
| EOPNOTSUPP | socket 不是支持 listen() 操作的socket |

6.注意

为了接收连接,需要进行以下步骤:

(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,不可改变。


select

遵循 POSIX.1 - 2008

1.库

cpp 复制代码
标准 c 库,libc, -lc

2.头文件

cpp 复制代码
<sys/select.h>

3.接口定义

cpp 复制代码
int select(int nfds, fd_set *_Nullable restrict readfds,
                  fd_set *_Nullable restrict writefds,
                  fd_set *_Nullable restrict exceptfds,
                  struct timeval *_Nullable restrict timeout);

4.接口描述

首先,我们需要注意 select 只能监听少于 FD_SETSIZE(1024) 个文件描述符,这在现在看来是非常不合理的,如果想不受这个限制,需要使用 poll 或者 epool。

select 可以同时监听多个文件描述符,只要有一个文件描述符有操作需求时即返回。文件描述符有操作需求指的是可以马上进行相关的 I/O 操作,比如 read 或者少量的写操作。

fd_set

一个表示一组文件描述符的结构体,根据 POSIX 要求,结构中最大文件描述符数量为 FD_SETSIZE。

File descriptor set

select() 接口重要的参数是 3 个文件描述符集合(以 fd_set 类型声明),这允许调用者在指定的文件描述符集合上等待 3 种类型的事件。每个 fd_set 参数都可以是 NULL,只要没有文件描述符集需要监听对应的事件。

值得注意的是,一旦接口返回,每个文件描述符集都被更新,来指示哪些文件描述符就绪了。因此,如果在一个循环中使用 select(),集合必须每次调用前重新初始化。

文件描述符集的内容可以使用以下宏来操作:

FD_ZERO()

这个宏用来清除集合中的所有文件描述符,是初始化文件描述符集的第一步。

FD_SET()

这个宏用来向集合中添加文件描述符,如果文件描述符已经存在,那么也不会报错,只是不进行任何操作。

FD_CLR()

这个宏用来从集合中移除指定文件描述符,如果文件描述符不存在,则不进行任何操作。

FD_ISSET()

select() 根据如下规则更新集合内容:select() 调用结束后,FD_ISSET() 宏用来检测指定文件描述符是否还位于集合中,如果存在则返回非 0 值,否则返回 0。

5.参数

(1)readfds

这个集合中的文件描述符用来监测其是否已经读就绪。一个文件描述读就绪指的是读操作不会阻塞,特别的是,EOF 也算是读就绪。

select() 函数返回后,readfds 中只会保留读就绪的文件描述符,其他都会被删除。

(2)writefds

这个集合中的文件描述符用来监测其是否已经写就绪。一个文件描述写就绪指的是写操作不会阻塞。不过即使一个文件描述符已经写就绪,但是大块的写操作可能也会阻塞。

select() 函数返回后,writefds 中只会保留写就绪的文件描述符,其他都会被删除。

(3)eceptfds

这个集合中的文件描述符用来监测其异常情况,一些异常情况的示例,在 poll() 的 POLLPRI 中会有讨论。

select() 返回后,exceptfds 中只保留发生异常情况的文件描述符。

(4)nfds

这个参数应该被设置为 3 个集合中文件描述符的最大值加 1。

(5)timeout

timeout 是一个 timeval 的结构,指定了 select() 等待文件描述符就绪的时间,这个接口会一直阻塞直到以下事件发生:

  • 文件描述符就绪
  • 调用被信号处理打断
  • timeout 超时

值得注意的是,timeout 值会向上(rounded up)近似到系统时钟粒度,另外由于系统调度延迟,可能会导致阻塞间隔比 timeout 稍微大一些。

如果 timeout 的两个成员都为 0,那么 select 会立即返回(通常用于轮询)。

如果 timeout 是 NULL,select 会无限期等待直到有文件描述符就绪。

6.pselect()

pselect() 系统调用能够允许应用更安全的等待文件描述符就绪或者信号发生。

它和 select() 是一样的,除了以下几个地方:

  • select() 使用 timeval 结构的 timeout,而 pselect() 使用 timespec 结构 的timeout
  • select() 可能会更新 timeout 参数来指示还有多少剩余时间,而 pselect() 不会
  • select() 没有信号屏蔽 sigmask 参数,相当于 pselect 的sigmask 参数为 NULL

sigmask 是一个指向信号屏蔽的指针。如果它不为空,那么 pselect() 首先会使用它代替当前的信号屏蔽,然后在进行 select(),最后再恢复原来的信号屏蔽。如果是 NULL,那么 pselect() 调用过程并不会改变信号屏蔽值。

除了时间精度上的差异,下面两端代码等效:

cpp 复制代码
  ready = pselect(nfds, &readfds, &writefds, &exceptfds,
                           timeout, &sigmask);
cpp 复制代码
sigset_t origmask;

pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);

设计 pselect() 的原因是想要等待信号发生或者文件描述符就绪,那么就需要一个原子测试来解决数据竞争问题。比如,一个信号处理函数设置了一个标志并返回,如果信号刚好在测试的附近到达导致数据竞争时, select() 后面测试这个标志有可能无限期卡住。而 pselect() 允许先屏蔽信号,处理已经发生的信号,然后使用指定 sigmask 来调用 pselect() ,避免了数据竞争。

timeout

select() 的 timeout 结构体定义如下:

cpp 复制代码
           struct timeval {
               time_t      tv_sec;         /* seconds */
               suseconds_t tv_usec;        /* microseconds */
           };

pselect() 对应的结构体时 timespec。

Linux 系统上 select() 会修改 timeout 值来反映未睡眠的时间,其他实现不是这么做的。POSIX.1 认为任何行为都是合法的。这就会导致 Linux 系统和其他系统之间的移植问题,所以,我们应该认为 timeout 在 select() 后是未知的值。

7.返回值

成功时,select() 和 pselect() 返回三个返回文件描述符集中的文件描述符总数(也就是 redfds、writefds、exceptfds 的中设置为 1 位数)。返回值可以为 0,表示在有文件描述符就绪前 timeout 超时。

发生错误时,返回 -1,并设置errno 来指示错误类型。文件描述符集并不会被修改,timeout 值是未定义的。

错误值定义如下:

|--------|----------------------------------------------------|
| EBADF | 集合中存在不合法的文件描述符,比如已经关闭的文件描述符或者发生错误的文件描述符),具体参见 BUGS |
| EINTR | 捕获了一个信号,具体参见 signal(7) |
| EINVAL | nfds 是负值,或者超过了 RLIMIT_NOFILE 资源限制,具体参见getrlimit(2) |
| EINVAL | timeout 中的数值不合法 |
| ENOMEM | 没有足够内存来分配内部表 |

在其他 UNIX 系统上,如果系统无法分配内核资源,select() 可能会返回 EAGAIN 错误而不是 ENOMEM。POSIX 为 poll() 定义了该错误,但是并没有为 select() 定义。考虑到程序的移植性,应该检查 EGAIN 并重新调用,就行 EINTR 处理一样。

8.注意

<sys/time.h> 也提供了 fd_set 的定义,fd_set 是一个固定大小的缓冲区,执行 FD_CLR 和 FD_SET 传入一个负值或者大于 FD_SETSIZE 的 fd 会导致不可预期的结果。此外,POSIX 要求 fd 是一个可用的文件描述符。

select() 和 pselect() 操作不受 O_NONBLOCK 标志的影响。

self-pipe 小技巧

在没有 pselect() 实现的系统上,可靠(更具有移植性)的信号捕捉可以通过 self-pipe 小技巧实现。这个技术在信号处理函数中向一个 pipe 中写入 1 字节,而该 pipe 的另一端由 select() 监听。为了防止满写阻塞和空读阻塞,pipe 的读写应采用非阻塞 I/O 方式。

模拟 usleep

在 usleep 出现前,一些代码使用 select() 来实现一种可移植的亚秒精度延迟,将所有集合设置为空,nfds 为 0,非空的 timeout值。

select() 和 poll() 间通知的映射

在 linux 代码树中,我们可以发现 select() 读、写、异常通知和 poll()/epoll() 事件通知之间的联系:

           #define POLLIN_SET  (EPOLLRDNORM | EPOLLRDBAND | EPOLLIN |
                                EPOLLHUP | EPOLLERR)
                              /* Ready for reading */
           #define POLLOUT_SET (EPOLLWRBAND | EPOLLWRNORM | EPOLLOUT |
                                EPOLLERR)
                              /* Ready for writing */
           #define POLLEX_SET  (EPOLLPRI)
                              /* Exceptional condition */

多线程应用

如果一个线程通过 select() 监听的文件描述符被另一个现场关闭,那么结果是未知的。在一些 UNIX 系统上,select() 会停止阻塞并返回,告知文件描述符就绪(后续操作会出错,除非刚好其他线程又打开了文件描述符并且就绪了)。在 Linux 及其他系统上,其他线程关闭文件描述符对 select() 没有任何影响。总结起来,应用如果依赖这些具体的行为的话,就会产生 bug。

C 库和内核的差异

Linux 内核允许文件描述符集是任意大小的,由 nfds 的值来决定具体的大小。而 glibc 将fs_set 类型设置为固定值。参考 BUGS。

我们这里讲述的 pselect() 接口是 glibc 实现的,底层系统调用名字是 pselect6(),系统调用的行为和 pselect() 有些许不同。

Linux 的 pselect6() 系统调用修改 timeout 参数,然而 glibc 通过本地缓存 timeout 值隐藏了该行为。因此,glibc pselect6() 没有修改 timeout 参数,这也符合 POSIX.1-2001 要求。

pselect6() 系统调用的最后一个参数不是 sigset_t * 指针类型,而是如下格式:

           struct {
               const kernel_sigset_t *ss;   /* Pointer to signal set */
               size_t ss_len;               /* Size (in bytes) of object
                                               pointed to by 'ss' */
           };

这使得系统调用可以获取信号集指针及其大小,并考虑到大多数系统支持最大 6 个系统调用参数这个事实。关于信号处理的差异之处,可以参考 sigprocmask 的讨论。

glibc 历史细节

gblic 2.0 提供了 pselect() 的错误版本,它并没有 sigmask 参数。

glibc 2.1 到 2.2.1,为了获得 <sys/select.h> 中的 pselect() 声明,必须定义 _GNU_SOURCE 宏。

9.BUGS

POSIX 允许实现通过 FD_SETSIZE 来定义文件描述符集中文件描述符的上限,Linux 内核并没有限制,但是 glibc 实现将 fd_set 定为固定长度并将 FD_SETSIZE 设置为 1024,FD_*() 宏根据这个限制操作。为了能够监测多余 1023 个文件描述符,可以使用 poll() 或者 epoll。

fd_set 参数的输入输出属性是一个错误的设计,已经在 poll() 和 epoll() 改正过来。

根据 POSIX 要求,select() 应该检查所有集合中的文件描述符不能超过 nfds - 1,但是,当前实现会忽略掉那些文件描述符值大于当前进程打开的最大文件描述符值。根据 POSIX 要求,这些文件描述符会导致 EBADF 错误。

从 glibc 2.1 开始,glibc 使用 sigprocmask() 和 select() 实现了 pselect() 模拟,这个实现却遗留了 pselect() 解决的数据竞争问题。现在版本的 glibc 通常使用内核提供的不受数据竞争影响的 pselect() 系统调用。

Linux 上,select()可能报告 socket 文件描述符读就绪,但是后续的读却会阻塞,这个常发生在数据已达到但是数据的校验和不对,数据被丢弃。当然,也可能是误报。所以使用 O_NONBLOCK 的 sockets 更安全些。

Linux 上的 select() 会在被信号打断的情况下更新 timeout 值,POSIX.1 并不允许这样做。Linux 的 pselect() 是同样的行为,但是 glibc 隐藏了这种行为。

10.代码实例

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

       int
       main(void)
       {
           int             retval;
           fd_set          rfds;
           struct timeval  tv;

           /* Watch stdin (fd 0) to see when it has input. */

           FD_ZERO(&rfds);
           FD_SET(0, &rfds);

           /* Wait up to five seconds. */

           tv.tv_sec = 5;
           tv.tv_usec = 0;

           retval = select(1, &rfds, NULL, NULL, &tv);
           /* Don't rely on the value of tv now! */

           if (retval == -1)
               perror("select()");
           else if (retval)
               printf("Data is available now.\n");
               /* FD_ISSET(0, &rfds) will be true. */
           else
               printf("No data within five seconds.\n");

           exit(EXIT_SUCCESS);
       }

poll

poll() 遵循 POSIX.1 - 2008

ppoll() 遵循 Linux

1.库

cpp 复制代码
标准 c 库,libc, -lc

2.头文件

cpp 复制代码
<poll.h>

3.接口定义

cpp 复制代码
       int poll(struct pollfd *fds, nfds_t nfds, int timeout);
       int ppoll(struct pollfd *fds, nfds_t nfds,
                 const struct timespec *_Nullable tmo_p,
                 const sigset_t *_Nullable sigmask);

4.接口描述

poll() 和 select() 做的事情差不多,它等待一个文件描述符集 I/O 就绪。Linux 的 epoll() 也是类似的,只是比 poll() 提供多了一些特性。

fds 参数是要监控的文件描述符集,是下面结构体的一个数组:

cpp 复制代码
           struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

由调用者指定 fds 的项数。

结构体中 fd 包含了一个打开的文件描述符,如果它是负值,那么 events 参数将被忽略,revents 返回 0。(也就是说 可以将 fd 设置为其补码就可以忽略它)。

events 参数是一个输入参数,通过按位掩码来标识应用感兴趣的文件描述符上的事件。参数可以设置为 0,那么就只能返回 POLLHUP/POLLERR/POLLNVAL 事件。

revents 是一个输出参数,由内核填充实际发生的事件。这些事件可以是 events 中指定的事件,也可以是 POLLHUP/POLLERR/POLLNVAL 中的一个。(events 中这三个事件对应的位并没有什么意义,只要对应的条件发生,revents 就会返回该事件。)

如果没有请求的事件(包括错误)发生,那么 poll() 会一直阻塞,直到有事件发生。

timeout 参数指定了 poll() 等待文件描述符就绪的毫秒数,该调用会一直阻塞直到:

  • 文件描述符就绪
  • 调用被信号打断
  • 发生超时

同样,timeout 值也会向上近似到系统时钟粒度,由于内核调度延迟阻塞的事件可能会稍微多一点。如果 timeout 是负值,表示超时时间是无限长。如果 timeout 设置为 0,那么 poll() 会马上返回,即使没有任何文件描述符就绪。

events 和 revents 中各个位在 poll.h 中定义:

POLLIN

有数据可以读。

POLLPRI

文件描述符上有异常发生,可能是(1)TCP socket 上有带外数据(2)处于报文模式的伪终端主机发现了从机状态变化(3)cgroup.events 文件被修改了。

POLLOUT

当前可写,但是写大于 socket 或 pipe 中可用空间的数据仍然会导致阻塞(除非设置了 O_NONBLOCK)。

POLLRDHUP

流 socket 对端关闭了连接或者在写半连接时关机。这个定义依赖于 _GNU_SOURCE 宏定。

POLLERR

发生错误。如果文件描述符指向了 pipe 的写端,而读端关闭了,那么也会返回这个错误。

POLLHUP

挂断。在读取 pipe 或者流 socket 时,这个事件只表示对端关闭了其通道,后面的数据读取时,在通道中数据读尽后再继续读会返回 0(EOF)。

POLLNVAL

请求不合法:fd 没有打开。

在使用 _XOPEN_SOURCE 宏编译时,还会有以下一些事件,不过也没有提供太多信息:

POLLRDNORM

等同于 POLLIN。

POLLRDBAND

优先带宽数据可以读(通常在 Linux 上用)

POLLWRNORM

等同于 POLLOUT

POLLWRBAND

可能写了优先数据

ppoll()

ppoll() 和 poll() 的关系就像 select() 和 pselect() 的关系一样,ppoll() 为应用提供了等待信号或者就绪事件的安全方法。

除了 timeout 时间精度上的差异,以下两段代码几乎等效

cpp 复制代码
           ready = ppoll(&fds, nfds, tmo_p, &sigmask);
cpp 复制代码
           sigset_t origmask;
           int timeout;

           timeout = (tmo_p == NULL) ? -1 :
                     (tmo_p->tv_sec * 1000 + tmo_p->tv_nsec / 1000000);
           pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
           ready = poll(&fds, nfds, timeout);
           pthread_sigmask(SIG_SETMASK, &origmask, NULL);

上面代码说成几乎等效而不是等效主要是因为负值的 timeout 会被 poll() 解释为一直等待,而 ppoll() 中负值的 *tmo_p 会报错。

可以参考 pselect(2) 来看为什么 ppoll 是必要的。

如果 sigmask 参数为 NULL,那么就不会有任何信号屏蔽操作,这时这两个接口唯一的区别就是时间精度。

tmo_p 指定了 ppoll() 会阻塞的时间上限,它是指向 timespec 结构体的指针,指针为空时,ppoll() 会一直阻塞。

5.返回值

成功时,poll() 返回一个非负数表示 pollfds 中有多少个文件描述符上有事件发生,即对应的 revents 有被更新为非 0 值。返回 0 表示没有任何文件描述符就绪并超时。

发生错误时,返回 -1,并设置errno 来指示错误类型。

错误值定义如下:

|--------|-------------------------------|
| EFAULT | fds 指向了进程外的地址空间 |
| EINTR | 请求事件发生前,发生了信号,具体参见 signal(7) |
| EIVAL | nfds 值超出了 RLIMIT_NOFILE 限制 |
| EINVAL | ppoll() 中的 *tmo_P 是一个非法值(负数) |
| ENOMEM | 没有足够内存来分配内核数据结构 |

一些其他 UNIX 系统上,如果内核无法发分配内核资源,poll() 可能会产生 EAGAIN 类的错误,而不像 Linux 上的 ENOMEM。POSIX 允许这种行为。所以,一个可移植的程序需要检测该错误,并重试,就像处理 EINTR 一样。

一些实现定义了非标准常量 INFTIM(-1),用作 poll() 的 timeout,但是这个常量并没有被被 glibc 提供。

6.注意

poll() 和 ppoll() 的行为不受 O_NONBLOCK 标志影响。

对于一个文件描述符正在被 poll() 监听却被另一个线程关闭了这种情况的讨论,可以参考 select(2)。

7.BUGS

可以参考 select(2) 中关于虚假就绪通知的讨论。

8.代码实例

该程序会打开命令行参数传进来的文件名并监听其 POLLIN 事件,程序会循环调用 poll() 来监听文件描述符,打印已经就绪的文件描述符数。对于每个就绪的文件描述符,程序会:

  • 以可读的格式显示返回的 revents
  • 如果文件描述符就绪,那么就从中读一些数据出来并打印
  • 如果文件描述符不可读,但是发生了一些其他事件(比如 POLLHUP),就关闭文件描述符

假定我们在一个终端运行程序,让他打开一个 FIFO:

cpp 复制代码
       $ mkfifo myfifo
       $ ./poll_input myfifo

在另一个终端打开 FIFO,并写入一些数据,然后关闭 FIFO:

cpp 复制代码
       $ echo aaaaabbbbbccccc > myfifo

我们将在运行程序的终端上看到如下信息:

cpp 复制代码
           Opened "myfifo" on fd 3
           About to poll()
           Ready: 1
             fd=3; events: POLLIN POLLHUP
               read 10 bytes: aaaaabbbbb
           About to poll()
           Ready: 1
             fd=3; events: POLLIN POLLHUP
               read 6 bytes: ccccc

           About to poll()
           Ready: 1
             fd=3; events: POLLHUP
               closing fd 3
           All file descriptors closed; bye

从上面我们可以看到 poll() 返回了三次:

  • 第一次返回是 POLLIN,表示文件描述符可读,另一个是 POLLHUP 表示文件描述符的另一个端关闭了。程序接着读取了一些可用的输入数据
  • 第二次返回同样是这两个事件,依然消费了一些可用数据
  • 最后一次返回,poll() 只有 POLLHUP 事件,然后关闭文件描述符并结束了程序。
cpp 复制代码
       /* poll_input.c

          Licensed under GNU General Public License v2 or later.
       */
       #include <fcntl.h>
       #include <poll.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>

       #define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
                               } while (0)

       int
       main(int argc, char *argv[])
       {
           int            ready;
           char           buf[10];
           nfds_t         num_open_fds, nfds;
           ssize_t        s;
           struct pollfd  *pfds;

           if (argc < 2) {
              fprintf(stderr, "Usage: %s file...\n", argv[0]);
              exit(EXIT_FAILURE);
           }

           num_open_fds = nfds = argc - 1;
           pfds = calloc(nfds, sizeof(struct pollfd));
           if (pfds == NULL)
               errExit("malloc");

           /* Open each file on command line, and add it to 'pfds' array. */

           for (nfds_t j = 0; j < nfds; j++) {
               pfds[j].fd = open(argv[j + 1], O_RDONLY);
               if (pfds[j].fd == -1)
                   errExit("open");

               printf("Opened \"%s\" on fd %d\n", argv[j + 1], pfds[j].fd);

               pfds[j].events = POLLIN;
           }

           /* Keep calling poll() as long as at least one file descriptor is
              open. */

           while (num_open_fds > 0) {
               printf("About to poll()\n");
               ready = poll(pfds, nfds, -1);
               if (ready == -1)
                   errExit("poll");

               printf("Ready: %d\n", ready);

               /* Deal with array returned by poll(). */

               for (nfds_t j = 0; j < nfds; j++) {
                   if (pfds[j].revents != 0) {
                       printf("  fd=%d; events: %s%s%s\n", pfds[j].fd,
                              (pfds[j].revents & POLLIN)  ? "POLLIN "  : "",
                              (pfds[j].revents & POLLHUP) ? "POLLHUP " : "",
                              (pfds[j].revents & POLLERR) ? "POLLERR " : "");

                       if (pfds[j].revents & POLLIN) {
                           s = read(pfds[j].fd, buf, sizeof(buf));
                           if (s == -1)
                               errExit("read");
                           printf("    read %zd bytes: %.*s\n",
                                  s, (int) s, buf);
                       } else {                /* POLLERR | POLLHUP */
                           printf("    closing fd %d\n", pfds[j].fd);
                           if (close(pfds[j].fd) == -1)
                               errExit("close");
                           num_open_fds--;
                       }
                   }
               }
           }

           printf("All file descriptors closed; bye\n");
           exit(EXIT_SUCCESS);
       }

accept

accept() 遵循 POSIX.1 - 2008

accept4() 遵循 Linux

1.库

cpp 复制代码
标准 c 库,libc, -lc

2.头文件

cpp 复制代码
<sys/socket.h>

3.接口定义

cpp 复制代码
       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);

4.接口描述

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) 说明来看这个标志的意义。

5.返回值

成功时,这个系统调用返回一个接收 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 实现是不一样。一个可移植的程序不应该对这些进行假设,显示的设置这些标志。

6.注意

我们收到 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 *。

7.代码

       #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");
       }

connect

connect() 遵循 POSIX.1 - 2008

1.库

cpp 复制代码
标准 c 库,libc, -lc

2.头文件

cpp 复制代码
<sys/socket.h>

3.接口定义

cpp 复制代码
        int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

4.接口描述

connect() 系统调用在 sockfd 指定的 socket 上连接 addr 指定的地址,addrlen 参数指定了 addr 的大小,addr 地址格式取决于 socket 的地址空间,可以参考 socket(2)。

如果 socket 是 SOCK_DGRAM 类型,那么 addr 是发送报文的默认地址,也是唯一接收报文的地址。如果 socket 类型是 SOCK_STREAM 或者 SOCK_SEQPACKET,那么这个调用就是尝试和绑定了 addr 地址的 socket 建立连接。

一些协议套接字(比如 UNIX 流套接字)只能成功连接一次。

一些协议套接字(比如 UNIX TCP 套接字和网络数据报套接字)可以多次 connect() 来修改连接。

一些协议套接字(比如 UNIX TCP 套接字和网络数据报套接字)可以通过将 sockaddr 的 sa_family 设置为 AF_UNSPEC 来消除连接,之后 socket 就可以连接到其他地址了。(AF_UNSPEC 在 Linux 2.2 之后支持)。

5.返回值

如果连接或者绑定成功,那么返回 0。

发生错误时,返回 -1,并设置errno 来指示错误类型。

错误值定义如下(这里指示普通 socket 的错误,还可能存在 domain-specific 错误码):

|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| EACCES | UNIX 域套接字通过路径名唯一标识,并且是套接字文件是没有写权限的,路径中任何一级的搜索权限也是没有的,可以参考 path_resolution(7) |
| EACCES/EPERM | 用户尝试连接到一个广播地址,却没有设置套接字的广播标记,或者请求被防火墙规则拦截了 |
| EACCES | 如果开启了 SELinux 策略,也可能会导致连接被拒绝(比如策略规定 HTTP 代理只能连接到 HTTP 服务器关联的端口,而 HTTP 代理却连接了其他端口) |
| EADDRINUSE | 本地地址已经在用了 |
| EADDRNOTAVAIL | (网络域套接字)sockfd 指定的套接字没有绑定到地址,并且在尝试将其绑定到临时端口时,临时端口用尽了 |
| EAFNOSUPPORT | 地址家族不正确 |
| EAGAIN | 对于非阻塞的 UNIX 域套接字,套接字是非阻塞的,连接无法立即完成。对于其他套接字家族,这个错误标识路由缓存没有足够的条目了 |
| EALREADY | 套接字是非阻塞的,并且之前的连接尝试还没有完成 |
| EBADF | sockfd 不是一个打开的文件描述符 |
| ECONNREFUSED | connect() 操作的流套接字发现没有人在监听对应的远程地址 |
| EFAULT | 套接字结构地址超出用户地址空间 |
| EINPROGRESS | 套接字是非阻塞的,连接不能立即完成。(UNIX 域套接字会返回 EAGAIN)。可以通过 select(2) 或者 poll(2) 查看套接字的可写事件,来确定连接完成。select(2) 指示可写后,使用 getsockopt(2) 来读取 SOL_SOCKET 级的 SO_ERROR 选项,来确定连接完全成功(SO_ERROR 为 0)或者未成功(SO_ERROR 为这里列出来的普通错误)。 |
| EINTR | 系统调用被信号打断 |
| EISCONN | 套接字已经连接 |
| ENETUNREACH | 网络不可达 |
| ENOTSOCK | 文件描述符并没有指向一个套接字 |
| EPROTOTYPE | 该套接字不支持指定的通信协议。这个错误可能在出现在连接一个 UNIX 域报文套接字到一个流套接字 |
| ETIMEDOUT | 连接超时。可能是服务器太忙了以至于无法接收新的连接。注意:当服务器开启 syncookies 时,IP 套接字的超时可能会非常长。 |

6.注意

如果 connect() 失败,那么套接字的状态是未知的。一个易于移植的程序应该关闭该套接字应该再创建一个新套接字,重新连接。

7.代码

这里我们展示下 select() 的用法示例,来将最近几篇内容串起来:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <errno.h>

#define SERVER_PORT  12345

#define TRUE             1
#define FALSE            0

main (int argc, char *argv[])
{
   int    i, len, rc, on = 1;
   int    listen_sd, max_sd, new_sd;
   int    desc_ready, end_server = FALSE;
   int    close_conn;
   char   buffer[80];
   struct sockaddr_in6   addr;
   struct timeval       timeout;
   struct fd_set        master_set, working_set;

   /*************************************************************/
   /* Create an AF_INET6 stream socket to receive incoming      */
   /* connections on                                            */
   /*************************************************************/
   listen_sd = socket(AF_INET6, SOCK_STREAM, 0);
   if (listen_sd < 0)
   {
      perror("socket() failed");
      exit(-1);
   }

   /*************************************************************/
   /* Allow socket descriptor to be reuseable                   */
   /*************************************************************/
   rc = setsockopt(listen_sd, SOL_SOCKET,  SO_REUSEADDR,
                   (char *)&on, sizeof(on));
   if (rc < 0)
   {
      perror("setsockopt() failed");
      close(listen_sd);
      exit(-1);
   }

   /*************************************************************/
   /* Set socket to be nonblocking. All of the sockets for      */
   /* the incoming connections will also be nonblocking since   */
   /* they will inherit that state from the listening socket.   */
   /*************************************************************/
   rc = ioctl(listen_sd, FIONBIO, (char *)&on);
   if (rc < 0)
   {
      perror("ioctl() failed");
      close(listen_sd);
      exit(-1);
   }

   /*************************************************************/
   /* Bind the socket                                           */
   /*************************************************************/
   memset(&addr, 0, sizeof(addr));
   addr.sin6_family      = AF_INET6;
   memcpy(&addr.sin6_addr, &in6addr_any, sizeof(in6addr_any));
   addr.sin6_port        = htons(SERVER_PORT);
   rc = bind(listen_sd,
             (struct sockaddr *)&addr, sizeof(addr));
   if (rc < 0)
   {
      perror("bind() failed");
      close(listen_sd);
      exit(-1);
   }

   /*************************************************************/
   /* Set the listen back log                                   */
   /*************************************************************/
   rc = listen(listen_sd, 32);
   if (rc < 0)
   {
      perror("listen() failed");
      close(listen_sd);
      exit(-1);
   }

   /*************************************************************/
   /* Initialize the master fd_set                              */
   /*************************************************************/
   FD_ZERO(&master_set);
   max_sd = listen_sd;
   FD_SET(listen_sd, &master_set);

   /*************************************************************/
   /* Initialize the timeval struct to 3 minutes.  If no        */
   /* activity after 3 minutes this program will end.           */
   /*************************************************************/
   timeout.tv_sec  = 3 * 60;
   timeout.tv_usec = 0;

   /*************************************************************/
   /* Loop waiting for incoming connects or for incoming data   */
   /* on any of the connected sockets.                          */
   /*************************************************************/
   do
   {
      /**********************************************************/
      /* Copy the master fd_set over to the working fd_set.     */
      /**********************************************************/
      memcpy(&working_set, &master_set, sizeof(master_set));

      /**********************************************************/
      /* Call select() and wait 3 minutes for it to complete.   */
      /**********************************************************/
      printf("Waiting on select()...\n");
      rc = select(max_sd + 1, &working_set, NULL, NULL, &timeout);

      /**********************************************************/
      /* Check to see if the select call failed.                */
      /**********************************************************/
      if (rc < 0)
      {
         perror("  select() failed");
         break;
      }

      /**********************************************************/
      /* Check to see if the 3 minute time out expired.         */
      /**********************************************************/
      if (rc == 0)
      {
         printf("  select() timed out.  End program.\n");
         break;
      }

      /**********************************************************/
      /* One or more descriptors are readable.  Need to         */
      /* determine which ones they are.                         */
      /**********************************************************/
      desc_ready = rc;
      for (i=0; i <= max_sd  &&  desc_ready > 0; ++i)
      {
         /*******************************************************/
         /* Check to see if this descriptor is ready            */
         /*******************************************************/
         if (FD_ISSET(i, &working_set))
         {
            /****************************************************/
            /* A descriptor was found that was readable - one   */
            /* less has to be looked for.  This is being done   */
            /* so that we can stop looking at the working set   */
            /* once we have found all of the descriptors that   */
            /* were ready.                                      */
            /****************************************************/
            desc_ready -= 1;

            /****************************************************/
            /* Check to see if this is the listening socket     */
            /****************************************************/
            if (i == listen_sd)
            {
               printf("  Listening socket is readable\n");
               /*************************************************/
               /* Accept all incoming connections that are      */
               /* queued up on the listening socket before we   */
               /* loop back and call select again.              */
               /*************************************************/
               do
               {
                  /**********************************************/
                  /* Accept each incoming connection.  If       */
                  /* accept fails with EWOULDBLOCK, then we     */
                  /* have accepted all of them.  Any other      */
                  /* failure on accept will cause us to end the */
                  /* server.                                    */
                  /**********************************************/
                  new_sd = accept(listen_sd, NULL, NULL);
                  if (new_sd < 0)
                  {
                     if (errno != EWOULDBLOCK)
                     {
                        perror("  accept() failed");
                        end_server = TRUE;
                     }
                     break;
                  }

                  /**********************************************/
                  /* Add the new incoming connection to the     */
                  /* master read set                            */
                  /**********************************************/
                  printf("  New incoming connection - %d\n", new_sd);
                  FD_SET(new_sd, &master_set);
                  if (new_sd > max_sd)
                     max_sd = new_sd;

                  /**********************************************/
                  /* Loop back up and accept another incoming   */
                  /* connection                                 */
                  /**********************************************/
               } while (new_sd != -1);
            }

            /****************************************************/
            /* This is not the listening socket, therefore an   */
            /* existing connection must be readable             */
            /****************************************************/
            else
            {
               printf("  Descriptor %d is readable\n", i);
               close_conn = FALSE;
               /*************************************************/
               /* Receive all incoming data on this socket      */
               /* before we loop back and call select again.    */
               /*************************************************/
               do
               {
                  /**********************************************/
                  /* Receive data on this connection until the  */
                  /* recv fails with EWOULDBLOCK.  If any other */
                  /* failure occurs, we will close the          */
                  /* connection.                                */
                  /**********************************************/
                  rc = recv(i, buffer, sizeof(buffer), 0);
                  if (rc < 0)
                  {
                     if (errno != EWOULDBLOCK)
                     {
                        perror("  recv() failed");
                        close_conn = TRUE;
                     }
                     break;
                  }

                  /**********************************************/
                  /* Check to see if the connection has been    */
                  /* closed by the client                       */
                  /**********************************************/
                  if (rc == 0)
                  {
                     printf("  Connection closed\n");
                     close_conn = TRUE;
                     break;
                  }

                  /**********************************************/
                  /* Data was received                          */
                  /**********************************************/
                  len = rc;
                  printf("  %d bytes received\n", len);

                  /**********************************************/
                  /* Echo the data back to the client           */
                  /**********************************************/
                  rc = send(i, buffer, len, 0);
                  if (rc < 0)
                  {
                     perror("  send() failed");
                     close_conn = TRUE;
                     break;
                  }

               } while (TRUE);

               /*************************************************/
               /* If the close_conn flag was turned on, we need */
               /* to clean up this active connection.  This     */
               /* clean up process includes removing the        */
               /* descriptor from the master set and            */
               /* determining the new maximum descriptor value  */
               /* based on the bits that are still turned on in */
               /* the master set.                               */
               /*************************************************/
               if (close_conn)
               {
                  close(i);
                  FD_CLR(i, &master_set);
                  if (i == max_sd)
                  {
                     while (FD_ISSET(max_sd, &master_set) == FALSE)
                        max_sd -= 1;
                  }
               }
            } /* End of existing connection is readable */
         } /* End of if (FD_ISSET(i, &working_set)) */
      } /* End of loop through selectable descriptors */

   } while (end_server == FALSE);

   /*************************************************************/
   /* Clean up all of the sockets that are open                 */
   /*************************************************************/
   for (i=0; i <= max_sd; ++i)
   {
      if (FD_ISSET(i, &master_set))
         close(i);
   }
}

recv

recv() 遵循 POSIX.1 - 2008

1.库

cpp 复制代码
标准 c 库,libc, -lc

2.头文件

cpp 复制代码
<sys/socket.h>

3.接口定义

cpp 复制代码
       ssize_t recv(int sockfd, void buf[.len], size_t len,
                        int flags);

       ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len,
                        int flags,
                        struct sockaddr *_Nullable restrict src_addr,
                        socklen_t *_Nullable restrict addrlen);

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

4.接口描述

recv()、recvfrom()、recvmsg() 调用用来从套接字接收消息,它们都可以用在连接或非连接套接字上。我们首先描述几个系统调用共同的特性,然后在介绍它们的差别。

recv() 和 read(2) 的唯一区别就是是否有 flags 标记,当 recv() 的标记为 0 时,它基本上等同于 read(2)(具体参见注意部分)。同样的,下面调用:

cpp 复制代码
recv(sockfd, buf, len, flags);

等同于

cpp 复制代码
recvfrom(sockfd, buf, len, flags, NULL, NULL);

三个调用都在成功时返回消息的长度,如果提供的 buffer 盛不下消息,那么超出的消息可能会被遗弃,这要取决于接收套接字的类型。

如果套接字上没有消息,那么接收调用会一直等到有消息到来,除非套接字是非阻塞的(参考 fcntl(2) ),这时会返回 -1 并将 errno 设置为 EAGAIN 或者 EWOULDBLOCK。接收调用正常情况只要有可用数据就会返回,接近请求的数据量,而不是一直等到接收到所有的请求数据量。

应用程序可以使用 select(2)、poll(2)、epoll(7) 来决定套接字上有更多数据发生的时机。

flags 参数

flags 是下面值的位或值:

MSG_CMSG_CLOEXEC (只有 recvmsg() 可用)

设置接收文件描述符的异常关闭标记,通过 UNIX 域文件描述符的 SCM_RIGHTS 操作实现。这个标记的用途和 O_CLOEXEC 类似。

MSG_DONTWAIT

使能非阻塞操作,如果操作要阻塞,那么调用会报出 EAGAIN、EWOULDBLOCK 错误。这个和设置 O_NONBLOCK 标记类似(通过 fcntl(2) F_SETFL 操作),不过 MSG_DONTWAIT 只对本次调用管用,而 O_NONBLOCK 是设置到了文件描述符上,这样就会影响所有调用进程的所有线程以及其他持有该套接字句柄的进程。

MSG_ERRQUEQUE

这个标记指定了排队错误应该被套接字错误队列接收,错误信息会以依赖具体协议的类型来传递(对于 IPv4 是 IP_RECVERR)。用户应该提供足够的 buffer 大小。导致错误的原始报文的载荷大小以正常数据的 msg_iovec 格式传递,导致错误的原始报文的地址以 msg_name 形式提供。

错误以 sock_extended_err 结构提供:

cpp 复制代码
                  #define SO_EE_ORIGIN_NONE    0
                  #define SO_EE_ORIGIN_LOCAL   1
                  #define SO_EE_ORIGIN_ICMP    2
                  #define SO_EE_ORIGIN_ICMP6   3

                  struct sock_extended_err
                  {
                      uint32_t ee_errno;   /* Error number */
                      uint8_t  ee_origin;  /* Where the error originated */
                      uint8_t  ee_type;    /* Type */
                      uint8_t  ee_code;    /* Code */
                      uint8_t  ee_pad;     /* Padding */
                      uint32_t ee_info;    /* Additional information */
                      uint32_t ee_data;    /* Other data */
                      /* More data may follow */
                  };

                  struct sockaddr *SO_EE_OFFENDER(struct sock_extended_err *);

ee_errno 包含了排队错误的 errno 值,ee_origin 是错误发源地的代码,其他域都是协议相关的。宏 SO_EE_OFFENDER 作为辅助信息返回错误发生点的网络对象地址。如果地址未知,那么 sockaddr 中的 sa_family 会包含 AF_UNSPEC,其他域为未知值。导致错误的报文的载荷以正常数据传递。

对于本地错误,不传递地址(可以通过 cmsghdr 的 cmsg_len 值来确认)。 收到错误时,msghdr 会设置 MSG_ERRQUEQUE 标记。错误传递后,套接字错误码会根据下一个队列错误重新生成,在下一个套接字操作发生时传递。

MSG_OOB

这个标记请求接收通常不会在正常数据量中接到的带外数据。一些协议会将加速数据放在正常数据队列的前面,这就会导致这个标记没办法在这些协议中使用。

MSG_PEEK

这个标记指定从接收队列的头部接收数据,并且不会将数据从队列中移除。因此,下一次接收调用会返回相同的值。

MSG_TRUNC

对于原始协议(AF_PACKET),Internet datagram、netlink、UNIX datagram、sequenced-packet 套接字会返回实际的分组或报文长度,即使它比提供的 buffer 大。

对于网络流套接字,参考 tcp(7)。

MSG_WAITALL

这个标记请求操作一直等到请求大小完全满足为止。然后,当信号、错误、连接断开、后面接收数据和之前的数据类型不同等发生时,调用仍然可能返回少于请求大小的数据。

recvfrom()

recvfrom() 将收到的消息放到缓冲区 buf 中,调用者必须通过 len 参数指定 buf 的大小。

如果 src_addr 不是 NULL,底层协议提供了消息的原地址,那么原地址会填到 src_addr 中,这种情况下 addrlen 是一个输入输出参数。调用前,它应该被初始化为 src_addr 缓冲区的大小,返回时会有原地址的实际大小更新。如果提供的缓冲器太小,那么返回地址就会被截断,这种情况下,addrlen 的值就会比提供的值大。

如果调用者对原地址不感兴趣,那么 src_addr 和 addrlen 都应该被设置为 NULL。

recv()

recv() 通常只能用于连接的套接字(参考 connect(2)),它相当于下面的调用:

cpp 复制代码
           recvfrom(fd, buf, len, flags, NULL, 0);

recvmsg()

recvmsg() 调用使用 msghdr 结构来减少需要传递参数的个数,结构体在 <sys/socket.h> 中定义,如下:

cpp 复制代码
           struct msghdr {
               void         *msg_name;       /* Optional address */
               socklen_t     msg_namelen;    /* Size of address */
               struct iovec *msg_iov;        /* Scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* Ancillary data, see below */
               size_t        msg_controllen; /* Ancillary data buffer len */
               int           msg_flags;      /* Flags on received message */
           };

msg_name 域指向用户分配的缓冲区,用来存放非连接套接字的源地址,调用者应该通过 msg_namelen 来设置缓冲器的大小,一旦成功返回,msg_namelen 会被设置为源地址的实际大小。如果应用不关心源地址,那么 msg_name 可以设置为 NULL。

msg_iov 和 msg_iovlen 描述 scatter-gather 方式的区域(就是一些类分散的缓冲区列表),在 readv(2) 中有讨论。

msg_control 域具有 msg_controllen 长度,是一个为其他协议控制消息或者各种辅助数据准备的缓冲区。当 recvmsg() 调用时,msg_controllen 应指定 msg_control 缓冲区可用大小,一旦成功返回,它将包含控制消息序列的大小。消息格式如下:

cpp 复制代码
           struct cmsghdr {
               size_t cmsg_len;    /* Data byte count, including header
                                      (type is socklen_t in POSIX) */
               int    cmsg_level;  /* Originating protocol */
               int    cmsg_type;   /* Protocol-specific type */
           /* followed by
               unsigned char cmsg_data[]; */
           };

辅助数据应该只能被 cmsg(3) 中定义的宏来访问。

作为例子,Linux 使用这个辅助数据机制在 UNIX 域套接字上传递扩展错误、IP 选项、文件描述符,参考 unix(7) 和 ip(7)。

msghdr 中的 msg_fags 域会在 recvmsg() 返回时更新,它可能包含以下一些标记:

MSG_EOR

指示记录结束,记录中所有数据都已返回(通常用在 SOCK_SEQPACKET 中)。

MSG_TRUNC

指示数据报文的结尾部分因为大于提供的缓冲区大小而被丢弃。

MSG_OOB

指示有带外或者加速数据到达

5.返回值

调用会返回接收到数据的字节数。

发生错误时,返回 -1,并设置errno 来指示错误类型。

当流套接字对端自己关闭了,那么将返回 0(传统意义的 EOF 返回)。

各个域中的数据报文套接字允许 0 长度报文,当这样的报文收到时,返回的值就是 0。

在流套接字请求接收 0 个字节时,返回值也可能是 0。

错误值定义如下:

|--------------------|--------------------------------------------------------------------------------------------------|
| EAGAIN/EWOULDBLOCK | 如果套接字被标记为非阻塞并且接收操作打算阻塞,或者设置了超时值,在数据到达前发生了超时。POSIX.1 允许使用两个错误值的任何一个,也不假设两个值相等,这就需要应用检查对两个错误都进行检查。 |
| EBADF | sockfd 参数是一个非法的文件描述符 |
| ECONNREFUSED | 远程主机拒绝网络连接(通常是没有运行请求的服务) |
| EFAULT | 接收缓冲区指针指向进程外地址 |
| EINTR | 接收操作在数据来临前被传递来的信号打断 |
| EINVAL | 参数不合法 |
| ENOMEM | 无法申请 recvmsg() 的内存 |
| ENOTCONN | 套接字是一个面向连接的套接字,但是没有连接(参考 connect(2) 和 accept(2)) |
| ENOTSOCK | 文件描述符不是一个套接字 |

6.注意

如果有 0 长度报文处于等待,那么 read(2) 和 标记为 0 的recv() 的处理行为是不同的。read(2) 没有任何影响(报文还在等待中),而 recv() 会消耗掉报文。

参考 recvmmsg(2) 来看 Linux 系统特定的系统调用来在一次调用中处理多个报文。

7.代码

cpp 复制代码
   Server program

       #include <netdb.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <string.h>
       #include <sys/socket.h>
       #include <sys/types.h>
       #include <unistd.h>

       #define BUF_SIZE 500

       int
       main(int argc, char *argv[])
       {
           int                      sfd, s;
           char                     buf[BUF_SIZE];
           ssize_t                  nread;
           socklen_t                peer_addrlen;
           struct addrinfo          hints;
           struct addrinfo          *result, *rp;
           struct sockaddr_storage  peer_addr;

           if (argc != 2) {
               fprintf(stderr, "Usage: %s port\n", argv[0]);
               exit(EXIT_FAILURE);
           }

           memset(&hints, 0, sizeof(hints));
           hints.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
           hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */
           hints.ai_flags = AI_PASSIVE;    /* For wildcard IP address */
           hints.ai_protocol = 0;          /* Any protocol */
           hints.ai_canonname = NULL;
           hints.ai_addr = NULL;
           hints.ai_next = NULL;

           s = getaddrinfo(NULL, argv[1], &hints, &result);
           if (s != 0) {
               fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
               exit(EXIT_FAILURE);
           }

           /* getaddrinfo() returns a list of address structures.
              Try each address until we successfully bind(2).
              If socket(2) (or bind(2)) fails, we (close the socket
              and) try the next address. */

           for (rp = result; rp != NULL; rp = rp->ai_next) {
               sfd = socket(rp->ai_family, rp->ai_socktype,
                            rp->ai_protocol);
               if (sfd == -1)
                   continue;

               if (bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0)
                   break;                  /* Success */

               close(sfd);
           }

           freeaddrinfo(result);           /* No longer needed */

           if (rp == NULL) {               /* No address succeeded */
               fprintf(stderr, "Could not bind\n");
               exit(EXIT_FAILURE);
           }

           /* Read datagrams and echo them back to sender. */

           for (;;) {
               char host[NI_MAXHOST], service[NI_MAXSERV];

               peer_addrlen = sizeof(peer_addr);
               nread = recvfrom(sfd, buf, BUF_SIZE, 0,
                                (struct sockaddr *) &peer_addr, &peer_addrlen);
               if (nread == -1)
                   continue;               /* Ignore failed request */

               s = getnameinfo((struct sockaddr *) &peer_addr,
                               peer_addrlen, host, NI_MAXHOST,
                               service, NI_MAXSERV, NI_NUMERICSERV);
               if (s == 0)
                   printf("Received %zd bytes from %s:%s\n",
                          nread, host, service);
               else
                   fprintf(stderr, "getnameinfo: %s\n", gai_strerror(s));

               if (sendto(sfd, buf, nread, 0, (struct sockaddr *) &peer_addr,
                          peer_addrlen) != nread)
               {
                   fprintf(stderr, "Error sending response\n");
               }
           }
       }
cpp 复制代码
   Client program

       #include <netdb.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <string.h>
       #include <sys/socket.h>
       #include <sys/types.h>
       #include <unistd.h>

       #define BUF_SIZE 500

       int
       main(int argc, char *argv[])
       {
           int              sfd, s;
           char             buf[BUF_SIZE];
           size_t           len;
           ssize_t          nread;
           struct addrinfo  hints;
           struct addrinfo  *result, *rp;

           if (argc < 3) {
               fprintf(stderr, "Usage: %s host port msg...\n", argv[0]);
               exit(EXIT_FAILURE);
           }

           /* Obtain address(es) matching host/port. */

           memset(&hints, 0, sizeof(hints));
           hints.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
           hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */
           hints.ai_flags = 0;
           hints.ai_protocol = 0;          /* Any protocol */

           s = getaddrinfo(argv[1], argv[2], &hints, &result);
           if (s != 0) {
               fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
               exit(EXIT_FAILURE);
           }

           /* getaddrinfo() returns a list of address structures.
              Try each address until we successfully connect(2).
              If socket(2) (or connect(2)) fails, we (close the socket
              and) try the next address. */

           for (rp = result; rp != NULL; rp = rp->ai_next) {
               sfd = socket(rp->ai_family, rp->ai_socktype,
                            rp->ai_protocol);
               if (sfd == -1)
                   continue;

               if (connect(sfd, rp->ai_addr, rp->ai_addrlen) != -1)
                   break;                  /* Success */

               close(sfd);
           }

           freeaddrinfo(result);           /* No longer needed */

           if (rp == NULL) {               /* No address succeeded */
               fprintf(stderr, "Could not connect\n");
               exit(EXIT_FAILURE);
           }

           /* Send remaining command-line arguments as separate
              datagrams, and read responses from server. */

           for (size_t j = 3; j < argc; j++) {
               len = strlen(argv[j]) + 1;
                       /* +1 for terminating null byte */

               if (len > BUF_SIZE) {
                   fprintf(stderr,
                           "Ignoring long message in argument %zu\n", j);
                   continue;
               }

               if (write(sfd, argv[j], len) != len) {
                   fprintf(stderr, "partial/failed write\n");
                   exit(EXIT_FAILURE);
               }

               nread = read(sfd, buf, BUF_SIZE);
               if (nread == -1) {
                   perror("read");
                   exit(EXIT_FAILURE);
               }

               printf("Received %zd bytes: %s\n", nread, buf);
           }

           exit(EXIT_SUCCESS);
       }

send

send() 遵循 POSIX.1 - 2008

MSG_CONFIRM 是 Linux 扩展

1.库

cpp 复制代码
标准 c 库,libc, -lc

2.头文件

cpp 复制代码
<sys/socket.h>

3.接口定义

cpp 复制代码
       ssize_t send(int sockfd, const void buf[.len], size_t len, int flags);

       ssize_t sendto(int sockfd, const void buf[.len], size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

       ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

4.接口描述

send()、sendto()、sendmsg() 调用用来向另一个套接字发送消息。

send() 通常只能用在连接状态(即接收者已知)的套接字上,send() 和 write(2) 的唯一不同是 send() 存在 flags 标记。当标记为 0 时,send() 和 write(2) 等效。同样的,下面的两个调用也是等效的。

cpp 复制代码
           send(sockfd, buf, len, flags);
cpp 复制代码
           sendto(sockfd, buf, len, flags, NULL, 0);

sockfd 参数是 发送套接字的文件描述符。

如果sendto() 用在连接模式(SOCK_STREAM、SOCK_SEQPACKET)的套接字上,那么参数 dest_addr 和 addrlen 将被忽略(当它们不是 NULL 或者 0时,会返回 EISCON 错误),并且当套接字没有连接时,会返回 ENOTCONN 错误。否则需要给定 addrlen 指定长度的 dest_addr 目标地址,目标地址由 msg.msg_name 给定,长度是 msg.msg_namelen 指定。

对于 send() 和 sendto(),发送的消息放在 len 长度的 buf 中。对于 sendmsg(),消息由 msg.msg_iov 数组元素指定,sendmsg() 调用同时允许发送一些辅助数据(也称为控制信息)。

如果消息太大以至于不能自动的透传给底层协议,调用会返回 EMSGSIZE 错误,该消息不会被发送。

send() 调用没有显示的关于传输失败的指示,在内部检测到这类错误时也只会返回 -1。

当消息不能装进套接字的发送缓冲区时,send() 正常会阻塞,除非套接字放进了非阻塞 I/O 模式,这种情况在非阻塞模式下会报告 EAGAIN 或者 EWOULDBLOCK 错误。这时可以使用 select(2) 来检测什么时候可以发送更多数据。

flags 参数

flags 参数可以是下面值的位或:

MSG_CONFIRM(Linux 2.3.15 后)

告诉链路层有进展发生:从对端收到了一个成功的回复。如果链路层没有收到这个,那么它通常会在骚扰邻居(通过 ARP 单播)。这个标记只在IPv4/IPv6 的 SOCK_DGRAM 和 SOCK_RAW 套接字中可用,详细信息可以参考 arp(7)。

MSG_DONTROUTE

不要使用向网关发送我们的数据包,只发给和我们直接连接的主机。这个通常只有诊断或者路由程序使用。这个只用于支持路由的协议家族,packet 套接字不支持。

MSG_DONTWAIT(Linux 2.2 后)

开启非阻塞操作。如果操作想要阻塞,那么就会返回 EAGAIN 或者 EWOULDBLOCK 错误。这个行为和设置 O_NONBLOCK 标记(通过 fcntl(2) F_SETFL 操作)行为类似,但是 MSG_DONTWAIT 只对当前调用生效,而 O_NONBLOCK 是设置到打开文件描述上(参考 open(2)),这会影响调用进程中的所有线程以及其他持有指向该打开文件描述的文件描述符的进程。

MSG_EOR(Linux 2.2 后)

结束一个记录(当支持该语义时,用于 SOCK_SEQPACKET 类型的套接字)

MSG_MORE(Linux 2.4.4 后)

调用者有更多数据要发送。这个标记用在 TCP 套接字中来获得和 TCP_CORK 套接字选项意向的效果,区别是这个标记只对当前调用生效。

MSG_NOSIGNAL(Linux 2.2 后)

如果对端关闭了流套接字,不要生成 SIGPIPE 信号,不过仍然会返回 EPIPE 错误。这和使用 sigaction(2) 来忽略 SIGPIPE 效果差不多,还是 MSG_NONSIGNAL 只对当前调用生效,并且忽略 SIGPIPE 设置的是进程属性,会影响进程中的所有线程。

MSG_OOB

在支持该带外数据的套接字(比如 SOCK_STREAM)上发送带外数据。 底层协议必须也支持带外数据传输。

MSG_FASTOPEN(Linux 3.7 后)

尝试 TCP 快速打开(RFC7413)并发送在 SYN 中发送数据,就像 connect(2) 和 write(2) 合并一样,进行了隐式的 connect(2) 操作。它会一直阻塞知道数据被缓存并且握手结束。对于一个非阻塞套接字,它会返回缓存数据的大小并且发送一个 SYN 包。如果本地 cookie 不可用,它会返回 EINPROGRESS,并自动发送一个 SYN 带着快速打开 cookie 请求。调用者需要在新连接的套接字上重新发送数据。发生错误时,它会在握手失败时设置和 connect(2) 相同的 errno。这个标记需要 sysctl net.ipv4.tcp_fastopen 来开启 TCP 快速打开客户端支持。

参考 tcp(7) TCP_FASTOPEN_CONNECT 套接字选项来查看一些可选的方法。

sendmsg()

sendmsg() 使用的 msghdr 结构定义如下:

cpp 复制代码
           struct msghdr {
               void         *msg_name;       /* Optional address */
               socklen_t     msg_namelen;    /* Size of address */
               struct iovec *msg_iov;        /* Scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* Ancillary data, see below */
               size_t        msg_controllen; /* Ancillary data buffer len */
               int           msg_flags;      /* Flags (unused) */
           };

msg_name 字段用来指定非连接数据包类型套接字的目标地址,它指向一个包含地址的缓冲区,msg_namelen 字段应该设置为地址的大小。对于连接套接字,这些字段应该对应的设置为 NULL 和 0。

msg_iov 和 msg_ivolen 字段指定了 scatter-gether 区域,和 writev(2) 类似。

我们可以使用 msg_control 和 msg_controllen 成员发送控制信息(辅助数据),内核能够处理套接字最大的控制缓冲区大小由 /proc/sys/net/core/optmem_max 指定,参考 socket(7)。对于其他域套接字关于辅助数据的信息,可以参考 unix(7) 和 ip(7)。

msg_flags 字段忽略。

5.返回值

调用成功时,返回已发送数据的字节数。

发生错误时,返回 -1,并设置errno 来指示错误类型。

下面这些标准错误值是由套接字层生成的,其他错误可能会由底层协议模块产生并返回,具体可以参考对应的手册页。

错误值定义如下:

|--------------------|------------------------------------------------------------------------------------------------------|
| EACCESS | (特定于由路径指定的 UNIX 域套接字)目的套接字写权限被拒绝,或者在目录前缀下的搜索权限被拒绝 |
| EAGAIN/EWOULDBLOCK | 套接字被设置为非阻塞,但是请求操作打算阻塞。POSIX.1-2021 允许随意哪个错误都可以,并且不假定两个值相等,所以移植程序应该对每个值都进行判断。 |
| EAGAIN | (网络域数据报套接字)sockfd 指定的套接字还没有绑定到地址,并且在尝试绑定到临时端口时,临时端口用尽了。可以看 /proc/sys/net/ipv4/ip_local_port_rang 的讨论 |
| EALREADY | 另一个快速打开(Fast Open)正在进行中 |
| EBADF | sockfd 不是一个打开的文件描述 |
| ECONNRESET | 对端重置了连接 |
| EDESTADDRREQ | 套接字不是连接模式,并且没有设置对端地址 |
| EFAULT | 参数中指定了用户空间不合法的地址 |
| EINTR | 数据发送完成前有信号发生,参考 signal(7) |
| EINVAL | 参数不合法 |
| EISCONN | 连接模式的套接字早已经连接过了,但是又指定了接收者。(目前要么返回这个错误,要么直接忽略掉接收者参数) |
| EMSGSIZE | 套接字类型要求消息自动发送,但是消息的大小却使得这个无法完成 |
| ENOBUFS | 网络接口输出队列满了。这个通常指示接口已经停止发送,但可能导致传输堵塞。(正常情况下,这个不会在 Linux 上发生,当设备队列溢出时,数据包会偷偷的被丢掉。) |
| ENOMEM | 没有内存了 |
| ENOTCONN | 套接字没连接,也没有指定目标地址 |
| ENOTSOCK | sockfd 文件描述符没有指向一个套接字 |
| EOPNOTSUPP | flags 里面有些位设置不正确 |
| EPIPE | 面向连接的套接字在本地被关闭,这种情况下,进程也会收到 SIGPIP 信号,除非我们设置了 MSG_NOSIGNAL。 |

6.注意

参考 sendmmsg(2) 来查阅更多 Linux 系统用于一次调用传输多个数据包的信息。

Linux 系统可能返回 EPIPE 而不是 ENOTCONN。

相关推荐
njnu@liyong4 小时前
图解HTTP-HTTP报文
网络协议·计算机网络·http
GISer_Jing5 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
ZachOn1y5 小时前
计算机网络:应用层 —— 应用层概述
计算机网络·http·https·应用层·dns
冰镇屎壳郎7 小时前
计算机网络 八股青春版
计算机网络
嵌入式大圣8 小时前
单片机UDP数据透传
单片机·嵌入式硬件·udp
网络安全King9 小时前
计算机网络基础(2):网络安全/ 网络通信介质
计算机网络·安全·web安全
hkNaruto13 小时前
【P2P】【Go】采用go语言实现udp hole punching 打洞 传输速度测试 ping测试
golang·udp·p2p
言成言成啊15 小时前
TCP与UDP的端口连通性
网络协议·tcp/ip·udp
敲代码娶不了六花15 小时前
对计算机网络中“层”的理解
网络·网络协议·tcp/ip·计算机网络
njnu@liyong1 天前
图解HTTP-HTTP状态码
网络协议·计算机网络·http