计算机网络实验 实验5 运输层和应用层协议解析

发布时间 2023-04-02 17:55:02作者: Lumen3ever

实验5 运输层和应用层协议解析

一、 实验目的

   本实验通过运用Wireshark对网络活动进行分析,观察TCP协议报文,分析通信时序,理解TCP的工作过程,掌握TCP工作原理与实现;学会运用Wireshark分析TCP连接管理、流量控制和拥塞控制的过程,发现TCP的性能问题。

二、 实验内容

任务1:TCP正常连接观察

实验准备:

 

 

 

用PC1 ping 一下PC2,看是否能ping通,观察到两主机能联通:

 

 

 

 

  1. 利用 python 自带的 SimpleHTTPServer 模块,在 PC2 上启动一个简易的 web 服务器。终端上运行 echo "TCP lab test" > index.html 创建 index.html 文件为测试站首页,运行 sudo python -m SimpleHTTPServer 80 启动一个简易 web 服务器;打开新终端,键入 ss -tln查看当前主机打开的 TCP 连接,确认 80 端口处于监听状态。

 

 

 

  1. 在 PC1 上打开一个终端,键入 sudo wireshark 启动抓包软件;再打开一个新终端,键入curl <PC2 的 IP> ;停止抓包,在 wireshark 过滤出 TCP 类型报文。观察首个 TCP 报文头,并分析各段值代表的意义。如果想要关闭相对序号/确认号,可以选择 Wireshark 菜单栏中的EditPreferenceprotocolsTCP,去掉 Relative sequence number 勾选项。使用 Wireshark 内置的绘制流功能,选择菜单栏中的 StatisticsFlow Graph, Flow Type 选择 TCP flows 可以直观地显示 TCP 序号和确认号是如何工作的。

 

 

 

实验要求:

  1. 利用Wireshark,抓包分析并截图,分析该报文TCP首部各字段的定义、值及其含义

(1)      利用Wireshark抓包并截图(保存在tcp1.pcapng中):

在Wireshark软件中设置display filter如下,进行报文过滤:

 

抓取到整个TCP报文流:

 

使用Statistics->Flow Graph工具,可以清楚地看到三次握手过程:

 

四次挥手过程:

 

可是,这里我观察到了一个“奇怪”的现象。不是说好了四次挥手吗?怎么只看到两次挥手。为了解决这个问题,我上网查找了很多资料。首先连接释放的时候不一定需要四次挥手,事实上,很多情况下都只有三次挥手。而满足三次挥手需要两个条件,当被动关闭方(上图的客户端)在TCP挥手过程中,[没有数据要发送]并且[开启了TCP延迟确认机制],那么第二次和第三次挥手就会合并传输,这样就出现了三次挥手。这里的合并传输也就是这个FIN包和ACK包合并传输了。

那么,分析完了四次挥手变成三次挥手的原因,可是我这里观察到的只有两次挥手啊,这是为什么呢?

这个问题困惑了我一整天,查阅资料发现,并不一定是客户端会主动断开连接,服务器端也有这个功能。于是我就去看了[FIN,ACK]之前的那个由服务器发给客户端的HTTP包,果然找到了答案:

 

我发现这个报文里的FIN标志位竟然是1,说明这是服务器向客户端发送的第一个FIN包,也就是第一次挥手。于是,四次挥手便再次显现出来。

(2)      分析报文TCP首部各字段的定义、值及其含义

这里,我选择了三次握手的第一个报文:SYN报文进行分析

 

下面依据TCP报文的首部格式分析该TCP报文首部各字段:

 

前两个字节是16位源端口号,该报文的源端口号为34318

 

接下来两个字节是16位目标端口号,该报文的目标端口号为80

 

接下来四个字节是32位客户端随机初始化的序列号,该报文的序列号为1551970934

 

接下来四个字节是32位的确认应答号,该报文是三次握手的第一个SYN报文,没有设置确认应答号,故该报文确认应答号为0

 

