2483 字
6 分钟
TCP&IP网络编程学习札记_8
2026-08-15
浏览量 8 · 访客 2

壁纸链接

多种I/O函数#

13.1 send和recv函数#

之前在LinuxLinux中主要通过readreadwritewrite收发数据,在WindowsWindows中则使用recvrecvsendsend。实际上,LinuxLinux同样提供sendsendrecvrecv,它们的基本作用与readreadwritewrite相同,但可以通过最后一个参数指定收发方式。

#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。

sockfdsockfd是已连接套接字的文件描述符,bufbufnbytesnbytes表示缓冲地址及数据长度。flagsflags用于设置可选项,没有特殊需求时传入00;多个选项可以通过按位或运算符|组合。

常见选项如下:

选项适用函数含义
MSG_OOBsend、recv发送或接收带外数据,即TCP紧急数据
MSG_PEEKrecv查看输入缓冲中的数据,但不将其删除
MSG_DONTROUTEsend不参考路由表,只在本地网络中寻找目标
MSG_DONTWAITsend、recv以非阻塞方式完成本次调用
MSG_WAITALLrecv尽量等待到请求长度的数据全部到达

不同操作系统对这些选项的支持可能存在差异,使用时仍要参考目标平台的接口说明。

MSG_OOB与紧急消息#

发送端为sendsend设置MSG_OOBMSG\_OOB,可以提示接收端尽快处理某段数据:

write(sock, "123", 3);
send(sock, "4", 1, MSG_OOB);
write(sock, "567", 3);
send(sock, "890", 3, MSG_OOB);

LinuxLinux中,套接字收到紧急数据时会产生SIGURGSIGURG信号。程序需要注册信号处理函数,并通过fcntlfcntl指定负责处理该套接字信号的进程:

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);
}

这里的“带外”容易引起误解。TCPTCP并没有另外创建一条速度更快的传输路径,设置MSG_OOBMSG\_OOB也不会让数据绕过原来的字节流或破坏传输顺序。它的重点是通知接收程序:当前数据需要被紧急处理。

替换文本

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

替换文本

所以MSG_OOBMSG\_OOB表达的是“请优先处理”,而不是“请优先送达”。真正采取何种紧急处理措施,仍由应用程序决定。

检查输入缓冲#

recvrecv设置MSG_PEEKMSG\_PEEK后,函数会复制输入缓冲中的数据,但不会把它们移除。随后再次正常调用recvrecv,仍能读到同一批内容。

如果只想立即确认是否已有数据,可以同时使用MSG_DONTWAITMSG\_DONTWAIT,避免在输入缓冲为空时阻塞:

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);
}

MSG_PEEKMSG\_PEEK只观察数据,MSG_DONTWAITMSG\_DONTWAIT只控制本次调用不等待,二者作用不同。检查完成后若要真正消费这些数据,还需调用一次未设置MSG_PEEKMSG\_PEEK的接收函数。

13.2 readv和writev函数#

普通writewrite每次只能指定一段连续缓冲区。如果待发送内容分散在多个位置,通常要多次调用writewrite,或先把数据复制到一块大缓冲中。writevwritev可以直接把多个缓冲的数据按顺序一次写出;readvreadv则能把一次读取的数据依次分配到多个缓冲中,这种方式也称为分散/聚集I/OI/O

#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。

ioviov指向ioveciovec结构体数组,iovcntiovcnt表示数组元素数量:

struct iovec
{
void *iov_base; //缓冲地址
size_t iov_len; //缓冲长度
};

利用writev整合发送#

下面把buf1buf1的前33字节和buf2buf2的前44字节一次写入文件描述符11,输出结果为ABC1234ABC1234

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);

替换文本

writevwritev不要求几段数据在内存中连续,也无需先把它们复制到临时大数组,适合同时发送协议首部与消息正文等分散数据。

利用readv分散接收#

readvreadv按照ioveciovec数组顺序填充缓冲。前一个缓冲达到指定长度后,剩余数据会继续写入下一个缓冲:

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);

使用readvreadvwritevwritev可以减少系统调用次数,也省去了在多个缓冲与临时连续缓冲之间复制数据的步骤。关闭NagleNagle算法时,多次小规模writewrite还可能分别触发数据传输;一次writevwritev把分散数据交给输出缓冲,更有利于集中处理。

替换文本

这并不保证数据在网络中一定只形成一个数据包,TCPTCP仍会根据缓冲、最大报文段等条件组织传输,但应用程序提交数据的次数确实减少了。

13.3 基于Windows的实现#

WindowsWindows中的sendsendrecvrecv及其可选项与LinuxLinux版本基本一致,主要区别仍是套接字使用SOCKETSOCKET类型,失败时返回SOCKET_ERRORSOCKET\_ERROR

WindowsWindows没有与LinuxLinux相同的SIGURGSIGURG信号处理方式,因此书中使用selectselect监视异常集合。普通数据使读取集合就绪,紧急数据则使异常集合就绪:

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);

书中指出WindowsWindows没有与readvreadvwritevwritev直接对应的同名函数,可以通过后续介绍的重叠I/OI/O实现相近的分散/聚集传输效果。

多播与广播#

14.1 多播#

普通TCPTCPUDPUDP通信通常以单个目标为单位。若服务器要向大量主机发送完全相同的数据,逐个建立连接或重复调用sendtosendto会让发送端和公共传输路径承受大量重复工作。

