- #include
- ssize_t send(
- int sockfd, //套接字文件描述符
- const void* buf, //保存待传输数据的缓冲地址值
- size_t nbytes, //待传输的字节数
- int flags //传输数据时指定的可选项信息
- );
- 成功返回发送的字节数,失败返回-1
- #include
- ssize_t recv(
- int sockfd, //套接字文件描述符
- const void* buf, //保存接收数据的缓冲地址值
- size_t nbytes, //接收的字节数
- int flags //接收数据时指定的可选项信息
- );
- 成功返回发送的字节数,失败返回-1
Linux的send和recv函数与windows相比,实际差别不大。
可选项 | 含义 | send | recv |
MSG_OOB | 用于传输带外数据(out-of-band data) | ✔ | ✔ |
MSG_PEEK | 验证输入缓冲中是否存在接收的数据 | ✘ | ✔ |
MSG_DONTROUTE | 数据传输过程中不参照路由表,在本地网络中寻找目的地 | ✔ | ✘ |
MSG_DONTWAIT | 调用I/O函数时不阻塞,用于非阻塞I/O | ✔ | ✔ |
MSG_WAITALL | 防止程序返回,直到接收全部请求的字节数 | ✘ | ✔ |
可以用过"|"运算,同时设置。
发送端使用MSG_OOB:
send(sockfd,"123",strlen("123"),MSG_OOB);
紧急消息的发送比较简单,直接指定MSG_OOB可选项即可。
接收端使用MSG_OOB:
- 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必须要有以下步骤:
- 发送端顺序:"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的作用只是督促消息处理,而非紧急传输形式受限的消息。
MSG_PEEK和MSG_DONTWAIT一般是通过"|"同时设置的,用来验证输入缓冲中是否存在接收的数据。但注意,设置了MSG_PEEK选项的recv函数时,读取了输入缓冲里的数据后,并不会删除这个数据。在下一次执行没有设置MSG_PEEK的recv函数时,会再一次读取,并在此次才会删除。所以两者合用,用以调用以非阻塞形式方式验证待读数据存在与否。
- 发送端:
- 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,没有将此数据从输入缓冲中删除。
- #include
- ssize_t writev(
- int filedes, //表示数据传输对象的套接字文件描述符,
- //不仅仅可以是套接字文件描述符,
- //也可以传递文件或标准输出描述符
- const struct iovec* iov, //iovec结构体数组的地址值,结构体中包含待发送数据的位置和大小
- int iovcnt //向第二个参数传递的数组长度
- );
- #include
- ssize_t readv(
- int filedes, //表示数据接收对象的套接字文件描述符,
- //不仅仅可以是套接字文件描述符,
- //也可以传递文件或标准输出描述符
- const struct iovec* iov, //iovec结构体数组的地址值,结构体中包含数据保存的位置和大小
- int iovcnt //向第二个参数传递的数组长度
- );
- struct iovec
- {
- void* iov_base; //缓冲地址
- size_t iov_len; //缓冲大小
- }
readv和writev函数的主要作用是:提高数据通信效率,适当的使用,可以减少I/O函数的调用次数提高性能。
writev(1,ptr,2);
第一个参数1,表示是向控制台输出数据,第二个参数ptr,保存有待发送数据信息的iovec数组指针,第三个参数2,表示从ptr指向的地址开始,读取2个iovec结构体变量,发送这些数据指向的缓冲数据。如图:
如代码:
- 发送端:
- 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里的数据分别按字节大小读取出来。
在windows操作系统中,没有如Linux那般的信号处理机制。所以在使用MSG_OOB时,Windows得做特殊处理。即使用select函数。在select函数中有个“发送异常的套接字”的参数,这里的异常指的是:不同寻常的程序执行流,所以收到的out-of-band数据也属于异常,所以利用这一特性,可以使用select函数来接收紧急消息。如代码:
- 发送端:
- 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);
- ......
- }
- ......
- }
- ......
- }
可以看 [C++ 网络协议] 重叠I/O模型 章节里讲述的WSASend和WSARecv函数,里面的第二个和第三个参数,就具有Linux的writev和readv函数功能。