TCP为应用层提供的是全双工服务,也就是说数据能在发送方和接收方两个方向上独立地进行传输。
在浅析TCP传输层协议之前先说说segment
, datagram
, frame
的区别。
严格说来,以上三个名词均为专业性术语。
message
- 用于应用层segment
- TCP的PDU(Protocol Data Unit)datagram
- UDP的PDUpacket
- IP的PDU(RFC791中也称之为datagram)frame
- 数据链路层的PDU
TCP segment structure
TCP segment由TCP头部和TCP数据部分组成,首先来瞅瞅TCP头部的包结构。
- Source port (16 bits)
- 16位源端口号(发送方使用)
- Destination port (16 bits)
- 16位目的端口号(接收方使用)
- Sequence number (32 bits)
- 32位序列号,有两个作用:
- 在
SYN
flag 置1时,此为当前连接的初始序列号(initial sequence number, ISN),数据的第一个字节序号为此ISN+1 - 在
SYN
flag 置0时,为当前连接报文段的累计数据包字节数。
- Acknowledgment number (32 bits)
- 32位确认序号,ACK flag置1时才有效,指接收方期待的下一个报文段的序列号。
- Data offset (4 bits)
- 指示TCP头部的32位元的数目。由于有Options域的存在,因此TCP头部最小为20字节,最大为(15*4 = 60)字节. 同时这个域也包含了TCP报文段实际数据的开始偏移量。
- Reserved (3 bits)
- 保留域
- Flags (9 bits) (aka Control bits)
- 控制域信息包含9个一位标志符(3个为新增)。
- ACK - 确认域使用。在初始SYN包之后由客户端发送到报文段都必须包含此控制信息
- RST - 重置连接
- SYN - 同步序列号,一般仅在连接双方发送第一个包时使用
- FIN - 发送方不再发送数据
- Window size (16 bits)
- 接收窗口的大小(默认以字节为单位),期望接收到报文段多少,常用于流控。
TCP 协议操作细节
TCP协议可分为三个阶段:
- connection establishment(连接建立) - 数据传输前的多次握手过程
- data transfer(数据传输) - 数据传输阶段
- connection termination(连接终止) - 关闭连接并释放所有资源
在整个TCP连接过程中客户端/服务器端将经历一系列的状态转移,之所以有这么复杂的状态转移——TCP想要的是在IP层之上建立『面向连接』的通讯机制。
- LISTEN
- (服务端) 等待任意远端使用TCP协议向指定端口发起的连接请求。
- SYN-SENT
- (客户端) 在发起连接请求后等待相应的连接。
- SYN-RECEIVED
- (服务端) 在接收和发送连接请求后等待确认连接请求的ACK.
- ESTABLISHED
- (服务端和客户端) 连接已打开,接收到的数据可以传送至用户。这个状态为数据传输的正常状态。
- FIN-WAIT-1
- (服务端和客户端) 等待远端 TCP 的连接终止请求或之前发送连接终止包的确认包。
- FIN-WAIT-2
- (服务端和客户端) 等待远端 TCP 的连接终止请求。
- CLOSE-WAIT
- (服务端和客户端) 等待本地用户的连接终止请求。
- CLOSING
- (服务端和客户端) 等待远端 TCP 的连接终止确认。
- LAST-ACK
- (服务端和客户端) 等待之前发送的终止请求包的确认包。
- TIME-WAIT
- (服务端和客户端) 等待足够时间以确保远端 TCP 收到了传输终止请求。(RFC793中规定最大报文段生存时间可为4分钟)
- CLOSED
- (服务端和客户端) 无任何连接状态。
常见的TCP状态转移图为Stevens的大作上截的,但其实并不是非常好理解,这里以 The TCP/IP Guide - TCP Operational Overview and the TCP Finite State Machine (FSM) 中给出的图作为参考,相对要清晰很多。
连接建立——三次握手
要想彻底理解三次握手期间客户端和服务器端的状态变化,强烈推荐如下两个资源:
- The TCP/IP Guide - TCP Connection Establishment Process: The “Three-Way Handshake”
- Unix 网络编程 - 2.6节 TCP连接的建立和终止
三次握手的状态转移时序图如下所示(图片来源为 The TCP/IP Guide):
- 首先Client 和 Server 均处于
CLOSED
状态 - Server准备好接受外来连接,这个过程通常称为被动打开(passive open),一般可由
socket
,bind
,listen
来完成。此时Server进入LISTEN
状态 - Client通过调用
connect
执行主动打开(active open),向Server发送SYN,此报文段含用于此次连接的初始序列号(ISN)。此时Client进入SYN-SENT
状态,等待来自Server返回的ACK - Server收到来自Client发送的SYN后,向Client返回SYN+ACK,返回的报文段中包含此次连接中Server端初始序列号(ISN)。此时Server进入
SYN-RECEIVED
状态,并等待来自Client的ACK - Client在收到来自Server的SYN+ACK之后,向Server返回ACK。此时Client进入
ESTABLISHED
状态 - Server在收到来自Client的ACK之后即进入
ESTABLISHED
状态
Q: 为何是「三次握手」而不是「四次握手」?
Server返回的SYN+ACK将 SYN 和 ACK 合二为一了。
具体执行这些报文段发送的 API 如下图所示。
双方同时建立连接时的TCP状态变化见 The TCP/IP Guide - TCP Connection Establishment Process: The “Three-Way Handshake”,这里不再赘述。
建立 TCP 连接和电话系统的对比
此小节参考《Unix网络编程》一书
socket
- 有电话可用bind
- 告诉别人你的电话号码,这样他们可以呼叫你listen
- 打开电话振铃,有外来电话到达时你可以听到connect
- 知道对方的电话号码并拨打它accept
- 发生在被呼叫的人应答电话之时,由accept返回客户标识(即客户IP和Port)
连接终止——四次挥手
TCP连接的终止比想象中难,它需要处理多种复杂情况,首先它为全双工协议,关闭连接需要在双方进行。下面先简述正常情况下的连接终止过程——即四次挥手,本节主要参考 The TCP/IP Guide - TCP Connection Termination
假如连接终止是由Client首先发起(主动关闭),则相应的TCP状态转移时序图通常如下所示(图片来源为 The TCP/IP Guide):
- 首先Client和Server均处于连接建立状态
- Client收到来自上层应用的关闭信号,故Client向Server发送FIN报文段,此时Client进入
FIN-WAIT-1
状态,等待来自Server的ACK和FIN。注意由于此时Server相应的应用还未准备好关闭,故不可将FIN+ACK组合发送 - Server收到来自Client的FIN,向Client返回ACK,并告诉应用准备关闭,此时Server进入
CLOSE-WAIT
状态,并等待来自上层应用的关闭信号,以准备向Client发送FIN - Client收到来自Server的ACK,但还未收到来自Server的FIN,此时Client进入
FIN-WAIT-2
状态,并继续等待来自Server的关闭信号FIN - Server的上层应用准备好关闭,向Client发送关闭信号FIN,此时Server进入
LAST-ACK
状态,并等待来自Client的ACK - CLient收到来自Server的FIN,向Server发送ACK,此时Client进入
TIME-WAIT
状态 - Server收到来自Client的ACK,此时Server已经确定可以关闭,进入最终状态
CLOSED
- Client等待两倍的MSL生存时间以确保发出去的ACK被Server正确接收
- 两倍MSL时间到期,Client自行关闭
TIME-WAIT
状态的必要性:
由于Client在发送完最后一个ACK后不能收到来自Server的任何信息(Server不对收到的ACK进行ACK),因此只能
- 可靠地实现TCP全双工连接的终止:确保发出去的ACK能被对端收到,如果未收到则Server有可能重传FIN,Client端必须保留此状态一段时间
- 允许老的重复分节在网络中消逝:防止老的重复分组在本次连接结束之前再次建立新连接。之前的分组+ACK=2MSL,故只要在2MSL后发起新连接,则可保证其不是老的重复分组。
可参考《Unix网络编程》2.7节TIME_WAIT状态 获得更为详细的信息。 由此可知大量短连接将造成巨大的资源浪费!!因此在服务器端可使用长连接或者执行被动关闭减少服务器资源的浪费。
使用Python socket 模拟TCP客户端和服务器
可参考 TCP/IP Client and Server - Python Module of the Week 自行做实验,以下简述下我做实验的过程。
服务器端的建立:
- 先建socket
- 随后bind到有效地址和端口
- 调用listen监听外部请求
- 接收发送数据
客户端的建立:
- 先建socket
- 向服务端发起connect
- 发送数据
- 关闭连接
使用IPython作为Python运行环境,使用tcpdump抓包并重定向标准输出到文本文件,便于后期分析。
tcpdump -i lo0 -nn 'tcp' > /tmp/socket.log # 仅查看本地环回地址的tcp包,并重定向标准输出至 /tmp/socket.log
awk '/10000|65247/' /tmp/socket.log > /tmp/socket_echo.log # 过滤含有10000和65247的行至 /tmp/socket_echo.log
我的ipynb文件和log文件可从以下文件下载:socket_echo_server.ipynb, socket_echo_client.ipynb, socket_echo.log
相对较为详细的图文教程 - tcp 三次握手和四次断连深入分析:连接状态和socket API的关系