网络链接错误分类及分析
链接错误主要分几个阶段
- 链接建立期
这个阶段主要是https 相关的错误,
- 证书问题 sanlist,cipher,或者cert不匹配,不存在
- 链接不通 connection timeout
- 链接期
- https response code 返回 非期望, 协议不匹配,header 的设置不正确导致tomcat 处理有问题 (比如form-data-urlencode),4xx,5xx
- 服务器拒绝链接 socket hung up - header 等有错误 或者proxy 断开
- read timeout
- 关闭期
- connection reset/connection reset by peer
- can not assign socket
- DB No more data to read from socket error
TCP 四次挥手

第一次挥手:客户端发送一个FIN为1,序列号随机生成的报文给服务器(假设序列号为M),进入FIN_WAIT_1状态;
第二次挥手:服务器收到这个报文之后,发送一个ACK为1,acknowledge number=M+1的应答报文给客户端,进入CLOSE_WAIT状态。此时客户端已经没有要发送的数据了,但仍可以接受服务器发来的数据。
第三次挥手:服务器发送一个FIN为1,序列号随机生成的报文给客户端(假设序列号为N),进入LAST_ACK状态;
第四次挥手:客户端收到服务器的FIN报文后,进入TIME_WAIT状态;接着发送一个ACK为1,acknowledge number=N+1给服务器;服务器收到后,确认acknowledge number是否为N+1,变为CLOSED状态,不再向客户端发送数据。客户端等待2*MSL(报文段最长寿命)时间后,也进入CLOSED状态。完成四次挥手。

-
为什么不能把服务器发送的ACK和FIN合并起来,变成三次挥手(大多数情况下)
应为服务器收到客户端的FIN报文时有可能还没有做好断开连接的准备(如还有部分数据没有发给客户端,还在继续发送),但需要先发送一个ACK报文告诉客户端我收到了断开连接的请求,等服务器准备好了,再发一个FIN报文给客户端,告诉客户端可以断开连接了。 -
如果第二次挥手时服务器的ACK报文没有送达客户端,会怎样?
由于客户端没有收到ACK报文,客户端会继续发送FIN报文给服务器 -
客户端等待2MSL的意义是什么
因为客户端给服务器发送的ACK报文可能会丢失,TIME_WAIT状态就是用来重发可能丢失的ACK报文。如果服务器没有收到ACK报文,就会重发FIN,如果客户端在2MSL的时间内收到了FIN,就会重新发送ACK并再次等待2MSL,防止服务器没有收到ACK而不断重发FIN。 -
为什么是2*MSL
MSL(Maximum Segment Lifetime),指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,客户端都没有再次收到FIN,那么客户端推断ACK报文已经被成功接收,则结束TCP连接。 -
什么情况下四次挥手可以变为三次
服务器收到客户端的FIN报文时,已经准备好了断开连接(没有数据要发送了)+开启了捎带应答,就可以讲ACK报文和FIN报文合并发送,变为三次挥手 -
什么是捎带应答机制
当发送没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。
为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认。
TCP 延迟确认的策略:
当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方
当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送
如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK
image.png
我们在日常工作中,经常要诊断网络连接中断问题, 那么就把这中问题归个类方便大家在处理日常的异常时候能够快速理解网络发生了什么。
Connection Reset
- Connection reset
两个连接端, 一端退出,但退出时并未关闭该连接,另一端如果在从连接中读数据则抛出该异常。
(Connection reset) - connection reset by peer
如果一端的Socket被关闭(或主动关闭,或因为异常退出而引起的关闭),另一端仍发送数据,发送的第一个数据包引发该异常(Connect reset by peer)。
在TCP首部中有6个标志位,其中一个标志位为RST,用于“复位”的。无论何时一个报文 段发往基准的连接( referenced connection)出现错误,TCP都会发出一个复位报文段。如果双方需要继续建立连接,那么需要重新进行三次握手建立连接
出现RST的原因
RST攻击、干扰
上面简单介绍了RST标志位的作用,很容易想到这样一个场景,如果中间网络节点想要破坏一个tcp连接,那么它只要伪造成其中一方发送RST报文到另一方,即可让双方连接失效。

