事情的原由是个人一个同窗让我帮他看一个问题,当时的描述是: 服务器在请求较多时,出现不响应的状况。
现场环境是html
网络拓扑大概是 client->内网穿透frp->httpServer 的样子
前置条件就是这样。java
首先 netstat na|grep port 看了下网络状态,并用awk统计了下链接的状态:web
CLOSE_WAIT 613 ESTABLISHED 53 TIME_WAIT 17
netstat result like :spring
[root@localhost backend]# netstat -nalt |grep 8077 tcp6 0 0 :::8077 :::* LISTEN tcp6 1 174696 192.168.2.195:8077 172.16.1.10:49588 CLOSE_WAIT tcp6 1 188280 192.168.2.195:8077 172.16.1.10:49576 CLOSE_WAIT ...
underTow 的http处理线程总共32个 且都处于下面的状态 :编程
"XNIO-1 task-32" #69 prio=5 os_prio=0 tid=0x0000000001c8e800 nid=0x1e10 runnable [0x00007f0a20dc1000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.PollArrayWrapper.poll0(Native Method) at sun.nio.ch.PollArrayWrapper.poll(PollArrayWrapper.java:115) at sun.nio.ch.PollSelectorImpl.doSelect(PollSelectorImpl.java:87) at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86) - locked <0x00000006cf91e858> (a sun.nio.ch.Util$3) - locked <0x00000006cf91e848> (a java.util.Collections$UnmodifiableSet) - locked <0x00000006cf91e5f0> (a sun.nio.ch.PollSelectorImpl) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:101) at org.xnio.nio.SelectorUtils.await(SelectorUtils.java:46) at org.xnio.nio.NioSocketConduit.awaitWritable(NioSocketConduit.java:263) at org.xnio.conduits.AbstractSinkConduit.awaitWritable(AbstractSinkConduit.java:66) at io.undertow.conduits.ChunkedStreamSinkConduit.awaitWritable(ChunkedStreamSinkConduit.java:379) at org.xnio.conduits.ConduitStreamSinkChannel.awaitWritable(ConduitStreamSinkChannel.java:134) at io.undertow.channels.DetachableStreamSinkChannel.awaitWritable(DetachableStreamSinkChannel.java:87) at io.undertow.server.HttpServerExchange$WriteDispatchChannel.awaitWritable(HttpServerExchange.java:2039) at io.undertow.servlet.spec.ServletOutputStreamImpl.writeBufferBlocking(ServletOutputStreamImpl.java:577) at io.undertow.servlet.spec.ServletOutputStreamImpl.write(ServletOutputStreamImpl.java:150) at org.springframework.security.web.util.OnCommittedResponseWrapper$SaveContextServletOutputStream.write(OnCommittedResponseWrapper.java:639) at org.springframework.util.StreamUtils.copy(StreamUtils.java:143) at com.berry.oss.service.impl.ObjectServiceImpl.handlerResponse(ObjectServiceImpl.java:732) ...
至此,咱们看到了两个疑点,浏览器
之前在cjh的产线问题中见过TIME_WAIT 太多的状况,CLOSE_WAIT 太多的状况我没见过,因此又回顾了一遍TCP的握手挥手,如图: 服务器
因此 CLOSE_WAIT 状态其实是客户端发送了挥手的FIN以后的服务器的状态,此时客户端已经完成本身的 write操做,只会read服务端发来的数据,而等服务端发送完了,就会发本身的FIN包。
查了些资料以后,看到不少人CLOSE_WAIT的缘由是由于 服务端有很是耗时的操做,致使会话超时,客户端发FIN,而服务端线程已经阻塞在操做上,致使服务端无法发FIN包。
因此我沿着jstack的线程栈看了下相关的方法,大体以下:网络
@GetMapping("/hello") public void hello(HttpServletResponse response) throws IOException { String path="C:\\Users\\Administrator\\Desktop\\over.mp4"; FileInputStream fileInputStream=new FileInputStream(path); OutputStream outputStream=response.getOutputStream(); response.setContentType("video/mp4"); int byteCount = 0; byte[] buffer = new byte[4096]; int bytesRead = -1; while ((bytesRead = fileInputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); byteCount += bytesRead; } outputStream.flush(); }
只是一个http发送大文件视频的服务,没有什么耗时操做。因此到这我已经机关用尽了。app
由于判断问题出在tcp上,因此咱们简单看了看tcpdump的抓包资料,而后决定抓包看下网络上到底发生了什么。抓到的大概信息以下: socket
能够看到在客户端发了 FIN以后,疑点:
因此win 表明什么意思,
经过查看tcp的格式:
咱们发现: 窗口大小 字段占了16位,指明TCP接收方缓冲区的长度,以字节为单位,最大长度是65535字节,0指明发送方应中止发送,由于接收方的TCP的缓冲区已满,
因此主要的问题是这个网络代理frp 有bug, 或者是部署这个服务的机器太垃圾,致使服务器一直阻塞在写方法上没法继续。
这部分我以为是一个很重要的,且之前都没有被我注意过的点,固然我也没什么机会作socket编程,查看了以后总结起来以下:
参考:
https://blog.csdn.net/zlfing/...
至此已经定位了这个CLOSE_WAIT的问题所在,但我手贱又测了几回,发现了一个很奇怪的现象:我在服务端tcpdump,本身浏览器作client,视频点开以后直接关闭页面,一直抓不到FIN包,且链接也一直ESTABLISHED,服务器也已经报错java.nio.channels.ClosedChannelException且中止数据传输, 这让我很疑惑,若是链接一直存在,那是什么中止了服务端的传输。
在尝试了无数次以后,终于发现了一个疑点:
00:52:57.745946 IP 116.237.229.239.64575 > izuf6buyhgwtrvp2bv981yz.8077: Flags [P.], seq 1400:1442, ack 2399686, win 513, length 42
每次在我关闭页面以后,抓包总会抓到客户端发的一个42长度的数据报。P表明马上上送到上层应用,无需等待。
因此看起来服务端中止传输数据是因为应用的行为致使,而并不禁TCP控制,漫无目的的测了好久后终于 在浏览器的控制台,看到了协议是h2,表明了使用的是http2协议。
关于http2的详解:
https://blog.wangriyu.wang/20...
关于http2 网上已经不少资料,我只简单记录下关键点:
http1.1 已经出现了tcp链接的复用(keep-alived) ,可是一个http的状态总归仍是由tcp的打开和关闭来掌管,且同一时刻tcp上只能存在一个http链接,即便使用了管道化技术,同时能够发不少个http请求,但服务器依旧是FIFO的策略进行处理并按顺序返回。
而在http2中客户端和服务器只须要一个tcp链接,每个http被称做一个流,帧被看成一个http报文的单位,每个帧会标明本身属于哪一个流,且帧会分类型,来控制流的状态:
这是我比较好奇的地方,服务端客户端究竟在哪里协商升级到http2。
参考:
https://imququ.com/post/proto...
简单来讲就是 Google 在 SPDY 协议中开发了一个名为 NPN(Next Protocol Negotiation,下一代协议协商)的 TLS 扩展,在这个扩展中会进行协议选择,很幸运的是我在本地wireshark报文中找到了他:
Extension: application_layer_protocol_negotiation (len=14) Type: application_layer_protocol_negotiation (16) Length: 14 ALPN Extension Length: 12 ALPN Protocol ALPN string length: 2 ALPN Next Protocol: h2 ALPN string length: 8 ALPN Next Protocol: http/1.1
能够看到 客户端传了 http1.1 h2给服务端 ,而服务端的握手返回:
Extension: application_layer_protocol_negotiation (len=5) Type: application_layer_protocol_negotiation (16) Length: 5 ALPN Extension Length: 3 ALPN Protocol ALPN string length: 2 ALPN Next Protocol: h2
这就是http2的协商握手过程。
我还须要确认一件事,就是 以前说的42长度的数据包究竟是什么样子,但由于http2必须基于https,致使wireshark没法看到这个包内容,搜了下资料操做以下:
https://zhuanlan.zhihu.com/p/...
最终看到这个42长度的包 是:
HyperText Transfer Protocol 2 Stream: RST_STREAM, Stream ID: 3, Length 4 Length: 4 Type: RST_STREAM (3) Flags: 0x00 0... .... .... .... .... .... .... .... = Reserved: 0x0 .000 0000 0000 0000 0000 0000 0000 0011 = Stream Identifier: 3 Error: CANCEL (8)
这个帧的状态是 RST_STREAM ,他会通知应用层中止这个流。
至此全部的疑惑都解除了。
当我在解决问题的时候,才会明白本身到底有多菜。