接下来4位bit是首部长度,该报文的首部长度为40Bytes

 

接下来6位标志位保留,报文中都为0

 

接下来6位标志位,该报文中只有SYN被置1

 

接下来两个字节是窗口大小,该报文的窗口大小为64240

 

接下来两个字节是校验和

 

接下来两个字节是紧急指针字段,该报文URG标志位为0,故这里的紧急指针字段也为0

 

最后的20字节是选项字段,这里不多分析

 

至此,该报文的首部各字段分析完毕。

  1. 画出该TCP流的流图

 

任务2:TCP异常传输观察分析

  1. 尝试连接未存活的主机或未监听端口

(1)      用 curl 访问一个不存在的主机 IP,抓包观察共发送了几次 SYN 报文。根据每次时间间隔变化,估算 RTO(重传超时)。

 

抓包观察到共发送了两次SYN报文:

 

 

估算RTO:

由上图可以大致估算出,RTO ≈ 9.514 – 6.443 = 3.071s

(2)      查看 Linux 主机的系统的 TCP 参数 SYN 重传设定:

`cat /proc/sys/net/ipv4/tcp_syn_retries`

 

(3)      更改 SYN 重传次数为 3:

`echo "3" > /proc/sys/net/ipv4/tcp_syn_retries`

注意,这里要切换到root用户下才能更改,否则会“Permission Denied”

 

(4)      再次 curl 访问,观察抓包内容。

 

 

(5)      关闭服务器端的 SimpleHTTPServer(ctrl+C 中断,或关闭所在终端),客户端 curl 访问服务器 80 端口,观察应答报文。

 

 

(6)      运行 nmap -sS <PC2 的 IP> 扫描服务器,并抓包。

 

 

(7)      在报告中总结以上观察结果,解释 SYN 扫描原理。

  1. 观察客户端发送了第一个SYN连接请求,服务器无响应的情景

(1)      服务器开启 telnet 或 ssh 服务,客户端先尝试连接服务器,连接成功后,在双方键入 ss -tan 查看所有 TCP 连接状态。我们看到的 TCP 连接建立过程同 1 中的 HTTP 访问类似。在客户端,利用 iptables 拦截服务器回应的 SYN ACK 包,命令如下:

` sudo iptables -I INPUT -s 192.168.13.128 -p tcp -m tcp --tcp-flags ALL SYN,ACK -j DROP `

为了让服务器能够开启ssh服务,需要先`sudo apt install openssh-server`

 

输入`sudo service ssh start`启动ssh服务

 

输入`sudo ps -e |grep ssh`查看ssh服务是否启动,观察到服务器已启动:

 

接下来尝试使用客户端连接服务器,服务器的ip为192.168.13.128

 

在客户机上使用`sudo ssh rongrong@192.168.13.128`远程连接服务器(注意这里需要root权限,否则会Permission Denied):

 

看到如上界面说明成功远程连接到了服务器。

连接成功后,在双方键入`ss -tan`查看所有TCP连接状态:

 

 

在客户端输入命令` sudo iptables -I INPUT -s 192.168.13.128 -p tcp -m tcp --tcp-flags ALL SYN,ACK -j DROP `利用iptables拦截服务器回应的SYN ACK包:

 

(2)      再次尝试连接并启动 wireshark 抓包,并在双方多次用 ss -tan 观察 TCP 状态。

 

服务器:

 

客户机:

 

(3)      观察 TCP 的状态变化,分析 wireshark 捕获的 TCP 异常报文。

 

会产生这样的异常报文的原因是,由于我们设置了防火墙,阻塞了从服务器端发过来的SYN&ACK包,这样客户机发第一个SYN包后,就始终不会收到服务器端的SYN&ACK包,它就会以为是传输过程中丢包了,于是重新传输SYN包;而服务器那边也就不会收到客户机发来的三次挥手中的最后一个ACK包,超时后就会重传SYN&ACK包。这就是产生图中异常报文的原因。

