第13章:多种I/O函数
之前使用了基于linux的read & write函数完成数据的I/O,基于win则使用了send & recv函数。
这里将在linux下使用 send 和 recv 以及readv writev函数
13.1 send&recv函数
之前光是说了一下,没讲解函数的具体内容,这里进行补充。
13.1.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: 传输数据时指定的可选项信息
虽然上面的send
函数与win中的send
相比,在声明的结构体名称三有些区别,但参数的顺序、含义、使用方法完全相同,因此实际区别不大。
下面看 recv
函数
#include <sys/socket.h>
ssize_t recv(int sockfd, void* buf, size_t nbytes, int flags);
-> 成功时返回接收的字节数(收到EOF时返回0),失败时返回 -1
sockfd: 表示与数据接收对象的连接的套接字文件描述符
buf: 保存待接收数据的缓冲地址值
nbytes: 可接收的最大字节数
flags: 接收数据时指定的可选项信息
上面的send
& recv
函数中的最后一个参数flags是 收发数据时的可选项。
该选项可利用位或(bit OR)运算(|运算符)同时传递多个信息。
具体的选项看下图。
但是上面的参数并不是所有操作系统都能用,需要了解不同该操作系统,下面讲解一下㧳操作系统差异影响的参数。
13.1.2 MSG_OOG:发送紧急消息
MSG_OOB->MESSAGE OUT OF BAND什么是传输带外数据???
设立单独的发送方法和通道以发送紧急消息。
下面示例中通过MSG_OOB可选项收发数据,我们先看代码
在下面这段代码中,我们通过调用wrtte函数和send函数进行对比,同时采用MSG_OOB参数,看看数据接收端到底会收到什么样的数据
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[])
{
/* code */
// 首先我们创建 数据发送端必须的套接字一条龙
int sock;
struct sockaddr_in recv_addr; // 数据发送目标地址
if(argc!= 3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&recv_addr, 0, sizeof(recv_addr));
recv_addr.sin_family = AF_INET;
recv_addr.sin_addr.s_addr = inet_addr(argv[1]);
recv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&recv_addr, sizeof(recv_addr)) == -1){
error_handling("connect error!");
}
// 这里我们开始用write 和 send 函数进行对比
write(sock, "123", strlen("123"));
usleep(20);
send(sock, "4", strlen("4"), MSG_OOB);
usleep(20);
write(sock, "567", strlen("567"));
usleep(20);
send(sock, "890", strlen("890"), MSG_OOB);
close(sock);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
oob_recv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.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, recv_sock; // 声明全局套接字
int main(int argc, char *argv[])
{
/* code */
if(argc != 2){
printf("usage: %s <port>\n", argv[0]);
exit(1);
}
// 声明套接字相关变量
struct sockaddr_in acpt_addr, recv_addr;
socklen_t recv_addr_sz;
char buf[BUF_SIZE];
int str_len;
// 声明信号相关变量
struct sigaction act; // 注册信号函数 sigaction的输入参数
int sigaction_state; // 接收注册信号函数 sigaction的 返回值(0 成功/ -1失败)
// 初始化信号变量
act.sa_handler = urg_handler; // 设置一下信号处理函数
act.sa_flags = 0; // 设置其他两个暂时用不上的为0
sigemptyset(&act.sa_mask);
// 并没有在这里注册信号,为什么呢? 后面看
// sigaction_state = sigaction(SIGCHLD, &act, 0);
// 初始化套接字、服务器地址啥的
acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
if(acpt_sock == -1){
error_handling("socket() error");
}
memset(&acpt_addr,0,sizeof(acpt_addr));
acpt_addr.sin_family = AF_INET;
acpt_addr.sin_addr.s_addr = htonl(INADDR_ANY);
acpt_addr.sin_port = htons(atoi(argv[1]));
// 服务器套接字一条龙
if(bind(acpt_sock, (struct sockaddr*)&acpt_addr, sizeof(acpt_addr)) == -1){
error_handling("bind() error");
}
if(listen(acpt_sock, 5) == -1){
error_handling("listen error");
}
// 客户端连接套接字
recv_addr_sz = sizeof(recv_addr);
recv_sock = accept(acpt_sock, (struct sockaddr*)&recv_addr, &recv_addr_sz);
// 这里有新东西 为什么在这里才注册信号?
fcntl(recv_sock, F_SETOWN, getpid());
sigaction_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;
printf("normal:");
puts(buf);
fflush(stdout);
}
close(recv_sock);
close(acpt_sock);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
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);
}
下面是测试结果:
(细心的朋友可能发现,上面在send文件中为什么 send函数和write函数之间调用了 usleep呢? 其实我也是在使用原来的代码调试之后发现并没有得到书上那样的结果,在排除了代码逻辑错误之后,思考了一下,为什么会出先下图这样的效果这样的结果明显是在进程传输的过程中出现了 传输的混乱,由于在调用send函数时,使用了MSG_OOB进行紧急传输,这回打断进程原有的执行顺序,因此可能引发各种各样奇奇怪怪的输出结果,因此在每次发送之后增加20ms的延时 ,让接收端有足够的时间能处理接收的数据。)
这里解释一下上面的fcntl函数
fcntl(recv_sock, F_SETOWN, getpid());
fcntl函数用于控制文件描述符,这里的作用是,
将文件描述符 recv_sock指向的套接字拥有者 设置为 getpid 函数的返回值,这个返回值是getpid函数作用的当前进程
在处理SIGURG信号(由于MSG_OOB产生)时,必须制定处理信号的进程,getpid返回调用此函数的进程id。
由于上面的代码中,只有一个进程,因此就由这个进程进行处理信号。
如果删去fcntl函数的结果是什么?
凡是与SIGURG相关的数据,都没了= - =。
看到运行结果,发现并没有因为采用了MSG_OOB而有限传输什么数据,一切的顺序依旧。 而且,在使用函数 urg_handler
时,也只能读取一个字节,剩下的数据只能通过未设置MSG_OOB
可选项的普通输入函数进行读取(数据接收方将通过调用常用输入函数读取剩余部分。)。
这是因为TCP不存在真正意义上的“带外数据”,实际上MSG_OOB
中的 out of band 并不是“带外数据”,真正的带外数据是:通过完全不同的通信路径传输的数据。
这里只是TCP的紧急模式(Urgent mode)进行传输,只能起到催促的作用。
13.1.3 紧急模式工作原理(MSG_OOB)
MSG_OOB可以带来下面的效果:
“这里有数据需要紧急处理,赶快进行”
啥意思呢?
MSG_OOB的真正作用在于,督促数据接受对象尽快处理数据,并非优先处理某一部分的数据。也就是说,TCP的 “保持传输顺序”的传输特性依然成立。
MSG_OOB可选项状态下,数据传输过程是什么样子的呢?
在调用了send(sock, "890", strlen("90"), MSG_OOB);
之后
紧急消息传输阶段的输出缓冲如图所示(假设前面的数据已经传出完成了)
字符串右侧偏移量为 3 的位置存有紧急指针,且紧急指针指向的偏移量之前的部分就是紧急消息。
也就是,实际只用一个字节表示紧急消息信息。可以通过下图中用于传输数据的TCP数据包(段)的结构看的更清楚。下图是设置URG的数据包
实际上TCP数据包有更多信息,但是这里只标注出来与我们有关的内容
- URG = 1: 载有紧急消息的数据包
- URG指针: 紧急指针位于偏移量为3 的位置。
但是我们无法得知,到底前面输出缓冲中的多少数据是真正的紧急数据,但是这并不影响什么。因为数据顺序是不会改变的,我们只需要加快这个过程。
换言之,紧急消息的意义在于督促消息处理,而非紧急传输形式受限的消息。
13.1.4 检查输入缓冲(MSG_PEEL + MSG_DONTWAIT)
同时设置 MSG_PEEL + MSG_DONTWAIT 选项,验证输入缓冲中是否存在接收的数据。MSG_PEEK
+ MSG_DONTWAIT
-> 瞥一眼不清空输入缓冲 + 就算没读到数据我也不阻塞。
MSG_PEEL
选项并调用recv
函数时,即使读取了输入缓冲的数据也不会删除。
因此,MSG_PEEL
经常和MSG_DONTWAIT
合作,用于调用 以非阻塞方式验证待读数据存在与否的函数。
下面通过示例看看这俩怎么回事
peek_send.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void error_handling(char* message);
int main(int argc, char *argv[])
{
/* code */
// 首先我们创建 数据发送端必须的套接字一条龙
int sock;
struct sockaddr_in send_addr; // 数据发送目标地址
if(argc!= 3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&send_addr, 0, sizeof(send_addr));
send_addr.sin_family = AF_INET;
send_addr.sin_addr.s_addr = inet_addr(argv[1]);
send_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&send_addr, sizeof(send_addr)) == -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 <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char* message);
int main(int argc, char *argv[])
{
/* code */
if(argc != 2){
printf("usage: %s <port>\n", argv[0]);
exit(1);
}
// 声明套接字相关变量
int acpt_sock, recv_sock;
struct sockaddr_in acpt_addr, recv_addr;
socklen_t recv_addr_sz;
char buf[BUF_SIZE];
int str_len, state;
// 初始化套接字、服务器地址啥的
acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
if(acpt_sock == -1){
error_handling("socket() error");
}
memset(&acpt_addr,0,sizeof(acpt_addr));
acpt_addr.sin_family = AF_INET;
acpt_addr.sin_addr.s_addr = htonl(INADDR_ANY);
acpt_addr.sin_port = htons(atoi(argv[1]));
// 服务器套接字一条龙
if(bind(acpt_sock, (struct sockaddr*)&acpt_addr, sizeof(acpt_addr)) == -1){
error_handling("bind() error");
}
if(listen(acpt_sock, 5) == -1){
error_handling("listen error");
}
// 客户端连接套接字
recv_addr_sz = sizeof(recv_addr);
recv_sock = accept(acpt_sock, (struct sockaddr*)&recv_addr, &recv_addr_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);
}
这里是测试结果:
可以看到peek_recv.c
代码中,两次调用了recv函数进行读取数据,第一次可选项采用了MSG_PEEK
+ MSG_DONTWAIT
-> 瞥一眼不清空输入缓冲 + 就算没读到数据我也不阻塞的作用。
13.2 readv & writev 函数
这俩函数和 read
& write
有什么区别呢? -》提高了通信效率,下面我们具体介绍。
13.2.1 使用readv & writev 函数
功能概括:对数据进行整合传输及发送的函数
writev函数可以将分别保存在多个缓冲中的数据一并发送,通过readv函数可以由多个缓冲分别接受。因此适当使用者2个函数可以减少I/O函数的调用次数。 下面先介绍writev函数。
(说道这里可能小伙伴们还是有点头晕,不太理解,我觉得可以把缓冲实体化,比如我们有两保存数据缓冲数组,writev 可以把两个不同的数组一起发出去。readv可以把一个东西分别保存到不同的数组中。)
#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec* iov, int iovcnt);
-> 成功时返回发送的字节数,失败时返回-1
filedes: (文件描述符)数据传输对象的套接字文件描述符。但是该函数并不只限于套接字,可以像read函数一样向其传递文件或标准输出描述符。
iov: (数据地址+大小)iovec结构体数组的地址值,结构体iovec中包含发送数据的位置和大小信息。
iovcnt: (数据数组个数!)向第二个参数中传递的数组长度。
上述函数的第二个参数中出现的数组iovec结构体的声明如下:
struct iovec
{
void* iov_base; // 缓冲地址(数组名)
void* iov_len; // 缓冲大小
}
结构体iovec
由保存 待发送数据的缓冲(char型数组)地址值 和 实际发送的数据长度信息构成。
我们通过下面的图片看看这个结构体的结构以及调用方法。
上图中的解释为:
输出目标:标准输出(控制台),输出内容及长度:ptr[0].iov
_base长度为3的空间数据 + ptr[1].iov_base长度为4的空间数据
说起来太绕口了,我们搞代码
writev.c
#include <stdio.h>
#include <sys/uio.h>
int main(int argc, char *argv[])
{
/* code */
struct iovec vec[2];
char buf1[] = "ABCDEFG";
char buf2[] = "1234567";
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;
}
可以看到,明明是两个数组中的数据,我们只调用一次writev
函数,就按照想要的顺序发送到了 控制台。
下面介绍readv
函数,它的用法与writev函数类似
#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec* iov, int iovcnt);
-> 成功时返回发送的字节数,失败时返回-1
filedes: (文件描述符)传递接收数据的套接字文件描述符。
iov: (数据地址+大小)数据保存位置和大小信息的iovec结构体数组的地址值
iovcnt: (数据数组个数!)向第二个参数中传递的数组长度。
示例:
#include <stdio.h>
#include <sys/uio.h>
#define BUF_SIZE 100
int main(int argc, char *argv[])
{
/* code */
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;
}
13.2.2 合理使用readv&7 writev函数
哪种情况下更适合使用readv和writev函数呢?
实际上:能用就用!
减少函数的调用次数能提高相应性能。更大程度上能够减少数据包的个数,
假设为了提高效率在服务器端阻止了Nagle算法
这时对于writev函数来说,更有价值,如下图,在关闭Nagle算法情况下
图中的待发送数据位于3个不同的地方,write函数需要3次
若为了提高速度关闭了Nagle算法,极有可能通过3个数据包传递数据
如果用writev将所有数据一次性写入输出缓冲,很有可能仅通过一个数据包传输数据
同时,如果将不同位置的数据按照发送顺序移动(复制)到一个大数组,并通过一次write函数调用传输。 这与调用writev函数效果相同!