如何偷窥到socket 缓冲区对应的内核缓冲区中有什么数据

2943人阅读
socket通信(2)
TCP可靠,UDP不可靠的实现
&&&&&&&& 每个TCP&socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工作模式以及TCP的滑动窗口便是依赖于这两个独立的buffer以及此buffer的填充状态。接收缓冲区把数据缓存入内核,应用进程一直没有调用read进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。再啰嗦一点,不管进程是否读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。read所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,仅此而已。进程调用send发送的数据的时候,最简单情况(也是一般情况),将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。换句话说,send返回之时,数据不一定会发送到对端去(和write写文件有点类似),send仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中。
&&&&&&&& 每个UDP socket都有一个接收缓冲区,没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以正确接收,所以不缓冲,不需要发送缓冲区。
&&&&&&&& 接收缓冲区被TCP和UDP用来缓存网络上来的数据,一直保存到应用进程读走为止。对于TCP,如果应用进程一直没有读取,buffer满了之后,发生的动作是:通知对端TCP协议中的窗口关闭。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。
UDP:当套接口接收缓冲区满时,新来的数据报无法进入接收缓冲区,此数据报就被丢弃。UDP是没有流量控制的;快的发送者可以很容易地就淹没慢的接收者,导致接收方的UDP丢弃数据报。
以上便是TCP可靠,UDP不可靠的实现。
socket阻塞与非阻塞,同步与异步的区分
&&&&&&& 比如你调用send函数发送一定的Byte,在系统内部send做的工作其实只是把数据传输(Copy)到TCP/IP协议栈的输出缓冲区,它执行成功并不代表数据已经成功的发送出去了,如果TCP/IP协议栈没有足够的可用缓冲区来保存你Copy过来的数据的话,这时候就体现出阻塞和非阻塞的不同之处了:对于阻塞模式的socket
send函数将不返回,直到系统缓冲区有足够的空间把你要发送的数据Copy过去以后才返回,而对于非阻塞的socket来说send会立即返回WSAEWOULDDBLOCK告诉调用者说:&发送操作被阻塞了!!!你想办法处理吧...&&
&&&&&&& 对于recv函数,同样道理,该函数的内部工作机制其实是在等待TCP/IP协议栈的接收缓冲区通知它说:嗨,你的数据来了.对于阻塞模式的socket来说如果TCP/IP协议栈的接收缓冲区没有通知一个结果给它它就一直不返回:耗费着系统资源....对于非阻塞模式的socket该函数会马上返回,然后告诉你:WSAEWOULDDBLOCK---&现在没有数据,回头在来看看&
在进行网络编程时,我们常常见到同步、异步、阻塞和非阻塞四种调用方式。这些方式彼此概念并不好理解。下面是我对这些术语的理解。
&&&&& 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。最常见的例子就是
SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的 LRESULT值返回给调用者。
&&&&& 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。以 CAsycSocket类为例(注意,CSocket从CAsyncSocket派生,但是起功能已经由异步转化为同步),当一个客户端通过调用
Connect函数发出一个连接请求后,调用者线程立刻可以朝下运行。当连接真正建立起来以后,socket底层会发送一个消息通知该对象。这里提到执行
部件和调用者通过三种途径返回结果:状态、通知和回调。可以使用哪一种依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。如果执行部
件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重
的错误)。如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
&&&& 阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同
步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。例如,我们在CSocket中调用Receive函数,如果缓冲区中没有数
据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。如果主窗口和调用函数在同一个线程中,除非你在特殊的界面操
作函数中调用,其实主界面还是应该可以刷新。socket接收数据的另外一个函数recv则是一个阻塞调用的例子。当socket工作在阻塞模式的时候,
如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。
&&&&& 非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
对象的阻塞模式和阻塞函数调用
对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状
态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:742850次
积分:7954
积分:7954
排名:第1844名
原创:147篇
转载:19篇
评论:92条
(1)(1)(1)(1)(1)(1)(1)(2)(1)(3)(1)(2)(1)(3)(3)(2)(7)(3)(12)(14)(15)(3)(2)(5)(1)(7)(1)(2)(1)(1)(1)(1)(1)(3)(6)(3)(16)(9)(12)(19)(5)再次深入理解TCP网络编程中的send和recv - bw_0927 - 博客园
posts - 9, comments - 31, trackbacks - 0, articles - 1123
http://blog.csdn.net/yusiguyuan/article/details/
http://blog.csdn.net/yusiguyuan/article/details/
先明确一个概念:每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工作模式以及TCP的滑动窗口便是依赖于这两个独立的buffer以及此buffer的填充状态。接收缓冲区把数据缓存入内核,应用进程一直没有调用read进行读取的话,此数据会一直缓存在相应 socket的接收缓冲区内。再啰嗦一点,不管进程是否读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。 read所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,仅此而已。进程调用send发送的数据的时候,最简单情况(也是一般情况),将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。换句话说,send返回之时,数据不一定会发送到对端去(和 write写文件有点类似),send仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中。后续我会专门用一篇文章介绍 read和send所关联的内核动作。每个UDP socket都有一个接收缓冲区,没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以正确接收,所以不缓冲,不需要发送缓冲区。
  接收缓冲区被TCP和UDP用来缓存网络上来的数据,一直保存到应用进程读走为止。对于TCP,如果应用进程一直没有读取,buffer满了之后,发生的动作是:通知对端TCP协议中的窗口关闭。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。 UDP:当套接口接收缓冲区满时,新来的数据报无法进入接收缓冲区,此数据报就被丢弃。UDP是没有流量控制的;快的发送者可以很容易地就淹没慢的接收者,导致接收方的UDP丢弃数据报。
  以上便是TCP可靠,UDP不可靠的实现。
  TCP_CORK TCP_NODELAY
  这两个选项是互斥的,打开或者关闭TCP的nagle算法,下面用场景来解释
  典型的webserver向客户端的应答,应用层代码实现流程粗略来说,一般如下所示:
  if(条件1){
  向buffer_last_modified填充协议内容&Last-Modified: Sat, 04 May 2012 05:28:58 GMT&;
  send(buffer_last_modified);
  if(条件2){
  向buffer_expires填充协议内容&Expires: Mon, 14 Aug :29 GMT&;
  send(buffer_expires);
  。。。
  if(条件N){
  向buffer_N填充协议内容&。。。&;
  send(buffer_N);
  对于这样的实现,当前的http应答在执行这段代码时,假设有M(M&=N)个条件都满足,那么会有连续的M个send调用,那是不是下层会依次向客户端发出M个TCP包呢?答案是否定的,包的数目在应用层是无法控制的,并且应用层也是不需要控制的。
  我用下列四个假设场景来解释一下这个答案
  由于TCP是流式的,对于TCP而言,每个TCP连接只有syn开始和fin结尾,中间发送的数据是没有边界的,多个连续的send所干的事情仅仅是:
  假如socket的文件描述符被设置为阻塞方式,而且发送缓冲区还有足够空间容纳这个send所指示的应用层buffer的全部数据,那么把这些数据从应用层的buffer,拷贝到内核的发送缓冲区,然后返回。
  假如socket的文件描述符被设置为阻塞方式,但是发送缓冲区没有足够空间容纳这个send所指示的应用层buffer的全部数据,那么能拷贝多少就拷贝多少,然后进程挂起,等到TCP对端的接收缓冲区有空余空间时,通过滑动窗口协议(ACK包的又一个作用----打开窗口)通知TCP本端:&亲,我已经做好准备,您现在可以继续向我发送X个字节的数据了&,然后本端的内核唤醒进程,继续向发送缓冲区拷贝剩余数据,并且内核向TCP对端发送TCP数据,如果send所指示的应用层buffer中的数据在本次仍然无法全部拷贝完,那么过程重复。。。直到所有数据全部拷贝完,返回。
  请注意,对于send的行为,我用了&拷贝一次&,send和下层是否发送数据包,没有任何关系。
  假如socket的文件描述符被设置为非阻塞方式,而且发送缓冲区还有足够空间容纳这个send所指示的应用层buffer的全部数据,那么把这些数据从应用层的buffer,拷贝到内核的发送缓冲区,然后返回。
  假如socket的文件描述符被设置为非阻塞方式,但是发送缓冲区没有足够空间容纳这个send所指示的应用层buffer的全部数据,那么能拷贝多少就拷贝多少,然后返回拷贝的字节数。多涉及一点,返回之后有两种处理方式:
  1.死循环,一直调用send,持续测试,一直到结束(基本上不会这么搞)。
  2.非阻塞搭配epoll或者select,用这两种东西来测试socket是否达到可发送的活跃状态,然后调用send(高性能服务器必需的处理方式)。
  综上,以及请参考本文前述的SO_RCVBUF和SO_SNDBUF,你会发现,在实际场景中,你能发出多少TCP包以及每个包承载多少数据,除了受到自身服务器配置和环境带宽影响,对端的接收状态也能影响你的发送状况。
  至于为什么说&应用层也是不需要控制发送行为的&,这个说法的原因是:
  软件系统分层处理、分模块处理各种软件行为,目的就是为了各司其职,分工。应用层只关心业务实现,控制业务。数据传输由专门的层面去处理,这样应用层开发的规模和复杂程度会大为降低,开发和维护成本也会相应降低。
  再回到发送的话题上来:)之前说应用层无法精确控制和完全控制发送行为,那是不是就是不控制了?非也!虽然无法控制,但也要尽量控制!
  如何尽量控制?现在引入本节主题----TCP_CORK和TCP_NODELAY。
  cork:塞子,塞住
  nodelay:不要延迟
  TCP_CORK:尽量向发送缓冲区中攒数据,攒到多了再发送,这样网络的有效负载会升高。简单粗暴地解释一下这个有效负载的问题。假如每个包中只有一个字节的数据,为了发送这一个字节的数据,再给这一个字节外面包装一层厚厚的TCP包头,那网络上跑的几乎全是包头了,有效的数据只占其中很小的部分,很多访问量大的服务器,带宽可以很轻松的被这么耗尽。那么,为了让有效负载升高,我们可以通过这个选项指示TCP层,在发送的时候尽量多攒一些数据,把他们填充到一个TCP包中再发送出去。这个和提升发送效率是相互矛盾的,空间和时间总是一堆冤家!!
  TCP_NODELAY:尽量不要等待,只要发送缓冲区中有数据,并且发送窗口是打开的,就尽量把数据发送到网络上去。
  很明显,两个选项是互斥的。实际场景中该怎么选择这两个选项呢?再次举例说明
  webserver,,下载服务器(ftp的发送文件服务器),需要带宽量比较大的服务器,用TCP_CORK。
  涉及到交互的服务器,比如ftp的接收命令的服务器,必须使用TCP_NODELAY。默认是TCP_CORK。设想一下,用户每次敲几个字节的命令,而下层在攒这些数据,想等到数据量多了再发送,这样用户会等到发疯。这个糟糕的场景有个专门的词汇来形容-----粘(nian拼音二声)包
==========
本篇我们用一个测试机上的阻塞socket实例来说明主题。文章中所有图都是在测试系统上现截取的。
需要理解的3个概念
1. TCP socket的buffer
每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工作模式以及TCP的流量(拥塞)控制便是依赖于这两个独立的buffer以及buffer的填充状态。接收缓冲区把数据缓存入内核,应用进程一直没有调用recv()进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。再啰嗦一点,不管进程是否调用recv()读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,并返回,仅此而已。进程调用send()发送的数据的时候,最简单情况(也是一般情况),将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。换句话说,send()返回之时,数据不一定会发送到对端去(和write写文件有点类似),send()仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中,发送是TCP的事情,和send其实没有太大关系。接收缓冲区被TCP用来缓存网络上来的数据,一直保存到应用进程读走为止。对于TCP,如果应用进程一直没有读取,接收缓冲区满了之后,发生的动作是:收端通知发端,接收窗口关闭(win=0)。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。查看测试机的socket发送缓冲区大小,cat /proc/sys/net/ipv4/tcp_wmem
第一个值是一个限制值,socket发送缓存区的最少字节数;第二个值是默认值;第三个值是一个限制值,socket发送缓存区的最大字节数;根据实际测试,发送缓冲区的尺寸在默认情况下的全局设置是16384字节,即16k。在测试系统上,发送缓存默认值是16k。proc文件系统下的值和sysctl中的值都是全局值,应用程序可根据需要在程序中使用setsockopt()对某个socket的发送缓冲区尺寸进行单独修改,详见文章《深入浅出TCP中的send和recv》,不过这都是题外话。
2. 接收窗口(滑动窗口)
TCP连接建立之时的收端的初始接受窗口大小是14600,细节如图2所示(129是收端,130是发端)
接收窗口是TCP中的滑动窗口,TCP的收端用这个接受窗口----win=14600,通知发端,我目前的接收能力是14600字节。后续发送过程中,收端会不断的用ACK(ACK的全部作用请参照博文《TCP之ACK发送情景》)通知发端自己的接收窗口的大小状态,如图3,而发端发送数据的量,就根据这个接收窗口的大小来确定,发端不会发送超过收端接收能力的数据量。这样就起到了一个流量控制的的作用。
图3说明21,22两个包都是收端发给发端的ACK包第21个包,收端确认收到的前7240个字节数据,7241的意思是期望收到的包从7241号开始,序号加了1.同时,接收窗口从最初的14656(如图2)经过慢启动阶段增加到了现在的29120。用来表明现在收端可以接收29120个字节的数据,而发端看到这个窗口通告,在没有收到新的ACK的时候,发端可以向收端发送29120字节这么多数据。第22个包,收端确认收到的前8688个字节数据,并通告自己的接收窗口继续增长为32000这么大。
3. 单个TCP的负载量和MSS的关系
MSS在以太网上通常大小是1460字节,而我们在后续发送过程中的单个TCP包的最大数据承载量是1448字节,这二者的关系可以参考博文《TCP之1460MSS和1448负载》。
实例详解send()
实例功能说明:接收端129作为客户端去连接发送端130,连接上之后并不调用recv()接收,而是sleep(1000),把进程暂停下来,不让进程接收数据。内核会缓存数据至接收缓冲区。发送端作为服务器接收TCP请求之后,立即用ret = send(sock,buf,70k,0);这个C语句,向接收端发送70k数据。我们现在来观察这个过程。看看究竟发生了些什么事。wireshark抓包截图如下图4
图4说明,包序号等同于时序1. 客户端sleep在recv()之前,目的是为了把数据压入接收缓冲区。服务端调用"ret = send(sock,buf,70k,0);"这个C语句,向接收端发送70k数据。由于发送缓冲区大小16k,send()无法将70k数据全部拷贝进发送缓冲区,故先拷贝16k进入发送缓冲区,下层发送缓冲区中有数据要发送,内核开始发送。上层send()在应用层处于阻塞状态;2. 11号TCP包,发端从这儿开始向收端发送1448个字节的数据;3. 12号TCP包,发端没有收到之前发送的1448个数据的ACK包,仍然继续向收端发送1448个字节的数据;4. 13号TCP包,收端向发端发送1448字节的确认包,表明收端成功接收总共1448个字节。此时收端并未调用recv()读取,目前发送缓冲区中被压入1448字节。由于处于慢启动状态,win接收窗口持续增大,表明接受能力在增加,吞吐量持续上升;5. 14号TCP包,收端向发端发送2896字节的确认包,表明收端成功接收总共2896个字节。此时收端并未调用recv()读取,目前发送缓冲区中被压入2896字节。由于处于慢启动状态,win接收窗口持续增大,表明接受能力在增加,吞吐量持续上升;6. 15号TCP包,发端继续向收端发送1448个字节的数据;7. 16号TCP包,收端向发端发送4344字节的确认包,表明收端成功接收总共4344个字节。此时收端并未调用recv()读取,目前发送缓冲区中被压入4344字节。由于处于慢启动状态,win接收窗口持续增大,表明接受能力在增加,吞吐量持续上升;8. 从这儿开始,我略去很多包,过程类似上面过程。同时,由于不断的发送出去的数据被收端用ACK确认,发送缓冲区的空间被逐渐腾出空地,send()内部不断的把应用层buf中的数据向发送缓冲区拷贝,从而不断的发送,过程重复。70k数据并没有被完全送入内核,send()不管是否发送出去,send不管发送出去的是否被确认,send()只关心buf中的数据有没有被全部送往发送缓冲区。如果buf中的数据没有被全部送往发送缓冲区,send()在应用层阻塞,负责等待发送缓冲区中有空余空间的时候,逐步拷贝buf中的数据;如果buf中的数据被全部拷入发送缓冲区,send()立即返回。9. 经过慢启动阶段接收窗口增大到稳定阶段,TCP吞吐量升高到稳定阶段,收端一直处于sleep状态,没有调用recv()把内核中接收缓冲区中的数据拷贝到应用层去,此时收端的接收缓冲区中被压入大量数据;10. 66号、67号TCP数据包,发端继续向收端发送数据;11. 68号TCP数据包,收端发送ACK包确认接收到的数据,ACK=62265表明收端已经收到62265字节的数据,这些数据目前被压在收端的接收缓冲区中。win=3456,比较之前的16号TCP包的win=23296,表明收端的窗口已经处于收缩状态,收端的接收缓冲区中的数据迟迟未被应用层读走,导致接收缓冲区空间吃紧,故收缩窗口,控制发送端的发送量,进行流量控制;12. 69号、70号TCP数据包,发端在接收窗口允许的数据量的范围内,继续向收端发送2段1448字节长度的数据;13. 71号TCP数据包,至此,收端已经成功接收65160字节的数据,全部被压在接收缓冲区之中,接收窗口继续收缩,尺寸为1600字节;14. 72号TCP数据包,发端在接收窗口允许的数据量的范围内,继续向收端发送1448字节长度的数据;15. 73号TCP数据包,至此,收端已经成功接收66609字节的数据,全部被压在接收缓冲区之中,接收窗口继续收缩,尺寸为192字节。16. 74号TCP数据包,和我们这个例子没有关系,是别的应用发送的包;17. 75号TCP数据包,发端在接收窗口允许的数据量的范围内,向收端发送192字节长度的数据;18. 76号TCP数据包,至此,收端已经成功接收66609字节的数据,全部被压在接收缓冲区之中,win=0接收窗口关闭,接收缓冲区满,无法再接收任何数据;19. 77号、78号、79号TCP数据包,由keepalive触发的数据包,响应的ACK持有接收窗口的状态win=0,另外,ACK=66801表明接收端的接收缓冲区中积压了66800字节的数据。20. 从以上过程,我们应该熟悉了滑动窗口通告字段win所说明的问题,以及ACK确认数据等等。现在可得出一个结论,接收端的接收缓存尺寸应该是66800字节(此结论并非本篇主题)。send()要发送的数据是70k,现在发出去了66800字节,发送缓存中还有16k,应用层剩余要拷贝进内核的数据量是N=70k-66800-16k。接收端仍处于sleep状态,无法recv()数据,这将导致接收缓冲区一直处于积压满的状态,窗口会一直通告0(win=0)。发送端在这样的状态下彻底无法发送数据了,send()的剩余数据无法继续拷贝进内核的发送缓冲区,最终导致send()被阻塞在应用层;21. send()一直阻塞中。。。图4和send()的关系说明完毕。那什么时候send返回呢?有3种返回场景
send()返回场景
场景1,我们继续图4这个例子,不过这儿开始我们就跳出图4所示的过程了
22. 接收端sleep(1000)到时间了,进程被唤醒,代码片段如图5
图5随着进程不断的用"recv(fd,buf,2048,0);"将数据从内核的接收缓冲区拷贝至应用层的buf,在使用win=0关闭接收窗口之后,现在接收缓冲区又逐渐恢复了缓存的能力,这个条件下,收端会主动发送携带"win=n(n&0)"这样的ACK包去通告发送端接收窗口已打开;
23. 发端收到携带"win=n(n&0)"这样的ACK包之后,开始继续在窗口运行的数据量范围内发送数据。发送缓冲区的数据被发出;24. 收端继续接收数据,并用ACK确认这些数据;25. 发端收到ACK,可以清理出一些发送缓冲区空间,应用层send()的剩余数据又可以被不断的拷贝进内核的发送缓冲区;26. 不断重复以上发送过程;27. send()的70k数据全部进入内核,send()成功返回。场景2,我们继续图4这个例子,不过这儿开始我们就跳出图4所示的过程了22. 收端进程或者socket出现问题,给发端发送一个RST,请参考博文《》;23. 内核收到RST,send返回-1。场景3,和以上例子没关系连接上之后,马上send(1k),这样,发送的数据肯定可以一次拷贝进入发送缓冲区,send()拷贝完数据立即成功返回。
send()发送结论
其实场景1和场景2说明一个问题send()只是负责拷贝,拷贝完立即返回,不会等待发送和发送之后的ACK。如果socket出现问题,RST包被反馈回来。在RST包返回之时,如果send()还没有把数据全部放入内核或者发送出去,那么send()返回-1,errno被置错误值;如果RST包返回之时,send()已经返回,那么RST导致的错误会在下一次send()或者recv()调用的时候被立即返回。场景3完全说明send()只要完成拷贝就成功返回,如果发送数据的过程中出现各种错误,下一次send()或者recv()调用的时候被立即返回。
概念上容易疑惑的地方
1. TCP协议本身是为了保证可靠传输,并不等于应用程序用tcp发送数据就一定是可靠的,必须要容错;2. send()和recv()没有固定的对应关系,不定数目的send()可以触发不定数目的recv(),这话不专业,但是还是必须说一下,初学者容易疑惑;3. 关键点,send()只负责拷贝,拷贝到内核就返回,我通篇在说拷贝完返回,很多文章中说send()在成功发送数据后返回,成功发送是说发出去的东西被ACK确认过。send()只拷贝,不会等ACK;4. 此次send()调用所触发的程序错误,可能会在本次返回,也可能在下次调用网络IO函数的时候被返回。
实际上理解了阻塞式的,就能理解非阻塞的。软件开发进阶(539)
网络编程(82)
& & & & 我们先来看MSG_PEEK的值
#include &stdio.h&
#include &winsock2.h&
int main()
printf(&%d\n&, MSG_PEEK); // 2
& & & &下面, 我们来看服务端程序:
#include &stdio.h&
#include &winsock2.h& // winsock接口
#pragma comment(lib, &ws2_32.lib&) // winsock实现
int main()
WORD wVersionR
// 双字节,winsock库的版本
WSADATA wsaD
// winsock库版本的相关信息
wVersionRequested = MAKEWORD(1, 1); // 0x0101 即:257
// 加载winsock库并确定winsock版本,系统会把数据填入wsaData中
WSAStartup( wVersionRequested, &wsaData );
// AF_INET 表示采用TCP/IP协议族
// SOCK_STREAM 表示采用TCP协议
// 0是通常的默认情况
unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrS
addrSrv.sin_family = AF_INET; // TCP/IP协议族
addrSrv.sin_addr.S_un.S_addr = inet_addr(&127.0.0.1&); // socket对应的IP地址
addrSrv.sin_port = htons(8888); // socket对应的端口
// 将socket绑定到某个IP和端口(IP标识主机,端口标识通信进程)
bind(sockSrv,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
// 将socket设置为监听模式,5表示等待连接队列的最大长度
listen(sockSrv, 5);
SOCKADDR_IN addrC
int len = sizeof(SOCKADDR);
// sockSrv为监听状态下的socket
// &addrClient是缓冲区地址,保存了客户端的IP和端口等信息
// len是包含地址信息的长度
// 如果客户端没有启动,那么程序一直停留在该函数处
unsigned int sockConn = accept(sockSrv,(SOCKADDR*)&addrClient, &len);
char sendBuf[100] = {0};
sprintf(sendBuf,&%s&, &hello&);
send(sockConn, sendBuf, strlen(sendBuf), 0); // 发送数据到客户端,最后一个参数一般设置为0
closesocket(sockSrv);
WSACleanup();
& & & 运行上面的服务端程序。
& & & 然后我们来看客户端程序:
#include &winsock2.h&
#include &stdio.h&
#pragma comment(lib, &ws2_32.lib&)
int main()
WORD wVersionR
WSADATA wsaD
wVersionRequested = MAKEWORD(1, 1);
WSAStartup( wVersionRequested, &wsaData );
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrS
addrSrv.sin_addr.S_un.S_addr = inet_addr(&127.0.0.1&);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(8888);
connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
char recvBuf[100] = {0};
int number = recv(sockClient, recvBuf, 100, 0); // 接收到5个字符, 分别是'h', 'e', 'l', 'l', 'o'
printf(&%s, %d\n&, recvBuf, number); // hello, 5
char re[100] = {0};
number = recv(sockClient, re, 100, 0); // 程序会阻塞在此处
printf(&%s, %d\n&, re, number);
closesocket(sockClient);
WSACleanup();
}& & & & 可见, 当recv函数的最后一个参数是0时候, recv函数负责把socket对应的内核缓冲区的数据剪切到程序缓冲区, 所以第二次recv的时候就没有了, 因此一直阻塞着。
& & & & 好, 我们关掉服务端, 并再次开启服务端。 下面, 我们再来看修改后的客户端代码:
#include &winsock2.h&
#include &stdio.h&
#pragma comment(lib, &ws2_32.lib&)
int main()
WORD wVersionR
WSADATA wsaD
wVersionRequested = MAKEWORD(1, 1);
WSAStartup( wVersionRequested, &wsaData );
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrS
addrSrv.sin_addr.S_un.S_addr = inet_addr(&127.0.0.1&);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(8888);
connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
char recvBuf[100] = {0};
int number = recv(sockClient, recvBuf, 100, MSG_PEEK); // 接收到5个字符
printf(&%s, %d\n&, recvBuf, number); // hello, 5
char re[100] = {0};
number = recv(sockClient, re, 100, MSG_PEEK); // 接收到5个字符
printf(&%s, %d\n&, re, number); // hello, 5
closesocket(sockClient);
WSACleanup();
}& & & &可见, 当recv的最后一个参数是MSG_PEEK时, recv函数将socket对应的内核缓冲区的数据拷贝到应用程序缓冲区, 注意, 此时是拷贝, 不是剪切, 只是去peek了一下, 仅仅是偷窥了一下, 并没有偷取。 因此, 内核缓冲区中的数据没有变化。
& & & &总之, 要区分偷取和偷窥, 该偷取就偷取, 该偷窥就偷窥, 视具体需求而定。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:1944414次
积分:31844
积分:31844
排名:第119名
原创:1437篇
转载:108篇
评论:1467条
(26)(32)(26)(15)(42)(63)(38)(39)(75)(43)(4)(20)(33)(17)(12)(7)(48)(53)(51)(52)(35)(20)(53)(41)(35)(13)(32)(36)(7)(1)(47)(23)(26)(4)(13)(4)(19)(72)(13)(2)(14)(45)(32)(7)(3)(28)(53)(106)(68)

我要回帖

更多关于 清空socket接收缓冲区 的文章

 

随机推荐