Http实战之Wireshark抓包分析

2022-11-18 17:03:26 浏览数 (1)

❝Http相关的文章网上一搜一大把,所以笔者这一系列的文章不会只陈述一些概念,更多的是通过实战(抓包 代码实现)的方式来跟大家讨论Http协议中的各种细节,帮助大家理解那些反反复复记不住的的概念! ❞

搭建测试项目

我们选用netty搭建一个服务端,使用httpclient来实现http客户端。

❝对netty或者httpclient不熟悉的同学不用担心,涉及到的代码都非常简单。 服务端我之所以选用这两个框架是因为相对来说,它们对http协议的封装较浅,在后面的文章中我可以带大家看看代码层次上http协议是如何封装的,这样可以将http协议理解的更加透彻,在本文中大家将注意力放到抓包的分析过程即可! ❞

代码如下:

pom文件引入依赖:

代码语言:javascript复制
<dependency>
  <groupId>io.netty</groupId>
  <artifactId>netty-all</artifactId>
  <version>4.1.65.Final</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpclient</artifactId>
  <version>4.5.13</version>
</dependency>

服务端代码:

代码语言:javascript复制
public class HttpServer {
  public static void main(String[] args) throws Exception {
    EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
      ServerBootstrap b = new ServerBootstrap();
      b.option(ChannelOption.SO_BACKLOG, 1024);
      b.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .handler(new LoggingHandler(LogLevel.INFO))
        .childHandler(new HttpHelloWorldServerInitializer());
      Channel ch = b.bind(8080).sync().channel();
      ch.closeFuture().sync();
    } finally {
      bossGroup.shutdownGracefully();
      workerGroup.shutdownGracefully();
    }
  }
}


public class HttpHelloWorldServerInitializer extends ChannelInitializer<SocketChannel> {
  @Override
  public void initChannel(SocketChannel ch) {
    ChannelPipeline p = ch.pipeline();
    // 我们搭建的是一个http服务器,要使用http编解码器
    p.addLast(new HttpServerCodec());
    // http请求处理核心类
    p.addLast(new HttpHelloWorldServerHandler());

  }
}

/**
 * 这个类是处理http请求的核心类,这里我们简单处理
 * 不论收到什么信息我们都返回Hello World
 */
public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<HttpObject> {
  private static final byte[] CONTENT = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' };

  @Override
  public void channelReadComplete(ChannelHandlerContext ctx) {
    ctx.flush();
  }

  @Override
  public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
    if (msg instanceof HttpRequest) {
      HttpRequest req = (HttpRequest) msg;
      FullHttpResponse response = new DefaultFullHttpResponse(req.protocolVersion(), OK,Unpooled.wrappedBuffer(CONTENT));
      response.headers()
        .set(CONTENT_TYPE, TEXT_PLAIN)
        .setInt(CONTENT_LENGTH, response.content().readableBytes());
      ctx.write(response);
    }
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    cause.printStackTrace();
    ctx.close();
  }
}

客户端代码如下:

代码语言:javascript复制
public class HttpClient {
    public static void main(String[] args) throws Exception {
        final CloseableHttpClient httpClient =
                HttpClientBuilder.create()
                        .setConnectionManager(new BasicHttpClientConnectionManager())
                        .build();
        final HttpGet httpGet = new HttpGet("http://127.0.0.1:8080");
        final CloseableHttpResponse execute = httpClient.execute(httpGet);
        System.out.println(EntityUtils.toString(execute.getEntity()));
        // 这里代码并不规范哈,正常应该try catch finally,不过这不是本文重点
        execute.close();
    }
}

我们将服务端启动后,运行客户端正常输出“Hello World”说明项目搭建成功

Wireshark简单介绍

Wireshark(前身 Ethereal)是一个网络包分析工具。该工具主要是用来捕获网络数据包,并自动解析数据包,为用户显示数据包的详细信息,供用户对数据包进行分析。

❝下载链接:https://www.wireshark.org/download.html ❞

下载成功后,我们打开主界面如下:

WireShark