(4)      服务端的 SYN-RECV 状态何时释放?

当终端显示”Connection timed out”时释放

(5)     SYN ACK 重传了几次,时间间隔有何变化?

 

可以观察到SYN ACK重传了11次

 

可以观察到时间间隔一开始呈指数级别增大,最后稳定在16s左右

(6)      参考 1 中的操作,在服务端修改 SYN ACK 重传次数 (tcp_synack_retries),再次观察,此任务结束后清空防火墙规则 (iptables -F)。

在服务端改重传次数为3次:

 

再次观察(注意这里我连续尝试连接了两次):

 

清空防火墙规则:

 

之后就可以顺利连接了:

 

任务3:拥塞控制

  1. 配置虚拟机设置:

 

  1. 使用ftp传输大文件:

首先,我自己编写了一个程序gen_ran.c,使用命令`./gen_ran <filename> [size(MB)]`

 

该程序可生成一个大小为size的文件,名称为filename,size可选,默认为100MB

使用`./gen_ran scpfile`生成一个大小为50MB的大文件scpfile:

 

 

使用ftp传输该文件,同时使用wireshark抓包:

首先在终端输入命令`ftp 192.168.13.128`与服务端连接:

 

连接成功后显示如上。

然后使用命令`put ftpfile`传输大文件,同时wireshark抓包:

 

 

  1. 传输完毕,进行结果分析(该结果存放在ftp.pcapng中):

 

 

刚开始时,执行慢开始算法,说是慢开始,其实它并不慢,因为它是呈指数增长的。

超时,网络发生拥塞,这时可能连续收到三个重复确认,执行快恢复算法,拥塞窗口变为原来的一半。

然后拥塞避免算法,加法增大拥塞窗口。

任务4:HTTP协议分析

  1. 搭建HTTP1.0服务器

任务1搭建的即为HTTP1.0服务器。使用命令` sudo python -m SimpleHTTPServer 80`在虚拟机UbuntuV2上搭建服务器,在虚拟机Ubuntu 64bit上使用命令`curl 192.168.13.128`向服务器请求数据,

即可抓到HTTP1.0报文:

 

 

 

data的内容即为网页中的内容。

  1. 搭建HTTP1.1服务器

在Ubuntu 64bit中编写python脚本(源代码文本见附件),将协议的版本类型修改为HTTP/1.1,即可搭建HTTP1.1服务:

 

终端输入命令运行服务器,并打开wireshark进行抓包:

 

输入后,终端会自动打开一个网页,该网页是我在该目录下写的一个html文件(源码见附件):

 

可以使用wireshark捕捉到HTTP1.1的报文:

 

  1. 搭建HTTP2.0服务器

在搭建HTTP1.1的基础上,将协议的类型修改为HTTP2.0即可:

 

与上一步类似,使用wireshark抓包,可以抓到TLS1.2的报文,该报文是HTTP2.0报文加密后的结果:

 

若想要分析HTTP2.0报文,需要对wireshark进行解密,步骤如下:

(1)     配置系统环境变量:

在~/.profile文件下加入`export SSLKEYLOFFILE=/home/rongrong/Desktop/sslkey.log`

终端输入命令:`source ~/.profile`

重启Ubuntu。

然后打开一个浏览器,会看到密钥已经写入到sslkey.log文件中了:

 

(2)     配置Wireshark:

Wireshark->Edit->Protocols->TLS下,将(Pre)-Master-Secret log filename设置为刚刚设置的环境变量值:

 

然后就可以看到HTTP2.0报文

 

点开该报文可以看到,内容与index.html中的一致:

 

 

  1. 接下来对三种HTTP版本的报文进行分析

(1)     HTTP1.0

 

右键点击该报文,选择Follow->HTTP Stream可以追踪HTTP流:

 

 

其中,红色的部分是请求格式,蓝色的部分是响应格式,可以很清楚地看到该HTTP流的请求响应流程。

可以分析一下HTTP1.0的报文结构:

起始行是状态行:HTTP/1.0 200 OK

