send()和recv()函数详解

send()函数


 int send( SOCKET s, const char FAR *buf, int len, int flags );

不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。

客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。
该函数的参数:

  1. 第一个参数指定发送端套接字描述符;
  2. 第二个参数指明一个存放应用程序要发送数据的缓冲区;
  3. 第三个参数指明实际要发送的数据的字节数;
  4. 第四个参数一般置0。

这里只描述同步Socket的send函数的执行流程。当调用该函数时:

(1) send先比较待发送数据的长度len和套接字s的发送缓冲区的长度,如果len大于s的发送缓冲区的长度,该函数返回 SOCKET_ERROR;

(2) 如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据,如果是,就等待协议把数据发送完;如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么 send就比较s的发送缓冲区的剩余空间和len,如果len大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发送完,如果len小于剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。


要注意send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR)
注意:在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

recv()函数


int recv( SOCKET s, char FAR *buf, int len, int flags); 

不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。

该函数的参数:

  1. 第一个参数指定接收端套接字描述符;
  2. 第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
  3. 第三个参数指明buf的长度;
  4. 第四个参数一般置0。

这里只描述同步Socket的recv函数的执行流程。当应用程序调用recv函数时,recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,如果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信号,进程对该信号的默认处理是进程终止。

从缓冲上看阻塞与非阻塞socket在发送接收上的区别


首先socket在默认情况下是阻塞状态的,这就使得发送以及接收操作处于阻塞的状态,即调用不会立即返回,而是进入睡眠等待操作完成。

一. 发送选用send(这里特指TCP)以及sendto(这里特指UDP)来描述

    首先需要说明的是,不管阻塞还是非阻塞,在发送时都会将数据从应用缓冲区拷贝到内核缓冲区(SO_RCVBUF选项声明,除非缓冲区大小为0)。 

    在阻塞模式下send操作将会等待所有数据均被拷贝到发送缓冲区后才会返回。

    如果当前发送缓冲总大小为8192,已经拷贝到缓冲的数据为8000,那剩余的大小为192,现在需要发送2000字节数据,那阻塞发送就会等待缓冲区足够把所有2000字节数据拷贝进去,如第一次拷贝进192字节,当缓冲区成功发送出1808字节后,再把应用缓冲区剩余的1808字节拷贝到内核缓冲区,而后send操作返回成功发送字节数。

    从上面的过程不难看出,阻塞的send操作返回的发送大小,必然是你参数中的发送长度的大小。

    在阻塞模式下的sendto操作不会阻塞。

    关于这一点的原因在于:UDP并没有真正的发送缓冲区,它所做的只是将应用缓冲区拷贝给下层协议栈,在此过程中加上UDP头,IP头,所以实际不存在阻塞。

    在非阻塞模式下send操作调用会立即返回。

    关于立即返回大家都不会有异议。还是拿阻塞send的那个例子来看,当缓冲区只有192字节,但是却需要发送2000字节时,此时调用立即返回,并得到返回值为192。从中可以看到,非阻塞send仅仅是尽自己的能力向缓冲区拷贝尽可能多的数据,因此在非阻塞下send才有可能返回比你参数中的发送长度小的值。

    如果缓冲区没有任何空间时呢,也就是发送方缓冲区空间为0。这时肯定也是立即返回,但是你会得到WSAEWOULDBLOCK/E WOULDBLOCK 的错误,也就是返回-1,同时设置errno为EAGAIN。此时表示你无法拷贝任何数据到缓冲区,你最好休息一下再尝试发送。

    在非阻塞模式下sendto操作 不会阻塞(与阻塞一致,不作说明)。

