欢迎访问陈同学博客原文
抓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。
欢迎关注公众号 [陈一乐],一起学习,一起成长