今天使用wireshark来分析一下tcp的一些原理。首先我们建立一个tcp服务器。
const net = require('net');
net.createServer().listen(11111);
再建立一个tcp客户端。
const net = require('net');
net.connect({port: 11111, host: '192.168.8.226'})
我们逐个情况分析。
1 不启动服务器,启动客户端。
我们看看这种情况下tcp的表现。先看看总览。
我们看到tcp首先发了一个syn包。
因为服务器没有启动,所以客户端没有收到sync+ack包,然后重传了两次。最后报错。
2 启动服务器,启动客户端。
我们看看一次完整的tcp握手是怎样的。
首先客户端发送seq等于0的sync包,然后服务器返回一个sync+ack的tcp包。并且确认的序号是1。即1之前的序列号已经收到了。最后客户端再发送一个ack包。这时候seq等于1,说明握手是占据序号的。ack也等于1。客户端告诉服务器1之前的序列已经收到。这就完成了三次握手。那么三次握手意味着什么呢?又是怎么实现的呢?三次握手的本质其实就是在两端记录了一些上下文。比如服务器端记录了我和哪个ip端口建立了连接。那么下次收到这个客户端的时候包的时候,服务器就会从这个表里找,是否有记录,有的话说明已经建立了连接,是个合法的请求。否则发送重置包给客户端(我们可以使用c语言构造一个tcp报文)。
3 客户端挂了(或者服务器挂了)
我们看一下,如果客户端直接挂机了,tcp是怎么处理的。
我们看到tcp会给服务器发一个重置包。
4 客户端(或服务器)正常关闭连接
我们先改一下客户端代码
const net = require('net');
const socket = net.connect({port: 11111, host: '192.168.8.226'});
socket.on('connect', (client) => {
socket.destroy()
})
上面的代码使得客户端完成三次握手后立刻开始四次握手关闭连接。我们看看tcp的表现。
我们只看后面四行(四次挥手)。首先客户端发送了一个fin包,但是我们发现seq是等于1,说明fin包是不消耗序列号的。同理,服务器首先返回了一个ack包。然后再发送一个fin包,等到客户端返回ack。客户端最后一次发送ack的时候,需要等到2msl。该ip和端口可以重用,除非设置了端口复用。
5 两端一起关闭
我们把服务器代码也改一下。
const net = require('net');
net.createServer((socket) => {
socket.destroy();
}).listen(11111);
服务器完成三次握手的时候,在回调里立刻发送fin包。那么这时候tcp会怎样处理呢?因为三次握手中,第三次握手是由客户端发送的,客户端发送第三次握手的时候,就进入了完成连接状态(established)。而这时候服务器还没有收到第三次握手的数据包。所以客户端会先发送fin包。那么问题就来了。客户端发送的fin包和第三次握手的ack包,哪个先到服务端,影响了后续的流程。下面就是这两种情况。
6 keep-alive
tcp默认情况下是不会自动断开的,需要调用方去控制。不过tcp还是做了一些优化,就是如果隔了一段时间,没有数据传输,那么tcp就会发送探测包,如果还没有数据传输或者没有收到探测包的ack,则每隔一段时间再次发送探测包(这两个一段时间的值,在window下貌似是一样的,linux下可以不一样)。发到一定的次数还没有任何动静,那么就发重置包给对端断开连接。
我们看到三次握手后,我们没有传输数据,tcp就会一直发送探测包。
总结:今天就到这里,tcp非常复杂,本文列举了一些例子,分析一些tcp的某些原理。