这里我们看到的这个列表是我们本机的网卡列表,我们在抓包之前要确认具体的网卡,常用的网卡就是我在图中框选的两个

  • lo0:回环网卡,对应localhost/127.0.0.1等本地服务 举个例子,如果我要请求 http://localhost:12345/xxx 这个服务,对应的网卡就选择lo0127.0.0.1的服务同理。
  • en0:外网请求,比如,我要在浏览器访问 www.baidu.com/ 这个服务,对应的网卡一般选en0

那么如何确定我们要抓取的数据包对应的网卡是哪个呢?

现在要确定当我们访问www.baidu.com使用的是哪个网卡,我们可以执行如下操作:

  1. 监听www.baidu.com数据:sudo tcpdump host www.baidu.com -xnt -v -A | grep -C3 www.baidu.com
  2. 访问百度:curl www.baidu.com

从这里可以看到我们访问百度时使用的ip地址(图中马塞克部分)

之后,通过执行ifconfig命令,就能查询到这个ip对应的网卡

在我本机对应的就是en0这张网卡。确认了具体网卡后,我们在主界面选定此网卡,双击即可,此时可能会出现如下报错:

这是因为网卡权限问题,我们只需要在终端中输入如下命令即可:sudo chmod 777 /dev/bpf*。执行完后记得要重启WireShark。

重启完成后,选定网卡双击会进入如下界面:

一进这个界面可能会有点懵,因为我们没有输入任何过滤表达式,所以此时整个界面上展示的是en0这个网卡上所有的数据包。关于WireShark的表达式不是本文重点,笔者也不打算过多介绍,本文用到的表达式都非常简单,每个表达式我会做简单解释。

此时我们想要抓取访问百度时的数据包,我们只需要输入如下表达式:

http and ip.addr==112.80.248.76

表达式中的第一个http代表,我们要抓取的是http协议相关的数据包,同理,你可以输入tcp,icmp等协议名称过滤出对应协议相关的数据包。这个表达式的意思是,我要抓取http协议的数据包,同时通信的某一方的ip地址为112.80.248.76,这个ip地址可以通过ping www.baidu.com得到。

根据上述表达式我们可以抓到如下数据包:

我们选中对应的报文,右键跟踪http流,即可得到具体的http报文信息


那么接下来我们正式开始抓包实验,确保你的测试项目及Wireshark都是ok的哦~

Http抓包分析

  1. wireShark要抓取的网卡是回环网卡(测试项目中客户端发起请求的URL是127.0.0.1:8080)
  2. 我们server在搭建时绑定的端口是8080,所以wireShark可以配置如下表达式: http and tcp.port==8080,代表我们要抓取8080端口上所有http协议的包(因为是抓取回环网卡网卡上的数据包,所以我们可以不指定IP)

接下来我们启动sever端,然后运行client端发起一个http请求,在WireShark上可以抓取到如下数据:

按照前文所述,我们追踪下这个http流可以看到如下数据:

协议格式

HTTP 协议的请求报文和响应报文的结构基本相同,由三大部分组成:

「起始行」(start line):描述请求或响应的基本信息,在请求中我们称之为请求行,响应中我们称之为状态行;

「头部字段集合」(header):使用 key-value 形式更详细地说明报文,在请求中我们称之为请求头,响应中我们称之为响应头。

