TCP三次握手与四次挥手

一 连接与关闭

三次握手和四次挥手的状态图大家都比较清楚,本文主要分析读书中的一些疑问,和一些异常情况。

这是一个真实的三次握手和四次挥手的抓包信息

注意到,每条消息其实都是包含sequenceId和ackId的

二 为什么要三次握手?

1、《wireshark网络分析就这么简单》的说法

  为什么要用三个包来建立连接呢,用两个不可以吗?其实也是可以的,但两个不够可靠。我们可以设想一个情况来说明这个问题:某条网络有多条路径,客户端请求建立连接的第一个包跑到了一条延迟严重的路径上了,所以迟迟没有到达服务器。因此客户端只能当做这个请求丢失了,不得不再请求一次。由于第二个请求走了正确的路径,所以很快完成工作并关闭了连接。对于客户端来说,事情似乎已经结束了。没想到它的第一个请求经过跋山涉水,还是达到了服务器。服务器并不知道这是一个旧的无效请求,所以按惯例回复了。加入TCP只要求两次握手,服务器上就这样建立了一个无效的连接。而在三次握手的机制下,客户端收到服务器的回复时,知道这个连接不是它想要的,所以就发一个拒绝包。服务器收到这个包后,也放弃这个连接。

  这是一个比较合理的解释,但是一开始的用两个包也是可以的,说的不是那么靠谱。

  当然,这是一本很好的书,推荐大家去读一下,并且尝试用好Wireshark,可以加深对TCP协议的理解。

2、stackexchange上这个问题的回答给出了更令人信服的解释:Why do we need a 3-way handshake? Why not just 2-way?

  首先TCP一个很重要的特点就是全双工的,而且可靠传输中的确认和超时重传机制都是依赖于SequenceId和ACK。

  在发生SYN同步信号进行握手的时候,里面包含的内容不仅仅是我请求和你建立一个TCP连接,还包含了这个连接session中,己方初始的SequenceId(即ISN),初始值不为0,而是一个与系统时间有关的数值。

  ISN,Initial Sequence Number,与系统时间有关,大多数实现是每隔9.5小时重置为0,每建立一个连接会增加64000,每隔0.5s也增加64000

  如果TCP连接的建立只需要两次握手的话,那么被动打开方可以对主动打开方的ISN进行确认,确保了主动打开方的传输是可靠的。但是被动打开方的ISN并没有被确认,被动打开方的无法保证自己的传输是可靠的。这违反了TCP是一个可靠的全双工传输连接的特性。
  从图3中可以看出,除了包含SYN和SeqId信息外,还至少包含了以下信息:
  Win,window size,与拥塞控制有关;len,告知此次消息的有效长度;MSS max segment size,单包最大长度;等等

三 你真的了解TIME_WAIT吗?

疑问

  在TCP/IP详解,卷一,P184页,有这么一段,刚看到的时候直接懵了。

  这暗示如果我们终止一个客户程序,并立即重新启动这个客户程序,则这个新客户程序将不能使用相同的本地端口。这不会带来什么问题,因为客户使用本地端口,而并不关心这个端口号是什么。

  然而,对于服务器,情况就有所不同,因为服务器使用熟知端口。如果我们终止一个已经建立连接的服务器程序,并试图立即重新启动这个服务器程序,服务器程序将不能把它的这个熟知端口复制给它的端点,因为那个端口是处于2MSL连接的一部分。在重新启动服务器程序前,它需要1~4分钟。

  可是我们平时上线操作过程中,无论是直接启动我们直接启动的服务,还是启动tomcat这类容器,重来没有遇到过这种情况,如果真是重启个服务需要花1~4分钟,难以接受。
  直接写了先测试程序模拟,因为是java程序员就直接用java写的,发现结果和自己一直以来的经验一致,还怀疑是不是翻译错了,直接去网上找了英文原版的书看,结果还是这样描述...真是太年轻了...后来去网上搜索TCP Bind TIME_WAIT关键词,终于找到了SO_REUSEADD这个关键解释。

SO_REUSEADD

  简明的说,设置SO_REUSEADD选项可以让一个进程使用仍处于2MSL等待的端口,以及兼容通配符IP监听和指定IP监听。Stack Overflow的这一篇,Socket options SO_REUSEADDR and SO_REUSEPORT, how do they differ? Do they mean the same across all major operating systems?,描述的非常详细。
  首先,一个机器可能有多块网卡,对于bind操作来说,可以只指定port,那么ip所使用的默认值将会是0.0.0.0,也就是bind在这台机器的所有网卡中。如果既指定了port,又指定了ip,则只会绑定在某一块网卡中。
  比如你将某个进程的监听端口绑定在127.0.0.1的80端口中,则只有你本机可以通过浏览器访问此页面,处于和你同一个局域网内的机器,想通过你在局域网中的地址来访问你这个页面,是connection refused,根本就没打开。这时候如果你也想启动另外一个进程,同样绑定在80端口,不过这次ip你想选择192.168.0.4(本地局域网分配的ip),在没有设置SO_REUSEADDR之前,会报EADDRINUSE的错误。
  但是当设置了SO_REUSEADDR的情况下,bind操作能够成功。但是这个也只能是reuse那些0.0.0.0的通配符IP,对于正在使用的确切的ip和port,reuse动作是不会成功的。
  
  另外一个大的用途就是上文中描述的,让进程可以使用仍处于2MSL等待的端口,而且需要注意到,SO_REUSEADDR这个值,只是对单个socket有效的,不能进行全局的设置。
  在上文描述的java程序中,直接ServerSocket.setReuseAddress(false),于是立马就出现了java.net.BindException: Address already in use的异常,所以说,我们之所以在之前的过程中没遇到这个问题,是因为默认值就设置了REUSEADDR为打开的状态。
  注意到,虽然SO_REUSEADD很强大,可以复用处于TIME_WAIT状态的端口,但是当此次连接的远程端口与上一次断开的远程端口一致时,也就是说是同一个Socket四元组时,依旧会报错。

