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) 来等待各种事件。
I/O 事件 | ||
事件 | 轮询标记 | 发生时机 |
读 | POLLIN | 新数据到达 |
读 | POLLIN | 一个连接配置已完成(对于面向连接的套接字) |
读 | POLLHUP | 对端发起了断开连接请求 |
读 | POLLHUP | 连接断开了(对于面向连接的套接字),当写套接字时,会发送 SIGPIPE 信号 |
写 | POLLOUT | 套接字具有足够的空间来写入新数据 |
读写 | POLLIN/POLLOUT | 向外连接的 connect(2) 完成 |
读写 | POLLERR | 发生了异步错误 |
读写 | POLLHUP | 对端关闭了一个方向的连接 |
异常 | POLLPRI | 紧急数据到达,随后会发送 SIGURG。 |
另一个代替 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 到应用的数据流处理更加高效。
SO_KEEPALIVE
在面向连接的套接字上开启保活消息发送,需要的是一个整型布尔型标记。
SO_LINGER
设置/获取 SO_LINGER 选项,参数是一个 struct linger 结构体:
struct linger {
int l_onoff; /* linger active */
int l_linger; /* how many seconds to linger for */
};
开启后,close(2) 和 shutdown(2) 操作会在所有消息都被发送后才返回,或者达到了存留超时值。否则调用会立即返回,而关闭操作会在后台进行。exit(2) 中关闭套接字时,它总是在后台留存(linger)。
SO_LOCK_FILTER
设置这个选项会阻止修改套接字的过滤器,这些过滤器包括通过 SO_ATTACH_FILTER、SO_ATTACH_BPF、SO_ATTACH_REUSEPORT_CBPF、SO_ATTACH_REUSEPORT_EBPF 设置的。
典型的应用场景是特权进程设置一个原始套接字(一个需要 CAP_NET_RAW 权限的操作)、应用一个限制过滤器、设置 SO_LOCK_FILTER 选项、然后或者放弃特权或者通过 UNIX 域套接字将套接字文件描述符传递给非特权进程。
一旦 SO_LOCK_FILTER 选项开启了,尝试修改或者移除套接字上的过滤器或者禁止 SO_LOCK_FILTER 选项会报错 EPERM。
SO_MARK(Linux 2.6.25 后)
设置由该套接字发送的数据包的标记(mark)(和 netfilter MARK 类似,只是基于套接字的)。修改标记可以用于没有 netfilter 或者分组过滤器的基于标记的路由。设置这个选项要求 CAP_NET_ADMIN 能力。
SO_OOBINLINE
如果这个选项开启,带外数据会直接放入接收数据流中。否则,带外数据只有在 MSG_OOB 标记设置时才会接收时才会这样做。
SO_PASSCRED
使能/禁能接收 SCM_CREDENTIALS 控制消息。更多信息,参考 unix(7)。
SO_PASSSEC
使能/禁能接收 SCM_SECURITY 控制消息。更多信息,参考 unix(7)。
更多选项,阅读下一篇 【计算机网络】网络编程接口 Socket API 解读(9)
5.注意
Linux 假定发送/接受缓冲区的一半用于内部内核结构,因此对应的 /proc 文件大小是线上可观测大小的两倍。
Linux 只有在之前调用 bind(2) 程序和新程序都设置了 SO_REUSEADDR 时才会允许端口重用。这个和其他实现(如 FreeBSD) 只要求新程序设置就好了是不一样的,对于服务器程序总是设置这个选项的情况,这个区别是不可见的。
6.示例代码
下面是一个 getsockopt 函数的使用代码:文章来源:https://www.toymoban.com/news/detail-716268.html
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 */
}
}
文章来源地址https://www.toymoban.com/news/detail-716268.html
到了这里,关于【计算机网络】网络编程接口 Socket API 解读(8)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!