linux send与recv函数详解

send和recv只是内核缓冲区和应用程序缓冲区之间的搬运工---严格来讲send和recv并不具备发送和接收功能

谈到网络socket编程, 我们不得不提两个基本也很重要的函数:send和recv.  对socket编程理解不深的童鞋容易产生这样一个错误的认识: send函数是用来发送数据, 而recv函数是用来接收数据的, 其实, 这种观点是稍微有点偏颇的, 掩盖了本质。

       下面, 我们看一幅图, 了解一下send和recv函数所处的位置(这幅图是我在网上找的, 不太清晰, 请凑合看一下):

         为了简便起见, 我们仅考虑单向的数据流, 即A(客户端)向B(服务端)发送数据。 在应用程序Program A中, 我们定义一个数组char szBuf[100] = "tcp"; 那么这个szBuf就是应用程序缓冲区(对应上图的Program A中的某块内存), send函数对应上面蓝色的Socket API, 内核缓冲区对应上面的黄色部分。 我们看到, send函数的作用是把应用程序缓冲区中的数据拷贝到内核缓冲区, 仅此而已。 内核缓冲区中的数据经过网卡, 经历网络传到B端的网卡(TCP协议), 然后进入B的内核缓冲区, 然后由recv函数剪切/复制到Program B的应用程序缓冲区。前面我们用过wireshark抓包, wireshark抓的正是流经网卡的数据。

        强调一下:

        1. 对于客户端A, 其发送的内核缓冲区和接收的内核缓冲区是不一样的, 互不干扰. 服务端B也同理。

        2. recv函数是剪切还是复制, 由最后一个参数决定, 我们在之前的博文已经讲述过了。

        

        下面, 我们不考虑recv函数, 仅仅玩转一下send(双向send), 并用wireshark抓包实验一下, 加深理解。 注意, Wireshark抓不了环回包, 所以, 需要在两台电脑上测试。

         服务端B的程序为:

 

  1. #include <stdio.h>
  2. #include <winsock2.h> // winsock接口
  3. #pragma comment(lib, "ws2_32.lib") // winsock实现
  4.  
  5. int main()
  6. {
  7. WORD wVersionRequested; // 双字节,winsock库的版本
  8. WSADATA wsaData; // winsock库版本的相关信息
  9.  
  10. wVersionRequested = MAKEWORD(1, 1); // 0x0101 即:257
  11.  
  12.  
  13. // 加载winsock库并确定winsock版本,系统会把数据填入wsaData中
  14. WSAStartup( wVersionRequested, &wsaData );
  15.  
  16.  
  17. // AF_INET 表示采用TCP/IP协议族
  18. // SOCK_STREAM 表示采用TCP协议
  19. // 0是通常的默认情况
  20. unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0);
  21.  
  22. SOCKADDR_IN addrSrv;
  23.  
  24. addrSrv.sin_family = AF_INET; // TCP/IP协议族
  25. addrSrv.sin_addr.S_un.S_addr = inet_addr("0.0.0.0"); // socket对应的IP地址
  26. addrSrv.sin_port = htons(8888); // socket对应的端口
  27.  
  28. // 将socket绑定到某个IP和端口(IP标识主机,端口标识通信进程)
  29. bind(sockSrv,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
  30.  
  31. // 将socket设置为监听模式,5表示等待连接队列的最大长度
  32. listen(sockSrv, 5);
  33.  
  34.  
  35. // sockSrv为监听状态下的socket
  36. // &addrClient是缓冲区地址,保存了客户端的IP和端口等信息
  37. // len是包含地址信息的长度
  38. // 如果客户端没有启动,那么程序一直停留在该函数处
  39. SOCKADDR_IN addrClient;
  40. int len = sizeof(SOCKADDR);
  41. unsigned int sockConn = accept(sockSrv,(SOCKADDR*)&addrClient, &len);
  42.  
  43. while(1)
  44. {
  45. getchar(); // 阻塞一下
  46. send(sockConn, "tcp", strlen("tcp") + 1, 0); // send来啦
  47. }
  48.  
  49. closesocket(sockConn);
  50. closesocket(sockSrv);
  51. WSACleanup();
  52.  
  53. return 0;
  54. }

      先启动服务端B.

 

 

      下面我们来看客户端A:

 

  1. #include <winsock2.h>
  2. #include <stdio.h>
  3. #pragma comment(lib, "ws2_32.lib")
  4.  
  5. int main()
  6. {
  7. WORD wVersionRequested;
  8. WSADATA wsaData;
  9. wVersionRequested = MAKEWORD(1, 1);
  10.  
  11. WSAStartup( wVersionRequested, &wsaData );
  12.  
  13. SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
  14.  
  15. SOCKADDR_IN addrSrv;
  16. addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.101");
  17. addrSrv.sin_family = AF_INET;
  18. addrSrv.sin_port = htons(8888);
  19. connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
  20.  
  21. while(1)
  22. {
  23. getchar();
  24. send(sockClient, "cpp", strlen("cpp") + 1, 0);
  25. }
  26.  
  27. closesocket(sockClient);
  28. WSACleanup();
  29.  
  30. return 0;
  31. }

      好, 再启动客户端A.

 

 

      我们在客户端A上安装wireshark并启动抓包, 实验发现: 

     1. 当A向B发送数据时, A上的wireshark可以抓到对应的包, 因为数据经过了A的网卡。(不管B是否有去recv)

     2. 当B向A发送数据时, A上的wireshark也可以抓到对应的包,    因为数据到达了A的内核缓冲区, 也经历了A的网卡。(不管A是否有去recv)

 

      相信通过本文的讨论, 又加深了对send和recv的认识。 未来, 路漫漫, 我们慢慢来。

