几个重要的结论:

1. read总是在接收缓冲区有数据时立即返回,而不是等到给定的read buffer填满时返回。

只有当receive buffer为空时,blocking模式才会等待,而nonblock模式下会立即返回-1(errno = EAGAIN或EWOULDBLOCK)

2. blocking的write只有在缓冲区足以放下整个buffer时才返回(与blocking read并不相同)

nonblock write则是返回能够放下的字节数,之后调用则返回-1(errno = EAGAIN或EWOULDBLOCK)

对于blocking的write有个特例:当write正阻塞等待时对面关闭了socket,则write则会立即将剩余缓冲区填满并返回所写的字节数,再次调用则write失败(connection reset by peer)。

socket分为阻塞模式和非阻塞模式。

  阻塞 非阻塞
read 接收缓冲区只要有数据就立即返回,只有当接收缓冲区为空时才阻塞等待 无论有无数据,都立即返回
write 只有发送缓冲区可以放下整个buffer时才返回,否则阻塞 无论发送缓冲区是否能放下整个buffer,都立即返回。返回能够放下的字节数

一. read/write的语义:为什么会阻塞?

先从write说起:

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

首先,write成功返回,只是buf中的数据被复制到了kernel中的TCP发送缓冲区。至于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知。

write在什么情况下会阻塞?当kernel的该socket的发送缓冲区已满时。对于每个socket,拥有自己的send buffer和receive buffer。从Linux 2.6开始,两个缓冲区大小都由系统来自动调节(autotuning),但一般在default和max之间浮动。

# 获取socket的发送/接受缓冲区的大小:(后面的值是在我在Linux 2.6.38 x86_64上测试的结果)
sysctl net.core.wmem_default       #126976
sysctl net.core.wmem_max     #131071
sysctl net.core.wmem_default #126976
sysctl net.core.wmem_max #131071

已经发送到网络的数据依然需要暂存在send buffer中,只有收到对方的ack后,kernel才从buffer中清除这一部分数据,为后续发送数据腾出空间。接收端将收到的数据暂存在receive buffer中,自动进行确认。但如果socket所在的进程不及时将数据从receive buffer中取出,最终导致receive buffer填满,由于TCP的滑动窗口和拥塞控制,接收端会阻止发送端向其发送数据。这些控制皆发生在TCP/IP栈中,对应用程序是透明的,应用程序继续发送数据,最终导致send buffer填满,write调用阻塞。

一般来说,由于接收端进程从socket读数据的速度跟不上发送端进程向socket写数据的速度,最终导致发送端write调用阻塞。

而read调用的行为相对容易理解,从socket的receive buffer中拷贝数据到应用程序的buffer中。read调用阻塞,通常是发送端的数据没有到达。

二. blocking(默认)和nonblock模式下read/write行为的区别:

将socket fd设置为nonblock(非阻塞)是在服务器编程中常见的做法,采用blocking IO并为每一个client创建一个线程的模式开销巨大且可扩展性不佳(带来大量的切换开销),更为通用的做法是采用线程池+Nonblock I/O+Multiplexing(select/poll,以及Linux上特有的epoll)。

