目录
一、socket 地址 API
1. 主机字节序和网络字节序
现代 CPU 的累加器一次都能装载(至少)4 字节(下面均考虑 32 位机),即一个整数。那么这 4 字节在内存中排列的顺序将影响它被累加器装载成的整数的值。这就是字节序问题。字节序分为大端字节序(big endian)和小端字节序(little endian)。
- 大端字节序:指一个整数的高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处;
- 小端字节序:指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。
现在 PC 大多采用小端字节序,因此小端字节序又被称为主机字节序。当格式化的数据(比如 32 bit 整型数和 16 bit 短整型数)在两台使用不同字节序的主机之间直接传递时,接收端必然错误地解释之。解决问题的方法是:发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。因此大端字节序也称为网络字节序,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证。
需要指出的是,即使是同一台机器上的两个进程(比如一个由 C 语言编写,另一个由 JAVA 编写)通信,也要考虑字节序的问题(JAVA虚拟机采用大端字节序)。
Linux 提供了 4 个函数来完成主机字节序和网络字节序之间的转换:
2. 通用 socket 地址
socket 网络编程接口中表示 socket 地址是结构体 sockaddr :
地址族类型通常与协议族类型对应,常见的协议族(protocol family,domain)和对应的地址族关系如下:
协议族 | 地址族 | 描述 |
PF_UNIX | AFUNIX | UNIX 本地域协议族 |
PF_INET | AF_INET | TCP/IPv4 协议族 |
PF_INET6 | AF_INET6 | TCP/IPv6 协议族 |
宏
PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,前后者的值完全相同,因此二者可以混用。sa_data 成员用于存放 socket 地址值,但是不同的协议族的地址值具有不同的含义和长度:协议族 | 地址值含义和长度 |
PF_UNIX | 文件的路径名,长度可达到 108 字节 |
PF_INET | 16 bit 端口号和 32 bit IPv4 地址,共 6 字节 |
PF_INET6 | 16 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID ,共 26 字节 |
由上表可知 14 字节的 sa_data 根本无法容纳多数协议族的地址,因此 Linux 定义了新的通用 socket 地址结构体:
3. 专用 socket 地址
上面两个通用的 socket 地址结构体显然很不好用,比如设置与获取 IP 地址和端口号就需要执行繁琐的位操作,所以 Linux 为各个协议族提供了专门的 socket 地址结构体:
- UNIX 本地域协议族:
- TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体:
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转换即可),因为所有 socket 编程接口使用的地址参数的类型都是 sockaddr 。
4. IP 地址转换函数
在计算机中使用 IP 地址,需要转换为二进制;而在记录日志中,则需要转换为供人可读的字符串:
二、创建 socket
在 Linux 中,所有东西都是文件,socket 本质上就是可读、可写、可控制、可关闭的文件描述符:
- domain:表示系统使用哪个底层协议族,对于 TCP/IP 里说,该参数应为
PF_INET;
- type:指定服务类型,对 TCP/IP 协议族而言
- SOCK_STREAM 表示传输层使用 TCP 协议;
- SOCK_DGRAM 表示传输层使用 UDP 协议;
- 自 2.6.17 内核版本起,该参数可接受 SOCK_NONBLOCK 和 SOCK_CLOEXEC ,分别表示将新创建的 socket 设为非阻塞的,以及用 fork 调用创建子程序时在子进程中关闭该 socket ;在该版本之前,文件描述符的这两个属性都需要使用额外的系统调用(fcntl)来设置。
- SOCK_STREAM 表示传输层使用 TCP 协议;
- SOCK_DGRAM 表示传输层使用 UDP 协议;
- 自 2.6.17 内核版本起,该参数可接受 SOCK_NONBLOCK 和 SOCK_CLOEXEC ,分别表示将新创建的 socket 设为非阻塞的,以及用 fork 调用创建子程序时在子进程中关闭该 socket ;在该版本之前,文件描述符的这两个属性都需要使用额外的系统调用(fcntl)来设置。
- protocol:选择一个具体的协议,不过这个值通常是唯一的,设为 0 ;
- socket 系统调用成功时返回一个 socket 文件描述符,失败时返回 -1 并设置 errno 。
三、命名 socket
创建 socket 时,我们给它制定了地址族,但是并未指定使用该地址族中的哪个具体 socket 地址。将一个 socket 与 socket 地址绑定称为给 socket 命名。在服务器程序中,我们通常要命名 socket ,因为只有命名后客户端才能知道该如何连接它。客户端则通常不需要命名 socket ,而是采用匿名方式,即使用操作系统自动分配的 socket 地址,系统调用如下:
- bind 将 my_addr 所指的 socket 地址分配给未命名的 sockfd 文件描述符,addrlen 参数指出该 socket 地址的长度;
- bing 成功时返回 0 ,失败时返回 -1 并设置 errno ,其中常见的两种 errno 如下:
- EACCES:被绑定的地址是受保护的地址,仅超级用户能够访问;
- EADDRINUSE:被绑定的地址正在使用中。
- EACCES:被绑定的地址是受保护的地址,仅超级用户能够访问;
- EADDRINUSE:被绑定的地址正在使用中。
四、监听 socket
socket 被命名之后,还不能马上接收客户连接,需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接:
- sockfd:指定被监听的 socket ;
- backlog:提示内核监听队列的最大长度,如果超过 backlog ,服务器将不受理新的客户连接,客户端收到 ECONNREFUSED 错误信息,在内核版本 2.2 之前,backlog 参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的 socket 的上限。之后的版本则表示处于完全连接状态的 socket 上限。处于半连接状态的 socket 上限由
/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。backlog 参数的典型值是 5 ,listen 成功时返回 0 ,失败时返回 -1 并设置 errno 。
下面编写程序来研究 backlog 对 listen 系统调用的实际影响,首先将以下代码运行在服务器端:
该服务器程序接收 3 各参数:IP 地址、端口号、backlog 值,此时用客户端多次执行 telnet 命令连接服务器,每建立一个连接就使用 netstat 命令查看服务器连接状态:
此时输出如下:
可以发现最多只能有 backlog + 1 个完整连接,说明监听队列中完整连接的上限通常比 backlog 大。
五、接收连接
- sockfd:执行过 listen 系统调用的监听 socket ;
- addr:用来获取被接收连接的远端 socket 地址,该地址的长度由 addrlen 指出;
- 接收成功时返回一个新的连接 socket ,它唯一地标识了被接受的这个连接,服务器可通过读写该 socket 来与被接受连接对应的客户端通信;失败时返回 -1 并设置 errno 。
需要注意的是,若再连接过程中客户端出线程序崩溃、网络掉线等问题,服务器端 accept 调用都能正常返回,说明服务器端 accept 只是从监听队列中取出连接,而不论连接处于什么状态,更不关心任何网络状况的变化。
六、发起连接
如果说服务器通过 listen 调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接:
- sockfd:由 socket 系统调用返回一个 socket ;
- serv_addr:是服务器监听的 socket 地址,addrlen 参数则指定这个地址的长度;
- 连接成功时返回 0 ,一旦连接建立,sockfd 就唯一地标识了这个连接,客户端就可以通过读写 sockfd 来与服务器通信;失败时返回 -1 并设置 errno :
- ECONNREFUSED:目标端口不存在,连接被拒绝;
- ETIMEDOUT:连接超时。
- ECONNREFUSED:目标端口不存在,连接被拒绝;
- ETIMEDOUT:连接超时。
七、关闭连接
关闭一个连接实际上就是关闭该连接对应的 socket ,这可以通过如下关闭普通文件描述符的系统调用来完成:
- fd:待关闭的 socket ,但是 close 系统调用并非总是立即关闭一个连接,而是将 fd 的引用计数减一,只有当计数为 0 时才真正关闭。多进程程序中,一次 fork 系统调用默认将使父进程中打开的 socket 的引用计数加一,因此必须在父进程和子进程中都对该 socket 执行 close 调用才能关闭连接。
相对的,还提供了强制终止连接:
- socket:待关闭的 socket ;
- howto:决定了关闭的行为:
可选值 | 含义 |
SHUY_RD | 关闭 sockfd 上读的这一半,应用程序不能再针对 socket 文件描述符执行读操作,并且该 socket 接收缓冲区中的数据都被丢弃 |
SHUT_WR | 关闭 sockfd 上写的这一半,sockfd 的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该 socket 文件描述符执行写操作,这种情况下连接处于半关闭状态 |
SHUT_RDWR | 同时关闭读写 |
可选值 | 含义 |
SHUY_RD | 关闭 sockfd 上读的这一半,应用程序不能再针对 socket 文件描述符执行读操作,并且该 socket 接收缓冲区中的数据都被丢弃 |
SHUT_WR | 关闭 sockfd 上写的这一半,sockfd 的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该 socket 文件描述符执行写操作,这种情况下连接处于半关闭状态 |
SHUT_RDWR | 同时关闭读写 |
- 关闭成功后返回 0 ,失败时返回 -1 并设置 errno 。
八、数据读写
1. TCP 数据读写
对文件的读写操作 read 和 write 同样适用于 socket 。但是 socket 编程接口提供了几个专门用于 socket 数据读写的系统调用,它们增加了对数据读写的控制。其中用于 TCP 流数据读写的系统调用是:
- recv:读取 sockfd 上的数据,buf 和 len 参数分别指定读缓冲区的位置和大小,flags 通常设为 0 ;读取成功时返回实际读取到的数据的长度,可能小于 len ,因此可能要多次调用 recv 才能读取到完整的数据;若返回值为 0 表示通信对方已经关闭连接;读取出错时返回 -1 并设置 errno ;
- send:向 sockfd 写数据,buf 和 len 参数分别指定写缓冲区的位置和大小。写成功时返回实际写入的数据的长度,失败时返回 -1 并设置 errno ;
- flags:为数据收发提供了额外的控制:
选项名 | 含义 | send | recv |
MSG_CONFIRM | 指示数据链路层协议持续监听对方的回应,直到得到答复。仅用于 SOCK_DGRAM 和 SOCK_RAW 类型的 socket | Y | N |
MSG_DONTROUTE | 不查看路由表,直接将数据发送给本地局域网络内的主机。这表示发送者确切地直到目标主机就在本地网络上 | Y | N |
MSG_DONTWAIT | 对 socket 的此次操作将是非阻塞的 | Y | Y |
MSG_MORE | 告诉内核程序还有更多数据要发送,内核将超时等待新数据写入 TCP 发送缓存区后一并发送,这样可以防止 TCP 发送过多小的报文段,从而提高传输效率 | Y | N |
MSG_WAITALL | 读操作仅在读取到指定数量的字节后才返回 | N | Y |
MSG_PEEK | 窥探读缓存中的数据,此次读操作不会导致这些数据被清除 | N | Y |
MSG_OOB | 发送或接受紧急数据 | Y | Y |
MSG_NOSIGNAL | 往读端关闭的管道或者 socket 连接中写数据时不引发 SIGPIPE 信号 | Y | N |
选项名 | 含义 | send | recv |
MSG_CONFIRM | 指示数据链路层协议持续监听对方的回应,直到得到答复。仅用于 SOCK_DGRAM 和 SOCK_RAW 类型的 socket | Y | N |
MSG_DONTROUTE | 不查看路由表,直接将数据发送给本地局域网络内的主机。这表示发送者确切地直到目标主机就在本地网络上 | Y | N |
MSG_DONTWAIT | 对 socket 的此次操作将是非阻塞的 | Y | Y |
MSG_MORE | 告诉内核程序还有更多数据要发送,内核将超时等待新数据写入 TCP 发送缓存区后一并发送,这样可以防止 TCP 发送过多小的报文段,从而提高传输效率 | Y | N |
MSG_WAITALL | 读操作仅在读取到指定数量的字节后才返回 | N | Y |
MSG_PEEK | 窥探读缓存中的数据,此次读操作不会导致这些数据被清除 | N | Y |
MSG_OOB | 发送或接受紧急数据 | Y | Y |
MSG_NOSIGNAL | 往读端关闭的管道或者 socket 连接中写数据时不引发 SIGPIPE 信号 | Y | N |
2. UDP 数据读写
- recvfrom:读取 sockfd 上的数据:
- buf:指定读缓冲区的位置;
- len:指定读缓冲区的大小;
- src_addr:由于 UDP 是无连接的,因此每次发送都需要获取发送端的地址;
- addrlen:指定地址的长度。
- buf:指定读缓冲区的位置;
- len:指定读缓冲区的大小;
- src_addr:由于 UDP 是无连接的,因此每次发送都需要获取发送端的地址;
- addrlen:指定地址的长度。
- sendto:向 sockfd 写入数据:
- buf:指定写缓冲区的位置;
- len:指定写缓冲区的大小;
- dest_addr:指定接收端的 socket 地址;
- addrlen:指定地址长度。
- buf:指定写缓冲区的位置;
- len:指定写缓冲区的大小;
- dest_addr:指定接收端的 socket 地址;
- addrlen:指定地址长度。
- flags 和返回值:与 TCP 相同;
- 特殊:两个系统调用均可用于面向连接的 socket 数据读写,只需要将最后两个参数设置为 NULL 即可。
3. 通用数据读写函数
下面的接口可用于 TCP 和 UDP :
- sockfd:指定被操作的目标 socket ;
- msg:结构体指针:
- msg_name:指向一个 socket 地址结构变量,指示通信双方的地址。对于 TCP 无意义,被置为 NULL;
- msg_namelen:指定了 msg_name 所指 socket 地址的长度;
- msg_iov:iovec 结构体指针;
- msg_iovlen:表示这样的指针有多少个。对于 recvmsg 来说,数据将被存放在 msg_iovlen 块分散的内存中,这些内存的位置和长度由 msg_iov 指向的数组指定,称为分散读(scatter read);对于 sendmsg 来说,msg_iovlen 块分散内存中的数据将被一并发送,称为集中写(gather writer);
- msg_control 和 msg_controllen:用于辅助数据的传送;
- msg_flags:无需设定,它会复制 recvmsg/sendmsg 的 flags 参数的内容以影响数据读写的过程。前者还会在调用结束前将某些更新后的标志设置到 msg_flags 中;
- flags 和返回值:与 TCP 相同;
- msg_name:指向一个 socket 地址结构变量,指示通信双方的地址。对于 TCP 无意义,被置为 NULL;
- msg_namelen:指定了 msg_name 所指 socket 地址的长度;
- msg_iov:iovec 结构体指针;
- msg_iovlen:表示这样的指针有多少个。对于 recvmsg 来说,数据将被存放在 msg_iovlen 块分散的内存中,这些内存的位置和长度由 msg_iov 指向的数组指定,称为分散读(scatter read);对于 sendmsg 来说,msg_iovlen 块分散内存中的数据将被一并发送,称为集中写(gather writer);
- msg_control 和 msg_controllen:用于辅助数据的传送;
- msg_flags:无需设定,它会复制 recvmsg/sendmsg 的 flags 参数的内容以影响数据读写的过程。前者还会在调用结束前将某些更新后的标志设置到 msg_flags 中;
- flags 和返回值:与 TCP 相同;
九、带外标记
实际应用中,我们通常无法预期带外数据(紧急数据)何时到来。好在 Linux 内核检测到 TCP 紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是:
- I/O 复用产生的异常事件;
- SIGURG 信号。
但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这一 点可通过如下系统调用实现:
用于判断 sockfd 是否处于带外标记,即下一个被读取到的数据是否是带外数据,如果是则返回 1 ,此时就可以利用带 MSG_OOB 标志的 recv 调用来接受带外数据;如果不是则返回 0 。
十、地址信息函数
在某些情况下,我们想知道一个连接 socket 的本端 socket 地址,以及远端的 socket 地址:
- getsockname:获取 sockfd 对应的本端 socket 地址,并将其存储与 address 参数指定的内存中,该地址的长度存储与 address_len 参数指向的变量中,如果实际地址长度大于内存区大小,那么将会被截断。成功 0 失败 -1 并设置 errno ;
- getpeername:获取 sockfd 对应的远端 socket 地址,参数与本端相同。
十一、socket 选项
如果说 fcntl 系统调用是控制文件描述符属性的通用 POSIX 方法,那么下面两个系统调用则是专门用来读取和设置 socket 文件描述符属性的方法:
- sockfd:指定被操作的目标 socket ;
- level:指定要操作哪个协议的选项(即属性),比如 IPv4 、IPv6 、TCP 等;
- option_name:指定选项的名字;
- option_value 和 option_len:分别是被操作选项的值和长度。不同的选项具有不同类型的值:
level | option name | 数据类型 | 说明 |
SOL_SOCKET(通用 socket 选项,与协议无关) | SO_DEBUG | int | 打开调试信息 |
ㅤ | SO_REUSEADDR | int | 重用本地地址 |
ㅤ | SO_TYPE | int | 获取 socket 类型 |
ㅤ | SO_ERROR | int | 获取并清除 socket 错误状态 |
ㅤ | SO_DONTROUTE | int | 不查看路由表,直接将数据发送给本地局域网内的主机 |
ㅤ | SO_RCVBUF | int | TCP 接收缓冲区大小 |
ㅤ | SO_SNDBUF | int | TCP 发送缓冲区大小 |
ㅤ | SO_KEEPALIVE | int | 发送周期性保活报文以维持连接 |
ㅤ | SO_OOBINLINE | int | 接收到的带外数据将存留在普通数据的输入队列中(在线留存),此时不能使用带 MSG_OOB 标志的读操作来读取带外数据,而应像普通数据那样读取 |
ㅤ | SO_LINGER | linger | 若有数据待发送,则延迟关闭 |
ㅤ | SO_RCVLOWAT | int | TCP 接收缓存区低水位标记 |
ㅤ | SO_SNDLOWAT | int | TCP 发送缓冲区低水位标记 |
ㅤ | SO_RCVTIMEO | timeval | 接收数据超时 |
ㅤ | SO_SNDTIMEO | timeval | 发送数据超时 |
IPPROTO_IP(IPv4 选项) | IP_TOS | int | 服务类型 |
ㅤ | IP_TTL | int | 存活时间 |
IPPROTO_IPv6(IPv6 选项) | IPV6_NEXTHOP | sockaddr_in6 | 下一跳 IP 地址 |
ㅤ | IPV6_RECVPKTINFO | int | 接受分组信息 |
ㅤ | IPV6_DONTFRAG | int | 禁止分片 |
ㅤ | IPV6_RECVTCLASS | int | 接受通信类型 |
IPPROTO_TCP(TCP 选项) | TCP_MAXSEG | int | TCP 最大报文段大小 |
ㅤ | TCP_NODELAY | int | 禁止 Nagle 算法 |
level | option name | 数据类型 | 说明 |
SOL_SOCKET(通用 socket 选项,与协议无关) | SO_DEBUG | int | 打开调试信息 |
ㅤ | SO_REUSEADDR | int | 重用本地地址 |
ㅤ | SO_TYPE | int | 获取 socket 类型 |
ㅤ | SO_ERROR | int | 获取并清除 socket 错误状态 |
ㅤ | SO_DONTROUTE | int | 不查看路由表,直接将数据发送给本地局域网内的主机 |
ㅤ | SO_RCVBUF | int | TCP 接收缓冲区大小 |
ㅤ | SO_SNDBUF | int | TCP 发送缓冲区大小 |
ㅤ | SO_KEEPALIVE | int | 发送周期性保活报文以维持连接 |
ㅤ | SO_OOBINLINE | int | 接收到的带外数据将存留在普通数据的输入队列中(在线留存),此时不能使用带 MSG_OOB 标志的读操作来读取带外数据,而应像普通数据那样读取 |
ㅤ | SO_LINGER | linger | 若有数据待发送,则延迟关闭 |
ㅤ | SO_RCVLOWAT | int | TCP 接收缓存区低水位标记 |
ㅤ | SO_SNDLOWAT | int | TCP 发送缓冲区低水位标记 |
ㅤ | SO_RCVTIMEO | timeval | 接收数据超时 |
ㅤ | SO_SNDTIMEO | timeval | 发送数据超时 |
IPPROTO_IP(IPv4 选项) | IP_TOS | int | 服务类型 |
ㅤ | IP_TTL | int | 存活时间 |
IPPROTO_IPv6(IPv6 选项) | IPV6_NEXTHOP | sockaddr_in6 | 下一跳 IP 地址 |
ㅤ | IPV6_RECVPKTINFO | int | 接受分组信息 |
ㅤ | IPV6_DONTFRAG | int | 禁止分片 |
ㅤ | IPV6_RECVTCLASS | int | 接受通信类型 |
IPPROTO_TCP(TCP 选项) | TCP_MAXSEG | int | TCP 最大报文段大小 |
ㅤ | TCP_NODELAY | int | 禁止 Nagle 算法 |
- 成功 0 失败 -1 并设置 errno 。
值得指出的是,对服务器而言,有部分 socket 选项只能在调用 listen 系统调用前针对监听 socket 设置才有效。这是因为连接 socket 只能由 accept 调用返回,而 accept 从 listen 监听队列中接受的连接至少已经完了 TCP 三次握手的前两个步骤(因为 listen 监听队列中的连接至少已进入 SYN_RCVD 状态),这说明服务器已经往被接受连接上发送出了 TCP 同步报文段。但有的 socket 选项却应该在 TCP 同步报文段中设置,比如 TCP 最大报文段选项。对这种情况,Linux 给开发人员提供的解决方案是:对监听 socket 设置这些 socket 选项,那么 accept 返回的连接 socket 将自动继承这些选项。这些 socket 选项包括:
- SO_DEBUG ;
- SO_DONTROUTE ;
- SO_KEEPALIVE ;
- SO_LINGER ;
- SO_OOBINLINE ;
- SO_RCVBUF ;
- SO_RCVLOWAT ;
- SO_SNDBUF ;
- SO_SNDLOWAT ;
- TCP_MAXSEG ;
- TCP_NODELAY 。
而对客户端而言,这些 socket 选项则应该在调用 connect 函数之前设置,因为 connect 调用成功返回之后,TCP 三次握手已完成。
1. SO_REUSEADDR 选项
通过选项 SO_REUSEADDR ,即使 sock 处于 TIME_WAIT 状态,与之绑定的 socket 地址也可以立即被重用,此外还可以设置
/proc/sys/net/ipv4/tcp_tw_recycle 来快速回收被关闭的 socket ,从而使得 TCP 连接根本就不进入 TIME_WAIT 状态,进而允许应用程序立即重用本栋 socket 地址。2. OS_RCVBUF 和 SO_SNDBUF 选项
SO_RCVBUF 和 SO_SNDBUF 选项分别表示 TCP 接收缓冲区和发送缓冲区的大小。不过在使用 setsockopt 来设置 TCP 的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。TCP 接收缓冲区的最小值是 256 字节,而发送缓冲区的最小值是 2048 字节(不同系统可能有不同默认最小值)。系统这样做的目的主要是确保一个 TCP 连接拥有足够的空闲缓冲区来处理拥塞 (比如快速重传算法就期望 TCP 接收缓冲区能至少容纳 4 个大小为 SMSS 的 TCP 报文段)。
可以直接修改内核数
/proc/sys/net/ipv4/tcp_rmem 和 /proc/sys/net/ipv4/tcp_wmem 来强制 TCP 接收缓冲区和发送缓冲区的大小没有最小值限制。3. SO_RCVLOW AT 和 SO_SNDLOWAT 选项
SO_RCVLOWAT 和 SO_SNDLOWAT 选项分别表示 TCP 接收缓冲区和发送缓冲区的低水位标记。它们一般被 I/O 复用系统调用来判断 socket 是否可读或可写。当 TCP 接收缓冲区中可读数据的总数大于其低水位标记时,I/O 复用系统调用将通知应用程序可以从对应的 socket 上读取数据;当 TCP 发送缓冲区中的空闲空间(可以写入数据的空间)大于其低水位标记时, I/O 复用系统调用将通知应用程序可以往对应的 socke 上写入数据。默认情况下,TCP 接收缓冲区的低水位标记和 TCP 发送缓冲区的低水位标记均为 1 字节。
4. SO_LINGER 选项
SO_LINGER 选项用于控制 close 系统调用在关闭 TCP 连接时的行为。默认情况下,当我们使用 close 系统调用来关闭一个 socket 时,close 将立即返回,TCP 模块负责把该 socket 对应的 TCP 发送缓冲区中残留的数据发送给对方。
设置(获取)SO_LINGER 选项的值时,我们需要给 setsockopt(getsockopt)系统调用传递一个 linger 类型的结构体,其定 义如下:
根据结构体中两个成员变量的不同值,可能产生三种行为:
- l_onoff = 0:此时 SO_LINGER 不起作用,close 用默认行为进行关闭;
- l_onoff != 0:此时 close 系统调用立即返回,TCP 模块将丢弃被关闭的 socket 对应的 TCP 发送缓冲区中残留的数据,同时给对方发送一个复位报文段。因此这种情况给服务器提供了异常终止一个连接的方法;
- l_onoff != 0 ,L_linger > 0:此时 close 的行为取决于两个条件:
- 被关闭的 socket 对应的 TCP 发送缓冲区中是否还有残留的数据;
- 该 socket 是阻塞的还是非阻塞的。
- 对于阻塞的 socket ,close 将等待一段长为 l_linger 的时间,直到 TCP 模块发送完所有残留数据并得到对方的确认;
- 如果这段时间内 TCP 模块没有发送完残留数据并得到对方的确认,那么 close 系统调用将返回 -1 并设置 errno 为 EWOULDBLOCK ;
- 如果 socket 是非阻塞的,close 将立即返回,此时需要根据其返回值和 errno 来判断残留数据是否已经发送完毕。
- 被关闭的 socket 对应的 TCP 发送缓冲区中是否还有残留的数据;
- 该 socket 是阻塞的还是非阻塞的。
- 对于阻塞的 socket ,close 将等待一段长为 l_linger 的时间,直到 TCP 模块发送完所有残留数据并得到对方的确认;
- 如果这段时间内 TCP 模块没有发送完残留数据并得到对方的确认,那么 close 系统调用将返回 -1 并设置 errno 为 EWOULDBLOCK ;
- 如果 socket 是非阻塞的,close 将立即返回,此时需要根据其返回值和 errno 来判断残留数据是否已经发送完毕。
十二、网络信息 API
socket 地址的两个要素,即 IP 地址和端口号,都是用数值表示的。 这不便于记忆,也不便于扩展(比如从IPv4转移到IPv6)。但是 telnet 可以使用服务名来代替端口号,下面两条语句作用相同:
上面的例子中,telnet 客户端程序通过调用某些网络信息 API 来实现主机名到 IP 地址的转换,以及服务名称到端口号的转换。
1. gethostbyname 和 gethostbyaddr
gethostbyname 函数根据主机名称获取主机的完整信息, gethostbyaddr 函数根据 IP 地址获取主机的完整信息。gethostbyname 函数通常先在本地的 /etc/hosts 配置文件中查找主机,如果没有找到,再去访问 DNS 服务器:
- name:指定目标主机的主机名;
- addr:指定目标主机的 IP 地址;
- len:指定 addr 所指 IP 地址的长度;
- type:指定 addr 所指 IP 地址的类型,其合法取值包括 AF_INET(用于IPv4地址)和 AF_INET6(用 于IPv6地址)
2. getservbyname 和 getservbyport
getservbyname 函数根据名称获取某个服务的完整信息,getservbyport 函数根据端口号获取某个服务的完整信息。它们实际上都是通过读取 /etc/services 文件来获取服务的信息的:
- name:指定目标服务的名字;
- port:指定目标服务对应的端口号;
- proto:指定服务类型,给它传递 tcp 表示获取流服务,给它传递 udp 表示获取数据报服务,给它传递 NULL 则表示获取所有类型的服务。
3. getaddrinfo
getaddrinfo 函数既能通过主机名获得 IP 地址(内部使用的是 gethostbyname 函数),也能通过服务名获得端口号(内部使用的是 getservbyname 函数)。它是否可重入取决于其内部调用的 gethostbyname 和 getservbyname 函数是否是它们的可重入版本。该函数的定义如下:
- hostname:可以接收主机名,也可以接收字符串表示的 IP 地址(IPv4 采用点分十进制字符串,IPv6 则采用十六进制字符串);
- service:可以接收服务名,也可以接收字符串表示的十进制端口号;
- hints:是应用程序给 getaddrinfo 的一个提示,以对 getaddrinfo 的输出进行更精确的控制。可以被设置为 NULL ,表示允许 getaddrinfo 反馈任何可用的结果;
- result:指向一个链表,该链表用于存储 getaddrinfo 反馈的结果。
- ai_protocol:指具体的网络协议,其含义和 socket 系统调用的第三个参数相同,通常被设置为 0 ;
- ai_flags:
选项 | 含义 |
AI_PASSIVE | 在 hints 参数中设置,表示调用者是否会将取得的 socket 地址用于被动打开。服务器通常需要设置它,表示接受任何本地 socket 地址上的服务请求,客户端不能设置 |
AI_CANONNAME | 在 hints 参数中设置,告诉 getaddrinfo 函数返回主机的别名 |
AI_NUMERICHOST | 在 hints 参数中设置,表示 hostname 必须是用字符串表示的 IP 地址,从而避免了 DNS 查询 |
AI_NUMERICSERV | 在 hints 参数中设置,强制 service 参数使用十进制端口号的字符串形式,而不能是服务名 |
AI_V4MAPPED | 在 hints 参数中设置,如果 ai_family 被设置为 AF_INET6 ,那么当没有满足条件的 IPv6 地址被找到时,将 IPv4 地址映射为 IPv6 地址 |
AI_ALL | 必须和 AI_V4MAPPED 同时使用,否则将被忽略,表示同时返回符合条件的 IPv6 地址以及由 IPv4 地址映射得到的 IPv6 地址 |
AI_ADDRCONFIG | 仅当至少配置有一个 IPv4 地址(除了回路地址)时,才返回 IPv4 地址信息;同样,仅当至少配置一个 PIv6 地址(除了回路地址)时,才返回 IPv6 地址信息,它和 AI_V4MAPPED 是互斥的 |
当使用 hints 参数时,可以设置其 ai_flags ,ai_family ,ai_socktype 和 ai_protocol 四个字段,其他字段则必须被设置为 NULL 。
4. getnameinfo
getnameinfo 函数能通过 socket 地址同时获得以字符串表示的主机名(内部使用的是 gethostbyaddr 函数)和服务名(内部使用的是 getservbyport 函数)。它是否可重入取决于其内部调用的 gethostbyaddr 和 getservbyport 函数是否是它们的可重入版本。该函数的定义如下:
- getnameinfo:将返回的主机名存储在 host 参数指向的缓存中,将服务名存储在 serv 参数指向的缓存中;
- hostlen 和 servlen:分别指定这两块缓存的长度;
- flags:控制 getnameinfo 的行为:
选项 | 含义 |
NI_NAMEREQD | 如果通过 socket 地址不能获得主机名,则返回一个错误 |
NI_DGRAM | 返回数据报服务,大部分同时支持流和数据报的服务使用相同的端口号来提供这两种服务。但端口 512 ~ 514 是例外。比如 TCP 中 514 端口提供 shell 登录服务;UDP 中端口 514 提供 syslog 服务 |
NI_NUMERICHOST | 返回字符串表示的 IP 地址,而不是主机名 |
NI_NUMERICSERV | 返回字符串表示的十进制端口号,而不是服务名 |
NI_NOFQDN | 仅返回主机域名的第一部分,比如对主机名 nebula.testing.com 只写入 nebula |
- 成功 0 ,失败错误码:
选项 | 含义 |
EAI_AGAIN | 调用临时失败,提示应用程序过后再试 |
EAI_BADFLAGS | 非法的 ai_flags 值 |
EAI_FAIL | 名称解析失败 |
EAI_FAMILY | 不支持的 ai_family 参数 |
EAI_MEMORY | 内存分配失败 |
EAI_NONAME | 非法的主机名或服务名 |
EAI_OVERFLOW | 用户提供的缓冲区溢出,仅发生在 getnameinfo 调用中 |
EAI_SERVICE | 没有支持的服务 |
EAI_SOCKTYPE | 不支持的服务类型 |
EAI_SYSTEM | 系统错误,错误值存储在 errno 中 |
Linux 下 strerror 函数能将数值错误码 errno 转换成易读的字符串形式。同样,下面的函数可将上表中的错误码转换成其字符串形式:
原创存证 · 本地哈希 · 8881686279ff...
算法:SHA-256
时间:2026-10-16
哈希:
8881686279ff2f89ceb953878f3957a7501c6b142597f787e73da7a750960281该信息用于辅助证明当前内容版本,不等同于法律公证或版权登记。
- 作者:BeZer0
- 链接:https://blog.bezer0.cn/%E8%80%83%E7%A0%94/3fb0bc13-b7ac-8064-ab6e-e50bd8d828c2
- 声明:本文为原创文章,采用 CC BY-NC-SA 4.0 协议,转载请注明作者和原文链接。