二. 接收选用recv(这里特指TCP)以及recvfrom(这里特指UDP)来描述

    在阻塞模式下recv,recvfrom操作将会阻塞 到缓冲区里有至少一个字节(TCP)或者一个完整UDP数据报才返回。

    在没有数据到来时,对它们的调用都将处于睡眠状态,不会返回。

    在非阻塞模式下recv,recvfrom操作将会立即返回。

    如果缓冲区 有任何一个字节数据(TCP)或者一个完整UDP数据报,它们将会返回接收到的数据大小。而如果没有任何数据则返回错误 WSAEWOULDBLOCK/E WOULDBLOCK。

 注:内核缓冲区就是协议栈的缓冲区,内核缓冲区又分为发送缓冲区和接收缓冲区。在应用程序代码里开辟的缓冲区称为应用缓冲区。


 

看几个实际的例子来说明阻塞和非阻塞send的区别:

 

先看看在阻塞模式下send的表现吧(注意缓冲区的大小,我这里是16k)
1. 发送一个小于16k的数据,send马上就返回了,也就说是,send把待发送的数据放入发送缓冲马上就返回了,前提是发送的数据字节数小于缓冲大小
2. 发送一个大于16k的数据,send没有马上返回,阻塞了一下,send一定要把所有数据放入缓冲区才会返回,
假设我们发32k的数据,当send返回的时候,有16k数据已经到达另一端,剩下16k还在缓冲里面没有发出去

在阻塞模式下:
如果发送成功,返回的nBytes一定等于len
        nBytes = send(m_socket,buf,len,0);
也就是在上面代码中那个发送循环其实是没有必要的

在非阻塞模式下的情况:
1,发送一个小于16k的数据,send马上返回了,而且返回的字节长度是等于发送的字节长度的,情况和阻塞模式是向相同的

2,发送一个大于16k的数据,send也是马上就返回了,返回的nByte小于待发送的字节数
      来模拟一下实际情况,假设我们有32k的数据要发送,

      第一次send,返回16384字节(16k),也就是填满了缓冲区
      第二次send,在这之前sleep了1000毫秒,这段时间可能已经有5000字节从缓冲区发出,到达另外一端了,于是缓冲区空了5000字节出来,
相应的,这次返回的是5000,表示新放入了5000字节到缓冲区
      第三次send  ,和第二次相同,又放了6000字节
      最后一次send,放入了剩下的字节数,这个时候缓冲还是有数据的。

在发送大于16k数据的情况下,send发送循环就是必须的了。

 

linux下缓冲区的大小查看


linux下可用 sysctl -a | grep net.ipv4.tcp_wmem 查看系统默认的发送缓存大小:

这有三个值:
第一个值是socket的发送缓存区分配的最少字节数
第二个值是默认值(该值会被net.core.wmem_default覆盖),缓存区在系统负载不重的情况下可以增长到这个值
第三个值是发送缓存区空间的最大字节数(该值会被net.core.wmem_max覆盖)

根据实际测试,如果手工更改了net.ipv4.tcp_wmem的值,则会按更改的值来运行,否则在默认情况下,协议栈通常是按net.core.wmem_default和net.core.wmem_max的值来分配内存的。

应用程序应该根据应用的特性在程序中更改发送缓存大小。

socklen_t sendbuflen = 0;
socklen_t len = sizeof(sendbuflen); 
getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len);
printf("default,sendbuf:%d/n", sendbuflen);

sendbuflen = 10240;
setsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, len); 
getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len); 
printf("now,sendbuf:%d/n", sendbuflen);

需要注意的是,虽然将发送缓存设置成了10k,但实际上,协议栈会将其扩大1倍,设为20k。


-------------------实例分析----------------------

在实际应用中,如果发送端是非阻塞发送,由于网络的阻塞或者接收端处理过慢,通常出现的情况是,发送应用程序看起来发送了10k的数据,但是只发送了2k到对端缓存中,还有8k在本机缓存中(未发送或者未得到接收端的确认).那么此时,接收应用程序能够收到的数据为2k.假如接收应用程序调用recv函数获取了1k的数据在处理,在这个瞬间,发生了以下情况之一,双方表现为:

A. 发送应用程序认为send完了10k数据,关闭了socket:
发送主机作为tcp的主动关闭者,连接将处于FIN_WAIT1的半关闭状态(等待对方的ack),并且,发送缓存中的8k数据并不清除,依然会发送给对端.如果接收应用程序依然在recv,那么它会收到余下的8k数据(这个前题是,接收端会在发送端FIN_WAIT1状态超时前收到余下的8k数据.), 然后得到一个对端socket被关闭的消息(recv返回0).这时,应该进行关闭.