1
2
3
4
5
6
7
8
// 设置一个文件描述符为nonblock
int set_nonblocking(int fd)
{
    int flags;
    if ((flags = fcntl(fd, F_GETFL, 0)) == -1)
        flags = 0;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

几个重要的结论:

1. read总是在接收缓冲区有数据时立即返回,而不是等到给定的read buffer填满时返回。

只有当receive buffer为空时,blocking模式才会等待,而nonblock模式下会立即返回-1(errno = EAGAIN或EWOULDBLOCK)

2. blocking的write只有在缓冲区足以放下整个buffer时才返回(与blocking read并不相同)

nonblock write则是返回能够放下的字节数,之后调用则返回-1(errno = EAGAIN或EWOULDBLOCK)

对于blocking的write有个特例:当write正阻塞等待时对面关闭了socket,则write则会立即将剩余缓冲区填满并返回所写的字节数,再次调用则write失败(connection reset by peer),这正是下个小节要提到的:

三. read/write对连接异常的反馈行为:

对应用程序来说,与另一进程的TCP通信其实是完全异步的过程:

1. 我并不知道对面什么时候、能否收到我的数据

2. 我不知道什么时候能够收到对面的数据

3. 我不知道什么时候通信结束(主动退出或是异常退出、机器故障、网络故障等等)

对于1和2,采用write() -> read() -> write() -> read() ->...的序列,通过blocking read或者nonblock read+轮询的方式,应用程序基于可以保证正确的处理流程。

对于3,kernel将这些事件的“通知”通过read/write的结果返回给应用层。

 

假设A机器上的一个进程a正在和B机器上的进程b通信:某一时刻a正阻塞在socket的read调用上(或者在nonblock下轮询socket)

当b进程终止时,无论应用程序是否显式关闭了socket(OS会负责在进程结束时关闭所有的文件描述符,对于socket,则会发送一个FIN包到对面)。

”同步通知“:进程a对已经收到FIN的socket调用read,如果已经读完了receive buffer的剩余字节,则会返回EOF:0

”异步通知“:如果进程a正阻塞在read调用上(前面已经提到,此时receive buffer一定为空,因为read在receive buffer有内容时就会返回),则read调用立即返回EOF,进程a被唤醒。

socket在收到FIN后,虽然调用read会返回EOF,但进程a依然可以其调用write,因为根据TCP协议,收到对方的FIN包只意味着对方不会再发送任何消息。 在一个双方正常关闭的流程中,收到FIN包的一端将剩余数据发送给对面(通过一次或多次write),然后关闭socket。

但是事情远远没有想象中简单。优雅地(gracefully)关闭一个TCP连接,不仅仅需要双方的应用程序遵守约定,中间还不能出任何差错。

假如b进程是异常终止的,发送FIN包是OS代劳的,b进程已经不复存在,当机器再次收到该socket的消息时,会回应RST(因为拥有该socket的进程已经终止)。a进程对收到RST的socket调用write时,操作系统会给a进程发送SIGPIPE,默认处理动作是终止进程,知道你的进程为什么毫无征兆地死亡了吧:)

from 《Unix Network programming, vol1》 3rd Edition:

"It is okay to write to a socket that has received a FIN, but it is an error to write to a socket that has received an RST."

通过以上的叙述,内核通过socket的read/write将双方的连接异常通知到应用层,虽然很不直观,似乎也够用。

这里说一句题外话:

不知道有没有同学会和我有一样的感慨:在写TCP/IP通信时,似乎没怎么考虑连接的终止或错误,只是在read/write错误返回时关闭socket,程序似乎也能正常运行,但某些情况下总是会出奇怪的问题。想完美处理各种错误,却发现怎么也做不对。

原因之一是:socket(或者说TCP/IP栈本身)对错误的反馈能力是有限的。

考虑这样的错误情况:

不同于b进程退出(此时OS会负责为所有打开的socket发送FIN包),当B机器的OS崩溃(注意不同于人为关机,因为关机时所有进程的退出动作依然能够得到保证)/主机断电/网络不可达时,a进程根本不会收到FIN包作为连接终止的提示。

如果a进程阻塞在read上,那么结果只能是永远的等待。

如果a进程先write然后阻塞在read,由于收不到B机器TCP/IP栈的ack,TCP会持续重传12次(时间跨度大约为9分钟),然后在阻塞的read调用上返回错误:ETIMEDOUT/EHOSTUNREACH/ENETUNREACH

假如B机器恰好在某个时候恢复和A机器的通路,并收到a某个重传的pack,因为不能识别所以会返回一个RST,此时a进程上阻塞的read调用会返回错误ECONNREST

恩,socket对这些错误还是有一定的反馈能力的,前提是在对面不可达时你依然做了一次write调用,而不是轮询或是阻塞在read上,那么总是会在重传的周期内检测出错误。如果没有那次write调用,应用层永远不会收到连接错误的通知。

write的错误最终通过read来通知应用层,有点阴差阳错?

四. 还需要做什么?

至此,我们知道了仅仅通过read/write来检测异常情况是不靠谱的,还需要一些额外的工作:

1. 使用TCP的KEEPALIVE功能?

cat /proc/sys/net/ipv4/tcp_keepalive_time
7200 cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75 cat /proc/sys/net/ipv4/tcp_keepalive_probes
9

以上参数的大致意思是:keepalive routine每2小时(7200秒)启动一次,发送第一个probe(探测包),如果在75秒内没有收到对方应答则重发probe,当连续9个probe没有被应答时,认为连接已断。(此时read调用应该能够返回错误,待测试)

但在我印象中keepalive不太好用,默认的时间间隔太长,又是整个TCP/IP栈的全局参数:修改会影响其他进程,Linux的下似乎可以修改per socket的keepalive参数?(希望有使用经验的人能够指点一下),但是这些方法不是portable的。

2. 进行应用层的心跳

严格的网络程序中,应用层的心跳协议是必不可少的。虽然比TCP自带的keep alive要麻烦不少(怎样正确地实现应用层的心跳,我或许会用一篇专门的文章来谈一谈),但有其最大的优点:可控。