SO_REUSEPORT

  如果想支持多个进程监听在同一个端口的话,这时候SO_REUSEPORT选项就派上用场了。
  什么时候会有这个需求呢?一开始我还以为如果多个进程监听在同一个端口,系统层面会将收到的流量拷贝,其实不是的,系统内核层面会做一个路由并做到负载均衡。目前的很多程序中,我们都使用一个线程去进行accept操作,当请求包比较小且量特别大的时候,其实网络的I/O还并未称为瓶颈,但是接收连接处理的线程先跑满了。即使多个线程去进行一个监听套接字的accept操作,由于竞争关系的锁资源的获取和释放也会影响处理的效率。而让多个不同的进程监听在同一个套接字,由内核层面去处理负载均衡是一个非常好的做法。
  SO_REUSEPORT选项并不是单独使用的,不仅仅后来bind的socket需要设置此选项为打开状态,之前已经打开并监听的那个Socket页应该设置为SO_REUSEPORT为打开状态,不然操作同样会失败。另外此套接字选项是后来才添加的,所以具体有没有此选项还依赖于操作系统的实现。比如因为JVM要适用于不同平台,目前我在JAVA中并没有找到设置此选项的值的方法。
  关于SO_REUSEPORT的使用,聂永的博客里这边文章写的比较详细,SO_REUSEPORT学习笔记

四 握手出现异常

1、SYN发送失败,或者没有收到响应,返回错误
(1)寻址失败,EHOSTUNREACH
(2)服务器关机,EHOSTUNREACH
(3)路由中发生错误
(4)backlog已满,服务端会忽略这个包

  客户端发出SYN连接请求后,SYN连接请求的IP包会迷失在网络中,SYN不会被对端确认,所以客户端会在一定时间后超时重传SYN,并再次设定定时器,当重复这个过程一定次数后,客户端停止尝试建立连接并向应用层报告超时错误。
  注意SYN重传的时间间隔分别为1s,2s,3s,4s,5s,7s等等,一个试了11此。这是系统(OS X)底层默认的TCP的设置,应用层也可以对这些进行改写。
2、发送完SYN后收到一个RST,ECONNREFUSED
  比如端口未打开,收到一个RST,这时候主动打开端会立即关闭,任何时候收到一个RST都会主动关闭。
3、收到了SYN+ACK,但是没有回复第三次ACK,或者被动打开端没有收到ACK
  (1)客户端只发送SYN便退出,服务端回复SYN+ACK没有响应,服务端会进行延时重试,如果客户端故意这么做,就相当与SYN Flood攻击行为。如果重新发送多次之后,仍没有确认报文,就发送一个复位报文RST,然后关闭TCP连接。
  (2)如果客户端在服务端收到最后一个ACK之前就发送数据,服务端的表现将是不会对这个包回复一个ACK。如果重新发送多次之后,仍没有确认报文,就发送一个复位报文RST,然后关闭TCP连接。

五 挥手出现异常

  正常的四次挥手流程可以这样描述,服务端先调用close,发送FIN,进入FIN_WAIT1状态,客户端回复ACK,分别进入FIN_WAIT2状态和CLOSE_WAIT,然后客户端也调用close,发送FIN,进入LAST_ACK状态,服务端接收到FIN包后发送ACK,进入TIME_WAIT状态。
  注意到,四次挥手是要求服务端和客户端都分别调用close方法才能走完的。在java程序中,也可以是直接退出java进程,发送FIN的工作由操作系统来实现。或许还可能是这个socket被gc后,系统也会发送FIN包,不过这仅仅是猜测。
  不正常的场景,就有很多了。
1、上一篇文章(如何判断Socket已经关闭)就分析过了半关闭的情况:一端调用close了,但是另一端并未调用close,主动发FIN的停留在FIN_WAIT2状态,被动方停留在CLOSE_WAIT状态,FIN_WAIT2在一端时间后会自动进入完全关闭状态,CLOSE_WAIT状态会持续停留。

2、服务端机器断电后重启,但是服务并没有重启,客户端试图向服务端发送数据,此时客户端会收到一个RST包,收到RST包的连接会直接进入close状态。

3、SO_LINGER选项,此选项可以选择打开与否,还可以设定一个超时时间。在设定了打开且未设置超时时间时,在调用close方法时,不会执行四次握手流程,直接发送RST包,将自己置为完全关闭状态,对端收到RST包后也将直接进入完全关闭状态。这种关闭方式称为“强制”或“失效”关闭,因为套接口的虚电路立即被复位,且丢失了未发送的数据。在远端的recv()调用将以WSAECONNRESET出错。
  默认的close操作是立即返回,如果有数据残留在套接口缓冲区中则系统将试着将这些数据发送给对方。若设置了SO_LINGER并确定了非零的超时间隔,则close方法阻塞进程,直到所剩数据发送完毕或超时,超时是抛出异常。这种关闭称为“优雅”或“从容”关闭。

六 参考文档

1、TCP/IP Illustrated Volume 1: The Protocols
2、wireshark网络分析就这么简单
3、Socket options SO_REUSEADDR and SO_REUSEPORT, how do they differ? Do they mean the same across all major operating systems?
4、SO_REUSEPORT学习笔记
5、How Tcp BackLog Works in Linux
6、UNIX Socket FAQ

posted on 2017-01-18 19:19  flystar32  阅读(765)  评论(0编辑  收藏  举报

导航