可以看到版本为HTTP1.0;状态码为200,表示成功;短语为OK。

(2)     HTTP1.1

 

与上一步的(1)相同,可以肯定看到HTTP stream:

 

可以对HTTP1.1的请求报文结构和响应报文结构进行分析:

起始行除了版本,其他与HTTP1.0相同

可以看到Server与HTTP/1.0不同,HTTP/1.1的Server为nginx;

HTTP/1.1报文还会将使用的是什么浏览器显示出来:Via: 1.1 google

HTTP/1.1相比于HTTP/1.0做了如下几点优化:

  • 使用长连接的方式改善了HTTP/1.0短连接造成的性能开销
  • 支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

(3)     HTTP2.0

 

与前面两步相同,我们同样可以在这里看到HTTP2.0的HTTP stream:

 

HTTP/2.0协议与HTTP/1.1协议有很大的不同,前者把后者存在的性能问题全部一一攻破了。

HTTP/2.0相比于HTTP/1.1在性能上的改进有如下几点:

l  头部压缩

HTTP/2 会压缩头(Header)如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的部分。

这就是所谓的 HPACK 算法:在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。

l  二进制格式

HTTP/2 不再像 HTTP/1.1 里的纯文本形式的报文,而是全面采用了二进制格式,头信息和数据体都是二进制,并且统称为帧(frame):头信息帧(Headers Frame)和数据帧(Data Frame)。

这一点观察报文也能稍微看出一些区别。

 

这样虽然对人不友好,但是对计算机非常友好,因为计算机只懂二进制,那么收到报文后,无需再将明文的报文转成二进制,而是直接解析二进制报文,这增加了数据传输的效率

比如状态码 200 ,在 HTTP/1.1 是用 '2''0''0' 三个字符来表示(二进制:00110010 00110000 00110000),共用了 3 个字节,如下图:

 

在 HTTP/2 对于状态码 200 的二进制编码是 10001000,只用了 1 字节就能表示,相比于 HTTP/1.1 节省了 2 个字节,如下图:

 

l  并发传输

l  服务器主动推送资源

三、 实验结果分析

本次实验遇到了很多奇怪的现象,比如对于任务1,始终无法观察到连接释放时候的四次挥手,只能观察到两次挥手:由客户机向服务器发送一个FIN包,然后服务器再向客户机发送一个ACK包,然后就结束了,并没有观察到服务器向客户机发送的FIN包,如下图:

 

可以看到只有两次。

多次尝试好不容易抓到了一个四次挥手的报文,可是奇怪的事情又发生了。按理来说第一次的FIN包应该由客户机发给服务器才对,但是我观察到的却是服务器先发送非客户机:

 

发生这个现象的原因可能是HTTP/1.0是短连接的,每发起一个请求都要新建一次TCP连接,这样服务端就会先发FIN包;若是使用HTTP/1.1则不会是这样的现象。

 

经过我的不懈努力,搜索了大量的有关TCP抓包的资料,终于解决了上述两个问题。

以及在做观察快恢复现象的时候无法观察到与教材上相同的现象,只能根据包的数量大致分析这个阶段是在执行哪个算法。

除此以外,本次实验都完成地比较顺利。

四、 实验小结与感想

据老师所说,本次实验是最后一个需要写实验报告的实验。太好啦!终于不用写实验报告啦!!!

不过,和wireshark打了快一个学期的交道了,我深深感受到了wireshark这个抓包工具的强大。它除了可以抓包外,还提供了可视化分析网络包的图形界面,还内置了一系列的汇总分析工具。就拿本次实验来说,我就用到了许多除了抓包以外的工具,比如Flow Graph以及IO Graph里面的HTTP Stream等工具。通过这些工具分析流,可比光看报文方便多了。

五、 思考题

  1. 在 TCP 状态机中,有些状态停留时间较长,易观察到,有些状态很短暂不易观察到。试列出不易观察到的状态,并考虑观察到它们的可能方法。