多播MulticastMulticast)基于UDPUDP实现。发送者只向一个多播组发送一次数据,网络中的路由器在需要分流的位置复制数据包,加入该组的接收者都可以收到它。

替换文本

多播的主要特点如下:

  1. 发送者只需面向多播组发送一份数据。
  2. 只有加入该组的主机才接收发往该组的数据。
  3. 数据包可跨越多个网络,由支持多播的路由器完成复制和转发。
  4. 多播组使用DDIPv4IPv4地址,即224.0.0.0224.0.0.0239.255.255.255239.255.255.255

与逐个单播相比,多播不会在相同的公共路径上反复传输同一份数据,因此适合实时音视频、网络广播等一对多场景。不过,多播基于UDPUDP,本身不保证可靠到达;部分网络设备也可能不支持或主动限制多播。

TTL与多播范围#

多播数据必须设置TTLTTLTime to LiveTime\ to\ Live,生存时间)。数据包每经过一个路由器,TTLTTL就减11;减到00后,路由器会丢弃该数据包。

替换文本

TTLTTL过小会使数据到达不了较远的接收者,过大则可能让数据扩散到不必要的网络。发送端通过IP_MULTICAST_TTLIP\_MULTICAST\_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));

加入多播组#

接收者只有加入指定多播组,才能接收发往该组的数据。加入操作通过IP_ADD_MEMBERSHIPIP\_ADD\_MEMBERSHIP选项完成,其参数为ip_mreqip\_mreq结构体:

struct ip_mreq
{
struct in_addr imr_multiaddr; //多播组地址
struct in_addr imr_interface; //本机接口地址
};

imr_multiaddrimr\_multiaddr指定希望加入的组,imr_interfaceimr\_interface指定接收多播的本地网络接口。若不限定接口,可以使用INADDR_ANYINADDR\_ANY

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));

多播发送者与接收者#

多播通信中通常使用发送者SenderSender)和接收者ReceiverReceiver),而不是服务器端和客户端。双方不建立连接,运行顺序也没有TCPTCP那样的限制。

发送者创建UDPUDP套接字、设置多播目标地址与TTLTTL,再通过sendtosendto发送数据:

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));

接收者把套接字绑定到相同端口,加入目标多播组后循环调用recvfromrecvfrom

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 广播#

广播BroadcastBroadcast)同样基于UDPUDP,也能一次向多个主机发送数据,但作用范围与多播不同:多播面向主动加入某个组的主机,并可跨越多个网络;广播面向同一网络中的所有主机,通常不会被路由器继续转发。

根据目标地址不同,广播可以分为两类:

  • 直接广播Directed BroadcastDirected\ Broadcast):保留目标网络地址,把主机位全部设为11。例如向192.12.34.255192.12.34.255发送,可表示面向192.12.34.0192.12.34.0网络的广播。
  • 本地广播Local BroadcastLocal\ Broadcast):使用255.255.255.255255.255.255.255,只面向发送主机所在的本地网络。

普通UDPUDP套接字默认不允许发送广播。发送端需要先把SO_BROADCASTSO\_BROADCAST设为11

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));

之后只需把sendtosendto的目标设为直接广播或本地广播地址:

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));

只有发送端需要开启SO_BROADCASTSO\_BROADCAST。接收端与普通UDPUDP接收程序相同,将套接字绑定到INADDR_ANYINADDR\_ANY和约定端口,再通过recvfromrecvfrom读取数据即可。

多播与广播的区别#

对比项多播广播
接收对象加入指定组的主机本地或目标网络中的所有主机
地址224.0.0.0224.0.0.0239.255.255.255239.255.255.255目标网络广播地址或255.255.255.255255.255.255.255
跨网络能力可由支持多播的路由器转发一般限制在广播域内
主要选项IP_MULTICAST_TTL、IP_ADD_MEMBERSHIPSO_BROADCAST

多播更像“订阅某个频道”,广播则像“通知当前区域内的所有人”。两者都避免发送端逐一传输,但适用范围和接收对象并不相同。

14.3 基于Windows的实现#

WindowsWindows中的多播和广播原理、套接字选项及整体流程与LinuxLinux基本一致。多播程序需要额外包含ws2tcpip.hws2tcpip.h,其中定义了IP_MULTICAST_TTLIP\_MULTICAST\_TTLIP_ADD_MEMBERSHIPIP\_ADD\_MEMBERSHIPip_mreqip\_mreq等内容:

#include <winsock2.h>
#include <ws2tcpip.h>

其余平台差异仍遵循WinsockWinsock的一般规则:先调用WSAStartupWSAStartup,使用SOCKETSOCKET保存句柄,通过sendtosendtorecvfromrecvfrom收发数据,结束时调用closesocketclosesocketWSACleanupWSACleanup

setsockopt(hSendSock, IPPROTO_IP, IP_MULTICAST_TTL,
(char *)&timeLive, sizeof(timeLive));
setsockopt(hRecvSock, IPPROTO_IP, IP_ADD_MEMBERSHIP,
(char *)&joinAdr, sizeof(joinAdr));

广播同样只需在发送套接字上设置SO_BROADCASTSO\_BROADCAST,接收端无需额外开启广播选项。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

TCP&IP网络编程学习札记_8
https://mkrari.cn/posts/tcp_ip_learn_8/
作者
Mkrari
发布于
2026-08-15
许可协议
CC BY-NC-SA 4.0