• 浅谈TCP/IP网络编程中socket的行为


    一. 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

    已经发送到网络的数据依然需要暂存在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)=>线程只会阻塞在多路复用,不会阻塞在某个socket处,如果采用阻塞I/O,线程可能会阻塞在某个socket处,其他就绪的socket得不到处理

    // 设置一个文件描述符为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)我的理解:只有对接收到fin包的socket调用read,才会返回0

    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的结果返回给应用层(即应用程序通过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。(以上是对方正常关闭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将双方的连接异常通知到应用层,虽然很不直观,似乎也够用。

    考虑这样的错误情况:

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

    如果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调用,应用层永远不会收到连接错误的通知。

    四. 还需要做什么?

    至此,我们知道了仅仅通过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 陈硕

    转自:http://www.cnblogs.com/promise6522/archive/2012/03/03/2377935.html


    socket可读可写条件

    要了解socket可读可写条件,我们先了解几个概念:
    1.接收缓存区低水位标记(用于读)和发送缓存区低水位标记(用于写):

    每个套接字有一个接收低水位和一个发送低水位。他们由select函数使用。

    接收低水位标记是让select返回"可读"时套接字接收缓冲区中所需的数据量。对于TCP,其默认值为1。

    发送低水位标记是让select返回"可写"时套接字发送缓冲区中所需的可用空间。对于TCP,其默认值常为2048.

    2.FIN: (结束标志,Finish)用来结束一个TCP回话.但对应端口仍处于开放状态,准备接收后续数据.

    首先来看看socket可读的条件.

    一、下列四个条件中的任何一个满足时,socket准备好读: 
    1. socket的接收缓冲区中的数据字节大于等于该socket的接收缓冲区低水位标记的当前大小。对这样的socket的读操作将不阻塞并返回一个大于0的值(也就是返回准备好读入的数据)。我们可以用SO_RCVLOWATsocket选项来设置该socket的低水位标记。对于TCP和UDP .socket而言,其缺省值为1.

    2. 该连接的读这一半关闭(也就是接收了FIN的TCP连接)。对这样的socket的读操作将不阻塞并返回0

    3.socket是一个用于监听的socket,并且已经完成的连接数为非0.这样的soocket处于可读状态(即有客户连接到达,但还没有accept),是因为socket收到了对方的connect请求,执行了三次握手的第一步:对方发送SYN请求过来,使监听socket处于可读状态;正常情况下,这样的socket上的accept操作不会阻塞;

    4.有一个socket有异常错误条件待处理.对于这样的socket的读操作将不会阻塞,并且返回一个错误(-1),errno则设置成明确的错误条件.这些待处理的错误也可通过指定socket选项SO_ERROR调用getsockopt来取得并清除;

    再来看看socket可写的条件. 

    二、下列三个条件中的任何一个满足时,socket准备好写: 

    1. socket的发送缓冲区中的可用数据字节大于等于该socket的发送缓冲区低水位标记的当前大小。对这样的socket的写操作将不阻塞并返回一个大于0的值(也就是返回准备好写入的数据)。我们可以用SO_SNDLOWAT socket选项来设置该socket的低水位标记。对于TCP和UDPsocket而言,其缺省值为2048

    2. 该连接的写这一半关闭。对这样的socket的写操作将产生SIGPIPE信号,该信号的缺省行为是终止进程。

    3.有一个socket异常错误条件待处理.对于这样的socket的写操作将不会阻塞并且返回一个错误(-1),errno则设置成明确的错误条件.这些待处理的错误也可以通过指定socket选项SO_ERROR调用getsockopt函数来取得并清除;

  • 相关阅读:
    Moss2010 部署命令
    socket形象描述
    Android UI 的更新
    android AIDL 进程间通信
    中文设置成粗体的方法
    android 主件之 Service
    android activity
    拦截Activity的后退键处理
    android 解析json数据格式
    防止事件导致的oncreate的多次调用
  • 原文地址:https://www.cnblogs.com/ljygoodgoodstudydaydayup/p/5698693.html
Copyright © 2020-2023  润新知