这里使用Java语言简单实现了客户端和服务端通信,使用Wireshark抓包工具抓取了过程中的网络包。
客户端IP地址:192.168.0.107
服务端IP地址:193.112.193.132
端口:10100
TCP报文
首先要看懂TCP的报文,一个TCP连接首先要有两个IP地址和两个端口号,分别唯一标识了客户端和服务端进程。构造一个Socket也需要传入IP地址和端口号。
1. 报文首部
下面是用Wireshark抓包工具看到的报文信息:
Transmission Control Protocol, Src Port: 54438, Dst Port: 10100, Seq: 157, Ack: 1, Len: 0
Source Port: 54438
Destination Port: 10100
[Stream index: 15]
[TCP Segment Len: 0]
Sequence number: 157 (relative sequence number)
Sequence number (raw): 1837642959
[Next sequence number: 158 (relative sequence number)]
Acknowledgment number: 1 (relative ack number)
Acknowledgment number (raw): 2818203645
0101 .... = Header Length: 20 bytes (5)
Flags: 0x011 (FIN, ACK)
Window size value: 517
[Calculated window size: 132352]
[Window size scaling factor: 256]
Checksum: 0x4423 [unverified]
[Checksum Status: Unverified]
Urgent pointer: 0
[Timestamps]
报文首部由几个重要的部分组成:
- 源端口 Source Port
- 目的端口 Destination Port
- 数据长度TCP Segment Len
Segment Len代表了这个包的大小,也就是这个报文的数据长度。这个数值会影响下一个报文的序列号。
假如当前Seq=1, Len=7,那么下一个报文Seq=8。而且SYN和FIN本身会消耗一个序列,虽然它们的包大小是0,但是因为这两种报文都需要确认ACK,所以会导致序列加1。 - 序列号 Sequence number
- 确认号 Acknowledgment number
- 标志位 Flags
TCP有6种标志位:SYN(synchronous建立连接),
ACK(acknowledgement 确认), PSH(push传送)
FIN(finish结束), RST(reset重置), URG(urgent紧急) - 窗口 Window size
- 校验和 Checksum
-
紧急指针 Urgent pointer
tcp.png
2. 时序图
TCP的连接过程是这样的:
- 客户端发出一个连接请求,SYN=1的报文;
- 服务端收到请求后发出一个ACK=1的确认报文;
此时服务端确认客户端的发送能力是正常的。 - 客户端收到确认报文后发出ACK=1的确认报文;
此时客户端确认自身的收发能力是正常的,服务端的收发能力也是正常的。
服务端收到ACK后也确认自身的收发能力正常,客户端收发能力正常。
于是建立了一个稳定连接。
TCP的释放过程是这样的: - 客户端发出一个FIN=1的结束报文;
此时客户端已经关闭连接,不会再发送数据了。 - 服务端收到报文后发出一个ACK=1的确认报文;
此时服务端可以继续发送数据,直到上层确认数据发送完毕。 - 服务端发出一个FIN=1的结束报文;
此时服务端也关闭,不会再发送数据。 -
客户端收到报文后发出一个ACK=1的确认报文。
客户端发出确认报文后,为了防止数据丢失,会再等待2MSL,然后完全关闭连接。
服务端收到确认报文后,也会完全关闭连接。
TCP的连接和释放.png
代码实现
这里使用Java语言实现了客户端和服务端的Socket通信
服务端
服务端写了一个线程,通过ServerSocket不停去监听,接受和处理客户端请求,没有请求时线程阻塞等待。
下面是核心代码,没有请求时会在accept()方法里面阻塞,当请求的内容里面包含"finish"字段会退出循环,线程终止。
ServerSocket serverSocket = new ServerSocket(PORT);
while(!finish) {
socket = serverSocket.accept();
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String lineString = null;
while ((lineString = bufferedReader.readLine()) != null) {
System.out.println(lineString);
if ("finish".equals(lineString)) {
finish = true;
break;
}
}
}
客户端
客户端发起一次请求,建立连接,
然后写入10个字符串,休眠2秒钟,再继续写入10个字符串,
然后休眠2秒钟,写入"finish"字段后主动关闭socket,释放连接。
OutputStream outputStream = null;
Socket socket = null;
try {
socket = new Socket(IP_STRING, PORT);
System.out.println("new Socekt port=" + socket.getPort());
outputStream = socket.getOutputStream();
for (int i = 0; i < 10; i++) {
String contentString = "first" + i + "\n";
outputStream.write(contentString.getBytes());
}
Thread.sleep(2000);
for (int i = 0; i < 10; i++) {
String contentString = "second" + i + "\n";
outputStream.write(contentString.getBytes());
}
Thread.sleep(2000);
outputStream.write("finish".getBytes());
参考:
https://blog.csdn.net/hnjb5873/article/details/48657001
https://blog.csdn.net/qzcsu/article/details/72861891
https://zhuanlan.zhihu.com/p/53374516
网友评论