「消息正文」(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据,也称之为请求体或响应体

HTTP 协议规定报文必须有 header,但可以没有 body,而且在 header 之后必须要有一个“空行”,也就是“CRLF”,十六进制的“0D0A”。

将抓包得到的报文用上述结构描述即如下图所示:

请求行

如下图所示,请求行中主要包含三部分信息

  1. 请求方法
  2. 请求URI
  3. 本次发起http请求时使用的http协议版本

三部分之间使用空格进行分隔

常见的请求方法

请求方法

描述信息

补充

GET

请求从服务器获取资源

这个资源既可以是静态的文本、页面、图片、视频,也可以是由 PHP、Java 动态生成的页面或者其他格式的数据

POST

向服务器提交数据(例如提交表单或者上传文件),数据包含在请求体中

POST 表示的是“新建”“create”的含义

PUT

PUT 的作用与 POST 类似,数据也包含在请求体中

通常 POST 表示的是“新建”“create”的含义,而 PUT 则是“修改”“update”的含义。

DELETE

指示服务器删除资源

在RESTful架构使用较多下使用较多

HEAD

类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头

HEAD 方法可以看做是 GET 方法的一个“简化版”或者“轻量版”。因为它的响应头与 GET 完全相同,所以可以用在很多并不真正需要资源的场合,避免传输 body 数据的浪费。

OPTIONS

方法要求服务器列出可对资源实行的操作方法,在响应头的 Allow 字段里返回。

它的功能很有限,用处也不大,有的服务器(例如 Nginx)干脆就没有实现对它的支持。

TRACE

用于对 HTTP 链路的测试或诊断,可以显示出请求 - 响应的传输路径。

它的本意是好的,但存在漏洞,会泄漏网站的信息,所以 Web 服务器通常也是禁止使用。

CONNECT

要求使用隧道协议连接代理

关于隧道大家可查看:https://www.zhihu.com/question/21955083,本文不再赘述

❝RESTful架构下,会使用四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:「GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。」

Get跟Post常见误区
  1. 请求参数长度限制:GET请求长度最多1024kb,POST对请求数据没有限制 答:GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制(HTTP协议本身对 URL长度并没有做任何规定)。
  2. GET请求一定不能用request body传输数据 答:RFC 规范并没有规定 GET 请求不能带 body 的。理论上,任何请求都可以带 body 的。只是因为 RFC 规范定义的 GET 请求是获取资源,所以根据这个语义不需要用到 body。另外,URL 中的查询参数也不是 GET 所独有的,POST 请求的 URL 中也可以有参数的。我们在传统的Spring环境下会发现下面两种写法都可以正常工作

状态行

如下图所示,请求行中主要包含三部分信息

  1. 使用的http协议版本
  2. 数字状态码
  3. 作为数字状态码补充,是更详细的解释文字,帮助人理解原因。

三部分之间使用空格进行分隔

状态码

RFC 标准把状态码分成了五类,用数字的第一位表示分类,这样状态码的实际可用范围就大大缩小了,由 000~999 变成了 100~599。

这五类的具体含义是:

1××:提示信息,表示目前是协议处理的中间状态,还需要后续的操作;

2××:成功,报文已经收到并被正确处理;

3××:重定向,资源位置发生变动,需要客户端重新发送请求;

4××:客户端错误,请求报文有误,服务器无法处理;

5××:服务器错误,服务器在处理请求时内部发生了错误。

❝实际上需要注意的是HTTP本身是一个协议,需要通信的双方共同遵守,但这并不是必须的。目前 RFC 标准里总共有 41 个状态码,但状态码的定义是开放的,允许自行扩展。所以 Apache、Nginx 等 Web 服务器都定义了一些专有的状态码。如果你自己开发 Web 应用,也完全可以在不冲突的前提下定义新的状态码。 ❞

1xx

1xx 类状态码属于「提示信息」,是协议处理中的一种中间状态。例如在需要进行协议升级时,服务器会响应101。如下图所示,使用websocket时,会进行一次协议升级:

2xx

2xx 类状态码表示服务器「成功」处理了客户端的请求,也是我们最愿意看到的状态。

  • 「200 OK」」是最常见的成功状态码,表示一切正常。如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。
  • 「204 No Content」」也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。
  • 「206 Partial Content」」是应用于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。状态码 「206」 通常还会伴随着头字段“Content-Range”,表示响应报文里 body 数据的具体范围,供客户端确认,例如“Content-Range: bytes 0-99/2000”,意思是此次获取的是总计 2000 个字节的前 100 个字节,这其实也就是http协议的「分段请求」功能,这部分内容我们之后会详细介绍!
3xx

3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是「重定向」

  • 「301 Moved Permanently」」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。
  • 「302 Found」」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。

301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。

  • 「304 Not Modified」」不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制。
4xx

4××类状态码表示客户端发送的请求报文有误,服务器无法处理,它就是真正的“错误码”含义了。

「400 Bad Request」」是一个通用的错误码,表示请求报文有错误,但具体是数据格式错误、缺少请求头还是 URI 超长它没有明确说,只是一个笼统的错误,客户端看到 400 只会是“一头雾水”“不知所措”。所以,在开发 Web 应用时应当尽量避免给客户端返回 400,而是要用其他更有明确含义的状态码。

「403 Forbidden」」实际上不是客户端的请求出错,而是表示服务器禁止访问资源。原因可能多种多样,例如信息敏感、法律禁止等,如果服务器友好一点,可以在 body 里详细说明拒绝请求的原因,不过现实中通常都是直接给一个“闭门羹”。

「404 Not Found」」可能是我们最常看见也是最不愿意看到的一个状态码,它的原意是资源在本服务器上未找到,所以无法提供给客户端。但现在已经被“用滥了”,只要服务器“不高兴”就可以给出个 404,而我们也无从得知后面到底是真的未找到,还是有什么别的原因,某种程度上它比 403 还要令人讨厌。

4××里剩下的一些代码较明确地说明了错误的原因,都很好理解,开发中常用的有:

  • 405 Method Not Allowed:不允许使用某些方法操作资源,例如不允许 POST 只能 GET;
  • 406 Not Acceptable:资源无法满足客户端请求的条件,例如请求中文但只有英文;
  • 408 Request Timeout:请求超时,服务器等待了过长的时间;
  • 409 Conflict:多个请求发生了冲突,可以理解为多线程并发时的竞态;
  • 413 Request Entity Too Large:请求报文里的 body 太大;
  • 414 Request-URI Too Long:请求行里的 URI 太大;
  • 429 Too Many Requests:客户端发送了太多的请求,通常是由于服务器的限连策略;
  • 431 Request Header Fields Too Large:请求头某个字段或总体太大;
5xx

5xx 类状态码表示客户端请求报文正确,但是「服务器处理时内部发生了错误」,属于服务器端的错误码。

  • 「500 Internal Server Error」」与 400 类型,是个笼统通用的错误码,服务器发生了什么错误,我们并不知道。
  • 「501 Not Implemented」」表示客户端请求的功能还不支持,类似“即将开业,敬请期待”的意思。
  • 「502 Bad Gateway」」通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。
  • 「503 Service Unavailable」」表示服务器当前很忙,暂时无法响应客户端,类似“网络服务正忙,请稍后重试”的意思。
Header

ttp header 消息通常被分为4个部分:general header, request header, response header, entity header。但是这种分法就理解而言,感觉界限不太明确。根据维基百科对http header内容的组织形式,大体分为Request(请求头)和Response(响应头)两部分。

Requests部分

Header

解释

示例

Accept

指定客户端能够接收的内容类型

Accept: text/plain, text/html

Accept-Charset

客户端可以接受的字符编码集。

Accept-Charset: iso-8859-5

Accept-Encoding

指定客户端可以支持的web服务器返回内容压缩编码类型。

Accept-Encoding: compress, gzip

Accept-Language

客户端可接受的语言

Accept-Language: en,zh

Accept-Ranges

可以请求网页实体的一个或者多个子范围字段

Accept-Ranges: bytes

Authorization

HTTP授权的授权证书

Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

Cache-Control

指定请求和响应遵循的缓存机制

Cache-Control: no-cache

Connection

表示是否需要持久连接。(HTTP 1.1默认进行持久连接)

Connection: close

Cookie

HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。

Cookie: $Version=1; Skin=new;

Content-Length

请求的内容长度

Content-Length: 348

Content-Type

请求的与实体对应的MIME信息

Content-Type: application/x-www-form-urlencoded

Date

请求发送的日期和时间

Date: Tue, 15 Nov 2010 08:12:31 GMT

Expect

请求的特定的服务器行为

Expect: 100-continue

From

发出请求的用户的Email

From: user@email.com

Host

指定请求的服务器的域名和端口号

Host: www.zcmhi.com

If-Match

只有请求内容与实体相匹配才有效

If-Match: “737060cd8c284d8af7ad3082f209582d”

If-Modified-Since

如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码

If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT

If-None-Match

如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变

If-None-Match: “737060cd8c284d8af7ad3082f209582d”

If-Range

如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。参数也为Etag

If-Range: “737060cd8c284d8af7ad3082f209582d”

If-Unmodified-Since

只在实体在指定时间之后未被修改才请求成功

If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT

Max-Forwards

限制信息通过代理和网关传送的时间

Max-Forwards: 10

Pragma

用来包含实现特定的指令

Pragma: no-cache

Proxy-Authorization

连接到代理的授权证书

Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

Range

只请求实体的一部分,指定范围

Range: bytes=500-999

Referer

先前网页的地址,当前请求网页紧随其后,即来路

Referer: http://www.zcmhi.com/archives/71.html

TE

客户端愿意接受的传输编码,并通知服务器接受接受尾加头信息

TE: trailers,deflate;q=0.5

Upgrade

向服务器指定某种传输协议以便服务器进行转换(如果支持)

Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11

User-Agent

User-Agent的内容包含发出请求的用户信息

User-Agent: Mozilla/5.0 (Linux; X11)

Via

通知中间网关或代理服务器地址,通信协议

Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1)