Linux网络编程--recv函数返回值详解

        转载地址: http://www.cnblogs.com/Genesis-007/p/5190552.html

        

        说明: 文中红色部分是我的备注

 

 

函数原型:

1
ssize_t recv( int  sockfd,  void  *buf,  size_t  len,  int  flags);

该函数第一个参数制定接收端套接字描述符;

第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;

第三个参数指明缓冲区buf的长度;

第四个参数一般置0; (标志位)

recv函数返回其实际copy的字节数(第四个参数为0的时候, 其实是剪切数据, 而不是copy数据)。如果recv在copy时出错,那么它返回SOCKET_ERROR; 如果recv函数在等待协议接收数据时网络中断了,那么它返回0(比如对端close socket);recv函数仅仅是copy数据, 真正的接收数据是协议来完成的(恩, 发送数据要靠协议栈)。

 

默认recv函数socket 是阻塞的
阻塞与非阻塞recv返回值没有区分(小于0的时候, 会有一点点区别, 如下会说到),都是
<0 出错
=0 连接关闭
>0 接收到数据大小,

特别:返回值<0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的,继续接收。
只是阻塞模式下recv会阻塞着接收数据,非阻塞模式下如果没有数据会返回,不会阻塞着读,因此需要循环读取)。

返回说明:
a. 成功执行时,返回接收到的字节数。另一端已关闭则返回0。

b. 失败返回-1,errno被设为以下的某个值
EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时
EBADF:sock不是有效的描述词
ECONNREFUSE:远程主机阻绝网络连接
EFAULT:内存空间访问出错
EINTR:操作被信号中断
EINVAL:参数无效
ENOMEM:内存不足
ENOTCONN:与面向连接关联的套接字尚未被连接上
ENOTSOCK:sock索引的不是套接字

c. 当返回值是0时,为正常关闭连接;

https://blog.csdn.net/stpeace/article/details/73435484

       转载地址: http://blog.csdn.net/sjin_1314/article/details/9565743

      

 

[csharp]  view plain  copy
 
  print ?
  1. 1 #include <sys/socket.h>  
  2. 2 ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);  
  3. 3 ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);  

 

 

recv 和send的前3个参数等同于read和write。

flags参数值为0或:

 
flags 说明 recv send
 MSG_DONTROUTE 绕过路由表查找      •
 MSG_DONTWAIT 仅本操作非阻塞    •       •
 MSG_OOB     发送或接收带外数据   •   •
 MSG_PEEK   窥看外来消息   •  
 MSG_WAITALL   等待所有数据    •  

 1. send解析

 sockfd:指定发送端套接字描述符。

 buff:    存放要发送数据的缓冲区

 nbytes:  实际要改善的数据的字节数

 flags:   一般设置为0

 1) send先比较发送数据的长度nbytes和套接字sockfd的发送缓冲区的长度,如果nbytes > 套接字sockfd的发送缓冲区的长度, 该函数返回SOCKET_ERROR;

 2) 如果nbtyes <= 套接字sockfd的发送缓冲区的长度,那么send先检查协议是否正在发送sockfd的发送缓冲区中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送sockfd的发送缓冲区中的数据或者sockfd的发送缓冲区中没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和nbytes

 3) 如果 nbytes > 套接字sockfd的发送缓冲区剩余空间的长度,send就一起等待协议把套接字sockfd的发送缓冲区中的数据发送完

 4) 如果 nbytes < 套接字sockfd的发送缓冲区剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把套接字sockfd的发送缓冲区中的数据传到连接的另一端的,而是协议传送的,send仅仅是把buf中的数据copy到套接字sockfd的发送缓冲区的剩余空间里)。

 5) 如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR; 如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。

 6) send函数把buff中的数据成功copy到sockfd的改善缓冲区的剩余空间后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR。(每一个除send的socket函数在执行的最开始总要先等待套接字的发送缓冲区中的数据被协议传递完毕才能继续,如果在等待时出现网络错误那么该socket函数就返回SOCKET_ERROR)

 7) 在unix系统下,如果send在等待协议传送数据时网络断开,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的处理是进程终止。

