且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

码出高效:Java开发手册-第1章(5)

更新时间:2022-02-26 05:19:02

1.5.3 TCP 建立连接

传输控制协议(Transmission Control Protocol,TCP),是一种面向连接、确保数据在端到端间可靠传输的协议。面向连接是指在发送数据前,需要先建立一条虚拟的链路,然后让数据在这条链路上“流动”完成传输。为了确保数据的可靠传输,不仅需要对发出的每一个字节进行编号确认,校验每一个数据包的有效性,在出现超时情况时进行重传,还需要通过实现滑动窗口和拥塞控制等机制,避免网络状况恶化而最终影响数据传输的极端情形。每个TCP 数据包是封装在IP 包中的,每一个IP 头的后面紧接着的是TCP 头,TCP 报文格式如图1-17 所示。

码出高效:Java开发手册-第1章(5)

图1-17 TCP 报文格式

协议第一行的两个端口号各占两个字节,分别表示了源机器和目标机器的端口号。这两个端口号与IP 报头中的源IP 地址和目标IP 地址所组成的四元组可唯一标识一条TCP 连接。由于TCP 是面向连接的,因此有服务端和客户端之分。需要服务端先在相应的端口上进行监听,准备好接收客户端发起的建立连接请求。当客户端发起第一个请求建立连接的TCP 包时,目标机器端口就是服务端所监听的端口号。比如广为人知的端口号——HTTP 服务的80 端口、HTTPS 服务的443 端口、SSH 服务的22 端口等。可通过netstat 命令列出机器上已建立的连接信息,其中包含唯一标识一条连接的四元组,以及各连接的状态等内容,如图1-18 所示,图中的红框代表端口号。

码出高效:Java开发手册-第1章(5)

图1-18 IP 地址与端口信息

协议第二行和第三行是序列号,各占4 个字节。前者是指所发送数据包中数据部分第一个字节的序号,后者是指期望收到来自对方的下一个数据包中数据部分第一个字节的序号。

由于TCP 报头中存在一些扩展字段,所以需要通过长度为4 个bit 的头部长度字段表示TCP 报头的大小,这样接收方才能准确地计算出包中数据部分的开始位置。TCP 的FLAG 位由6 个bit 组成, 分别代表SYN、ACK、FIN、URG、PSH、RST,都以置1 表示有效。我们重点关注SYN、ACK 和FIN。SYN(Synchronize Sequence Numbers)用作建立连接时的同步信号;ACK(Acknowledgement)用于对收到的数据进行确认,所确认的数据由确认序列号表示;FIN(Finish)表示后面没有数据需要发送,通常意味着所建立的连接需要关闭了。

TCP 报头中的其他字段可以阅读RFC793 来掌握,本书在此不加赘述。接下来重点分析TCP 中连接建立的原理。图1-19 展示了正常情形下通过三次握手建立连接的过程。A 与B 的机器标识并不是绝对意义上的服务器与客户端。发起请求的也可能是服务器,向另一台其他后端服务器发送TCP 连接请求。前者需要在后者发起连接建立请求时先打开某个端口等待数据传输,否则将无法正常建立连接。三次握手指的是建立连接的三个步骤:

    • A 机器发出一个数据包并将SYN 置 1,表示希望建立连接。这个包中的序列号假设是x 。
    • B 机器收到 A 机器发过来的数据包后,通过 SYN 得知这是一个建立连接的请求,于是发送一个响应包并将SYN 和ACK 标记都置1。假设这个包中的序列号是y ,而确认序列号必须是x +1,表示收到了A 发过来的SYN。在TCP 中,SYN 被当作数据部分的一个字节。
    • A 收到 B 的响应包后需进行确认,确认包中将 ACK 置 1,并将确认序列号设置为y +1,表示收到了来自B 的SYN。

码出高效:Java开发手册-第1章(5)

图1-19 TCP 三次握手创建连接

这里为什么需要第3 次握手?它有两个主要目的:信息对等和防止超时。先从信息对等角度来看,如表1-8 所示,双方只有确定4 类信息,才能建立连接。在第2 次握手后,从B 机器视角看还有两个红色的NO 信息无法确认。在第3 次握手后,B 机器才能确认自己的发报能力和对方的收报能力是正常的。

表1-8 TCP 三次握手待确认信息码出高效:Java开发手册-第1章(5)

连接三次握手也是防止出现请求超时导致脏连接。TTL 网络报文的生存时间往往都会超过TCP 请求超时时间,如果两次握手就可以创建连接,传输数据并释放连接后,第一个超时的连接请求才到达B 机器的话,B 机器会以为是A 创建新连接的请求,然后确认同意创建连接。因为A 机器的状态不是SYN_SENT,所以直接丢弃了B 的确认数据,以致最后只是B 机器单方面创建连接完毕,简要示意图如图1-20所示。

码出高效:Java开发手册-第1章(5)

图1-20 两次握手导致的TCP 脏连接

如果是三次握手,则B 机器收到连接请求后,同样会向A机器确认同意创建连接,但因为A 机器不是SYN_SENT 状态,所以会直接丢弃,B 机器由于长时间没有收到确认信息,最终超时导致连接创建失败,因而不会出现脏连接。根据抓包分析,呈现出三次握手请求过程,SYN+ACK 的应答,告诉A 机器期望下一个数据包的第一个字节序号为1,如图1-21 所示。

码出高效:Java开发手册-第1章(5)

图1-21 TCP 三次握手抓包分析

从编程的角度,TCP 连接的建立是通过文件描述符(File Descriptor,fd)完成的。通过创建套接字获得一个fd,然后服务端和客户端需要基于所获得的fd 调用不同的函数分别进入监听状态和发起连接请求。由于fd 的数量将决定服务端进程所能建立连接的数量,对于大规模分布式服务来说,当fd 不足时就会出现“open too many files”错误而使得无法建立更多的连接。为此,需要注意调整服务端进程和操作系统所支持的最大文件句柄数。通过使用ulimit -n 命令来查看单个进程可以打开文件句柄的数量。如果想查看当前系统各个进程产生了多少句柄,可以使用如下的命令:

lsof -n | awk '{print $2}'| sort|uniq -c |sort -nr|more

执行结果如图1-22 所示,左侧列是句柄数,右侧列是进程号。lsof 命令用于查看当前系统所打开fd 的数量。在Linux 系统中,很多资源都是以fd 的形式进行读写的,除了提到的文件和TCP 连接,UDP 数据报、输入输出设备等都被抽象成了fd。

码出高效:Java开发手册-第1章(5)

图1-22 文件句柄与进程ID 的对应关系

想知道具体的PID 对应的具体应用程序是谁,使用如下命令即可:

ps -ax|grep 32764

Java 进程示例如图1-23 所示。

码出高效:Java开发手册-第1章(5)

图1-23 根据进程ID 查询具体进程

TCP 在协议层面支持Keep Alive 功能,即隔段时间通过向对方发送数据表示连接处于健康状态。不少服务将确保连接健康的行为放到了应用层,通过定期发送心跳包检查连接的健康度。一旦心跳包出现异常不仅会主动关闭连接,还会回收与连接相关的其他用于提供服务的资源,确保系统资源最大限度地被有效利用。