Warning

关于消息实体的警告信息

Warn: 199 Miscellaneous warning

Responses 部分

Header

解释

示例

Accept-Ranges

表明服务器是否支持指定范围请求及哪种类型的分段请求

Accept-Ranges: bytes

Age

从原始服务器到代理缓存形成的估算时间(以秒计,非负)

Age: 12

Allow

对某网络资源的有效的请求行为,不允许则返回405

Allow: GET, HEAD

Cache-Control

告诉所有的缓存机制是否可以缓存及哪种类型

Cache-Control: no-cache

Content-Encoding

web服务器支持的返回内容压缩编码类型。

Content-Encoding: gzip

Content-Language

响应体的语言

Content-Language: en,zh

Content-Length

响应体的长度

Content-Length: 348

Content-Location

请求资源可替代的备用的另一地址

Content-Location: /index.htm

Content-MD5

返回资源的MD5校验值

Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==

Content-Range

在整个返回体中本部分的字节位置

Content-Range: bytes 21010-47021/47022

Content-Type

返回内容的MIME类型

Content-Type: text/html; charset=utf-8

Date

原始服务器消息发出的时间

Date: Tue, 15 Nov 2010 08:12:31 GMT

ETag

请求变量的实体标签的当前值

ETag: “737060cd8c284d8af7ad3082f209582d”

