TCP/IP 网络编程(十二)---多种 I/O 函数

54 阅读10分钟

send & recv 函数

(一)send 函数

#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd:用于标识套接字的文件描述符。

  • buf:指向包含待发送数据的缓冲区的指针。

  • len:要发送的字节数。

  • flags:发送数据时的标志,通常为 0。可选值有:

    • MSG_DONTROUTE:数据不通过路由,而直接发送到本地网络。
    • MSG_OOB:发送带外数据(适用于 TCP)。
    • MSG_DONTWAIT:非阻塞发送。

(二)recv 函数

#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd:用于标识套接字的文件描述符。

  • buf:指向用于存放接收数据的缓冲区的指针。

  • len:可接收的最大字节数,即缓冲区的大小。

  • flags:接收数据时的标志,通常为 0。可选值有:

    • MSG_PEEK:读取数据但不从系统缓冲区移除。
    • MSG_OOB:接收带外数据。
    • MSG_WAITALL:等待接收全部数据,除非遇到超时或出错。
    • MSG_DONTWAIT:调用 I/O 函数时不阻塞,如果数据在缓冲区中不可用,函数会立即返回而不是等待数据到达

(三)可选项 MSG_OOB :发送紧急消息(带外数据)

  • 发送紧急数据:当你希望通过 TCP 连接发送高优先级数据(如中断信号或控制命令)时,可以使用 send 函数并设置 MSG_OOB 标志。

  • 接收紧急数据:接收端可以使用 recv 函数并设置 MSG_OOB 标志来接收这些紧急数据。

下面给出示例

(1)oob_send.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sock;
	struct sockaddr_in recv_adr;

	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	 }

	sock=socket(PF_INET, SOCK_STREAM, 0);
 	memset(&recv_adr, 0, sizeof(recv_adr));
	recv_adr.sin_family=AF_INET;
	recv_adr.sin_addr.s_addr=inet_addr(argv[1]);
	recv_adr.sin_port=htons(atoi(argv[2]));
  
	if(connect(sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr))==-1)
		error_handling("connect() error!");
	
	write(sock, "123", strlen("123"));
	send(sock, "4", strlen("4"), MSG_OOB);
	write(sock, "567", strlen("567"));
	send(sock, "890", strlen("890"), MSG_OOB);
	close(sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
  • 第31、33行紧急传输数据。正常顺序应该是123、4、567、890,但紧急传输了4和890,所以接收数据也将改变。

(2)oob_recv.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>

#define BUF_SIZE 30
void error_handling(char *message);
void urg_handler(int signo);

int acpt_sock;
int recv_sock;

int main(int argc, char *argv[])
{
	struct sockaddr_in recv_adr, serv_adr;
	int str_len, state;
	socklen_t serv_adr_sz;
	struct sigaction act;
	char buf[BUF_SIZE];

	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]); 
		exit(1);	
	 }
	
	act.sa_handler=urg_handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0; 
	
	acpt_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&recv_adr, 0, sizeof(recv_adr));
	recv_adr.sin_family=AF_INET;
	recv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	recv_adr.sin_port=htons(atoi(argv[1]));

	if(bind(acpt_sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr))==-1)
		error_handling("bind() error");
	listen(acpt_sock, 5);

	serv_adr_sz=sizeof(serv_adr);
	recv_sock=accept(acpt_sock, (struct sockaddr*)&serv_adr, &serv_adr_sz);
	
	fcntl(recv_sock, F_SETOWN, getpid()); 
	state=sigaction(SIGURG, &act, 0);
	
	while((str_len=recv(recv_sock, buf, sizeof(buf), 0))!= 0) 
	{
		if(str_len==-1)
			continue;
		buf[str_len]=0;
		puts(buf);
	}
	close(recv_sock);
	close(acpt_sock);
	return 0; 
}