当然,也可以简单一点,针对连接做timeout,关闭一段时间没有通信的”空闲“连接。这里可以参考一篇文章:

Muduo 网络编程示例之八:Timing wheel 踢掉空闲连接 by 陈硕

read函数只是一个通用的读文件设备的接口。是否阻塞需要由设备的属性和设定所决定。一般来说,读字 符终端、网络的socket描述字,管道文件等,这些文件的缺省read都是阻塞的方式。如果是读磁盘上的文件,一般不会是阻塞方式的。但使用锁和 fcntl设置取消文件O_NOBLOCK状态,也会产生阻塞的read效果。

怎么样以非阻塞的方式从管道中读取数据?因为我用read函数时,如果管道没有数据就会阻塞住???

我的代码是: 
char chBuff[32]; 
read(file_pipe[0], chBuff, 16);

如果管道没有数据我这个函数就永远不返回,请问有没有其它的方式,如果管道有数据就读取,没数据也立即返回??

或者在open的时候,传入O_NONBLOCK参数,按非阻塞方式打开 / fcntl(fd, F_SETFL, O_NONBLOCK)

在mode设置O_NONBLOCK标志

每一个TCP套接口有一个发送缓冲区,可以用SO_SNDBUF套接口选项来改变这个缓冲区的大小。当应用进程调用 write时,内核从应用进程的缓冲区中拷贝所有数据到套接口的发送缓冲区。如果套接口的发送缓冲区容不下应用程序的所有数据(或是应用进程的缓冲区大于 套接口发送缓冲区,或是套接口发送缓冲区还有其他数据),应用进程将被挂起(睡眠)。这里假设套接口是阻塞的,这是通常的缺省设置。内核将不从write 系统调用返回,直到应用进程缓冲区中的所有数据都拷贝到套接口发送缓冲区。因此从写一个TCP套接口的write调用成功返回仅仅表示我们可以重新使用应 用进程的缓冲区。它并不告诉我们对端的 TCP或应用进程已经接收了数据。 
    TCP取套接口发送缓冲区的数据并把它发送给对端TCP,其过程基于TCP数据传输的所有规则。对端TCP必须确认收到的数据,只有收到对端的ACK,本端TCP才能删除套接口发送缓冲区中已经确认的数据。TCP必须保留数据拷贝直到对端确认为止。

1 输入操作: read、readv、recv、recvfrom、recvmsg

如果某个进程对一个阻塞的TCP套接口调用这些输入函数之一,而且该套接口的接收缓冲区中没有数据可读,该进程将被投入睡眠,直到到达一些数据。既然 TCP是字节流协议,该进程的唤醒就是只要到达一些数据:这些数据既可能是单个字节,也可以是一个完整的TCP分节中的数据。如果想等到某个固定数目的数 据可读为止,可以调用readn函数,或者指定MSG_WAITALL标志。

既然UDP是数据报协议,如果一个阻塞的UDP套接口的接收缓冲区为空,对它调用输入函数的进程将被投入睡眠,直到到达一个UDP数据报。

对于非阻塞的套接口,如果输入操作不能被满足(对于TCP套接口即至少有一个字节的数据可读,对于UDP套接口即有一个完整的数据报可读),相应调用将立即返回一个EWOULDBLOCK错误。

2 输出操作:write、writev、send、sendto、sendmsg

对于一个TCP套接口,内核将从应用进程的缓冲区到该套接口的发送缓冲区拷贝数据。对于阻塞的套接口,如果其发送缓冲区中没有空间,进程将被投入睡眠,直到有空间为止。

对于一个非阻塞的TCP套接口,如果其发送缓冲区中根本没有空间,输出函数调用将立即返回一个EWOULDBLOCK错误。如果其发送缓冲区中有一些空间,返回值将是内核能够拷贝到该缓冲区中的字节数。这个字节数也称为不足计数(short count)

UDP套接口不才能在真正的发送缓冲区。内核只是拷贝应用进程数据并把它沿协议栈向下传送,渐次冠以UDP头部和IP头部。因此对一个阻塞的UDP套接口,输出函数调用将不会因为与TCP套接口一样的原因而阻塞,不过有可能会因其他的原因而阻塞

