抓TCP报文诊断 HTTP Content-Length 问题

欢迎访问陈同学博客原文

抓TCP报文诊断 HTTP Content-Length 问题

本文分享一个 HTTP Content-Length 有误时场的景,以 tcpdump 抓包来做真实演示,同时结合TCP状态进行分析。

关于 Content-Length 的场景,比如提供文件下载的服务,需要设置好 Content-Length 以及断点下载的一些参数。

小例子

下面是 Spring Boot 应用中一段代码,设置 Content-Length 为100字节,实际却不返回任何数据。

@GetMapping("demo")
public void demo(HttpServletResponse response) {
    response.setContentLength(100);
}

用 curl 测试:

curl -X GET http://localhost:8080/demo

控制台卡住1分钟,然后输出:

curl: (18) transfer closed with 100 bytes remaining to read

如果用HTTP客户端(eg: HttpClient)调用,线程会一直处于 RUNABLE 状态,下面是 jstack 拿到的线程状态,socketRead0 是一个 native 方法,会使用socket的原生方法读取数据。

java.lang.Thread.State: RUNNABLE
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
    at java.net.SocketInputStream.read(SocketInputStream.java:170)
    at java.net.SocketInputStream.read(SocketInputStream.java:141)
    at org.apache.http.impl.conn.LoggingInputStream.read(LoggingInputStream.java:84)

线程会一直阻塞在这里,如果应用中有大量这样的线程,可能会耗尽应用线程,导致应用无响应。

下面看看TCP报文传输情况。

TCP 连接状态

为便于理解,先简述TCP的三次握手、四次挥手,熟悉的可直接跳过。

下面两图, INITIATOR 可看作client,RECEIVER 看做server。

三次握手

[外链图片转存失败(img-b4azcsDM-1564881937137)(https://imgcdn.chenyongjun.vip/2019/08/03/1.png)]

  • client:你好,我是client。报文含SYN标志位,SYN即同步、请求连接的意思,状态变为 SYN_SENT
  • server:收到,我是server。响应含 SYN+ACK 两个标记的报文,ACK是对 client SYN 的确认,SYN表示请求连接,状态变为 SYN_RECEIVED
  • client:收到。对server的SYN做ACK,然后CS两端状态变为ESTABLISHED

此时,连接便已创建,可以进行通信。

四次挥手:

[外链图片转存失败(img-u2ncgwpW-1564881937138)(https://imgcdn.chenyongjun.vip/2019/08/03/2.png)]

  • client:再见,我不说话了(不能再发数据)。发送FIN标记的报文(FIN表示finish即结束),状态变为 FIN_WAIT_1 即等着 server 说再见
  • server:收到。向client发送ACK,server变为 CLOSE_WAIT 即等待关闭连接(不急,等自己活干完再关);client 收到ACK后,状态变为 FIN_WAIT_2,等着server结束。
  • server:再见,我活干完了。发送FIN标记报文,状态变为LAST_ACK,此时server也不能再发送数据。
  • client:收到。状态变为TIME_WAIT,即过一段时间就自动关闭,然后对server的FIN做ACK。server收到后就CLOSED,client 过一会也自行CLOSED。

TCP 报文监控

上面介绍了TCP连接状态,现在用 tcpdump 监控网卡8080端口(应用在8080端口)的数据。

sudo tcpdump  -n -i any port 8080

应用跑在本机,下面是tcpdump的动态输出(为了便于展示,仅摘取了关键字段)

65241 是client分配的临时端口,8080是应用端口。Flags 表示标记位,S、P、F分别表示SYN、PSH、FIN,代表请求连接、推送数据、结束连接标记位。

建立TCP连接的三次握手报文, 对应 SYN、SYN+ACK、ACK 三个步骤

::1.65241 > ::1.8080: Flags [S], seq 2672664932, length 0
::1.8080 > ::1.65241: Flags [S.], seq 1257336122, ack 2672664933, length 0
::1.65241 > ::1.8080: Flags [.], ack 1, length 0

client发送请求数据的报文

第二行 client 以 HTTP/1.0 GET 请求 /demo,server 做了ACK表示收到

::1.8080 > ::1.65241: Flags [.], ack 1, win 6371, length 0
::1.65241 > ::1.8080: Flags [P.], seq 1:83, ack 1, length 82: HTTP: GET /demo HTTP/1.1
::1.8080 > ::1.65241: Flags [.], ack 83, win 6370, length 0

client 的报文如下:

GET /demo HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.54.0
Accept: */*

server推送数据的报文

server推送带P标记位的报文,报文长度116,client马上做了ACK

::1.8080 > ::1.65241: Flags [P.], seq 1:117, ack 83, length 116: HTTP: HTTP/1.1 200 
::1.65241 > ::1.8080: Flags [.], ack 117, win 6370, length 0

server的报文如下,其中 Content-Length 为100。

HTTP/1.1 200 
X-Application-Context: application:8080
Content-Length: 100
Date: Sat, 03 Aug 2019 12:52:40 GMT

漫长等待阶段

由于server告知client HTTP请求体中有100字节要推,实际上server又没有推任何数据。此时,

  • 脑补 server:client 咋没任何反应,数据都给你了,读完后你倒是断开连接呀。
  • 脑补 client:搞啥呢,有100个字节咋还不推过来,我再等等把。

两方就干耗着,一起站着茅坑(占用了TCP连接、端口等资源),文章最上面Java线程的RUNNABLE状态就对应在这里。

结束

经过1分钟,server 主动发起了FIN报文,终止了连接,下面对应着四次挥手:

::1.8080 > ::1.65241: Flags [F.], seq 117, ack 83, length 0
::1.65241 > ::1.8080: Flags [.], ack 118, length 0

::1.65241 > ::1.8080: Flags [F.], seq 83, ack 118, length 0
::1.8080 > ::1.65241: Flags [.], ack 84, length 0

当然,如果client设置 socketTimeout,假设为2秒,那2秒之后,就变成client主动发起FIN报文来中断连接了。

小结

实际工作中,有些问题需要去排查TCP连接的状态甚至TCP报文的情况,本文以一个简单的例子做了分享。

关于RCP的状态流转,可参考 RFC793


欢迎关注公众号 [陈一乐],一起学习,一起成长

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章