1. Linux的send&recv函数
1.1 send函数和recv函数
cpp
#include <sys/socket.h>
ssize_t send(
int sockfd, //套接字文件描述符
const void* buf, //保存待传输数据的缓冲地址值
size_t nbytes, //待传输的字节数
int flags //传输数据时指定的可选项信息
);
成功返回发送的字节数,失败返回-1
cpp
#include <sys/socket.h>
ssize_t recv(
int sockfd, //套接字文件描述符
const void* buf, //保存接收数据的缓冲地址值
size_t nbytes, //接收的字节数
int flags //接收数据时指定的可选项信息
);
成功返回发送的字节数,失败返回-1
Linux的send和recv函数与windows相比,实际差别不大。
1.2 flags参数(传输时指定的可选项信息)
|---------------|----------------------------|------|------|
| 可选项 | 含义 | send | recv |
| MSG_OOB | 用于传输带外数据(out-of-band data) | ✔ | ✔ |
| MSG_PEEK | 验证输入缓冲中是否存在接收的数据 | ✘ | ✔ |
| MSG_DONTROUTE | 数据传输过程中不参照路由表,在本地网络中寻找目的地 | ✔ | ✘ |
| MSG_DONTWAIT | 调用I/O函数时不阻塞,用于非阻塞I/O | ✔ | ✔ |
| MSG_WAITALL | 防止程序返回,直到接收全部请求的字节数 | ✘ | ✔ |
可以用过"|"运算,同时设置。
1.2.1 MSG_OOB(发送紧急消息)
发送端使用MSG_OOB:
cpp
send(sockfd,"123",strlen("123"),MSG_OOB);
紧急消息的发送比较简单,直接指定MSG_OOB可选项即可。
接收端使用MSG_OOB:
cpp
void Message_Handle(int signo)
{
......
int recv_len=recv(clientfd,buff,sizeof(buff),MSG_OOB);
......
}
int main()
{
sigaction act;
act.sa_handler=Message_Handle;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
......
fcntl(clientfd,F_SETOWN,getpid()); //(1)
int state=sigaction(SIGURG,&act,0); //(2)
......
}
fcntl函数的意义:将文件描述符clientfd指向的套接字引发的SIGURG信号处理进程,变为将getpid函数返回值用作ID的进程。
为什么要执行fcntl函数?
因为:如果通过了fork函数创建了子进程,此时文件描述符也会一并复制一份给子进程,这样的话,当SIGURG信号触发时,那应该用哪个进程来处理信号呢?所以这里要使用fcntl函数,来指定处理信号的进程是当前主进程。
综上,接收端使用MSG_OOB必须要有以下步骤:
- 执行fcntl函数
- 注册SIGURG信号
- 在处理函数中接收紧急消息。
cpp
发送端顺序:"123","4","567","890";
其中"4"、"890"是紧急消息
接收结果:"123","4","567","0","89";
根据上述结果得知,MSG_OOB的作用,不像我们想象的那样,指定了紧急消息的信息会先到达。并且,我们发送的是一串紧急消息,但最终只读取了一个字节。
实际上,MSG_OOB的作用是:督促数据接收对象尽快处理数据,而不是让信息传输最快。(就像病人送去急诊,MSG_OOB的作用不是让病人尽快到达医院,而是当病人到达医院后,通知医生,让医生来紧急治疗这个病人。这个医生就是程序员)
这是因为,虽然OOB是需要通过单独的通信路径高速传输数据,但是TCP并不另外提供,TCP只是利用其紧急模式来进行传输。
TCP紧急模式工作原理:
当设置了MSG_OOB时,发送端的输出缓冲中的状态为如下形式:
以这个数据缓冲的最左端为偏移量0,基于这个偏移量,这个数据缓冲的尾部之后的一个位置,如上图中的偏移量为3的位置,就存有紧急指针,紧急指针指向紧急消息的下一个位置。当传递时,会告知对方主机,在这个紧急指针的前面的部分都是紧急消息。实际上,只用一个字节来表示紧急消息的信息。即发送端接收到紧急消息只会接收到"0",而余下的部分,将会由常用输入函数(read)读取。
为什么收到的紧急消息不能直接读取0,90,890?
答:因为这些并不重要,URG_OOB的作用只是督促消息处理,而非紧急传输形式受限的消息。
1.2.2 MSG_PEEK和MSG_DONTWAIT
MSG_PEEK和MSG_DONTWAIT一般是通过"|"同时设置的,用来验证输入缓冲中是否存在接收的数据。但注意,设置了MSG_PEEK选项的recv函数时,读取了输入缓冲里的数据后,并不会删除这个数据。在下一次执行没有设置MSG_PEEK的recv函数时,会再一次读取,并在此次才会删除。所以两者合用,用以调用以非阻塞形式方式验证待读数据存在与否。
cpp
发送端:
write(sockfd,"123",strlen("123"));
接收端:
int rec_len=recv(sockfd,buff,sizeof(buff),MSG_PEEK|MSG_DONTWAIT);
//这时如果rec_len>0那么就意味着有数据存在。
//此时buff里存有数据123
recv(sockfd,buff,sizeof(buff),0);
//这时读取的buff里仍然是123,因为上一句使用了MSG_PEEK,没有将此数据从输入缓冲中删除。
2. readv&writev函数
cpp
#include<sys/uio.h>
ssize_t writev(
int filedes, //表示数据传输对象的套接字文件描述符,
//不仅仅可以是套接字文件描述符,
//也可以传递文件或标准输出描述符
const struct iovec* iov, //iovec结构体数组的地址值,结构体中包含待发送数据的位置和大小
int iovcnt //向第二个参数传递的数组长度
);
cpp
#include<sys/uio.h>
ssize_t readv(
int filedes, //表示数据接收对象的套接字文件描述符,
//不仅仅可以是套接字文件描述符,
//也可以传递文件或标准输出描述符
const struct iovec* iov, //iovec结构体数组的地址值,结构体中包含数据保存的位置和大小
int iovcnt //向第二个参数传递的数组长度
);
cpp
struct iovec
{
void* iov_base; //缓冲地址
size_t iov_len; //缓冲大小
}
readv和writev函数的主要作用是:提高数据通信效率,适当的使用,可以减少I/O函数的调用次数提高性能。
cpp
writev(1,ptr,2);
第一个参数1,表示是向控制台输出数据,第二个参数ptr,保存有待发送数据信息的iovec数组指针,第三个参数2,表示从ptr指向的地址开始,读取2个iovec结构体变量,发送这些数据指向的缓冲数据。如图:
如代码:
cpp
发送端:
int main()
{
char buf1[]="ABCDEFG";
char buf2[]="1234567";
iovec vec[2];
vec[0].iov_base=buf1;
vec[0].iov_len=3;
vec[1].iov_base=buf2;
vec[1].iov_len=4;
writev(1,vec,2);
}
控制台输出为:ABC1234
一次writev即可将两个缓冲buf里的数据分别按字节大小读取出来。
3. Windows的不同
3.1 MSG_OOB的读取与Linux不同
在windows操作系统中,没有如Linux那般的信号处理机制。所以在使用MSG_OOB时,Windows得做特殊处理。即使用select函数。在select函数中有个"发送异常的套接字"的参数,这里的异常指的是:不同寻常的程序执行流,所以收到的out-of-band数据也属于异常,所以利用这一特性,可以使用select函数来接收紧急消息。如代码:
cpp
发送端:
int main()
{
......
send(hSocket,"4",1,MSG_OOB);
......
}
接收端:
int main()
{
......
int result=select(0,&readCopy,0,&exceptCopy,&timeout);
if(result>0)
{
if(FD_ISSET(hRecvSock, &exceptCopy)
{
stelen=recv(hRecvSock,buf,sizeof(buf),MSG_OOB);
......
}
......
}
......
}