Expires

响应过期的日期和时间

Expires: Thu, 01 Dec 2010 16:00:00 GMT

Last-Modified

请求资源的最后修改时间

Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT

Location

用来重定向接收方到非请求URL的位置来完成请求或标识新的资源

Location: http://www.zcmhi.com/archives/94.html

Pragma

包括实现特定的指令,它可应用到响应链上的任何接收方

Pragma: no-cache

Proxy-Authenticate

它指出认证方案和可应用到代理的该URL上的参数

Proxy-Authenticate: Basic

refresh

应用于重定向或一个新的资源被创造,在5秒之后重定向(由网景提出,被大部分浏览器支持)

Refresh: 5; url=http://www.zcmhi.com/archives/94.html

Retry-After

如果实体暂时不可取,通知客户端在指定时间之后再次尝试

Retry-After: 120

Server

web服务器软件名称

Server: Apache/1.3.27 (Unix) (Red-Hat/Linux)

Set-Cookie

设置Http Cookie

Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1

Trailer

指出头域在分块传输编码的尾部存在

Trailer: Max-Forwards

Transfer-Encoding

文件传输编码

Transfer-Encoding:chunked

Vary

告诉下游代理是使用缓存响应还是从原始服务器请求

Vary: *

Via

告知代理客户端响应是通过哪里发送的

Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1)

Warning

警告实体可能存在的问题

Warning: 199 Miscellaneous warning

WWW-Authenticate

表明客户端请求实体应该使用的授权方案

WWW-Authenticate: Basic

总结

通过这篇文章我们搭建了测试项目,对wireShark有了一定了解,也知道了http协议的整体结构。仅仅是这样我们很难对http有一个直观深入的了解,所以下篇文章我会跟大家一起探讨目前的主流框架是如何实现http协议的,例如:http的长连接在代码层次是怎么实现?服务器跟客户端做了什么去实现长连接呢?

下篇文章将从代码实现的角度来分析http协议,跟着我卷起来~

参考:

  1. 《图解Http》
  2. https://xiaolincoding.com/network/2_http/http_interview.html
  3. https://www.zhihu.com/column/c_1397958370813493248
  4. https://kb.cnblogs.com/page/92320/

0 人点赞