TCP(Transmission Control Protocol传输控制协议)协议是基于IP协议,面向连接的、可靠的、基于字节流的传输层通信协议。
协议就是计算机与计算机之间通过网络实现通信时事先达成的一种“约定”。了解TCP协议报文之前,先简单回顾下【OSI七层模型】和【TCP/IP协议】。
OSI(Open System Interconnect),开放系统互联,一般称它OSI参考模型,它是ISO(国际标准化组织)为了更好普及网络而推出的规范。OSI定义了网络互联的七层框架:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层,每一层实现各自的协议和功能,实现和相邻层的网络通信。
TCP/IP协议不是单单指TCP、IP协议,是指由HTTP、FTP、SMTP、TCP、UDP、IP等协议组成的协议簇,是对这些通信协议的统称。至于为啥叫它TCP/IP协议,我猜可能是TCP协议和IP协议比较有代表性吧~
至于OSI参考模型和TCP/IP四层协议的区别,我理解OSI参考模型是学术上定义的标准,是一个理论上的网络通信模型,没有对协议进行详细的定义。而TCP/IP协议是参考这个标准的具体实现,是在实际的实现中运行的网络协议。OSI参考模型注重“通信协议必要的功能是什么”,而TCP/IP则更强调“在计算机上实现协议应该开发哪种程序”。
以 curl -H "Content-Type:application/json" -X POST --data '{"param1": "value2", "param2": "value2"}' http://192.168.2.188/service
为例,来分析一下通过HTTP协议从客户端向服务端发送数据,数据如何传输的:
客户端为发送端,服务端为接收端。
1、应用层把【Content-Type:application/json】放到HTTP头,把【{“param1”: “value2”, “param2”: “value2”}】放到HTTP BODY(上图HTTP内容)中,组成一个HTTP报文;
2、传输层把接收到的HTTP报文作为TCP的内容,加上TCP头(包含源端口号、目标端口号等),拼接成一个数据段(TCP报文),如果TCP的内容(HTTP报文)很大,在传输层可能会把TCP的内容拆分为多份,拼接成多个数据段(TCP报文)
3、网络层把接收到的TCP报文加上IP头(包含源IP地址、目标IP地址等),组成一个数据包(IP报文)
4、数据链路层把接收到的IP报文加上以太网头部(包含源MAC地址、目标MAC地址等)和以太网尾部(FCS 帧检验序列),组成一个数据帧。
5、数据链路层的数据帧封装完成后会通过物理层转换成比特流在物理介质上传输。比特流也就是二进制流(01010101010101010101010101),在介质就是以电流的高低电平的形式的形式传输。
发送端从应用层、传输层、网络层、数据链路层由上至下按照顺序传输数据,接收端则从数据链路层、网络层、传输层、应用层由下至上向每个上一级分层传输数据。每个分层上,在处理由上一层传过来的数据时可以附上当前分层的协议所必须的首部信息,然后接收端对收到的数据进行数据“首部”与“内容”的分离,再转发给上一分层,并最终将发送端的数据恢复为原状。
前面说TCP是面向连接传输的,通信双方通过三次握手建立“连接”。这里的“连接”是指通信双方知道对方的存在,双方有对应的socket资源,有对应的发送缓存和接收缓存,有相应的拥塞控制策略等。连接并不是真实存在的,只是一种状态,通信双方通过一定的数据结构来维持这种状态。
分析三次握手之前,先了解一下TCP报文的内容,可能会理解地更容易一些。
标志位 | 说明 |
---|---|
URG | 占1位,表示紧急指针字段有效。URG位指示报文段里的上层实体(数据)标记为“紧急”数据。当URG=1时,其后的紧急指针指示紧急数据在当前数据段中的位置(相对于当前序列号的字节偏移量),TCP接收方必须通知上层实体。 |
ACK | 占1位,置位ACK=1表示确认号字段有效;TCP协议规定,接建立后所有发送的报文的ACK必须为1;当ACK=0时,表示该数据段不包含确认信息。当ACK=1时,表示该报文段包括一个对已被成功接收报文段的确认序号Acknowledgment Number,该序号同时也是下一个报文的预期序号。 |
PSH | 占1位,表示当前报文需要请求推(push)操作;当PSH=1时,接收方在收到数据后立即将数据交给上层,而不是直到整个缓冲区满。 |
RST | 占1位,置位RST=1表示复位TCP连接;用于重置一个已经混乱的连接,也可用于拒绝一个无效的数据段或者拒绝一个连接请求。如果数据段被设置了RST位,说明报文发送方有问题发生。 |
SYN | 占1位,在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1。 综合一下,SYN置1就表示这是一个连接请求或连接接受报文。 |
FIN | 占1位,用于在释放TCP连接时,标识发送方比特流结束,用来释放一个连接。当 FIN = 1时,表明此报文的发送方的数据已经发送完毕,并要求释放连接。 |
三次握手建立连接的过程:
通信双方建立连接,通常情况下都是Server端先打开一个服务端套接字(ServerSocket)来监听对方的连接请求,称为被动打开;Server端被动打开后,Client端就可以主动向Server端发起连接请求了,称为主动打开。
1、Client生成序列号Sequence Number(假设生成的seq值为x),SYN标志位为1,向Server发送一次TCP请求报文,表示请求与对方建立连接,同时Client进入SYN-SENT状态。【服务端老哥,我想请求和你建立连接】
2、Server在LISTEN的状态下接收到SYN请求后,生成序列号Sequence Number(假设生成的seq值为y),ACK标志位设为1,SYN标志位设为1,确认序号Acknowledgement Number(ack)为Client第一次握手报文的seq+1(x+1),表示已收到对方的请求,并且向对方确认建立连接,然后将这个数据包发送给Client,之后Server进入SYN_RCVD状态,同时Server会创建一个与Client通信的Socket放到半连接队列中。【客户端老弟,我知道了,我同意和你建立连接】
3、Client在收到Server的第二次握手报文(FIN+ACK)后,会向Server回复一个报文,ACK标志位为1,确认序号Acknowledgement Number(ack)为Server第二次握手报文的seq+1(y+1),表示已经收到对方的确认连接请求,同时进入ESTABLISHED状态。当Server收到Client的第三次握手报文后,也进入ESTABLISHED状态,同时会把对应的Socket放入全连接队列中。【好的,我知道了】
完成这三个过程后,双方都认为对方已经具备接收数据的条件了,就可以开始传输数据了。
【思考】
四次挥手断开连接的过程:
1、Client主动关闭连接,序列号Sequence Number为和Server通信时Server的最后一次ACK报文的ack的值(假设seq值为x),FIN标志位为1,向Server发送一次TCP请求报文,表示请求和对方断开连接,同时Client进入FIN_WAIT_1状态。【服务端老哥,我想和你断开连接,我不再向你发送数据了】
2、Server收到客户端的FIN报文后,会回发ACK报文,ACK标志位为1,确认序号Acknowledgement Number(ack)为Client第一次挥手报文的seq+1(x+1)。【收到,请客户端老弟稍等一下,我准备好了跟你说】
3、当Server可以断开连接的时候(手头上没有需要处理的任务),跟Client第一次挥手一样,向Client发送FIN报文,FIN标志位为1,ACK标志位为1,序列号Sequence Number的值为和Client通信时Client的最后一次ACK报文的ack的值(假设seq=z),确认序号Acknowledgement Number(ack)为Client第一次握手报文的seq+1(x+1)。【客户端老弟,根据你的断开请求我已经准备好,我要断开连接了】
4、Client收到Server断开连接的FIN报文后,会回复一个ACK报文,ACK标志位为1,确认序号Acknowledgement Number(ack)为Server第三次挥手报文的seq+1(y+1)。这时Client会等待2MSL(数据包在网络中存活的时间是一个MSL,通信中一来一回就是2个MSL),确保Server收到第四次ACK报文,如果Server没收到,会在2MSL之内重新发送FIN报文,并重新等待2MSL。【好的,我知道了】
至此,Client和Server之间的TCP连接断开,都进入CLOSED状态。
思考:
了解了理论后,通过tcpdump来抓包分析下TCP三次握手、传输数据、四次挥手的过程是怎样的。找两台机器分别充当服务端(10.246.100.61)和客户端(10.246.131.47)。
在服务端执行sudo tcpdump -S -nn -i en0 port 8080
来监听服务端8080端口的网络数据
在服务端(MacOS Big Sur11.3.1)启动监听程序:
public class Server {
public static void main(String[] args) {
ServerSocket serverSocket = new ServerSocket(8080);
while (true){
Socket clientSocket = serverSocket.accept(); //如果没有客户端连接会在此堵塞
System.out.println("接收到一个客户端");
while (true) {
InputStream inputStream = clientSocket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String content = bufferedReader.readLine();
if (content == null) {
clientSocket.close();
System.out.println("clientSocket.close();");
break;
}
System.out.println("服务端接收到信息:" + content);
}
}
}
}
在客户端(MacOS Big Sur11.5.2 )向服务端建立TCP通信、发送数据:
public class Client {
public static void main(String[] args) throws IOException {
Socket socket =null;
try {
socket = new Socket("10.246.100.61", 8080);
String content = StringUtils.randomAlphanumeric(10);
OutputStream outputStream = socket.getOutputStream();
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write(content); //发送10个字节的字符串
bufferedWriter.flush();
bufferedWriter.write(content+content); //发送20个字节的字符串
bufferedWriter.flush();
bufferedWriter.write(content+content+content); //发送30个字节的字符串
bufferedWriter.flush();
bufferedWriter.write(content+content+content+content); //发送40个字节的字符串
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1、服务端启动监听,客户端以debug模式运行,监听到的报文是这样的:
10:18:41.756843 IP 10.246.131.47.63912 > 10.246.100.61.8080: Flags [S], seq 1495498772, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 3619360037 ecr 0,sackOK,eol], length 0
10:18:41.757370 IP 10.246.100.61.8080 > 10.246.131.47.63912: Flags [S.], seq 2016084851, ack 1495498773, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 2360293984 ecr 3619360037,sackOK,eol], length 0
10:18:41.765520 IP 10.246.131.47.63912 > 10.246.100.61.8080: Flags [.], ack 2016084852, win 2058, options [nop,nop,TS val 3619360151 ecr 2360293984], length 0
10:18:41.765662 IP 10.246.100.61.8080 > 10.246.131.47.63912: Flags [.], ack 1495498773, win 2058, options [nop,nop,TS val 2360293992 ecr 3619360151], length 0
10:18:58.575970 IP 10.246.131.47.63912 > 10.246.100.61.8080: Flags [P.], seq 1495498773:1495498783, ack 2016084852, win 2058, options [nop,nop,TS val 3619376924 ecr 2360293992], length 10: HTTP
10:18:58.576153 IP 10.246.100.61.8080 > 10.246.131.47.63912: Flags [.], ack 1495498783, win 2058, options [nop,nop,TS val 2360310774 ecr 3619376924], length 0
10:19:02.545456 IP 10.246.131.47.63912 > 10.246.100.61.8080: Flags [P.], seq 1495498783:1495498803, ack 2016084852, win 2058, options [nop,nop,TS val 3619380793 ecr 2360310774], length 20: HTTP
10:19:02.545660 IP 10.246.100.61.8080 > 10.246.131.47.63912: Flags [.], ack 1495498803, win 2058, options [nop,nop,TS val 2360314738 ecr 3619380793], length 0
10:19:03.753664 IP 10.246.131.47.63912 > 10.246.100.61.8080: Flags [P.], seq 1495498803:1495498833, ack 2016084852, win 2058, options [nop,nop,TS val 3619382076 ecr 2360314738], length 30: HTTP
10:19:03.753819 IP 10.246.100.61.8080 > 10.246.131.47.63912: Flags [.], ack 1495498833, win 2057, options [nop,nop,TS val 2360315943 ecr 3619382076], length 0
10:19:05.307899 IP 10.246.131.47.63912 > 10.246.100.61.8080: Flags [P.], seq 1495498833:1495498873, ack 2016084852, win 2058, options [nop,nop,TS val 3619383518 ecr 2360315943], length 40: HTTP
10:19:05.308117 IP 10.246.100.61.8080 > 10.246.131.47.63912: Flags [.], ack 1495498873, win 2057, options [nop,nop,TS val 2360317496 ecr 3619383518], length 0
10:19:10.325498 IP 10.246.131.47.63912 > 10.246.100.61.8080: Flags [F.], seq 1495498873, ack 2016084852, win 2058, options [nop,nop,TS val 3619388584 ecr 2360317496], length 0
10:19:10.325731 IP 10.246.100.61.8080 > 10.246.131.47.63912: Flags [.], ack 1495498874, win 2057, options [nop,nop,TS val 2360322498 ecr 3619388584], length 0
10:19:10.332732 IP 10.246.100.61.8080 > 10.246.131.47.63912: Flags [F.], seq 2016084852, ack 1495498874, win 2057, options [nop,nop,TS val 2360322505 ecr 3619388584], length 0
10:19:10.341464 IP 10.246.131.47.63912 > 10.246.100.61.8080: Flags [.], ack 2016084853, win 2058, options [nop,nop,TS val 3619388641 ecr 2360322505], length 0
当客户端代码执行完socket = new Socket("192.168.2.202", 8080);
后,tcpdump监听到了第1-4行报文,可以发现1-3行是就是客户端与服务端三次握手建立连接的过程:
第4行报文用Wireshark分析可以看到有TCP Window Update的标志,这是服务端根据自己处理能力,调整TCP窗口为131712,可以发现后面的报文中Win都变成131712了。
然后客户端每执行一次bufferedWriter.flush();
,就会有一条【客户端向服务端发送数据】和【服务端回复ack】的报文。代码中客户端一共发送了4次数据,tcpdump也监听到了4组对应发送数据和ack的报文。
最后4行就是四次挥手的过程。
2、服务端启动监听,客户端以run模式直接运行程序(中间没有停顿),监听到的报文是这样的:
10:27:40.877203 IP 10.246.131.47.63941 > 10.246.100.61.8080: Flags [S], seq 1402389697, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 1119685773 ecr 0,sackOK,eol], length 0
10:27:40.877672 IP 10.246.100.61.8080 > 10.246.131.47.63941: Flags [S.], seq 1063401628, ack 1402389698, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 1726411899 ecr 1119685773,sackOK,eol], length 0
10:27:40.887556 IP 10.246.131.47.63941 > 10.246.100.61.8080: Flags [.], ack 1063401629, win 2058, options [nop,nop,TS val 1119685830 ecr 1726411899], length 0
10:27:40.887669 IP 10.246.100.61.8080 > 10.246.131.47.63941: Flags [.], ack 1402389698, win 2058, options [nop,nop,TS val 1726411909 ecr 1119685830], length 0
10:27:40.893232 IP 10.246.131.47.63941 > 10.246.100.61.8080: Flags [P.], seq 1402389698:1402389708, ack 1063401629, win 2058, options [nop,nop,TS val 1119685836 ecr 1726411899], length 10: HTTP
10:27:40.893310 IP 10.246.100.61.8080 > 10.246.131.47.63941: Flags [.], ack 1402389708, win 2058, options [nop,nop,TS val 1726411914 ecr 1119685836], length 0
10:27:40.894503 IP 10.246.131.47.63941 > 10.246.100.61.8080: Flags [FP.], seq 1402389708:1402389798, ack 1063401629, win 2058, options [nop,nop,TS val 1119685837 ecr 1726411899], length 90: HTTP
10:27:40.894541 IP 10.246.100.61.8080 > 10.246.131.47.63941: Flags [.], ack 1402389799, win 2057, options [nop,nop,TS val 1726411915 ecr 1119685837], length 0
10:27:40.895056 IP 10.246.100.61.8080 > 10.246.131.47.63941: Flags [F.], seq 1063401629, ack 1402389799, win 2057, options [nop,nop,TS val 1726411915 ecr 1119685837], length 0
10:27:40.901068 IP 10.246.131.47.63941 > 10.246.100.61.8080: Flags [.], ack 1063401630, win 2058, options [nop,nop,TS val 1119685842 ecr 1726411915], length 0
1-4行跟上面的抓包的报文没什么区别,客户端第一次发送数据(10字节的字符串)也跟上面抓包的报文类似,报文长度都是10(length=10)。
但是在第7行,客户端明明发送了第2、第3、第4条数据,但TCP是通过一个TCP报文发送给服务端的,报文length=90,刚好是第2、第3、第4条数据的长度之和(20+30+40=90),这就是常说的粘包。
并且最后四次挥手也简化了,同时客户端在发送第7行的这条报文时,也把FIN控制为置为1,相当于在发送最后一次数据报文时就开始四次挥手断开连接了(客户端:“发送了这个报文,我就要和你断开连接啦”),这样的好处客户端最后一次发送消息、服务端ack这两次报文顺便就把四次挥手的前两次的工作给做了,节省了发送两次报文的时间。
3、服务端启动监听,客户端以run模式直接运行程序,只发送1长度为1500字节的数据:
Client段代码:
public class Client {
public static void main(String[] args) throws IOException {
Socket socket =null;
try {
socket = new Socket("10.246.100.61", 8080);
String content = StringUtils.randomAlphanumeric(1500);
OutputStream outputStream = socket.getOutputStream();
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write(content);
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
监听到的报文:
18:07:15.126983 IP 10.246.131.47.65168 > 10.246.100.61.8080: Flags [S], seq 4127151943, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 1726020163 ecr 0,sackOK,eol], length 0
18:07:15.127462 IP 10.246.100.61.8080 > 10.246.131.47.65168: Flags [S.], seq 1459700619, ack 4127151944, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 3332064821 ecr 1726020163,sackOK,eol], length 0
18:07:15.134597 IP 10.246.131.47.65168 > 10.246.100.61.8080: Flags [.], ack 1459700620, win 2058, options [nop,nop,TS val 1726020212 ecr 3332064821], length 0
18:07:15.134744 IP 10.246.100.61.8080 > 10.246.131.47.65168: Flags [.], ack 4127151944, win 2058, options [nop,nop,TS val 3332064828 ecr 1726020212], length 0
18:07:15.163846 IP 10.246.131.47.65168 > 10.246.100.61.8080: Flags [.], seq 4127151944:4127153392, ack 1459700620, win 2058, options [nop,nop,TS val 1726020239 ecr 3332064828], length 1448: HTTP
18:07:15.163856 IP 10.246.131.47.65168 > 10.246.100.61.8080: Flags [P.], seq 4127153392:4127153444, ack 1459700620, win 2058, options [nop,nop,TS val 1726020239 ecr 3332064828], length 52: HTTP
18:07:15.163958 IP 10.246.100.61.8080 > 10.246.131.47.65168: Flags [.], ack 4127153444, win 2035, options [nop,nop,TS val 3332064857 ecr 1726020239], length 0
18:07:15.165164 IP 10.246.131.47.65168 > 10.246.100.61.8080: Flags [F.], seq 4127153444, ack 1459700620, win 2058, options [nop,nop,TS val 1726020240 ecr 3332064828], length 0
18:07:15.165233 IP 10.246.100.61.8080 > 10.246.131.47.65168: Flags [.], ack 4127153445, win 2048, options [nop,nop,TS val 3332064858 ecr 1726020240], length 0
18:07:15.166150 IP 10.246.100.61.8080 > 10.246.131.47.65168: Flags [F.], seq 1459700620, ack 4127153445, win 2048, options [nop,nop,TS val 3332064858 ecr 1726020240], length 0
18:07:15.171678 IP 10.246.131.47.65168 > 10.246.100.61.8080: Flags [.], ack 1459700621, win 2058, options [nop,nop,TS val 1726020247 ecr 3332064858], length 0
文章前面提到MSS最大为1460,也就是TCP报文最多可以携带1460字节的数据,上面发送了1500字节的数据,果然被拆分到两个TCP报文分别发送了,第一次发送了1448字节数据(length=1448),第二个报文发送了剩余的52字节数据(length=42)。数据太大了拆成2个报文可以理解,但是为什么1550字节的数据被拆分成了1448字节+52字节,而不是1460+40字节呢?通过Wireshark分析可以发现,可选项部分占用了12字节(包含两个各占一个字节的NOP标志位和占10字节的Timestamps时间戳),所以可携带的数据最多只有1460-12=1448字节了:
上面的报文还可以看出,客户端是同时把第5、第6行这两个报文发了出去,发出第一个报文后没有等待服务端的ack,就紧接着吧第二个报文发出去了,实际情况下,客户端发送数据不用必须等接收到服务端上一个ack报文后才会发送,客户端会一次性发送多个报文,服务端接收到后会ack最后收到的报文。比如客户端同时发送了三个报文,每个报文seq分别为1、2、3,假如服务端只收到seq为1的报文,回复的报文ack为2(表示2之前的报文都收到了,请发送seq为2的报文吧);假如服务端收到seq为1、2的报文,回复的报文ack为3(表示3之前的报文都收到了,请发送seq为3的报文吧);假如服务端短时间内都收到了,就只会回复一个ack为4的报文(表示4之前的报文都收到了,请发送seq为4的报文吧)。
TCP通信过程中可能遇到很多问题,还有很多复杂的场景,这里只是简单抓个包分析下,如果有不对的地方,希望包涵给予指正。
转载请注明出处——胡玉洋《TCP协议学习笔记、报文分析》