B. 发送应用程序再次调用send发送8k的数据:
假 如发送缓存的空间为20k,那么发送缓存可用空间为20-8=12k,大于请求发送的8k,所以send函数将数据做拷贝后,并立即返回8192;

假如发送缓存的空间为12k,那么此时发送缓存可用空间还有12-8=4k,send()会返回4096,应用程序发现返回的值小于请求发送的大小值后,可以认为缓存区已满,这时必须阻塞(或通过select等待下一次socket可写的信号),如果应用程序不理会,立即再次调用send,那么会得到-1的值, 在linux下表现为errno=EAGAIN.

C. 接收应用程序在处理完1k数据后,关闭了socket:
接收主机作为主动关闭者,连接将处于FIN_WAIT1的半关闭状态(等待对方的ack).然后,发送应用程序会收到socket可读的信号(通常是 select调用返回socket可读),但在读取时会发现recv函数返回0,这时应该调用close函数来关闭socket(发送给对方ack);

如果发送应用程序没有处理这个可读的信号,而是在send,那么这要分两种情况来考虑,假如是在发送端收到RST标志之后调用send,send将返回 -1,同时errno设为ECONNRESET表示对端网络已断开,但是,也有说法是进程会收到SIGPIPE信号,该信号的默认响应动作是退出进程,如果忽略该信号,那么send是返回-1,errno为EPIPE(未证实);如果是在发送端收到RST标志之前,则send像往常一样工作;

以上说的是非阻塞的send情况,假如send是阻塞调用,并且正好处于阻塞时(例如一次性发送一个巨大的buf,超出了发送缓存),对端socket关闭,那么send将返回成功发送的字节数,如果再次调用send,那么会同上一样.

D. 交换机或路由器的网络断开:
接收应用程序在处理完已收到的1k数据后,会继续从缓存区读取余下的1k数据,然后就表现为无数据可读的现象,这种情况需要应用程序来处理超时.一般做法是设定一个select等待的最大时间,如果超出这个时间依然没有数据可读,则认为socket已不可用.

发送应用程序会不断的将余下的数据发送到网络上,但始终得不到确认,所以缓存区的可用空间持续为0,这种情况也需要应用程序来处理.

如果不由应用程序来处理这种情况超时的情况,也可以通过tcp协议本身来处理,具体可以查看sysctl项中的:
net.ipv4.tcp_keepalive_intvl
net.ipv4.tcp_keepalive_probes
net.ipv4.tcp_keepalive_time


再来看一下“阻塞”与“非阻塞”、“同步”与“异步”的概念:

1.同步与异步

同步与异步关注的是消息通信机制(synchronous communication/asy)

所谓同步,就是在发出一个“调用”时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。

而异步则是相反,“调用”在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出以后,调用者不会立刻得到结果。而是在“调用”发出后,“被调用者”通过状态、通知来通知调用者,或通过回调函数处理这个调用。

 典型的异步编程模型比如Node.js

举个通俗的例子:

你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等待查询结果,然后查好了(可能是5秒,也可能是一天) 

告诉你结果(返回结果)。
而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。

2. 阻塞与非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

阻塞调用是指调用结果返回前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

还是上面的例子:

你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边玩去,当然你也要偶尔过几分钟check一下老板有没有返回结果。

在这里是否阻塞同是否同步异步无关,也就是阻塞与否跟老板用什么方式回答你结果无关。

 

 

 

 

 

 

 

 

 

 

 

http://www.cnblogs.com/fczjuever/archive/2013/04/05/3000680.html

http://www.cnblogs.com/fczjuever/archive/2013/04/17/3026694.html

http://blog.csdn.net/fengxo/article/details/5357626

 

posted @ 2015-12-30 21:08  stemon  阅读(4204)  评论(0编辑  收藏  举报