八:多种I/O函数


  之前的示例中,基于Linux的使用 read&write 函数完成数据 ν0,基于 Windows 的则使用 send & recv 函数。 原因已经在第 l 章进行了充分阐述。 本章的Linux示例也将使用 send& recv 函数,并讲解其与 read&write函数相比的优点所在。 还将介绍几种其他的I/O函数。


1 send & recv 函数

1.1 Linux中的send & recv

#include <sys/socket.h>

/**
* @param  sockfd 数据传输对象的套接字文件描述符
* @param  buf  待传输数据的缓冲地址值
* @param  nbytes 待传输的字节数
* @param  flags  传输数据时指定的可选项信息
*/
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);

/**
* @param  sockfd 数据传输对象的套接字文件描述符
* @param  buf  保存数据的缓冲地址值
* @param  nbytes 可接收的字节数
* @param  flags  传输数据时指定的可选项信息
*/
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
send&recv 函数的可选项
可选项含义
MSG_OOB用于传输外带数据
MSG_PEEK验证输入缓冲中是否存在接收的数据
MSG_DONTROUTE数据传输过程中不参照路由表,在本地网络中寻找目的地
MSG_DONTWAIT调用IO函数时不阻塞,用于使用非阻塞IO
MSG_WAITALL防止函数返回,直到接收全部请求的字节数

1.2 MSG_OOB:发送紧急消息

  MSG_OOB可选项用于发送"带外数据"紧急消息。 假设医院里有很多病人在等待看病,此时若有急诊患者该怎么办? “当然是优先处理”。MSG_OOB可选项就用于创建特殊发送方法和通道以发送紧急消息。通过下面例子进行理解。


这里要注意,书上的程序很有可能达不到你要的效果,比如输出是1234567890 或者 1234567 所以我在发送的程序里加上了睡眠。具体的原因可以参考这个老哥的帖子 链接link 请一定要看


在这里插入图片描述
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 ErrorHandling(char *message) {
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

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

    int sock;
    struct sockaddr_in recv_adr;
    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) {
        ErrorHandling("connect error");
    }

    sleep(1);
    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;
}

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

int acpt_sock;
int recv_sock;

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

void UrgHandler(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);
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
    struct sockaddr_in recv_addr, serv_addr;
    int str_len, state;
    socklen_t serv_adr_sz;
    struct sigaction act;
    char buf[BUF_SIZE];

    act.sa_handler = UrgHandler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&recv_addr, 0, sizeof(recv_sock));
    recv_addr.sin_family = AF_INET;
    recv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    recv_addr.sin_port = htons(atoi(argv[1]));

    if (bind(acpt_sock, (struct sockaddr*)&recv_addr, sizeof(recv_addr)) == -1) {
        ErrorHandling("bind error");
    }
    listen(acpt_sock, 5);

    serv_adr_sz = sizeof(serv_addr);
    recv_sock = accept(acpt_sock, (struct sockaddr*)&serv_addr, &serv_adr_sz);

    //将文件描述符recv-sock指向的套接字拥有者 (F-SETOWrN) 改为把getpid函数返回值用作ID的进程
    fcntl(recv_sock, F_SETOWN, getpid());
    //收到OOB消息会产生此信号
    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;
}

1.3 紧急工作模式原理

  MSG_OOB的真正的意义在于督促数据接收对象尽快处理数据。 这是紧急模式的全部内容,而且TCP "保持传输顺序"的传输特性依然成立。
    那么则能称为紧急消息呢!
  这确实是紧急消息!因为发送消息者是在催促数据处理的情况下传输数据的。 急诊患者的及时救治需要如下两个条件.

  • 迅速入院
  • 医院急救
      无法快速把病人送到医院,并不意味着不需要医院进行急救。 TCP的紧急消息无法保证及时入院,但可以要求急救。 当然,急救措施应由程序员完成。 之前的示例oobJew-c的运行过程中也传递了紧急消息,这可以通过事件处理函数确认。这就是MSG OOB模式数据传输的实际意义。下面给出设置MSG OOB可选项状态下的数据传输过程,如图13-1所示:

&emps; 图13-1给出的是示例。ob-send-c的第32行中调用如下函数后的输出缓冲状态。 此处假设已传输之前的数据。
   send(sock, "890", strlen("890"), MSG_OOB);

   如果将缓冲最左端的位置视作偏移量为0,字符0保存于偏移量为2的位置。 另外,字符0右侧偏移量为3的位置存有紧急指针(Urgent Pointer )。 紧急指针指向紧急消息的下一个位置(偏移量加1 ),同时向对方主机传递如下信息:

“紧急指针指向的偏移量为3之前的部分就是紧急消息!”

  也就是说,实际只用1个字节表示紧急消息信息。 这一点可以通过图13-1中用于传输数据的TCP数据包(段)的结构看得更清楚,如图13-2所示:

