TCP协议在双方建立连接的时候需要三次握手,首先客户端发送SYN标志为1的TCP数据包,然后服务器端收到之后,也会发送一个SYN标志置位,并且带有ack应答的数据包,最后客户端再发送给服务端一个应答,这样就建立起了通信。
首先看TCP数据包头部各个字段:
在三次握手和四次挥手过程中,主要看UAPRSF6个标志和seq ack的变化。首先使用telnet程序测试,比如telnet 8.8.8.8 443,使用tcpdump -i enp0s3 -X -vv tcp -s0 命令看通信过程
192.168.0.10.53548 > 14.215.177.39.https 表示从192.168.0.10的53548端口发送到了14.215.177.39的https(443)端口,Flags [S]表示发送的SYN,也就是连接标志,seq 1436271743表示序号, win 29200表示窗口大小,options [mss 1460,sackOK,TS val 16451924 ecr 0,nop,wscale 7]是在正式建立连接前告诉对方自己的一些选项字段,比如mss表示自己的最大报文长度是1460,length:0表示数据字段部分为0,就是没有数据。
以太网帧完整数据:
0x0000: 4510 003c cd2b 4000 4006 eccf c0a8 000a E..<.+@.@....... 0x0010: 0ed7 b127 d12c 01bb 559b c47f 0000 0000 ...'.,..U....... 0x0020: a002 7210 80df 0000 0204 05b4 0402 080a ..r............. 0x0030: 00fb 0954 0000 0000 0103 0307 ...T........ 主要看TCP头部相关的内容和TCP头部图对比下,第二行的 0x559b c47f表示seq,0x0000 0000表示ack,第三行的0xa002表示TCP头部长度是10 * 4 = 40字节,并且SYN为1表示发起连接请求。
下面看服务器端的回复
14.215.177.39.https > 192.168.0.10.53548: Flags [S.], cksum 0x418e (correct), seq 2784679373, ack 1436271744, win 8192, options [mss 1440,sackOK,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,wscale 5], length 0 表示14.215.177.39的443端口发送给192.168.0.10的53548端口的SYN连接,seq是2784679373, ack就是客户端发去的seq +1,其中服务端的mss是1440
以太网帧如下:
0x0000: 4500 003c cd2b 4000 3806 f4df 0ed7 b127 E..<.+@.8......' 0x0010: c0a8 000a 01bb d12c a5fa d5cd 559b c480 .......,....U... 0x0020: a012 2000 418e 0000 0204 05a0 0402 0101 ....A........... 0x0030: 0101 0101 0101 0101 0103 0305 ............ 其中标志为变为了0x12,ACK和SYN都为1,表示发送的是带有ack回应的SYN连接。
看最后一次握手,客户端发送给服务器端的确认
192.168.0.10.53548 > 14.215.177.39.https: Flags [.], cksum 0x80cb (incorrect -> 0xc571), seq 1, ack 1, win 229, length 0,发现没了选项字段,说明在默认情况下,选项字段是在三次握手中前两次握手时确定了双方的各种属性。
以太网帧如下:
0x0000: 4510 0028 cd2c 4000 4006 ece2 c0a8 000a E..(.,@.@....... 0x0010: 0ed7 b127 d12c 01bb 559b c480 a5fa d5ce ...'.,..U....... 0x0020: 5010 00e5 80cb 0000 P.......
seq0x559b c480也就是服务端发来的ack字段,ack为服务端的seq + 1,标志变成了0x10只置位了ACK字段,说明这就是一个简单的确认报文。
三次握手过程如下图:
图中的UAPRSF表示TCP头部的标志位字段, 客户端会有两种状态:SYS_SENT, ESTABLISHED,我们使用netstat -npt命令可以看到这种状态的转换,比如telnet一个不存在的IP端口号,会看telnet一直处于SYS_SENT,直到进程退出,如果连接成功会变成ESTABLISHED, 服务端有三种状态LISTEN, SYS_RCVD, ESTABLISHED, LISTNE表示正在监听某一个端口,在接受到客户端发送的SYN时会变为SYS_RCVD,然后再发送一个SYN报文给客户端,客户端回复确认后,会从SYS_RCVD变为ESTABLISHED.也就是服务端和客户端已经连接成功。
再看四次挥手,四次挥手如果使用telnet 百度网站的话如果输入quit,是由服务端发起关闭连接的,如果直接关掉telnet程序的话,客户端会一直发送FIN报文,但是服务端不会响应了,所以这次自己写个socket服务端程序和客户端程序,先看具体报文
发现只有三段报文,书上说四次挥手,为啥不一样呢,怎么变成了三次,TCP/TP协议详解四次挥手过程是1.客户端发送FIN报文2.服务端响应发送ack 3.服务端发送FIN报文 4.客户端发送响应ack报文。而抓包结果是第二和第三也就是服务端发送的ack和FIN合并成了一个报文。先具体分下下各个报文
192.168.0.10.55728 > 192.168.0.10.8887: Flags [F.], cksum 0x818b (incorrect -> 0x07a3), seq 1, ack 1, win 342, options [nop,nop,TS val 17498958 ecr 17497745], length 0 客户端的55728发送给服务端的8887端口,FIN标志为1
以太网帧如下:
0x0000: 4500 0034 7214 4000 4006 474b c0a8 000a E..4r.@.@.GK.... 0x0010: c0a8 000a d9b0 22b7 eb37 8c84 f7c4 7c80 ......"..7....|. 0x0020: 8011 0156 818b 0000 0101 080a 010b 034e ...V...........N 0x0030: 010a fe91 .... 标志位为 0x11,ACK 和FIN为1,所以客户端发送的是关闭连接请求。
192.168.0.10.8887 > 192.168.0.10.55728: Flags [F.], cksum 0x818b (incorrect -> 0x02e5), seq 1, ack 2, win 342, options [nop,nop,TS val 17498958 ecr 17498958], length 0 服务端回复的也是FIN报文
以太网帧如下:
0x0000: 4500 0034 8211 4000 4006 374e c0a8 000a E..4..@.@.7N.... 0x0010: c0a8 000a 22b7 d9b0 f7c4 7c80 eb37 8c85 ....".....|..7.. 0x0020: 8011 0156 818b 0000 0101 080a 010b 034e ...V...........N 0x0030: 010b 034e ...N 标志位为0X11, ACK和FIN为1,如果按照书本上说的应该是0X10的,只发送ACK,不发送FIN,但是这里两个合并了。
192.168.0.10.55728 > 192.168.0.10.8887: Flags [.], cksum 0x818b (incorrect -> 0x02e5), seq 2, ack 2, win 342, options [nop,nop,TS val 17498958 ecr 17498958], length 0 服务端发送ack应答,断开连接完毕。
所以这里有个疑问点,为啥断开连接只交互了三次,而不是四次,因为我写的程序里客户端关闭连接后,服务端立马关闭连接,没有了任何数据传输,而且都是调用的close,所以服务端将第二次第三次合并成了一个报文,这样做就是捎带ACK,减少一次通信。服务端代码如下
package per.pzt.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
private static final Integer PORT = 8888;
private Socket socket;
private ServerSocket serverSocket;
public Server(int port) {
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
Server server = new Server(PORT);
BufferedReader br = null;
PrintWriter pw=null;
while((server.socket = server.serverSocket.accept()) != null) {
br = new BufferedReader(new InputStreamReader(server.socket.getInputStream()));
pw = new PrintWriter(new OutputStreamWriter(server.socket.getOutputStream()));
String msg = null;
while((msg = br.readLine()) != null){
System.out.println("client:" + msg);
if(msg.equals("exit")){
break;
}
pw.println(msg);
pw.flush();
}
//server.closeSocket();
server.socket.shutdownInput();
pw.println("goodbye");
pw.flush();
server.socket.shutdownOutput();;
}
server.serverSocket.close();
}
}
如果客户端和服务端采用半连接方式通信,也就是客户端关闭时先调用socket.shutdownOutput,对应的系统调用api是shutdown(fd,1)关闭输出通道,服务端关闭时先调用shutdownInput对应的系统调用api是shutdown(fd,0),然后服务端发送最后的数据goodbye给客户端,然后服务端调用shutdownOutput彻底关闭双向通道,客户端接收后调用shutdownInput也关闭输入通道。此时的报文为
五段报文,1.客户端发送FIN。2.服务端回应FIN,并且携带数据goodbye。3.服务端发送FIN。4.客户端回应goodbye。5.客户端回应服务端FIN。看回应哪个报文就看seq和ack的对应值就可以了。所以除了回应goodbye的报文,确实是四次挥手。1,2,3,5构成了四次挥手。
接着改造把服务端和客户端都改为socket.close调用,也就是不用半关闭,直接全部关闭双向通道。但是在服务端close之前Thread.sleep(1)。服务端休眠1ms,得到的报文如下
这就是标准的4次挥手,可以得处一个结论,如果在客户端关闭后,服务端无任何操作也是直接关闭,则服务端会将四次挥手第二三次挥手合并为1个报文,也就是FIN+ACK,捎带ACK机制,如果是半连接状态,则会使用四次挥手,或者如果服务端没有立即关闭通道,而是做了其它的操作,再关闭,则也是四次挥手。看了LWIP的TCP/IP协议栈实现源码,确实是这样操作的,客户端有一种情况直接从FIN_WAIT_1到TIME_WAIT状态,服务端也有一种是从CLOSE_WAIT到LAST_ACK状态是在一个通信报文段实现的,也就是正常情况下CLOSE_WAIT到LAST_ACK需要发送两段报文,但是在三次挥手的情况下,只发送一段报文就可以了。所以在客户端关闭后,服务端无任何操作立即关闭的情况下,只需要三次挥手即可,减少一次通信。
看TCP断开连接图:
正常情况四次挥手。
双方都立即断开的情况下只需要三次挥手(捎带ACK),其中服务端CLOSE_WAIT 到LAST_ACK 我们使用netstat -npt看不到(除非debug调试),我看LWIP协议栈对这段处理,CLOSE_WAIT只作为了一个中间状态,在发送完FIN+ACK的报文后,直接变为了LAST_ACK。