Socket 是网络协议栈暴露给编程人员的 API,相比复杂的计算机网络协议,API 对关键操作和配置数据进行了抽象,简化了程序编程。
本文讲述的 socket 内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解 socket 编程。
socket(7)
1.库
标准 c 库,libc, -lc
2.头文件
<sys/socket.h>
3.接口定义
sockfd = socket(int socket_family, int socket_type, int protocol);
4.接口描述
本文主要描述 Linux 网络套接字层的用户编程接口。 BSD 兼容的套接字是用户进程和内核网络协议栈的统一接口。各协议模块被分配不同的协议家族(AF_INET、AF_IPX、AF_PACKET 等)以及不同的套接字类型(如 SOCK_STREAM、SOCK_DGRAM),参考 socket(2) 获取更多关于协议家族和类型的信息。
套接字层函数
这些套接字层函数是用户进程用来发送和接收数据包以及其他套接字操作。
socket(2) 创建一个套接字,connect(2) 连接一个套接字到一个远程套接字地址,bind(2) 将一个套接字绑定到一个本地套接字地址上,listen(2) 告知套接字有新连接需要被接受,accept(2) 用来获取新来连接的新的套接字,socketpair(2) 返回两个连接的匿名套接字(只有类似 AF_UNIX 的本地套接字才有这个实现)。
send(2)、sendto() 和 sendmsg(2) 在套接字上发送数据,recv(2)、recvfrom、recvmsg(2) 从套接字上接收数据。poll(2) 和 select(2) 等待数据来临或者是否发送数据就绪。此外,标准的 I/O 操作类似 write(2)/writev(2)/sendfile(2)/read(2)/readv(2) 可以用来读写套接字上的数据。
getsockname(2) 返回本地套接字地址,getpeername(2) 返回远程套接字地址。getsockopt(2) 和 setsockopt(2) 用来设置/获取套接字层或者协议层选项。ioctl(2) 可以用来设置或者读取一些其他选项。
close(2) 用来关闭一个套接字。shutdown(2) 关闭全双工套接字双方。
Seeking 或者 pread(2)、pwrite(2) 这种从非 0 位置读写操作套接字是不支持的。
可以通过 fcntl(2) 来设置一个套接字文件描述符的非阻塞标记,实现套接字的非阻塞 I/O 操作。一旦设置,套接字上所有可能导致阻塞状态通常都会返回 EAGAIN(表示操作稍后需要重试),connect(2) 会返回 EINPROGRESS 错误。用户可以通过 poll(2) 或者 select(2) 来等待各种事件。
另一个代替 poll(2)/select(2) 的方式是内核通过 SIGIO 信号通知应用程序,对于这种方式,必须通过 fcntl(2) 来设置套接字文件描述符的 O_ASYNC 标记,然后通过 sigaction(2) 安装 SIGIO 的信号处理函数,可以参考后面关于信号的讨论。
套接字地址结构
每个套接字域(domain)都有自己的套接字地址格式。每个结构以一个整型的“家族”字段(sa_family_t) 来指示地址结构的类型,各种系统调用,比如 connect(2)、bind(2)、accept(2)、getsockname(2)、getpeername(2) 是各个套接字域通用的,可以通过家族和类型来区分不同域的特定套接字地址。
为了允许任何类型的套接字地址都可以传递到各个套接字 API,我们定义了 struct sockaddr,目的是将各域特定的地址类型转换为通用的类型,避免调用套接字 API 时编译器报告类型不匹配警告。
此外,套接字 API 也提供了 struct sockaddr_storage 数据类型。这个类型足以装下所有域特定的套接字地址结构,并且处理了对齐问题。(尤其是它已经能够装下 IPv6 套接字地址。)数据结构包含下面的字段,这个字段可以用来识别结构中实际存储的套接字地址类型:
sa_family_t ss_family;
sockaddr_storage 结构在以通用方式处理套接字地址时非常有用(也就是程序同时处理 IPv4 和 IPv6 套接字地址)。
套接字选项
下面列出的套接字选项可以通过 setsockopt(2) 来设置,也可以通过 getsockopt(2) 设置套接字级别参数为 SOL_SOCKET 来读取这些选项。除非特别说明,否则 optval 是一个指向整型数据的指针。
SO_ACCEPTCONN
返回值指示套接字是否被标记为可以通过 listen(2) 接收连接。返回 0 表示非可监听套接字,返回 1 表示是一个监听套接字。这个选项是只读的。
SO_ATTACH_FILTER(Linux 2.2 后),SO_ATTACH_BPF(Linux 3.19 后)
挂载一个经典的 BPF(SO_ATTACH_FILTER)或者扩展 BPF 程序到套接字上,来过滤进来的数据包。如果程序返回 0,那么数据包会被丢弃,如果返回值比数据包长度小,那么数据包会被截断。如果返回值大于等于数据包长度,那么数据包可以被原封不动的处理。
SO_ATTACH_FILTER 定义在 <linux/filter.h> 中,是一个 sock_fprog 类型的结构体:
struct sock_fprog {
unsigned short len;
struct sock_filter *filter;
};
SO_ATTACH_BPF 的参数是一个通过 bpf(2) 系统调用返回的文件描述符,必须指向一个 BPF_PROG_TYPE_SOCKET_FILTER 类型的程序。
对于指定套接字,这些选项可以设置多次,新的设置会覆盖之前的设置。经典和扩展版本可以在同一个套接字上使用,但是之前的过滤器总是会被新的过滤器代替,也就是说一个套接字上同一时刻只能定义一个过滤器。
经典和扩展 BPF 在 Linux 内核源码文件 /Documentation/networking/filter.txt 中有解释。
SO_ATTACH_REUSEPORT_CBPF,SO_ATTACH_REUSESETPORT_EBPF
在使用 SO_REUSEPORT 选项时,用户可以用这些选项来设置经典 BPF(SO_ATTACH_REUSEPORT_CBPF) 或者扩展 BPF(SO_ATTACH_REUSEPORT_EBPF)程序,这些程序定义了reuseport 端口组中的套接字的数据包如何过滤(也就是所有设置了 SO_REUSEPORT 并使用相同本地地址接收数据包的套接字)。
BPF 程序必须返回一个 0 到 N-1 的索引值表示哪个套接字应该接收数据包(N 是套接字组中套接字的数量)。如果 BPF 程序返回非法索引值,套接字选择会回退到没设置这些选项时的 SO_REUSEPORT 机制。
为了将套接字加入到组中,每个套接字都按照加入的顺序编号(即,UDP 套接字按照 bind(2) 调用的顺序, TCP 套接字按照 listen(2) 调用的顺序)。新加入 reuse 组的套接字会继承 BPF 程序,移除时,最后一个套接字会移动到该套接字位置。
这些选项可以在组内任何套接字上设置多次,来更新组内所有套接字使用的 BPF 程序。
SO_ATTACH_REUSEPORT_CBPF 和 SO_ATTACH_FILTER 携带相同的参数类型,SO_ATTACH_REUSEPORT_EBPF 和 SO_ATTACH_BPF 携带相同的参数类型。
UDP 从 Linux 4.5 后支持这个特性,TCP 是从 Linux 4.6 后支持的。
SO_BINDTODEVICE
将一个套接字绑定到特定的诸如 "eth0" 这样的设备上,在传递的接口名称中指定。如果名字是一个空字符串或者选项长度是 0,那么套接字绑定会被移除。传进来的选项是一个变长、‘\0’ 结尾的接口名称字符串,最大长度为 IFNAMESIZ。如果套接字被绑定到特定接口,那么套接字只会处理该接口进来的数据包。值得注意的是,这个只对特定套接字类型有用,尤其是 AF_INET 套接字。分组(packet)套接字不支持这个特性(使用普通的 bind(2))。
在 Linux 3.8 之前,这个套接字选项可以设置但是不能通过 getsockopt(2) 获取,Linux 3.8 后就可以读了。optlen 参数包含用于接收设备名字的缓冲区大小,建议设置为 IFNAMSIZ 字节,真实的设备名字长度会在 optlen 参数报告出来。
SO_BROADCAST
设置/获取广播标记。开启后,数据报套接字可以向广播地址发送数据包,这个选项对于流套接字无效。
SO_BSDCOMPAT
开启 BSP 错误兼容。这个只在 Linux 2.0 和 2.2 的 UDP 协议模块中使用。如果使能,UDP 套接字的 ICMP 错误不会被传递给用户程序,后面的内核版本中逐步淘汰这个选项。Linux 2.4 悄悄的忽略这个设置,Linux 2.6 会在用户设置这个选项时生成内核警告(printk())。Linux 2.0 对于原始套接字默认开启了这个选项,但是很快就在 Linux 2.2 中就移除了这个设置。
SO_DEBUG
开启套接字调试。只允许具有 CAP_NET_ADMIN 能力、或者有效用户 ID 为 0 的进程设置开启该选项。
SO_DETACH_FILTER(Linux 2.2 后),SO_DETACH_BPF(Linux 3.19 后)
这两个选项意思相同,可以用来移除套接字上使用 SO_ATTACH_FILTER 或者 SO_ATTACH_BPF 绑定的经典/扩展 BDF 程序,选项值会被忽略。
SO_DOMAIN(Linux 2.6.32 后)
获取套接字的域(整数值),返回类似 AF_INET6 这样的值,参考 socket(2) 更多详细信息,这个套接字选项是只读的。
SO_ERROR
获取/清除套接字上的错误。这个套接字选项也是只读的,返回一个整型数值。
SO_DONTROUTE
不要通过网关发送,直接发送到连接的主机。这个和 send(2) 时设置 MSG_DONTROUTE 标记效果相同。期待返回整型布尔标记。
SO_INCOMING_CPU(Linux 3.19 后可读取,Linux 4.4 后可设置)
获取或者设置套接字的 CPU 亲和性,是一个整型标记:
int cpu = 1;
setsockopt(fd, SOL_SOCKET, SO_INCOMING_CPU, &cpu,
sizeof(cpu));
因为一个单独流上的所有数据包都是在一个特定 CPU 上的 RX 队列中到达,这个选项的典型应用是每个 RX 队列使用一个监听进程,然后将监听进程和 RX 队列处理进程处于同一个 CPU 上。这个是 NUMA 上的最佳用法,能够保持 CPU 缓存热度。
SO_INCOMING_NAPI_ID(Linux 4.12 后可读取)
返回一个系统级的唯一 ID,即 NAPI ID,这个 ID 是套接字上最后收到数据包所在 RX 队列的标识。
应用程序可以用这个来对不同 RX 队列上的数据流使用不同的工作线程来进行分流,这就允许每个工作线程和一个 NIC 硬件接收队列关联,为这个上面 RX 队列上接收到的所有连接服务。硬件 NIC 队列到应用线程上的映射使得 NIC 到应用的数据流处理更加高效。
5.注意
Linux 假定发送/接受缓冲区的一半用于内部内核结构,因此对应的 /proc 文件大小是线上可观测大小的两倍
6.示例代码
下面是一个 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 */
}
}