TCP数据包实际包含更多信息,但图13-2只标注了与我们的主题相关的内容。 TCP头中含有
如下两种信息。
  - URG=1: 载有紧急消息的数据包
  - URG指针:紧急指针位于偏移量为3的位置

  指定MSG OOB选项的数据包本身就是紧急数据包,并通过紧急指针表示紧急消息所在位置。 但通过图13-2无法得知以下事实
     "紧急消息是字符串890,还是90? 如若不是,是否为单个字符0?“
  但这并不重要。 如前所述,除紧急指针的前面1个字节外,数据接收方将通过调用常用输入函数读取剩余部分。换言之,紧急消息的意义在于督促消息处理,而非紧急传输形式受限的消息。

1.4 检查输入缓冲

  同时设置MSG-PEEK选项和MSG-DONTWAIT选项,以验证输入缓冲中是否存在接收的数据。 设置MSG PEEK选项并调用recv函数时,即使读取了输入缓冲的数据也不会删除。 因此该选项通常与MSG DONTWAIT合作,用于调用以非阻塞方式验证待读数据存在与否的函数。 下面通过示例了解二者含义。
在这里插入图片描述

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

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)
	{
        //调用recv函数的同时传递MSG-PEEK可选项,这是为了保证即使不存在待读取数据也不会进入阻塞状态。
		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);
 	//再次调用recv函数。 这次并未设置任何可选项,因此,本次读取的数据将从输入缓冲中删除
	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);
}

2 readv & writev 函数

2.1 使用readv & writev 函数

  通过writev可以将分散保存在多个缓冲区中的数据一并发送,通过readv函数可以由多个缓冲区分别接收,适当使用这两个函数可以减少IO函数的调用次数。

#include <sys/uio.h>

/**
* @param filedes  数据传输对象的套接字文件描述符,也可以传递文件或标准输出描述符
* @param iov 结构体地址值,包含待发送数据的位置和大小信息
* @param iovcnt 向第二个参数传递的数组长度 
*/
ssize_t writev(int fileds, const struct iovec* iov, int iovcnt);
struct iovec {
	void* iov_base; //缓冲地址
	size_t iov_len; //缓冲大小
}

  可以看到, 结构体iovecl*1保存待发送数据的缓冲(char型数组) 地址值和实际发送的数据长度信息构成。 给山上述函数的调用示例前,先通过图13-4了解该函数的使用方法。

  图 13-4 中writev的第一个参数1是文件描述符,因此向控制台输出数据, ptr是存有待发送数据信息的iovec数组指针。 第3个参数为2,因此,从ptr指向的地址开始,共浏览2个iovec结构体变量,发送这些指针指向的缓冲数据。 攘下来仔细观察图中的iovec结构体数组。 ptr[0] (数组第一个元素)的iov_base指向以A开头的字符串,同时iov_len为3,故发送ABC。 而ptr[1] (数组的第二个元素)的iov_base指向数字1 ,同时iov-len为4,故发送1234
writev.c
在这里插入图片描述

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

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

    int str_len;
    vec[0].iov_base = buf1;
    vec[0].iov_len = 3;
    vec[1].iov_base = buf2;
    vec[1].iov_len = 4;

    str_len = writev(1, vec, 2);
    puts("");
    printf("Write bytes: %d \n", str_len);
    return 0;
}
#include <sys/uio.h>

/**
* @param filedes  数据传输对象的套接字文件描述符,也可以传递文件或标准输出描述符
* @param iov 结构体地址值,包含数据保存的位置和大小信息
* @param iovcnt 向第二个参数传递的数组长度 
*/
ssize_t writev(int fileds, const struct iovec* iov, int iovcnt);

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,};
    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.2 合理使用readv & writev函数

  哪种情况适合使用readv和writev函数?实际上,能使用该函数的所有情况都适用。 例如, 需要传输的数据分别位于不同缓冲(数组)时,需要多次调用write函数。 此时可以通过1次writev函数调用替代操作, 当然会提高效率。 同样,需要将输入缓冲中的数据读人不同位置时,可以不必多次调用read函数而是利用1次readv函数就能大大提高效率。

  即使仅从C语言角度看,减少函数调用次数也能相应提高性能。 但其更大的意义在于减少数据包个数。假设为了提高效率而在服务器端明确配t了Nagle算法。其实writev函数在不采用Nagle算法时更有价值,如图13-5所示。

  上述示例中待发送的数据分别存在3个不同的地方,此时如果使用write函数则需要3次函数调用。但若为提高速度而关闭了Nagle算法,则极有可能通过3个数据包传递数据。反之,若使用writev函数将所有数据一次性写入输出缓冲,则很有可能仅通过1个数据包传输数据。 所以writev函数和readv函数非常有用。
  再考虑一种情况:将不同位置的数据按照发送顺序移动(复制)到1个大数组,并通过1次write函数调用进行传输。 这种方式是否与调用writev函数的效果相同?当然!但使用writev函数更为便利。 因此,如果遇到writev函数和readv函数的适用情况,希望各位不要错过机会。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值