注:本章所讲的所有函数都是基于Linux环境。
send & recv 函数
函数原型
俩函数是一对,因此需要一起介绍,我们要把生产者-消费者模型去理解send()与recv()函数功能,每次recv()缓冲区就会减少数据。直至为空
#include<sys/socket.h>
ssize_t send(int sockfd, const void* buf, size_t nbytes, int flags);
/*
sockfd: 表示与数据传输对象的连接的套接字文件描述符
buf: 保存待传输数据的缓冲地址
nbytes: 待传输的字节数
flags: 传输数据时指定的可选信息
*/
#include<sys/socket.h>
ssize_t recv(int sockfd, void* buf, size_t nbytes, int flags);
/*
sockfd: 表示与数据接收对象的连接的套接字文件描述符
buf: 保存接收数据的缓冲地址
nbytes: 可接收的最大字节数
flags: 接收数据时指定的可选项信息
*/
send函数与recv函数的最后一个参数是收发数据时的可选项,该可选项利用“位或运算”(|运算符),可同时传递多个信息。下表是可选项的种类和定义。
可选项
可选项 | 含义 | send | recv |
---|---|---|---|
MSG_OOB | 传输带外数据(用于发送紧急消息) | Y | Y |
MSG_PEEK | 验证输入缓冲中是否存在接收的数据 | Y | |
MSG_DONTROUTE | 数据传输过程中不参照路由表,在本地网络中寻找目的地 | Y | |
MSG_DONTWAIT | 调用I/O函数时不阻塞,用于使用非阻塞I/O | Y | Y |
MSG_WAITALL | 防止函数返回,直到接收全部请求的字节数 | Y |
这里只介绍其中的MSG_OOB与MSG_DONTWAIT可选项。
MSG_OOB
该选项用于发送“带外数据”紧急消息,这里说的紧急消息不是指传输速度要快于普通消息,而是提示接收对象尽快处理消息,TCP的紧急消息无法保证及时亦或加速到达目的地,但是能实现优先处理。
MSG_OOB测试程序,可以试试多次运行,观察输出顺序是否有什么不同
//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 const *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);
}
/**
* @brief 信号处理
* @note
* @retval None
*/
act.sa_handler=urg_handler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
/**
* @brief 套接字操作
* @note
* @retval None
*/
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)-1,0))!=0)
{
if(str_len==-1)continue;
buf[str_len]=0;
puts(buf);
}
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);
}
//oob_send.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30;
void error_handling(char *message);
int main(int argc, char const *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",sizeof("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);
}
MSG_OOB+MSG_DONTWAIT选项
我们同时设置MSG_OOB以及MSG_DONTWAIT选项,验证输入缓冲中是否存在可接收的数据。当我们设置了MSG_PEEK选项并调用recv函数时,即使读取了输入缓冲的数据也不会删除。因此该选项通常与MSG_DONTWAIT合作,用于调用以非阻塞的方式验证待读数据存在与否的函数
//peek_send.c
#include<stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
void error_handling(char *message);
int main(int argc, char const *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 <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char*message);
int main(int argc, char const *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);
}
编译运行:
编译:
gcc peek_send.c -o peek_send
gcc peek_recv.c -o peek_recv
-----------------------------------------
运行结果:
Buffering 3 bytes :123
Read again : 123
通过运行结果可知,我们从输入缓冲中读取了两次,第一次是非清除式读取,第二次读取是在正常的生产者-消费者模型。
readv & writev函数
read()以及write()函数为什么还需要readv(),write()函数?是因为read()与write()函数只能在连续内存进行读取或者写入。但如果我们有多个分布在不同内存空间的数据需要一起读取或者写入,当然能通过多次调用read()与write函数进行读取,但从操作系统角度看这就增加了访问内存的次数,因此是一种非常低效的方式;而readv()与writev()函数出现就是解决数据不连续的读取、写入问题。我们依然通过例子来进行理解。
函数原型
#include<sys/uio.h>
ssize_t writev(int filedes,const struct iovec *iov,int iovcnt);
/*
filedes: 表示数据传输对象的套接字文件描述符、目标文件、标准输出描述符
iov: iovec结构体数组地址值,该结构体包含待发送数据的位置和大小信息。
iovcnt: 向第二个参数传递的数组长度
*/
#include<sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
/*
filedes: 传递接收数据的文件描述符、套接字
iov: 包含数据保存位置和大小信息的iovec结构体数组的地址值
iovcnt: 第二个参数中数组长度
*/
struct iovec
{
void * iov_base; //缓冲地址
size_t iov_len; //缓冲大小
}
注意理解iovec结构体,该结构体里面包含一个指向某个内存空间的指针以及一个需要从该空间读取数据大小的变量。例如write(1 , ptr , 2 )表示向标准输出写入2块(第三个参数指明)数据,这两块待写入的位置被iovec结构体保存。也就是说这两个结构体在内存空间上是连续的,只是我们待写入的空间不连续而已。
//*********************测试writev函数*****************
//即测试当数据来源多个地址空间,如和写入数据
#include<stdio.h>
#include<sys/uio.h>
int main(int argc, char const *argv[])
{
struct iovec vec[2];
char buf1[]="ABCDEFG";
char buf2[]="1234567";
int str_len;
vec[0].iov_base=buf1;
vec[0].iov_len=3; //虽然buf1有七个字符,但我们测试只读取三个
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;
}
运行结果:
ABC1234
write bytes :7
//******************测试readv函数********************
//即测试接受数据时,缓冲地址不连续如何读取数据
#include <stdio.h>
#include <sys/uio.h>
#define BUF_SIZE 100
int main(int argc, char const *argv[])
{
struct iovec vec[2];
char buf1[BUF_SIZE]={0,}; //缓冲区1
char buf2[BUF_SIZE]={0,}; //缓冲区2
int str_len;
vec[0].iov_base=buf1;
vec[0].iov_len=5; //设置缓冲区1存放数据大小
vec[1].iov_base=buf2;
vec[1].iov_len=BUF_SIZE;//设置缓冲区2存放数据大小
/**
* @brief 从标准输入端(也就是控制端)读取
* @note
* @retval
*/
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;
}
运行结果
//运行结果1
Program/code/Chapter_13$ ./readv
1234567890
Read Bytes:11
First message :12345
Second message: 67890
//运行结果2
Program/code/Chapter_13$ ./readv
123456
Read Bytes:7
First message :12345
Second message: 6
结语
readv与writev函数基本上可以替代read()与write函数,但是对于连续空间的读取与写入还是使用read()与write()函数方便。但是在网络编程当中,如果为了快速传递数据而禁用了Nagle算法,采用readv()+writev()函数更加合适,因为数据不一定连续,采用readv()+writev()函数能使得多个数据通过一个数据包发送出去,而不会造成资源浪费。因为如果采用read()+write()函数,发送的的每一个数据包只能对应其中一块地址空间的数据,即使该地址空间数据非常少也是占一个数据包。