send和recv内核(系统)缓冲区与自己定义的buf的关系;阻塞与非阻塞问题

转载: https://blog.csdn.net/stpeace/article/details/43719449

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


      下面我们来看客户端A:


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


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

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

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

以recv为例当应用程序调用时:
(1) recv先等待socket的发送缓冲区(内核/系统)中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR
(2) 如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据。那么recv就一直等待(阻塞),直到协议把数据接收完毕。
当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf
(注意协议接收到的数据可能大于buf的长度所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据真正的接收数据是协议来完成的)
recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR.如果recv函数在等待协议接收数据时网络中断了那么它返回0。
注意在Unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

以send为例当应用程序调用时:
(1) send先比较待发送数据的长度len和套接字s的发送缓冲的长度,如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR。
(2) 如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完。
如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len
(3) 如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完
(4) 如果len小于剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里
(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。

阻塞与非阻塞:
阻塞就是干不完不准回来
非组赛就是你先干,我现看看有其他事没有,完了告诉我一声
我们拿最常用的send和recv两个函数来说吧.
比如你调用send函数发送一定的Byte,在系统内部send做的工作其实只是把数据传输(Copy)到TCP/IP协议栈的输出缓冲区,它执行成功并不代表数据已经成功的发送出去了,
如果TCP/IP协议栈没有足够的可用缓冲区来保存你Copy过来的数据的话.这时候就体现出阻塞和非阻塞的不同之处了,
对于阻塞模式的socket send函数将不返回直到系统缓冲区有足够的空间把你要发送的数据Copy过去以后才返回,
而对于非阻塞的socket来说send会立即返回WSAEWOULDDBLOCK告诉调用者说"发送操作被阻塞了,你想办法处理吧."


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




     


转载: https://blog.csdn.net/stpeace/article/details/43719449

猜你喜欢

转载自blog.csdn.net/weixin_43056962/article/details/83748159