
多种I/O函数
13.1 send和recv函数
之前在中主要通过和收发数据,在中则使用和。实际上,同样提供与,它们的基本作用与、相同,但可以通过最后一个参数指定收发方式。
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);//成功时返回收发的字节数,失败时返回-1;//recv收到EOF时返回0。是已连接套接字的文件描述符,和表示缓冲地址及数据长度。用于设置可选项,没有特殊需求时传入;多个选项可以通过按位或运算符组合。
常见选项如下:
| 选项 | 适用函数 | 含义 |
|---|---|---|
| MSG_OOB | send、recv | 发送或接收带外数据,即TCP紧急数据 |
| MSG_PEEK | recv | 查看输入缓冲中的数据,但不将其删除 |
| MSG_DONTROUTE | send | 不参考路由表,只在本地网络中寻找目标 |
| MSG_DONTWAIT | send、recv | 以非阻塞方式完成本次调用 |
| MSG_WAITALL | recv | 尽量等待到请求长度的数据全部到达 |
不同操作系统对这些选项的支持可能存在差异,使用时仍要参考目标平台的接口说明。
MSG_OOB与紧急消息
发送端为设置,可以提示接收端尽快处理某段数据:
write(sock, "123", 3);send(sock, "4", 1, MSG_OOB);write(sock, "567", 3);send(sock, "890", 3, MSG_OOB);在中,套接字收到紧急数据时会产生信号。程序需要注册信号处理函数,并通过指定负责处理该套接字信号的进程:
fcntl(recv_sock, F_SETOWN, getpid());sigaction(SIGURG, &act, 0);
void urg_handler(int signo){ char buf[BUF_SIZE]; int len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_OOB); buf[len] = 0; printf("Urgent message: %s\n", buf);}这里的“带外”容易引起误解。并没有另外创建一条速度更快的传输路径,设置也不会让数据绕过原来的字节流或破坏传输顺序。它的重点是通知接收程序:当前数据需要被紧急处理。

通过首部中的标志和紧急指针表示紧急位置。书中示例向缓冲写入时,紧急指针指向这段数据之后的位置;在常见的套接字接口语义下,接收端通过取得紧急位置前的一个字节,其余内容仍由普通按原有顺序读取。

