Http实战之Wireshark抓包分析

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

搭建测试项目

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

对netty或者httpclient不熟悉的同学不用担心,涉及到的代码都非常简单。

服务端我之所以选用这两个框架是因为相对来说,它们对http协议的封装较浅,在后面的文章中我可以带大家看看代码层次上http协议是如何封装的,这样可以将http协议理解的更加透彻,在本文中大家将注意力放到抓包的分析过程即可!

代码如下:

pom文件引入依赖:

<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>

服务端代码:

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();
  }
}

客户端代码如下:

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)是一个网络包分析工具。该工具主要是用来捕获网络数据包,并自动解析数据包,为用户显示数据包的详细信息,供用户对数据包进行分析。

下载链接:www.wireshark.org/download.ht…

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

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

iShot2022-05-29 13.38.03

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

image-20220529171038626

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

image-20220529171253849

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

image-20220529231924548

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

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

image-20220529233413465

一进这个界面可能会有点懵,因为我们没有输入任何过滤表达式,所以此时整个界面上展示的是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得到。

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

image-20220530001116857

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

image-20220530001148066


那么接下来我们正式开始抓包实验,确保你的测试项目及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上可以抓取到如下数据:

image-20220530174209638

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

image-20220530174800147

协议格式

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

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

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

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

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

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

image-20220605174045516

请求行

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

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

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

image-20220605175945762

常见的请求方法

请求方法 描述信息 补充
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 要求使用隧道协议连接代理 关于隧道大家可查看:www.zhihu.com/question/21…,本文不在赘述

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环境下会发现下面两种写法都可以正常工作

    image-20220605211358633

状态行

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

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

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

image-20220605213013369

状态码

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

这五类的具体含义是:

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

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

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

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

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

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

1xx

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

image-20220606231019487

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: [email protected]
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: www.zcmhi.com/archives/71…
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: www.zcmhi.com/archives/94…
Pragma 包括实现特定的指令,它可应用到响应链上的任何接收方 Pragma: no-cache
Proxy-Authenticate 它指出认证方案和可应用到代理的该URL上的参数 Proxy-Authenticate: Basic
refresh 应用于重定向或一个新的资源被创造,在5秒之后重定向(由网景提出,被大部分浏览器支持) Refresh: 5; url=www.zcmhi.com/archives/94…
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. xiaolincoding.com/network/2_h…
  3. www.zhihu.com/column/c_13…
  4. kb.cnblogs.com/page/92320/

猜你喜欢

转载自juejin.im/post/7115415919777546271