第13章 多种IO函数

13.1 send&recv函数

1.Linux中的send&recv函数

#include<sys/socket.h>
ssize_t send(int sockfd,const void * buf,size_t nbytes,int flags)

成功返回发送的字节数,失败返回-1
sockfd    表示与数据传输对象的连接的套接字文件描述符。
buf       保存待传输数据的缓冲地址值
nbytes    待传输的字节数
flags     传输数据时指定的可选项信息
#include<sys/socket.h>
ssize_t recv(int sockfd,void * buf,size_t nbytes,int flags)

成功时返回接收的字节数,失败返回-1
sockfd    表示与数据接收对象的连接的套接字文件描述符。
buf       保存接收数据的缓冲地址值
nbytes    待接收的的最大字节数
flags     接收数据时指定的可选项信息

flags代表可选项,可利用位或运算传输多个信息

send&recv函数的可选项及含义

可选项 含义 send recv
MSG_OOB 用于传输带外数据 可用 可用
MSG_PEEK 验证输入缓冲中是否存在接收的数据 不可用 可用
MSG_DONTROUTE 数据传输过程中不参照路由表(Routing),在本地Local网网络中寻找目的地 可用 不可用
MSG_DONTWAIT 调用I/O函数时不阻塞,用于使用非阻塞I/O 可用 可用
MSG_WAITALL 防止函数返回,直到接收全部请求的字节数 不可用 可用

2.MSG_OOB:发送紧急消息

MSG_OOB可用于发送“带外数据”紧急消息 MSG_OOB用于创建特殊发送方法和通道以发送紧急消息。

send

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

recv

#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()); //将该recv_sock指向的套接字引发的SIGURG信号的处理进程改为以getpid返回值作为进程ID的进程(也就是本进程)
	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);//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);
}

SIGURG信号处理函数只能有一个,所以处理SIGURG信号时必须指定信号处理的进程,而getpid函数返回调用此函数的进程ID,所以fcntl(recv_sock, F_SETOWN, getpid());这条语句,意思就是将当前进程指定为处理SIGURG信号的主体。

运行以上程序,是正常的传输顺序,而且紧急消息只有一个字节通过MSG_OOB可选项传递数据时不会加快传输速度,信号处理函数读取数据时也只读一个字节。剩余的数据只能通过未设置MSG_OOB可选项的普通输入函数读取。

真正意义上的OOB(Out-of-band)需要通过单独的通信路径高速传输数据,但TCP不另外提供,只利用TCP的紧急模式进行传输。

3.紧急工作模式原理

MSG_OOB真正的意义在于督促数据接收对象尽快处理数据。而TCP“保持传输顺序”的传输特性依然成立。

紧急消息的最后一个字符右侧存有紧急指针。紧急指针指向紧急消息的下一个位置,同时告诉对方主机紧急指针指向的位置前面就是紧急消息。实际只用一个字节表示紧急消息 TCP数据包头包含下面两个消息

  • URG=1:载有紧急消息的数据包序号为1

  • URG指针:紧急指针的位置

    紧急消息890是字符串890还是90?如若不是,是否为单个字符0

这并不重要,除紧急指针前面的一个字节外,数据接收方通过调用常用输入函数接收剩余部分,换而言之,紧急消息意义在于督促消息处理,而非紧急传输想传输的消息

4.检查输入缓冲

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

13.2readv&writev函数

1.使用readv&writev函数

功能概括:对数据进行整合一起发送 writev函数可以将分散保存在多个缓冲中的数据一并发送,通过readv函数可以从多个缓冲分别接收,可以减少I/O函数的调用次数。

#include<sys/uio.h>
ssize_t writev(int filedes,const struct iovec * iov,int iovcnt);
成功时返回发送的字节数,失败返回-1
filedes    表示数据传输对象的文件描述符。(不仅限于套接字,可以是文件和标准输出文件描述符)
iov        iovec结构体数组的地址值,结构体iovec包含待发送数据位置和大小信息
iovcnt     第二参数的数组长度

iovec结构体

struct iovec
{
    void * iov_base;  //缓冲地址
    size_t iov_len;   //缓冲大小
}

结构体iovec保存待发送数据的缓冲地址值和实际发送的数据长度构成

writev(1,ptr,2);

第三个参数为2说明struct iovec数组长度为2,也就是ptr指针就扫描两个iovec结构体。

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

参数含义以及结构体与前面一样

#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,};
	char buf2[BUF_SIZE]={0,};
	int str_len;

	vec[0].iov_base=buf1;
	vec[0].iov_len=5;
	vec[1].iov_base=buf2;
	vec[1].iov_len=BUF_SIZE;

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

由运行结果图片可以看出,接收的消息被分别存储到了不同数组,当第一个数组长度不够,会存储到第二个数组。

2.合理地使用readv&writev函数

什么情况都适用readv&writev函数,假设要传输的数据分别位于不同位置,可以通过一次writev(readv)传输读取,不需要多次调用write(read)函数,提高了效率。 其更大的意义在于可以减少数据包的个数。 假设有些服务器为了提高效率禁用了Nagle算法 假设数据位于三个地方,用write函数发送极有可能通过三个数据包发送,用writev可能只需要一个数据包。

将不同位置的数据按照位置放在一个大数组再用write函数发送也可以做到相同的效果,但是writev函数更便利。

13.3 基于Windows的实现

Windows并不存在像Linux那样的信号处理机制

MSG_OOB可选项的设置,在Windows中没有针对该选项的事件处理,所以通过select函数解决这一问题。

select监视的对象:

  • 是否存在套接字接收数据
  • 无需阻塞传输数据的套接字有哪些
  • 哪些套接字发生了异常

“异常”是不同寻常的程序执行流,Out-of-band也属于异常。也就是说可以用select函数这一特性监视Windows平台接收的Out-of-band数据。

oob_send_win

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

oob_recv_win

#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))//发生变化时,检测异常的数组是否注册了接收套接字的文件描述符(对应位是否为1),如果注册了说明接收到了紧急消息,需要处理。
			{
				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);
}

Windows并没有与writev和readv对应的函数,但是可以通过重叠I/O得到同样的效果


本文章使用limfx的vscode插件快速发布