2.recv函数

sockfd: 接收端套接字描述符

buff:   用来存放recv函数接收到的数据的缓冲区

nbytes: 指明buff的长度

flags:   一般置为0

 1) recv先等待s的发送缓冲区的数据被协议传送完毕,如果协议在传送sock的发送缓冲区中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR

 2) 如果套接字sockfd的发送缓冲区中没有数据或者数据被协议成功发送完毕后,recv先检查套接字sockfd的接收缓冲区,如果sockfd的接收缓冲区中没有数据或者协议正在接收数据,那么recv就一起等待,直到把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲区中的数据copy到buff中(注意协议接收到的数据可能大于buff的长度,所以在这种情况下要调用几次recv函数才能把sockfd的接收缓冲区中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的)

 3) recv函数返回其实际copy的字节数,如果recv在copy时出错,那么它返回SOCKET_ERROR。如果recv函数在等待协议接收数据时网络中断了,那么它返回0。

 4) 在unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用 recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

 

下面是非阻塞模式下的安全读写

[csharp]  view plain  copy
 
  print ?
  1. static int tcpSockRead(int fd, void *buf, int nSize)  
  2. {  
  3.     int rleft = nSize;  
  4.     int rlen;  
  5.     char *ptr = (char *)buf;  
  6.     time_t start,end;  
  7.     time(&start);  
  8.   
  9.     while (rleft > 0) {   
  10.         if ((rlen = read(fd,ptr,rleft)) < 0) {  
  11.             if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN ) {  
  12.                 rlen = 0;  
  13.             }else{  
  14.                 //LogEvent(ANT_EVENT_INFO, "RTP Read error: %s\n", strerror(errno));  
  15.                 return -1;  
  16.             }  
  17.         } else if (rlen == 0){   
  18.             break;  
  19.         }  
  20.   
  21.         rleft -= rlen;  
  22.         ptr += rlen;  
  23.         time(&end);  
  24.         if(end-start >= 30){  
  25.             LogTcpRtp("###%s(),time is too long!\n",__FUNCTION__);  
  26.             return -1;  
  27.         }  
  28.     }  
  29.       
  30.     if (rleft > 0){  
  31.         return -1;  
  32.     }else{  
  33.         return 0;  
  34.     }   
  35. }  
  36.   
  37. static int tcpSockWrite(int fd, void *buf, int nSize)  
  38. {  
  39.     int wleft = nSize;  
  40.     int wlen;  
  41.     char *ptr = (char *)buf;  
  42.   
  43.     while (wleft > 0) {  
  44.         if ((wlen = write(fd, ptr, wleft)) <= 0) {  
  45.             if (wlen < 0 && (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)){  
  46.                 wlen = 0;  
  47.             }else{  
  48.                 //LogEvent(ANT_EVENT_INFO, "RTP write error: %s\n", strerror(errno));  
  49.                 return -1;  
  50.             }  
  51.         }   
  52.         wleft -= wlen;  
  53.         ptr += wlen;  
  54.     }  
  55.   
  56.     if (wleft > 0){  
  57.         return -1;         
  58.     }else{   
  59.         return 0;  
  60.     }  
  61. }  

errno  EINTR 、EWOULDBLOCK 、EAGAIN在errno.h文件中有详细的宏定义,可以参考百度百科。
EAGAIN 11    /* Try again */
EWOULDBLOCK  用于非阻塞模式,不需要重新读或者写(如何设置成非阻塞模式,这个必须要加,在测试中,经常会出现这个错误码!)
EINTR        表示由于信号中断,没写成功任何数据!

 

posted @ 2021-12-10 10:03  CharyGao  阅读(192)  评论(0)    收藏  举报