所以表达的是“请优先处理”,而不是“请优先送达”。真正采取何种紧急处理措施,仍由应用程序决定。
检查输入缓冲
为设置后,函数会复制输入缓冲中的数据,但不会把它们移除。随后再次正常调用,仍能读到同一批内容。
如果只想立即确认是否已有数据,可以同时使用,避免在输入缓冲为空时阻塞:
str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_PEEK | MSG_DONTWAIT);
if (str_len > 0){ buf[str_len] = 0; printf("Buffering %d bytes: %s\n", str_len, buf);}只观察数据,只控制本次调用不等待,二者作用不同。检查完成后若要真正消费这些数据,还需调用一次未设置的接收函数。
13.2 readv和writev函数
普通每次只能指定一段连续缓冲区。如果待发送内容分散在多个位置,通常要多次调用,或先把数据复制到一块大缓冲中。可以直接把多个缓冲的数据按顺序一次写出;则能把一次读取的数据依次分配到多个缓冲中,这种方式也称为分散/聚集。
#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
ssize_t readv(int filedes, struct iovec *iov, int iovcnt);//成功时返回实际收发的字节数,失败时返回-1。指向结构体数组,表示数组元素数量:
struct iovec{ void *iov_base; //缓冲地址 size_t iov_len; //缓冲长度};利用writev整合发送
下面把的前字节和的前字节一次写入文件描述符,输出结果为:
struct iovec vec[2];char buf1[] = "ABCDEFG";char buf2[] = "1234567";
vec[0].iov_base = buf1;vec[0].iov_len = 3;vec[1].iov_base = buf2;vec[1].iov_len = 4;
writev(1, vec, 2);
不要求几段数据在内存中连续,也无需先把它们复制到临时大数组,适合同时发送协议首部与消息正文等分散数据。
利用readv分散接收
按照数组顺序填充缓冲。前一个缓冲达到指定长度后,剩余数据会继续写入下一个缓冲:
struct iovec vec[2];char buf1[5] = {0};char buf2[100] = {0};
vec[0].iov_base = buf1;vec[0].iov_len = sizeof(buf1);vec[1].iov_base = buf2;vec[1].iov_len = sizeof(buf2);
str_len = readv(0, vec, 2);使用和可以减少系统调用次数,也省去了在多个缓冲与临时连续缓冲之间复制数据的步骤。关闭算法时,多次小规模还可能分别触发数据传输;一次把分散数据交给输出缓冲,更有利于集中处理。

这并不保证数据在网络中一定只形成一个数据包,仍会根据缓冲、最大报文段等条件组织传输,但应用程序提交数据的次数确实减少了。
13.3 基于Windows的实现
中的、及其可选项与版本基本一致,主要区别仍是套接字使用类型,失败时返回。
没有与相同的信号处理方式,因此书中使用监视异常集合。普通数据使读取集合就绪,紧急数据则使异常集合就绪:
FD_ZERO(&reads);FD_ZERO(&excepts);FD_SET(hRecvSock, &reads);FD_SET(hRecvSock, &excepts);
read_copy = reads;except_copy = excepts;
select(0, &read_copy, NULL, &except_copy, &timeout);
if (FD_ISSET(hRecvSock, &except_copy)) recv(hRecvSock, buf, BUF_SIZE - 1, MSG_OOB);
if (FD_ISSET(hRecvSock, &read_copy)) recv(hRecvSock, buf, BUF_SIZE - 1, 0);书中指出没有与、直接对应的同名函数,可以通过后续介绍的重叠实现相近的分散/聚集传输效果。
多播与广播
14.1 多播
普通或通信通常以单个目标为单位。若服务器要向大量主机发送完全相同的数据,逐个建立连接或重复调用会让发送端和公共传输路径承受大量重复工作。
多播()基于实现。发送者只向一个多播组发送一次数据,网络中的路由器在需要分流的位置复制数据包,加入该组的接收者都可以收到它。

多播的主要特点如下:
- 发送者只需面向多播组发送一份数据。
- 只有加入该组的主机才接收发往该组的数据。
- 数据包可跨越多个网络,由支持多播的路由器完成复制和转发。
- 多播组使用类地址,即~。
与逐个单播相比,多播不会在相同的公共路径上反复传输同一份数据,因此适合实时音视频、网络广播等一对多场景。不过,多播基于,本身不保证可靠到达;部分网络设备也可能不支持或主动限制多播。
TTL与多播范围
多播数据必须设置(,生存时间)。数据包每经过一个路由器,就减;减到后,路由器会丢弃该数据包。

过小会使数据到达不了较远的接收者,过大则可能让数据扩散到不必要的网络。发送端通过选项设置它:
int send_sock;int time_live = 64;
send_sock = socket(PF_INET, SOCK_DGRAM, 0);setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void *)&time_live, sizeof(time_live));加入多播组
接收者只有加入指定多播组,才能接收发往该组的数据。加入操作通过选项完成,其参数为结构体:
struct ip_mreq{ struct in_addr imr_multiaddr; //多播组地址 struct in_addr imr_interface; //本机接口地址};指定希望加入的组,指定接收多播的本地网络接口。若不限定接口,可以使用。
struct ip_mreq join_adr;
join_adr.imr_multiaddr.s_addr = inet_addr(argv[1]);join_adr.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(recv_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void *)&join_adr, sizeof(join_adr));多播发送者与接收者
多播通信中通常使用发送者()和接收者(),而不是服务器端和客户端。双方不建立连接,运行顺序也没有那样的限制。
发送者创建套接字、设置多播目标地址与,再通过发送数据:
send_sock = socket(PF_INET, SOCK_DGRAM, 0);
mul_adr.sin_family = AF_INET;mul_adr.sin_addr.s_addr = inet_addr(argv[1]);mul_adr.sin_port = htons(atoi(argv[2]));
setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void *)&time_live, sizeof(time_live));
sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr *)&mul_adr, sizeof(mul_adr));接收者把套接字绑定到相同端口,加入目标多播组后循环调用:
recv_sock = socket(PF_INET, SOCK_DGRAM, 0);
adr.sin_family = AF_INET;adr.sin_addr.s_addr = htonl(INADDR_ANY);adr.sin_port = htons(atoi(argv[2]));bind(recv_sock, (struct sockaddr *)&adr, sizeof(adr));
setsockopt(recv_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void *)&join_adr, sizeof(join_adr));
str_len = recvfrom(recv_sock, buf, BUF_SIZE - 1, 0, NULL, 0);多播属于无连接通信。接收者如果在数据发出后才加入组,就无法取得先前的数据,发送端也不会为后来加入者自动重传。
14.2 广播
广播()同样基于,也能一次向多个主机发送数据,但作用范围与多播不同:多播面向主动加入某个组的主机,并可跨越多个网络;广播面向同一网络中的所有主机,通常不会被路由器继续转发。
根据目标地址不同,广播可以分为两类:
- 直接广播():保留目标网络地址,把主机位全部设为。例如向发送,可表示面向网络的广播。
- 本地广播():使用,只面向发送主机所在的本地网络。
普通套接字默认不允许发送广播。发送端需要先把设为:
int send_sock;int so_brd = 1;
send_sock = socket(PF_INET, SOCK_DGRAM, 0);setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (void *)&so_brd, sizeof(so_brd));之后只需把的目标设为直接广播或本地广播地址:
broad_adr.sin_family = AF_INET;broad_adr.sin_addr.s_addr = inet_addr("255.255.255.255");broad_adr.sin_port = htons(9190);
sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr *)&broad_adr, sizeof(broad_adr));只有发送端需要开启。接收端与普通接收程序相同,将套接字绑定到和约定端口,再通过读取数据即可。
多播与广播的区别
| 对比项 | 多播 | 广播 |
|---|---|---|
| 接收对象 | 加入指定组的主机 | 本地或目标网络中的所有主机 |
| 地址 | ~ | 目标网络广播地址或 |
| 跨网络能力 | 可由支持多播的路由器转发 | 一般限制在广播域内 |
| 主要选项 | IP_MULTICAST_TTL、IP_ADD_MEMBERSHIP | SO_BROADCAST |
多播更像“订阅某个频道”,广播则像“通知当前区域内的所有人”。两者都避免发送端逐一传输,但适用范围和接收对象并不相同。
14.3 基于Windows的实现
中的多播和广播原理、套接字选项及整体流程与基本一致。多播程序需要额外包含,其中定义了、和等内容:
#include <winsock2.h>#include <ws2tcpip.h>其余平台差异仍遵循的一般规则:先调用,使用保存句柄,通过和收发数据,结束时调用与。
setsockopt(hSendSock, IPPROTO_IP, IP_MULTICAST_TTL, (char *)&timeLive, sizeof(timeLive));
setsockopt(hRecvSock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&joinAdr, sizeof(joinAdr));广播同样只需在发送套接字上设置,接收端无需额外开启广播选项。
