前面在阅读TCP/IP中socket行为的时候,有提到这两个error,本身自己在做项目的时候,也遇到了这些问题。刚好在这里,我们把他们记录下来。

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

  1. 我并不知道对面什么时候和能否收到我的数据
  2. 我不知道什么时候能够收到对面的数据
  3. 我不知道什么时候可以信息结束(主动退出或者是异常退出,机器故障,网络故障等等)

对于1和2,采用write -> read() -> write() -> read() 的行为序列,通过blocking read 或者 nonblocking 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
  • 异步通知:如果进程a正在阻塞在read调用上(前面已经提到,此时receive buffer一定为空,因为read在receive buffer有内容时就会返回),则read调用立即返回EOF, 进程a被唤醒

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

假如b进程是异常终止的,发送FIN包是OS代劳的,b进程已经不复存在。当b机器再次收到socket的消息时,会回应RST(因为拥有该socket的进程已经终止)。a进程收到RST的socket调用write时候,操作系统会给a进程发送SIGPIPE, 默认处理动作是终止进程。在这里还要说明一些细节

  • B返回了“RST”时,A此时正在从socket套接字的输出流中读数据则会提示“connection reset”
  • B返回了“RST”时,如果此时A正在往socket中输入流中写数据则会提示“connection reset by peer”

通过以上的场景描述,内核通过socket的read/write将双方的连接异常通知到引用层,虽然很不直观,但似乎也是够用的。我们在处理TCP/IP通信时,似乎没有怎么考虑过连接的终止和错误,只是在read.write错误返回时关闭socket, 程序似乎也能正常运行。但是对某些情况下总是会出现奇怪的错误,想完美地处理这些错误,却发现怎么都是不完美的。 这里或许还有另外一个原因,那就是scoket(或者说TCP/IP协议栈本身)对错误的反馈能力是有限的。

场景二

不同于b进程退出, 但b机器的OS崩溃(不是正常关闭,而是主机断电,网络不可达),a进程根本不会收到FIN包作为连接终止的的提示。

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

如果a进程先write然后阻塞在read上,由于收不到B机器上的ack, tcp会持续重传12次,时间跨度为9 mins(真的吗?), 然后阻塞在read调用上返回错误(ETIMEDOUT/EHOSTUNREACH/ENETUNREACH)

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

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

下面简单介绍一下keeplive的一些参数:

  1. sysctl net.inet.tcp
  2. net.inet.tcp.keepidle: 7200000
  3. net.inet.tcp.keepintvl: 75000
  4. net.inet.tcp.maxseg_unacked: 8

以上参数的大致意思就是:
keeplive routine每2小时(7200s)启动一次,发送第一个probe(探测包), 如果在75s内没有收到对方的应答则重发probe,当连续9个probe没有被应答的,认为连接已经断了(这里值得商榷)。

在严格的网络程序中,应用层的心跳协议是必不可少的,虽然比TCP自带的keep alive要麻烦不少。其次,我们可以针对连接做timeout, 关闭一端时间没有通信的“空闲连接”。

参考
浅谈TCP/IP网络编程中socket的行为
Linux-TCP 出现 RST 的几种情况