Socket 是网络协议栈暴露给编程人员的 API,相比复杂的计算机网络协议,API 对关键操作和配置数据进行了抽象,简化了程序编程。
本文讲述的 socket 内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解 socket 编程。
send() 遵循 POSIX.1 - 2008
MSG_CONFIRM 是 Linux 扩展
标准 c 库,libc, -lc
sockfd = socket(int socket_family, int socket_type, int protocol);
续 【计算机网络】网络编程接口 Socket API 解读(8)
SO_PEEK_OFF(Linux 3.4 后)
这个选项目前只有 unix(7) 套接字支持,在 recv(2) 系统调用使用了 MSG_PEEK 标记时,可以用它来设置 “peek offset”,即窥探偏移。
当这个选项值设置为负值时(对于所有新套接字,这个值都被设置为了 -1),窥探行为会采用传统行为,即携带 MSG_PEEK 标记的 recv(2) 会从队列头窥探数据。
当这个选项值大于等于 0 时,那么下一次窥探位置就是这个选项指定的偏移地址。同时,窥探偏移会加上之前已经偏移过的值,这样下一次窥探才会返回队列中下一个数据。
如果 recv(2) 或者其他接口没有使用 MSG_PEEK 标记导致数据从队列头移出,那么窥探偏移就会减去这个移出值。 换句话说,不使用 MSG_PEEK 标记会导致窥探偏移调整以保持正确的相对位置,保证后面窥探到的数据和没有移除数据时窥探到的数据相同。
对于数据报套接字,如果窥探偏移指向了一个数据包的中间,那么返回的数据会被标记为 MSG_TRUNC。
下面的例子解释 SO_PEEK_OFF 如何使用,假如流套接字里排队的输入数据如下:
aabbccddeeff
下面的 recv() 调用效果如注释所述:
int ov = 4; // Set peek offset to 4
setsockopt(fd, SOL_SOCKET, SO_PEEK_OFF, &ov, sizeof(ov));
recv(fd, buf, 2, MSG_PEEK); // Peeks "cc"; offset set to 6
recv(fd, buf, 2, MSG_PEEK); // Peeks "dd"; offset set to 8
recv(fd, buf, 2, 0); // Reads "aa"; offset set to 6
recv(fd, buf, 2, MSG_PEEK); // Peeks "ee"; offset set to 8
SO_PEERCRED
返回连接到套接字的对端进程的凭证。更多详细信息,参考 unix(7)。
SO_PEERSEC(Linux 2.6.2 后)
返回连接到套接字的对端进程的安全上下文。更多信息,参考 unix(7)。
SO_PRIORITY
设置套接字上所有要发送数据包协议定义的优先级。Linux 使用这个值来给网络队列排序:具有高优先级的数据包可以被优先处理,依赖于选定设备的排队规则。设置超出 0~6 的优先级需要 CAP_NET_ADMIN 能力。
SO_PROTOCOL(Linux 2.6.32 后)
获得套接字协议值,返回值类似 IPPROTO_SCTP 这样的整型值。参考 socket(2) 获取更多信息。这个套接字选项是只读的。
SO_RCVBUF
设置/获取最大套接字接收缓冲区大小(字节数)。当使用 setsockopt(2) 设置这个值时,内核会使用这个值的两倍(考虑到其他记录结构开销),使用 getsockopt(2) 返回两倍的值。默认值在 /proc/sys/net/core/rmem_default 文件设置,最大值由 /proc/sys/net/core/rmem_max 文件设置。最小值(双倍的)是 256。
SO_RCVBUFFORCE(Linux 2.6.14 后)
具备 CAP_NET_ADMIN 能力的特权进程可以通过这个选项进行和 SO_RCVBUF 类似的设置,但是这个可以设置 rmem_max 最大限制值。
SO_RCVLOWAT 和 SO_SNDLOWAT
设置套接字层将数据交由底层协议的最小缓冲量(SO_SNDLOWAT)以及用户接收到数据的最小值(SO_RCVLOWAT)。这两个值初始化为 1,SO_SNDLOWAT 在 Linux 上不可改(setsockopt(2) 会返回 ENOPROTOOPT 错误),SO_RCVLOWAT 在 Linux 2.4 后可以修改。
Linux 2.6.28 之前,Linux 上 select(2)/poll(2)/epoll(7) 不看 SO_RCVLOWAT 设置,即使有一个字节也认为套接字是读就绪的,接下来从套接字读取会阻塞直到 SO_RCVLOWAT 字节可用。Linux 2.6.28 后,这几个接口只有在字节达到 SO_RCVLOWAT 值时才会套接字标记为读就绪。
SO_RCVTIMEO 和 SO_SNDTIMEO
指定接收和发送超时值,参数是 struct timeval 类型。如果一个输入/输出函数阻塞这么长时间,并且发送或者接收了一部分数据,那么会返回已传输数据量,如果没有传输任何数据,那么会返回 -1 并设置 errno 为 EAGAIN 或者 EWOULDBLOCK,或者套接字设置为非阻塞时返回 EINPROGRESS(只对 connect(2) 有效)。如果超时值设置为 0(默认值),那么对应操作永不超时。超时只对进行 I/O 操作的系统调用生效(比如 accept(2)、connect(2)、read(2)、recvmsg(2)、send(2)、sendmsg(2)),对于 select(2)、poll(2)、epoll_wait(2) 等无效。
SO_REUSEADDR
表示 bind(2) 中用于验证地址有效性的规则允许本地地址重用。对于 AF_INET 套接字,套接字可以绑定到正在被监听的本地地址外的任何地址。当监听套接字绑定了到 INADDR_ANY 并指定了端口,那么任何地址上都不能再绑定该端口。参数是一个整型布尔标记。
SO_REUSEPORT(Linux 3.9 后)
允许多个 AF_INET 或者 AF_INET6 套接字绑定到相同的套接字地址上。这个选项必须在 bind(2) 前设置到每个套接字上(包括第一个套接字)。为了防止端口劫持,绑定到相同地址的所有进程必须具有相同的有效 UID。这个选项可以用于 TCP 和 UDP 套接字上。
对于 TCP 套接字,这个选项允许 accept(2) 在多线程服务器上进行负载分配,每个线程使用一个不同的监听套接字。这种方式比传统方式高效很多,比如使用一个单线程进行 accept(2) 分配连接,或者有多个线程在同一个套接字上进行 accept(2) 竞争。
对于 UDP 套接字,比起传统的多个进程在同一个套接字上竞争接收数据报,这种方式能够允许多进程(线程)对过来的数据报进行更好的负载分配。
SO_RXQ_OVFL(Linux 2.6.33 后)
表示需要携带一个 32 位的辅助消息来接收 skbs,skbs 是自从套接字创建以来丢包总数。
SO_SELECT_ERR_QUEQUE(Linux 3.10 后)
设置这个选项的套接字,套接字上的错误不仅仅会通过 select(2) 的 exceptfds 通知,同样 poll(2) 也会在返回 POLLERR 事件时,返回一个 POLLPRI 错误。
背景:这个选项就是为了不同在 select(2) 的 readfdf、writefds 上监听错误,而是使用 exceptfds 参数来监听,这样实现很好的解耦。Linux 4.16 后,这个选项就没有什么意义了,尽管如此,为了向后兼容,该选项还是保留了。
SO_SNDBUF
设置/获取最大套接字发送缓冲器大小。当使用 setsockopt(2) 设置这个缓冲区大小时,内核会将这个值翻倍,并且后续通过 getsockopt(2) 返回的就是这个翻倍的值。默认值通过文件 /proc/sys/net/core/wmem_default 设置,最大值通过 /proc/sys/net/core/wmem_max 文件设置,最小值是 2048。
SO_SNDBUFFORCE(Linux 2.6.14 后)
特权进程(具有 CAT_NET_ADMIN 权限)可以使用这个选项设置发送缓冲区大小,但是可以覆盖 wmem_max 的值。
SO_TIMESTAMP
使能/禁能接收 SO_TIMESTAMP 控制消息。时间戳控制信息是通过 SOL_SOCKET 级别和 cmsg_type 类型的 SCM_TIMESTAMP 发送的,cmsg_data 字段是一个 struct timeval 数值表示传递给用户调用最后一个数据包的时间。参考 cmsg(3) 获得更多控制消息的详细信息。
SO_TIMESTAMPNS
使能/禁能接收 SO_TIMESTAMPNS 控制消息。时间戳控制信息是通过 SOL_SOCKET 级别和 cmsg_type 类型的 SCM_TIMESTAMPNS 发送的,cmsg_data 字段是一个 struct timespec 数值表示传递给用户调用最后一个数据包的时间。时间戳采用的是 CLOCK_REALTIME 时钟。参考 cmsg(3) 获得更多控制消息的详细信息。
套接字不能混用 SO_TIMESTAMP 和 SO_TIMESTAMPNS:这两种模式互斥。
SO_TYPE
获取套接字类型,套接字类型是一个类似 SOCK_STREAM 这样的整型值,这是一个只读选项。
SO_BUSY_POLL(Linux 3.11 后)
在阻塞读取而没有数据时,设置一个大概的微秒级的忙轮询时间。增加这个值需要 CAP_NET_ADMIN 权限,默认值由 /proc/sys/net/core/busy_read 文件控制。
这个文件中的值决定了 select(2) 和 poll(2) 在设置了 SO_BUSY_POLL 的套接字上的忙轮询时间。
在这两种情况下,忙轮询只有在接收到数据的套接字支持这个选项才会生效。
虽然忙轮询会提升一些应用的延迟,但是使用时需要注意,因为这个会增加 CPU 利用率和功耗。
信号
向一个已经关闭(本地关闭或者对端关闭)的套接字里写数据会收到 SIGPIPE 信号,并会返回 EPIPE 错误码。如果写调用指明了 MSG_NOSIGNAL 标记的话,那么就不会发送这个信号。
当使用 fcntl(2) FIOSETOWN 或者 ioctl(2) SIOCSPGRP 请求时,一旦发生 I/O 事件,就会发送 SIGIO 信号。可以在信号处理函数中使用 poll(2) 或者 select(2) 来查看是哪个套接字上发生的事件。另一个可选(Linux 2.2)是通过 F_SETSIG fcntl(2) 设置实时信号,实时信号的处理函数会在 siginfo_t 的 si_fd 字段携带套接字的文件描述符。更多信息,参考 fcntl(2)。
在特定情况下(多进程访问同一个套接字),当进程响应信号时,触发 SIGIO 的条件可能早就消失了。如果发生这种情况,进程需要继续等待,因为 Linux 会重新发送这个信号。
/proc 接口
核心套接字网络参数可以通过 /proc/sys/net/core 访问。
rmem_default
包含套接字接收缓冲区默认大小设置
rmem_max
包含用户通过 SO_RCVBUF 选项设置的最大接收缓冲区字节数
wmem_default
包含套接字发送缓冲区默认设置大小
wmem_max
包含用户通过 SO_SNDBUF 选项设置的最大发送缓冲区字节数
message_cost 和 message_burst
配置令牌桶过滤器,用来加载外部网络事件导致的限制警告信息
netdev_max_backlog
全局输入队列的最大数据包数量
optmem_max
辅助数据和用户控制数据(比如每个套接字的 iovecs)的最大长度
Ioctls
上述有些操作可以通过 ioctl(2) 来访问:
error = ioctl(ip_socket, ioctl_type, &value_result);
SIOCGSTAMP
给用户返回最近接收数据包的接收时间戳(struct timeval)。这个对于精确测量往返传输时间 RTT 非常有用。参考 setitimer(2) 查看关于 struct timval 的描述。这个 ioctl 只能在没有设置 SO_TIMSTAMP 和 SO_TIMESTAMPNS 的套接字上使用。否则就会返回没有设置 SO_TIMESTAMP 和 SO_TIMESTAMPNS 时的最后一个数据包的时间戳或者没有收到这样数据包时报错(即, ioctl(2) 返回 -1 并设置 errno 为 ENOENT)。
SIOCSPGRP
设置 I/O 可用或者紧急数据可用时处理 SIGIO/SIGURG 信号的进程或者进程组号。这个参数是一个指向 pid_t 类型的指针。更多信息,参考 fcntl(2) 中关于 F_SETOWN 的描述。
FIOASYNC
修改 O_ASYNC 标记来开启/禁止套接字的异步 I/O 模式。异步 I/O 模式当有新的 I/O 事件发生时会触发 SIGIO 或者 F_SETSIG 设置的信号。
参数是一个布尔标记值。(这个操作在 fcntl(2) 设置了 O_ASYNC 标记时是同步的。)
SIOCGPGRP
获取当前接收 SIGIO/SIGURG 信号的进程/进程组,没有设置时返回 0。
可用的 fcntl(2) 操作:
FIOGETOWN
和 ioctl(2) SIOCGPGRP 相同。
FIOSETOWN
和 ioctl(2) SIOCSPGRP 相同。
Linux 假定收发缓冲区的一半用于内部内核结构,也就是说 /proc 文件中的值是实际值的两倍。
Linux 只有在执行 bind(2) 程序和重用端口程序都设置了 SO_REUSEADDR 选项时才允许端口重用。这个和其他实现不同(比如 FreeBSD),其他实现只要求后面的程序设置 SO_REUSEADDR 即可。这点差别通常情况下是不可见的,因为服务器程序通常会设计为设置这个选项。
下面是一个 getsockopt 函数的使用代码:
int rc;
int s;
int option_value;
int option_len;
struct linger l;
int getsockopt(int s, int level, int option_name,
char *option_value,
int *option_len);
⋮
/* Is out-of-band data in the normal input queue? */
option_len = sizeof(int);
rc = getsockopt(
s, SOL_SOCKET, SO_OOBINLINE, (
char *) &option_value, &option_len);
if (rc == 0)
{
if (option_len == sizeof(int))
{
if (option_value)
/* yes it is in the normal queue */
else
/* no it is not
*/
}
}
⋮
/* Do I linger on close? */
option_len = sizeof(l);
rc = getsockopt(
s, SOL_SOCKET, SO_LINGER, (char *) &l, &option_len);
if (rc == 0)
{
if (option_len == sizeof(l))
{
if (l.l_onoff)
/* yes I linger */
else
/* no I do not */
}
}