1 环境搭建
建议阅读此文前先了解TCP的原理。此文章仅为了加深对TCP的理解。
为了在抓包过程中捕获尽可能多种类的TCP报文,本文需要自己编写java socket程序,并安装Wireshark配套软件。
为了方便理解TCP传输过程,仅客户端向服务端发送数据。
1.1 编写java程序
程序中需要注意的几点:
- 客户端发送数据,服务端接受数据。将服务端buffer大小设置的明显小于客户端,是为了捕获
流量控制
报文。 - 客户端发送的数据(即d://bb.jpg),应该选择合适的大小。本文中bb.jpg大小为7M(推荐),这是为了捕获足够多的样本来进行分析。
- 服务端中并没有关闭socket,这是为了捕获
reset
报文。
/**
* 服务端
*
* @author youngaoo
* @created 2018年5月16日上午11:09:51
*/
public class Server {
public static void main(String[] args) {
Server s = new Server();
s.doServer();
}
public void doServer() {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8081));
SocketChannel socketChannel = serverSocketChannel.accept();
ByteBuffer dst = ByteBuffer.allocate(1024);
FileOutputStream fileOut = new FileOutputStream("d://cc.jpg");
FileChannel fileChannel = fileOut.getChannel();
int len = 0;
while ((len = socketChannel.read(dst)) != -1) {
dst.flip();
fileChannel.write(dst);
dst.clear();
}
fileOut.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 客户端
*
* @author youngaoo
* @created 2018年5月15日下午12:06:15
*/
public class Client {
public static void main(String[] args) {
Client c = new Client();
c.doClent();
}
public void doClent() {
try {
FileInputStream fileInputStream = new FileInputStream("d://bb.jpg");
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer dst = ByteBuffer.allocate(1024*10);
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8081));
int len = 0;
while ((len = fileChannel.read(dst)) != -1) {
dst.flip();
socketChannel.write(dst);
dst.clear();
}
fileInputStream.close();
socketChannel.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.2 安装并配置Wireshark
安装和Wireshark和npcap。并打开Wireshark,做如下配置:
- 网卡选择本地回环
- 过滤器填写
port 8081 and host 127.0.0.1
2 抓包并分析
开启Wireshark抓包功能,然后依次运行服务端和客户端,得到tcp报文段,如图(图中报文段并不完整,省略了多个意义重复的报文段):
可以看到,图中包含了多种报文类型。编号为1的红框为三次握手
过程;编号为3的代表“二次挥手”
和reset
报文段。
图中凡是带有中括号[]的文字,均是Wireshark添加的注释,并不是TCP协议的内容。可以看到传输数据过程中,发生了多次流量控制,具体体现在[TCP Window Update]报文和编号为②的报文段上。
下面,将会详细分析几种报文的含义,和出现的原因。
2.1 TCP协议首部格式
TCP协议的几乎所有功能都与首部相关,因此这里放上此图,供后文说明使用。
2.2 三次握手
编号为①的红框为三次握手的过程。在这个阶段,通信双方通过协商,得出了一系列信息,包括初始序列号
,自己的接收窗口
大小,最大报文段长度MSS
,窗口扩大选项WS
,选择确认SACK
。三次握手完成以后,通信双方就可以根据这些信息,分别构建出自己发送窗口,MSS等。此时,一条逻辑上的连接就建立成功了。
红框中的内容,就对应TCP首部的各个部分。同样,使用中括号[]括起来的文字是Wireshark添加的,便于使用者理解。
2.3 传送数据
三次握手下面紧接着,就是传送数据的报文。31306 → 8081 [ACK] Seq=1 Ack=1 Win=8192 Len=1460
。其中Seq
代表本报文段发送数据的第一个字节的序号;Win
代表发送本报文段一方的接收窗口大小,在这里,即代表客户端的大小;Len
即发送的数据的长度(单位为字节);当建立连接完毕以后,所发送的所有报文段,Ack
字段都必须为1。
该报文段发送的数据长度(Len),受到MSS值控制。之所以要协商MSS值,是因为从网络利用率来考虑。
2.4 流量控制
流量控制发生在接收方来不及处理数据时,接收方要求发送发降低发送速率,即减小发送方的发送窗口大小。在第一个[Tcp Window Update]报文和其前一个报文,即流量控制报文。前一个报文将Win改为768,即告诉客户端,我的接收窗口为768个字节,你应该据此修改自己的发送窗口。后面的[Tcp Window Update]报文又将自己的接受窗口增大至4096字节。
最坏的情况是服务端的应用程序读取数据的速度太慢,导致接受缓存达到最大容量,使接收窗口变为0。那么此时服务端就应该发送流量控制报文,[TCP ZeroWindow] 8081 → 31306 [ACK] Seq=1 Ack=31421 Win=0 Len=0
,将Win设置为0,通知客户端不要再发送报文段了,等我处理完积压数据再通知你。注意,在[TCP ZeroWindow]报文前,发送了一个[TCP Window Full]报文。此报文是客户端根据服务端的接收窗口大小,MSS值计算出来的。计算方法为:5120/1460=3…740。
当服务器处理完毕积压数据,又有了一些缓存空间,于是发送Win=3584的报文段。在Wireshark中,凡是扩大窗口的报文段都被注释为[Tcp Window Update]。
2.5 RESET报文
编号为③的报文段中,前两个是四次挥手
的前两次挥手,此时客户端到服务端的单向连接已经被关闭。接着应该进行服务端到客户端连接的关闭。但是服务端的应用程序并没有向TCP发送close命令,当应用程序进程结束后,操作系统的TCP就向客户端发送了reset报文。
2.6 PSH报文
发生在TCP层清空缓存时,才将push置为1。