TCP Socket的一些行为的更多相关文章

  1. C# TCP socket发送大数据包时,接收端和发送端数据不一致 服务端接收Receive不完全

    简单的c# TCP通讯(TcpListener) C# 的TCP Socket (同步方式) C# 的TCP Socket (异步方式) C# 的tcp Socket设置自定义超时时间 C# TCP ...

  2. C# 的TCP Socket (异步方式)

    简单的c# TCP通讯(TcpListener) C# 的TCP Socket (同步方式) C# 的TCP Socket (异步方式) C# 的tcp Socket设置自定义超时时间 C# TCP ...

  3. C# 的TCP Socket (同步方式)

    简单的c# TCP通讯(TcpListener) C# 的TCP Socket (同步方式) C# 的TCP Socket (异步方式) C# 的tcp Socket设置自定义超时时间 C# TCP ...

  4. C# 的tcp Socket设置自定义超时时间

    简单的c# TCP通讯(TcpListener) C# 的TCP Socket (同步方式) C# 的TCP Socket (异步方式) C# 的tcp Socket设置自定义超时时间 C# TCP ...

  5. 重新想象 Windows 8 Store Apps (62) - 通信: Socket TCP, Socket UDP

    [源码下载] 重新想象 Windows 8 Store Apps (62) - 通信: Socket TCP, Socket UDP 作者:webabcd 介绍重新想象 Windows 8 Store ...

  6. Nginx 中 fastcgi_pass 监听端口 unix socket和tcp socket差

    Nginx 中 fastcgi_pass 监听端口 unix socket和tcp socket差别   Nginx连接fastcgi的方式有2种:unix domain socket和TCP,Uni ...

  7. C# Socket编程(5)使用TCP Socket

    TCP 协议(Transmission Control Protocol,传输控制协议)是TCP/IP体系中面向连接(connection oriented)的传输层(transport layer) ...

  8. 再次回首 TCP Socket服务器编程

    转载:http://www.cnblogs.com/zc22/archive/2010/06/27/1766007.html ------------------ 前言 --------------- ...

  9. TCP SOCKET中backlog参数的用途是什么? ---图解

    recv_queue中的包大小,为内核的包大小,而不是ip包大小. 如果发出去的包太大,需要修改write_queue和tx_queue两个参数,tx_queue主要是流量控制. 多进程必须在sock ...

  10. Python Tcp Socket

    socket(套接字),传输层通信的端点,由IP和端口号组成(IP,Port),可以通过socket精确地找到服务器上的进程并与之通信 python2.6实现,基于AF_INET(网络套接字) 类型S ...

随机推荐

  1. No.1 selenium学习之路之浏览器操作

    selenium基础,首先就是浏览器的相关操作 下面描述几种浏览器的常用操作 1.打开浏览器 webdriver后面添加想要打开的浏览器 Ie或者Chrome 2.打开指定页面(百度) 3.休眠时间 ...

  2. 洛谷P3378堆

    传送门啦 #include <iostream> #include <cstdio> #include <cstring> #include <algorit ...

  3. java 判断字符串是否相等

    判断字符串相等我们经常习惯性的写上if(str1==str2),这种写法在Java中可能会带来问题. java中判断字符串是否相等有两种方法: 1.用“==”运算符,该运算符表示指向字符串的引用是否相 ...

  4. Java @SuppressWarnings

    @SuppressWarnings() 注解以@开头可以接受参数 @SuppressWarnings("unchecked") 不受检查的警告信息应该被抑制 //: holding ...

  5. 2016-2017-2 20155309南皓芯《java程序设计》第七周学习总结

    教材学习内容总结 Lambda 一种匿名方法 表达式构成 括号以及括号里用逗号分隔的参数列表 仅有一个参数的可以省略括号 ->符号 花括号以及花括号里的语句 仅有一条语句时可以省略花括号,并且这 ...

  6. 减小VirtualBox虚拟硬盘文件的大小

    虚拟机使用久了就会发现虚拟硬盘越来越大,但是进入虚拟机里的系统用命令看了下,实际占用的空间远没有虚拟硬盘大小那么大,这个让人很不爽,而且在分享虚拟机镜像的时候也很不方便.VirtualBox似乎没有提 ...

  7. Hive(七)Hive分析窗口函数

    一数据准备 cookie1,2015-04-10,1 cookie1,2015-04-11,5 cookie1,2015-04-12,7 cookie1,2015-04-13,3 cookie1,20 ...

  8. MFC+WinPcap编写一个嗅探器之二(界面)

    选择新建->项目->MFC应用程序->基于对话框完成,这里文件名为sniffer 打开资源视图中的Dialog列表,打开项目总默认创建的话框,将对话框中的所有控件删除,之后按照最终效 ...

  9. Linux查看使用端口的进程

    netstat –apn | grep 8080 lsof -i:8080

  10. linux kernel.shmall shemax shemin解释

        Linux X86-64操作系统,Oracle 10g数据库,由8G加到16G,把kernel.shmmax参数改到17179869184(16G)后,发现只要修改sga_max_size和s ...