不易观察到的状态有: FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、LAST_ACK。可以阻断中间的某个报文,以观察到接下来预计达到的状态。

  1. TCP 在不可靠的 IP 层上建立了可靠的端对端连接,如果要在不可靠的 UDP 上建立可靠的端对端传输系统,需要考虑哪些方面?

现在市面上已经有基于 UDP 协议实现的可靠传输协议的成熟方案了,那就是 QUIC 协议,已经应用在了 HTTP/3。

要基于 UDP 实现的可靠传输协议,那么就要在应用层下功夫,也就是要设计好协议的头部字段。

需要把TCP 可靠传输的特性(序列号、确认应答、超时重传、流量控制、拥塞控制)在应用层全部实现一遍。

六、 附件

gen_ran.c

#include<stdio.h>

#include<stdlib.h>

#include<time.h>

#include<stdint.h>

 

#define ONE_MB_SIZE 262144

int32_tout[ONE_MB_SIZE];

 

intmain(intargc, char* argv[]) {

    if(argc < 2 || argc > 3) {

        fprintf(stderr, "Usage: ./gen_ran <filename> [size(MB)]\n");

        exit(1);

    }

    int size;

    if(argc == 2) size = 100; // 如果只有两个参数即只有一个文件名称默认生成100MB的文件

    else size = atoi(argv[2]);

    FILE* outfile;

    if((outfile = fopen(argv[1], "wb")) == NULL) {

        fprintf(stderr, "open error\n");

        exit(1);

    }

    srand((unsignedint)time(0));

    int i, j;

    for(i = 1; i <= size; i++) {

        for(j = 0; j < ONE_MB_SIZE; j++) {

            out[j] = rand();

        }

        fwrite(out, sizeof(int32_t), ONE_MB_SIZE, outfile);

    }

    fprintf(stdout, "random input file %s was generated successfully\n", argv[1]);

    exit(0);

}

 

HTTPserver.py

   import json

from http.server import HTTPServer, SimpleHTTPRequestHandler

import webbrowser

 

ip = "localhost"  # 监听IP,配置项

port = 8800  # 监听端口,配置项

index_url = "http://%s:%d/index.html" % (ip, port)  # 监听主页url,配置项

 

# 创建http server

classGetHttpServer(SimpleHTTPRequestHandler):

    protocol_version = "HTTP/1.0"

    server_version = "PSHS/0.1"

    sys_version = "Python/3.9.x"

    target = "./"  # 监听目录,配置项

 

    defdo_get(self):

        ifself.path.find("/json/") > 0:

            print(self.path)

            self.send_response(200)

            self.send_header("Content-type", "json")

            self.end_headers()

            req = {"success": "ok"}

            self.wfile.write(req.encode("utf-8"))

        else:

            SimpleHTTPRequestHandler.do_GET(self)

 

    defdo_post(self):

        ifself.path == "/signin":

            print("postmsg recv, path right")

        else:

            print("postmsg recv, path error")

            data = ""

            data = json.loads(data)

            self.send_response(200)

            self.send_header("Content-type", "text/html")

            self.end_headers()

            rspstr = "recv ok, data = "

            rspstr += json.dumps(data, ensure_ascii=False)

            self.wfile.write(rspstr.encode("utf-8"))

 

defhttp_server():

    server = HTTPServer((ip, port), GetHttpServer)

    try:

        # 弹出窗口

        webbrowser.open(index_url)

        # 输出信息

        print("服务器监听地址: ", index_url)

        server.serve_forever()

    exceptKeyboardInterrupt:

        server.socket.close()

 

# 执行服务器脚本

http_server()

 

index.html

<!DOCTYPEhtml>

<htmllang="en">

<head>

    <metacharset="UTF-8">

    <metahttp-equiv="X-UA-Compatible"content="IE=edge">

    <metaname="viewport"content="width=device-width, initial-scale=1.0">

    <title>Document</title>

</head>

<body>

    hello world

</body>

</html>