上图中,三次握手建立了tcp连接,由于是https连接,需要进行加密操作。这时,对方发送RST,双方并没有进行“四次挥手”,而是连接直接失效。这时客户端发起重拾,重新建立连接,但是很快又被“对方”重置了。和客户端连接tcp连接以及发送RST报文的,都不一定是真正的服务端,而可能是中间节点。我们使用HTTPS可以避免受到中间人攻击。对方无法使用中间人攻击,但是想阻止我们访问,所以可以通过RST来重置连接。
请求一个不存在的端口
当客户端访问服务端一个没有监听的端口时,服务端会发送RST报文。

如上图所示,客户端(192.168.2.192)访问了服务端(192.168.2.1)一个未监听的端口(9090),服务端发送了RST报文。
2.3 异常终止一个连接
终止一个连接的正常方式是一方发送 FIN。有时这也称为有序释放(orderly release),因为在所有排队数据都已发送之后才发送 FIN,正常情况下没有任何数据丢失。但也有可能发送一个复位报文段而不是 FIN来中途释放一个连接。有时称这为异常释放 (abortive release)。

上图,当客户端连接redis后,我们手动关闭redis服务,redis服务端会发送RST来强制终止一个连接。客户端通常收到这样的错误:Connection reset by peer,或者:远程主机强迫关闭了一个现有的连接。
半打开连接
如果一方已经关闭或异常终止连接而另一方却还不知道,我们将这样的TCP连接称为半打开(Half-Open)的。只要不打算在半打开 连接上传输数据,仍处于连接状态的一方就不会检测另一方已经出现异常。
排查思路
实际开发过程中,前面三个问题比较容易识别和解决。最困难的是最后一种半打开连接,原因往往很难发现。因为网络正常的情况下,都会通过正常关闭或者2.3的方式来关闭连接。现在客户端和服务端的网络非常复杂,有各种nat,代理,防火墙等设备,这些中间设备可能配置了一些安全策略,导致断开连接而不通知。
通过查询客户端日志,定位出现异常的时间。查询服务端日志,查询有无连接断开日志;查询连接是否存在;如果不存在,查询断开的时间。通过这些可以判断是不是由于半打开连接导致的问题。半打开连接一般是由于中间设备或者网络问题断开连接,而客户端不知道。解决方案就是找到对应的设备,配置连接,避免长连接断开。
具体的错误例子大家可以参照这篇博文 https://hongbshi.github.io/2020/12/13/nginx-connection-reset/
socket hang up
socket hang up 和connection reset 不一样,不是连接关闭。而是server 端主动连接拒绝。
- header 不符合规范。 比如发送headers头的时候,设置的Content-length不等于post过去的数据。
- 在使用request模块发起请求时,请求头信息中使用了Connection: keep-alive,持久连接。服务端不支持持久链接
- 服务端程序处理时间长,超出了默认请求超时时间,导致socket断开。 比如在springboot 里可以通过设置请求超时时间降低出现的概率
spring.mvc.async.request-timeout=180000
。 node.js提供的httpserver默认会设置超时时间为2分钟。请求超时就会被socket关闭掉。
具体问题可以看这个例子
http://mrdede.com/?p=3536
Cannot assign requested address. Socket TIME_WAIT
在2MSL 的时间内,client 端发出的请求超出了系统的端口数量。这样情况通常
- client 端没有主动关闭连接,尤其是在用Jersey client 时候没有主动调用response.getEntity方法
- client端就是在2msl 单位时间内发出了超过端口数量的请求。
通常这类问题都可以调整系统的sysctl的参数可以 net.ipv4.ip_local_port_range, 特别强调一下在k8s中,这个参数是安全参数。完全可以在spec 里修改。
https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/
connection ConnectTimeout & ReadTimeout
ConnectTimeout
指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间。
在java中,网络状况正常的情况下,例如使用HttpClient或者HttpURLConnetion连接时设置参数connectTimeout=5000即5秒,如果连接用时超过5秒就是抛出java.net.SocketException: connetct time out的异常。
ReadTimeout
指的是建立连接后从服务器读取到可用资源所用的时间。
在这里我们可以这样理解ReadTimeout:正常情况下,当我们发出请求时可以收到请求的结果,也就是页面上展示的内容,但是当网络状况很差的时候,就会出现页面上无法展示出内容的情况。另外当我们使用爬虫或者其他全自动的程序时,无法判断当前的网络状况是否良好,此时就有了ReadTimeout的用武之地了,通过设置ReadTimeout参数,例:ReadTimeout=5000,超过5秒没有读取到内容时,就认为此次读取不到内容并抛出Java.net.SocketException: read time out的异常。
根据上面关于ConnectTimeout和ReadTimeout的描述,在我们使用需要设置这两项参数的服务或程序时,应该对两项参数一起设置。
URLConnection connection = url.openConnection();
connection.setConnectTimeout(5000); // 5秒 连接主机的超时时间(单位:毫秒)
connection.setReadTimeout(5000); // 5秒 从主机读取数据的超时时间(单位:毫秒)
No more data to read from socket error
这个错误是DB 链接的错误。该异常通常是因为使用了连接池,当从连接池取得的connection失效或者超时的时候,使用这个连接来进行数据库操作就会抛出以上异常。
- 数据库意外重启后,原先的数据库连接池能自动废弃老的无用的链接,建立新的数据库链接
- 网络异常中断后,原先的建立的 tcp 链接,应该能进行自动切换。比如网站演习中的交换机重启会导致网络瞬断
- 分布式数据库中间件,比如 cobar 会定时的将空闲链接异常关闭,客户端会出现半开的空闲链接。
我们最近碰到的一个情况就是在istio 的模型中app 使用isito-proxy作为代理,进行数据库链接。app 的连接池有些链接一小时以内没有被使用过,结果被istio-proxy作为idle 的链接关闭了。当需要用到关闭的链接时候,就会出现No more data to read from socket error的错误。
java.io.IOException : Server returns HTTP response code
505
HTTP Version Not Supported 服务器不支持请求中所指明的HTTP版本。
- 通常是http1.0 去访问http1.1的服务器。
- URL 不规范。
我的代码中有基于 HTML 的查询,并且在收到来自服务器的 505 响应时,一种特定类型的查询似乎会引发 IOExceptions。我和其他似乎有类似问题的人一起查看了 505 响应。显然 505 代表 HTTP 版本不匹配,但是当我将相同的查询 URL 复制到任何浏览器(尝试过 firefox、seamonkey 和 Opera)时,似乎没有问题。当同一个查询通过我的 Java 代码时,为什么我会遇到问题?
private InputStream openURL(String urlName) throws IOException{
URL url = new URL(urlName);
URLConnection urlConnection = url.openConnection();
return urlConnection.getInputStream();
Others
https://blog.csdn.net/wo240/article/details/80702935
javax.net.ssl.SSLHandshakeException
- 不同tls 版本
- 不支持cipher suite 导致
调试ssl握手 - Java程序添加vm参数:-Djavax.net.debug=all
查看报文
main, WRITE: TLSv1 Handshake, length = 163
main, READ: TLSv1.2 Alert, length = 2
main, RECV TLSv1 ALERT: fatal, handshake_failure
发现ssl不一致ssl不一致,则更换jdk版本,或者修改服务器ssl加密版本

若结果为
RMI TCP Connection(3)-127.0.0.1, WRITE: TLSv1.2 Handshake, length = 143
RMI TCP Connection(3)-127.0.0.1, READ: TLSv1.2 Alert, length = 2
RMI TCP Connection(3)-127.0.0.1, RECV TLSv1.2 ALERT: fatal, handshake_failure
RMI TCP Connection(3)-127.0.0.1, called closeSocket()
RMI TCP Connection(3)-127.0.0.1, handling exception: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
RMI TCP Connection(3)-127.0.0.1, called close()
RMI TCP Connection(3)-127.0.0.1, called closeInternal(true)
此时ssl版本一致任然握手失败,则为加密套件不可用 此时加密密钥长度>128,jdk1.8默认支持证书密钥长度小于128

jdk1.8版本只要修改Java\jre\lib\security\java.security文件,启用crypto.policy。
crypto.policy=unlimited
javax.net.ssl.SSLException
缺少访问指定网址的证书
sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provid
证书不被信任
总结
在service集群进行istio 迁移中,我们发现链接问题由于有个istio-proxy ,由以前A ->C 模型变成了 A->B-> C 的模型,同样的traffic 如果出现网络链接问题,可能和之前并不完全相同。 比如socket hung up ,以前不会出现,而是以connection reset 。但是由于B-> C的中断, 可能会体现为socket hung up 。