void urg_handler(int signo)
{
	int str_len;
	char buf[BUF_SIZE];
	str_len=recv(recv_sock, buf, sizeof(buf)-1, MSG_OOB);
	buf[str_len]=0;
	printf("Urgent message: %s \n", buf);
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
  • 设置信号处理器

    •   struct sigaction act;
        act.sa_handler = urg_handler;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
      
    • sigaction 结构体用于设置信号处理器。sa_handler 被设置为自定义的处理函数 urg_handler,它会在接收到 SIGURG 信号时被调用。
    • sigemptyset 初始化 sa_mask,使其不阻塞任何信号。
    • sa_flags 设置为 0,表示使用默认行为。
  • 设置接收带外数据的进程

    •   fcntl(recv_sock, F_SETOWN, getpid()); 
        state = sigaction(SIGURG, &act, 0);
      
    • fcntl() 函数设置 recv_sock 的拥有者为当前进程,使得当前进程能够接收到该套接字的 SIGURG 信号(表示带外数据到达)。
    • F_SETOWNfcntl() 函数的一个选项,用于设置某个套接字文件描述符的“所有者进程”或“所有者进程组”。
    • getpid() 返回当前进程的 PID(进程 ID),将这个 PID 设置为 recv_sock 的所有者。
    • sigaction() 函数将 SIGURG 信号与之前设置的 urg_handler 处理函数关联。当带外数据到达时,系统会发送 SIGURG 信号。

(四)基于 Windows 的 MSG_OOB

在 Windows 中无法完成针对 MSG_OOB 的事件处理,需要考虑其他方法。可以通过 select 函数解决这一问题,因为之前提到 select 函数可以监视异常套接字,而 Out-of-band 数据也属于异常

(1)oob_send_win.c

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#define BUF_SIZE 30
void ErrorHandling(char *message);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN sendAdr;
	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");

	hSocket=socket(PF_INET, SOCK_STREAM, 0);
	memset(&sendAdr, 0, sizeof(sendAdr));
	sendAdr.sin_family=AF_INET;
	sendAdr.sin_addr.s_addr=inet_addr(argv[1]);
	sendAdr.sin_port=htons(atoi(argv[2]));


	if(connect(hSocket, (SOCKADDR*)&sendAdr, sizeof(sendAdr))==SOCKET_ERROR)
		ErrorHandling("connect() error!");

	send(hSocket, "123", 3, 0);
	send(hSocket, "4", 1, MSG_OOB);
	send(hSocket, "567", 3, 0);
	send(hSocket, "890", 3, MSG_OOB);
	
	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

(2)oob_recv_win.c

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#define BUF_SIZE 30
void ErrorHandling(char *message);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hAcptSock, hRecvSock;
	
	SOCKADDR_IN recvAdr;
	SOCKADDR_IN sendAdr;  
	int sendAdrSize, strLen;
	char buf[BUF_SIZE];
	int result;
	
	fd_set read, except, readCopy, exceptCopy;
	struct timeval timeout;
	
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);  
		exit(1);
	}
	
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error!");
	
	hAcptSock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&recvAdr, 0, sizeof(recvAdr));
	recvAdr.sin_family=AF_INET;
	recvAdr.sin_addr.s_addr=htonl(INADDR_ANY);
	recvAdr.sin_port=htons(atoi(argv[1]));

	if(bind(hAcptSock, (SOCKADDR*)&recvAdr, sizeof(recvAdr))==SOCKET_ERROR)
		ErrorHandling("bind() error");
	if(listen(hAcptSock, 5)==SOCKET_ERROR)
		ErrorHandling("listen() error");
	
	sendAdrSize=sizeof(sendAdr);
	hRecvSock=accept(hAcptSock, (SOCKADDR*)&sendAdr, &sendAdrSize);
	FD_ZERO(&read);
	FD_ZERO(&except);
	FD_SET(hRecvSock, &read);
	FD_SET(hRecvSock, &except);

	while(1)
	{  
		readCopy=read;
		exceptCopy=except;
		timeout.tv_sec=5;
		timeout.tv_usec=0; 
		
		result=select(0, &readCopy, 0, &exceptCopy, &timeout);

		if(result>0)
		{
			if(FD_ISSET(hRecvSock, &exceptCopy))
			{
				strLen=recv(hRecvSock, buf, BUF_SIZE-1, MSG_OOB);
				buf[strLen]=0;
				printf("Urgent message: %s \n", buf);
			}	

			if(FD_ISSET(hRecvSock, &readCopy))
			{
				strLen=recv(hRecvSock, buf, BUF_SIZE-1, 0);
				if(strLen==0)
				{
					break;
					closesocket(hRecvSock);
				}
				else 
				{	   
					buf[strLen]=0;
					puts(buf); 
				}	
			}				
		}
	}
	
	closesocket(hAcptSock);
	WSACleanup();
	return 0; 
}

void ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

(四)检查输入缓冲

同时设置 MSG_PEEKMSG_DONTWAIT 选项,以验证输入缓冲中是否存在接收的数据。设置 MSG_PEEK 选项并调用 recv 函数时,即使读取了输入缓冲中的数据也不会删除。 因此该选项通常与 MSG_DONTWAIT 合作,用于调用以非阻塞方式验证待读数据存在与否的函数。

(1)peek_send.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sock;
	struct sockaddr_in send_adr;
	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	 }

	sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&send_adr, 0, sizeof(send_adr));
	send_adr.sin_family=AF_INET;
	send_adr.sin_addr.s_addr=inet_addr(argv[1]);
	send_adr.sin_port=htons(atoi(argv[2]));
  	
	if(connect(sock, (struct sockaddr*)&send_adr, sizeof(send_adr))==-1)
		error_handling("connect() error!");
	
	write(sock, "123", strlen("123"));
	close(sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

(2)peek_recv.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int acpt_sock, recv_sock;
	struct sockaddr_in acpt_adr, recv_adr;
	int str_len, state;
	socklen_t recv_adr_sz;
	char buf[BUF_SIZE];
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	
	acpt_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&acpt_adr, 0, sizeof(acpt_adr));
	acpt_adr.sin_family=AF_INET;
	acpt_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	acpt_adr.sin_port=htons(atoi(argv[1]));
	
  	if(bind(acpt_sock, (struct sockaddr*)&acpt_adr, sizeof(acpt_adr))==-1)
		error_handling("bind() error");
	listen(acpt_sock, 5);
	
	recv_adr_sz=sizeof(recv_adr);
	recv_sock=accept(acpt_sock, (struct sockaddr*)&recv_adr, &recv_adr_sz);
	
	while(1)
	{
		str_len=recv(recv_sock, buf, sizeof(buf)-1, MSG_PEEK|MSG_DONTWAIT);
		if(str_len>0)
			break;
	}

	buf[str_len]=0;
	printf("Buffering %d bytes: %s \n", str_len, buf);
 	
	str_len=recv(recv_sock, buf, sizeof(buf)-1, 0);
	buf[str_len]=0;
	printf("Read again: %s \n", buf);
	close(acpt_sock);
	close(recv_sock);
	return 0; 
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
  • 接收数据

    • recv(recv_sock, buf, sizeof(buf)-1, MSG_PEEK|MSG_DONTWAIT):使用 MSG_PEEK 标志查看接收缓冲区中的数据而不从缓冲区中移除数据。 MSG_DONTWAIT 表示在没有数据可用时立即返回。这个调用会阻塞直到有数据可用。str_len 记录读取到的数据长度。
    • buf[str_len] = 0:将缓冲区末尾添加 null 字符,形成有效的 C 字符串。
    • printf("Buffering %d bytes: %s \n", str_len, buf):打印缓冲区中数据的内容和长度。
  • 再次接收数据

    • recv(recv_sock, buf, sizeof(buf)-1, 0):使用普通模式接收数据,接收到的数据将会从缓冲区中移除。str_len 记录读取到的数据长度。
    • buf[str_len] = 0:将缓冲区末尾添加 null 字符,形成有效的 C 字符串。
    • printf("Read again: %s \n", buf):打印第二次接收的数据的内容。
image.png

readv & writev 函数

readv & writev 函数的功能可概括如下:

对数据进行整合传输及发送的函数。”

