关于TCP协议的重传策略,是TCP数据传输正确性的重要保证。
由于下层网络层协议可能出现的包丢失、重复、失序包等问题,当TCP协议基于某种策略确认当前包已经发生以上情况,就会启动重传。
TCP拥有两套机制来完成重传:基于超时时间;基于确认消息(SACK);
本文主要来模拟下基于超时时间的重传。
1.环境准备笔者准备两台机器,一台启动ServerSocket服务,另一台就启动telnet命令进行连接发送请求等操作
笔者这里使用的是标准的java ServerSocket,来启动一个端口监听,代码如下:
public class BIOServerSocket {
private String address;
private int port;
public BIOServerSocket(String address, int port) {
this.address = address;
this.port = port;
}
public void startServer() {
try {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(address, port));
System.out.println("bio server start...");
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("client connect...");
// 写入 thread
ServerWriteThread serverWriteThread = new ServerWriteThread(clientSocket);
serverWriteThread.start();
// 读取数据
read(clientSocket);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void read(Socket clientSocket) {
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String msg = "";
while ((msg = bufferedReader.readLine()) != null) {
System.out.println("receive msg: " + msg);
}
} catch (IOException e) {
try {
clientSocket.close();
} catch (IOException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}
}
public static void main(String[] args) {
String address = "192.168.3.8";
int port = 9999;
BIOServerSocket bioServerSocket = new BIOServerSocket(address, port);
bioServerSocket.startServer();
}
}
/**
* 从Scanner获取输入信息,并写回到client
*/
class ServerWriteThread extends Thread {
private Socket socket;
private PrintWriter writer;
private Scanner scanner;
public ServerWriteThread(Socket socket) throws IOException{
this.socket = socket;
scanner = new Scanner(System.in);
this.writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
}
@Override
public void run() {
String msg = "";
try {
while ((msg = scanner.nextLine()) != null) {
if (msg.equals("bye")) {
socket.close();
break;
}
writer.println(msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
代码很简单,笔者不再多述。
1.2 启动客户端telnet笔者在另一台机器上(Ubuntu docker)启动一个telnet命令,用来创建对上述ServerSocket的连接
root@93de58bae514:/# telnet 192.168.3.8 9999
Trying 192.168.3.8...
Connected to 192.168.3.8.
Escape character is '^]'.
1.3 wireshark监听
笔者在这里提前启动wireshark监听服务,当1.2步骤中的telnet执行完成后,我们可以看到三次握手建立完成。如下所示:
这里的超时重传,我们本质上是在模拟当客户端发送数据包后,在规定时间内没有收到服务端的ACK包时,自发的一种重传当前数据包策略。
我们通过客户端来模拟下该动作
2.1 正常收发包客户端正常发包
root@93de58bae514:/# telnet 192.168.3.8 9999
Trying 192.168.3.8...
Connected to 192.168.3.8.
Escape character is '^]'.
# 以下数据包逐个发送出去
a
b
c
服务端正常收到以上数据包
client connect...
receive msg: a
receive msg: b
receive msg: c
2.2 模拟网络异常后的发包
如果模拟两台机器之间的网络异常呢?
笔者就直接把服务端的网络禁止掉(服务端笔者使用的是Windows电脑,直接设置成飞行模式,这样外部请求就进不来),此时服务端程序还在,但是无法收发数据包
此时客户端再次发送数据包
root@93de58bae514:/# telnet 192.168.3.8 9999
Trying 192.168.3.8...
Connected to 192.168.3.8.
Escape character is '^]'.
# 以下数据包逐个发送出去
a
b
c
# 网络异常后再次发送数据包
d
Connection closed by foreign host.
这时我们再看服务端,没有出现receive msg: d,说明客户端的数据包确实没有发送到服务端。
那么数据在哪呢?我们还是通过wireshark来看下包的发送情况:
在163行数据第一次发送数据d失败后,后续一直在重试,一直重试到336行数据为止。
2.3 TCP超时重传规律根据wireshark的包发送情况,我们可以总结出来这么些规律
1.与SYN包重传策略类似,数据包的重传基本也是指数级避退的(看第二列数据[发送时间]可以看出这个规律)
2.数据包的重传有个最大限制,上图中重传了15次,最终直接关闭了连接
2.4 TCP超时重传阈值设置TCP拥有两个阈值来决定如何重传同一个报文段。
R1表示TCP再向IP层传递消极建议前,愿意尝试重传的次数;
R2表示指示TCP应放弃当前连接的时机
可以通过查看系统参数获取:
root@93de58bae514:/# sysctl -a | grep net.ipv4.tcp_retries
sysctl: reading key "net.ipv6.conf.all.stable_secret"
net.ipv4.tcp_retries1 = 3 # R1
net.ipv4.tcp_retries2 = 15 # R2
基于R2而言,比较符合我们上述测试的结果,在重试了15次之后,终于放弃重试,关闭当前连接。
总结:笔者在模拟这个重传还是比较困难的,一直想不到好的办法来模拟,直接关闭服务端进程的话,同时所有的客户端连接也被直接关闭了。
后来才想明白,直接关闭网络,太为难了。
TCP超时重传策略看了N多遍,但总是忘记,所以最好的学习的办法还是实战。实战一次之后就特别清晰了。
与君共勉之!