也就是说,通过 writev 函数可以将分散保存在多个缓冲中的数据一并发送;通过 readv 函数可以由多个缓冲分别接收。

(一)writev 函数

#include <sys/uio.h>
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
  • fd:要读取的文件描述符。

  • iov:指向 iovec 结构体数组的指针。iovec 结构体用于描述每个缓冲区的起始地址和长度。

  • iovcntiovec 结构体数组的元素数量。

  • iovec 结构体:

    struct iovec {
        void  *iov_base; // 指向数据缓冲区的指针
        size_t iov_len;  // 缓冲区的长度
    };
    

函数使用方法:

image.png

(1)writev.c

#include <stdio.h>
#include <sys/uio.h>

int main(int argc, char *argv[])
{
    struct iovec vec[2];
    char buf1[] = "ABCDEFG";
    char buf2[] = "1234567";
    int str_len;

    // 设置 vec[0] 指向 buf1,并指定 buf1 的长度
    vec[0].iov_base = buf1;
    vec[0].iov_len = 3;  // 只写入 buf1 的前 3 个字节

    // 设置 vec[1] 指向 buf2,并指定 buf2 的长度
    vec[1].iov_base = buf2;
    vec[1].iov_len = 4;  // 只写入 buf2 的前 4 个字节

    // 使用 writev 函数将数据写入标准输出(文件描述符 1)
    str_len = writev(1, vec, 2);  // 写入 2 个 iovec 结构描述的缓冲区
    puts("");  // 输出一个换行符
    printf("Write bytes: %d \n", str_len);  // 打印实际写入的字节数

    return 0;
}
image.png

(二)readv 函数

#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

参数顺序和含义与 writev 的一致。

(1)readv.c

#include <stdio.h>
#include <sys/uio.h>
#define BUF_SIZE 100

int main(int argc, char *argv[])
{
    struct iovec vec[2];
    char buf1[BUF_SIZE] = {0,};  // 初始化 buf1 为全零
    char buf2[BUF_SIZE] = {0,};  // 初始化 buf2 为全零
    int str_len;

    // 配置 vec[0] 以读取到 buf1
    vec[0].iov_base = buf1;
    vec[0].iov_len = 5;  // 读取 5 个字节到 buf1

    // 配置 vec[1] 以读取到 buf2
    vec[1].iov_base = buf2;
    vec[1].iov_len = BUF_SIZE;  // 读取 BUF_SIZE 个字节到 buf2

    // 从标准输入读取数据到两个缓冲区
    str_len = readv(0, vec, 2);

    // 打印读取的字节数
    printf("Read bytes: %d \n", str_len);
    // 打印两个缓冲区的内容
    printf("First message: %s \n", buf1);
    printf("Second message: %s \n", buf2);
    
    return 0;
}

image.png

(三)合理使用 readv & writev 函数

  • writev 函数在不采用 Nagle 算法时更有价值。
image.png

上述示例中, Nagle 算法关闭。待发送的数据分别存在三个不同的地方,此时如果使用 write 函数则需要3次函数调用,但若为提高速度而关闭了 Nagle 算法,则极有可能通过3个数据包传递数据。反之,若使用 write 函数将所有数据一次性写入输出缓冲,则很有可能仅通过一个数据包传送数据。

  • 再考虑一种情况,将不同位置的数据按照发送顺序移动(复制)到1个大数组,并通过一次 write 函数调用进行传输。这种方式与调用writev 函数的效果相同.

问答

(一)TCP 中设置 MSG_OOB 选项的数据先到达对方主机吗?

  • 带外数据的发送:使用 MSG_OOB 标志标记数据为带外数据。

  • 接收顺序:带外数据的到达顺序不一定优于其他数据,TCP 的传输机制并不保证带外数据在网络上优先到达。

  • 应用层处理:应用程序需要专门的处理逻辑来处理带外数据,并可能需要用到如信号或特定的 API 函数来处理。

  • MSG_OOB 无法脱离 TCP 默认数据传输方式,即使设置了 MSG_OOB,也会保持原有传输顺序。该选项只用于要求接收方紧急处理。