网络编程(1)-TCP问题分析
主目录见:Android高级进阶知识(这是总目录索引)
[written by 无心追求]
TCP问题分析
网络的五层协议
物理层
数据链路层
-
网络层
,IP协议,ICMP协议(ping) -
传输层
,传输层有两个协议,面向连接的TCP和无连接的UDP,TCP是点对点的可靠连接,保证数据顺序必达,UDP是无连接的,不保证数据顺序必达,UDP的传输效率要比TCP高,但是可能会丢包,而且一个UDP分段最多只能发送65535个字节,TCP则是数据流的形式进行数据传输的,对于应用层来说,并没有限制一次性可发送的数据,只有在TCP协议这一层会对应用层传输下来的数据做分段重组,这个跟SYN的MMS(Max segment size)有关,表示TCP往另一端发送的最大块数据的长度 -
应用层
,FTP,HTTP,HTTPS,STMP等这些基于TCP协议实现的,而DNS,DHCP(动态主机配置协议,一种局域网协议)等是基于UDP协议实现的
image.png
TCP协议
-
TCP协议的Header组成,TCP协议首部,如果除去Options(选项)的长度,有20个字节长度,IP协议首部也有20个字节的长度
![tcp1.jpg](http:https://img.haomeiwen.com/i1609288/0260d9484bc76dfb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
TCP三次握手
[图片上传中...(tcp3.jpg-a081f-1513993970933-0)]
[图片上传中...(tcp4.jpg-9173c2-1513993970933-1)]
这边有几个比较重要的点,主机A发起SYN,假设序号Seq为a,发送成功后A进入了SYN_SENT状态,主机B收到A发的SYN后给一个SYN+ACK,发送成功后B进入SYN_RCVD状态,假设序号Seq为b,那么确认号Ack必须为a+1,以此来标记确实收到了A发过来的SYN,同时也说明下一次A要是再发消息过来,那么序号Seq必须是a+1,主机A收到B发过来的SYN+ACK后,进入ESTABLISHED状态,表示A的TCP已经可用,然后再回复一个ACK给B,序号Seq为a+1,确认号Ack为b+1,B收到后状态也变为ESTABLISHED状态,这时候TCP的三次握手就完成了
TCP四次挥手
tcp7.jpg tcp6.jpg和三次握手比较类似,主机A主动发送FIN,Seq为a,ack为x,然后A进入FIN_WAIT1状态,B进入CLOSE_WAIT状态,主机B收到A发过来的FIN之后,先回复一个FIN+ACK,Seq为b,ack为a+1,然后B进入LAST_ACK状态,主机A收到B发的FIN+ACK后,回复一个ACK,Seq为a+1,Ack为b+1,A进入TIME_WAIT状态,B收到A的ACK后,进入CLOSED,然后A会再等一小段时间(2MSL超时)后进入CLOSED状态,TCP4次挥手完成,其实从抓包来看,并没有发生四次交互,因为B的ACK和FIN合并在一起发出去了,这个也是TCP传输的一种策略,减少一次交互,增加了网络的效率,4次挥手我认为是逻辑上的4个步骤
以下内容都特么是从书里抄的
- TIME_WAIT状态也称为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生
存时间MSLMaximum Segment Lifetime)。它是任何报文段被丢弃前在网络内的最长时间。
我们知道这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制
其生存时间的TTL字段 - 当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)
- 在FIN_WAIT_2状态我们已经发出了FIN,并且另一端也已对它进行确认。除非我们在实
行半关闭,否则将等待另一端的应用层意识到它已收到一个文件结束符说明,并向我们发一
个FIN来关闭另一方向的连接。只有当另一端的进程完成这个关闭,我们这端才会从FIN_WAIT_2状态进入TIME_WAIT状态,这意味着我们这端可能永远保持这个状态,另一端也将处于CLOSE_WAIT状态,并一直保持这个状态直到应用层决定进行关闭
TCP选项
- MSS最大报文长度就是其中一个选项
- Timestamps时间戳(TCP Option - Timestamps: TSval 4294941817, TSecr 0)TSval为发起端发送这个消息包的本地时间,TSecr为发起端收到对方消息包的发送时间,即对方消息包中的TSval,由于这个是SYN包,所以此时并没有收到任何对方的消息,TSecr为0
- Window scale窗口扩大因子,用来控制Window窗口的大小
拥塞控制
Nagle算法
- 该算法要求一个TCP连接上最多只能有一个未被确认的未完成的小分组,在该分组的确认到达之前不能发送其他的小分组。相反,TCP收集这些少量的分组,并在确认到来时以一个分组的方式发出去。该算法的优越之处在于它是自适应的:确认到达得越快,数据也就发送得越快。而在希望减少微小分组数目的低速广域网上,则会发送更少的分组
/**
* Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm).
*
* @param on <code>true</code> to enable TCP_NODELAY,
* <code>false</code> to disable.
*
* @exception SocketException if there is an error
* in the underlying protocol, such as a TCP error.
*
* @since JDK1.1
*
* @see #getTcpNoDelay()
*/
public void setTcpNoDelay(boolean on) throws SocketException {
if (isClosed())
throw new SocketException("Socket is closed");
getImpl().setOption(SocketOptions.TCP_NODELAY, Boolean.valueOf(on));
}
以上是应用层提供的是否开启Nagle算法的接口,默认为false,即开启Nagle算法
tcp9.jpg tcp10.jpg
这个是未用Nagle算法的抓包信息
滑动窗口
tcp11.jpg对于发送方来说:
- Sent and Acknowledged:这些数据表示已经发送成功并已经被确认的数据,这些数据其实的位置是在窗口之外了,因为窗口内顺序最低的被确认之后,要移除窗口,实际上是窗口进行合拢,同时打开接收新的带发送的数据
- Send But Not Yet Acknowledged:这部分数据称为发送但没有被确认,数据被发送出去,没有收到接收端的ACK,认为并没有完成发送,这个属于窗口内的数据
- Not Sent,Recipient Ready to Receive:这部分是尽快发送的数据,这部分数据已经被加载到缓存中,也就是窗口中了,等待发送,其实这个窗口是完全有接收方告知的,接收方告知还是能够接受这些包,所以发送方需要尽快的发送这些包
- Not Sent,Recipient Not Ready to Receive: 这些数据属于未发送,同时接收端也不允许发送的,因为这些数据已经超出了发送端所接收的范围
对于接收方来说:
- Received and ACK Not Send to Process:这部分数据属于接收了数据但是还没有被上层的应用程序接收,也是被缓存在窗口内
- Received Not ACK: 已经接收,但是还没有回复ACK,这些包可能输属于Delay ACK的范畴了
- Not Received:有空位,还没有被接收的数据
窗口的动态调整:
- 客户端给服务器发送了50个字节,然后服务器回了Ack,并且告知Win=0,客户端收到服务器的Ack之后发现服务器这时候已经不能够接收数据了,客户端就会把数据先缓存起来,并不在发送数据,等收到服务器的窗口更新后Win=20,发现服务器重新能够接收20个字节了,这时候客户端才继续发送数据,这里其实也就是流量控制,同时避免过多的数据写入到网络链路中,加入链路的带宽很小,过多的数据写入会造成拥塞
慢启动
- 慢启动为发送方的TCP增加了另一个窗口:拥塞窗口(congestion window),记为cwnd;当与另一个网络的主机建立 TCP连接时,拥塞窗口被初始化为 1个报文段(即另一端通告的报文段大小);每收到一个ACK,拥塞窗口就增加一个报段(cwnd以字节为单位,但是慢启动以报文段大小为单位进行增加);发送方取拥塞窗口与通告窗口中的最小值作为发送上限;拥塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制
拥塞避免
- ssthresh,这个是一个阀值,当cwnd小于这个阀值的时候采用慢启动,那么cwnd就会以指数增长,当cwnd大于ssthresh的时候,这时候就不用慢启动算法,改用拥塞避免算法,拥塞避免算法是在每次收到消息的ack的时候cwnd增加1,这时候cwnd就变成线性增长
快速重传
- 连续收到3次或者3次以上的重复Ack,就会触发一次快速重传
超时重传
- RTT,消息包发送和收到Ack的往返时间,每一个消息包的RTT都可能不一样,TCP会根据消息包的RTT去计算下一次重传的时间间隔,RTT的计算公式是:R← R+ ( 1- )M,这里的 是一个推荐值为 0.9的平滑因子,每次进行新测量的时候,这个被平滑的RTT将得到更新,每个新估计的90%来自前一个估计,而10%则取自新的测量
- RTO(Retransmission TimeOut),这个是重传超时时间,也就是下重传消息需要等待Ack的时间,超过这个时间就再次发起下一次重传
TCP抓包分析
- wireshark工具来查看tcpdump的抓包
- 对于有root过的android手机,如果手机系统中已经内置了tcpdump,可以直接使用tcpdump命令开启抓包:tcpdump -i any -p -w /sdcard/netlog/dumpFileName,这个是我平时比较常用的抓包命令,是全部都抓,无论是无线网卡还是数据网络,这种抓包命令在网络切换,网络断线重连的时候依然能保持抓包,如果采用指定数据网络的抓包,例如:tcpdump -i “aaa” -p -w /sdcard/netlog/dumpFileName,其中“aaa”是数据网络的一个名称,那当设备网络从数据网络切换到无限网络,这时候抓包就会断了,或者数据网络断线重连这时候抓包也会停止,因此推荐采用any的抓包方式,输入命令后可以用ps | grep tcpdump命令看下抓包进程是否运行
- 如果手机系统中把tcpdump模块给裁剪掉,那么需要去网上下载tcpdump,然后执行如下步骤:
adb push tcpdump /sdcard/
adb Shell
su
cat /sdcard/tcpdump > /system/bin/tcpdump
上一条命令如果提示没有权限,接着执行如下命令尝试给 /system 目录增加写权限:
su
mount
在mount结果中找到包含/system的一行,类似如下:
/dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro,seclabel,relatime,data=ordered 0 0
去处/system前半行,即/dev/block/platform/msm_sdcc.1/by-name/system,执行如下命令:
mount -o remount /dev/block/platform/msm_sdcc.1/by-name/system /system
这个时候/system就拥有写权限了,继续执行:
cat /sdcard/tcpdump > /system/bin/tcpdump
chmod 777 /system/bin/tcpdump
到此为止,tcpdump就成功安装到了/system/bin/目录下,接着用如下命令还是抓包
Java中的Socket异常分析
connection reset
- 在TCP消息协议包中,RST标志代表连接终止,对于应用层来说可能会报connection reset
- 当A发送一个消息包给B,但是迟迟没有收到B的Ack,这时候A开始重传,并且重传一直不成功,知道最后一次重传失败之后,A会发起一个reset标志
- 当A关闭的TCP连接,这时候还收到B发过来的数据,A会立马触发一个reset
SocketException: Software caused connection abort
- 这个异常对于Android来说,是设备断网了,同时会有一个网络切换广播
socket closed
- 当java中的socket被close掉之后,如果继续往socket中写数据,就会报socket closed异常
SocketTimeoutException:connect timeout/ConnectException: Connection timed out
-
从字面意义上来理解就是连接超时,从抓包层面来看就是A对B发起一个TCP连接,那么A就会发起一个SYN,但是迟迟没有收到B的SYN+ACK,这时候SYN会进入重传,应用层通常会设置一个连接超时时间,当这个连接超时时间溢出的时候就会报超时异常
tcp12.jpg
UnknowHostException
- 通常采用域名去连接的时候,域名会先通过dns解析成ip最终去用ip连接,但是在dns解析ip的时候没有解析出ip或者解析失败就会报这个异常,dns是采用udp协议,在Android设备上会连续连续尝试8次,每次超时时间为5秒,因此如果dns解析超时,一共耗时40秒,也就是说:
InetSocketAddress inetSocketAddress = new InetSocketAddress(hostInfo.getHostname(), hostInfo.getPort());
这个代码会卡40秒,具体抓包信息如下:
tcp13.jpg
ConnectException:network is unreachable
- 这个很明显,网络不可用会报此异常
NoRouteToHostException: No route to host
- 这个是由于DHCP(Dynamic Host Configuration Protocol,动态主机配置协议,使用UDP协议)租约过期造成的,在局域网中,DHCP的主要作用就是给局域网中的设备分配IP,分配策略如下:
- 自动分配方式(Automatic Allocation),DHCP服务器为主机指定一个永久性的IP地址,一旦DHCP客户端第一次成功从DHCP服务器端租用到IP地址后,就可以永久性的使用该地址
- 动态分配方式(Dynamic Allocation),DHCP服务器给主机指定一个具有时间限制的IP地址,时间到期或主机明确表示放弃该地址时,该地址可以被其他主机使用
- 手工分配方式(Manual Allocation),客户端的IP地址是由网络管理员指定的,DHCP服务器只是将指定的IP地址告诉客户端主机
三种地址分配方式中,只有动态分配可以重复使用客户端不再需要的地址,也就是说动态分配的IP地址是可能被回收的,如果一台设备分配的IP被回收了,那这台设备的网络就不可用,这时候如果往再和外网去建立socket连接,就会报NoRouteToHostException: No route to host异常,那么为了给IP续约,Android设备中会去实现DHCP协议,定期30分钟去发起一个DHCP请求来更新IP信息,保持设备网络可用,但是当这个DHCP请求失败的时候,此时IP要是过了有效时段,那么这段时间内的设备网络是无法访问外网的,直到DHCP请求重试成功为止
Connection Refused
- 连接被拒绝,通常客户端向服务器的指定端口发起一个TCP连接,要是服务器的并没有监听这个端口,此时会拒绝TCP连接,会报Connection Refused异常
TCP端口重用
- 在同一个进程中,一个端口如果已经被一条TCP占用,那么当第二条TCP连接还想申请使用这个端口的时候会报端口重用异常
socket read返回-1
- -1就是文件结束符(EOF),如果A和B之间有一条TCP连接,A端的socket的read的时候返回-1,那这条socket就不再有可读取的数据,造成这个-1的原因是B端的TCP发起了一个FIN,可能是调用了socket的close方法
tcpdump抓包分析
tcp dup ack
- 重复的Ack,#前面的数组表示丢包的包序号,后面表示第几次丢失,之所以会重复的Ack是可能是因为在网络延迟较高,或者链路拥塞的时候一个包重传了多次,但是重传的这几次最终都被收到了,在接收端第一次收到这个包的时候对这个包进行了Ack,然后再收到后面几次重传,再回复重复的Ack
tcp out of order
- 收到的包是乱序的,TCP协议确保消息必达并且顺序,但是TCP协议是基于IP协议的,IP报文并不确保消息的顺序性,所以先发出的IP报文可能比后发的IP报文先到达,这里可能是链路拥塞,网络延迟,丢包,Client到Server有多条网络路径导致IP报文到达的顺序发生乱序,TCP协议在收到消息包后发现如果顺序乱了,就缓存起来暂时步抛给应用层,直到前面的消息包都收到为止,然后再重新组织消息的顺序,抛给应用层,所以在实际场景中tcp out of order并不一定是代表异常,但是看到tcp out of order发生之后可能就怀疑链路是否发生拥塞,或者网络延迟,丢包等等来去判断此时网络是否稳定
tcp retransmission
- TCP协议既然要保证消息必达,所以在一个消息发送出去之后,就会等待对方确认收到这个消息的Ack,在消息发送出去之后会启动一个定时器来检测是否在规定时间内有收到Ack,如果没有收到Ack,这时候就会触发此消息重传,这里还涉及到RTT(消息的回显时间),RTO(消息重传超时),每一次消息包发送都可能(这里并不一定会触发RTT计算,要看这时候的计算RTT的定时器是否启动,如果启动,那么这次消息包发送就不会计算RTT)会不断的去调整计算RTT,然后根据计算出来的RTT,再根据指定的公式计算出RTO
- 关于超时重传的一个疑问:当有一个消息包正在TCP底层进行重传的过程中,应用层此时再写入数据的话,那写入的这个数据会再发送出去吗?我的解答是:1.如果发生了超时重传,那此时可能就存在网络拥塞,tcp协议针对网络拥塞控制就有几种策略,Nagle算法,滑动窗口,慢启动,拥塞避免,快速重传后快速恢复(拥塞避免);在Nagle算法的情况下,由于改算法需要等待上次消息的Ack回来之后才把后面的消息写入到网络中,所以此时应用层写入的数据并不会发送出去;而对于滑动窗口,如果接收窗口(提供窗口)为0,那数据也不会发送出去;对于慢启动,慢启动还是需要在前一条消息的Ack回来之后才允许后续消息继续发送,拥塞窗口只是控制发送消息的大小,防止向网络中写入过多的数据加重拥塞;所以应用层如果在TCP底层有一个消息正在重传,此时再写入一个数据的话,此数据如果立即发送并且超时那么有可能在目前已有重传过程中一起被发送出去(此操作需要根据当前的接收窗口是否允许发送这么大的数据或者根据拥塞窗口是否发送这个大的数据来决定新写入的数据是否跟着重传一起发送出去),也有可能一直缓存这,等待上一个重传成功收到Ack之后才写出去
12-17 10:03:40.994 2725 2960 I SYNC-PUSH: {ConnectionService$7.onWrite--1} write [107] bytes.
12-17 10:04:02.234 2725 3052 I SYNC-PUSH: {ConnectionService$7.onWrite--1} write [7] bytes.
tcp14.jpg
应用层将107字节发写入之后,发生了重传,这时候在10:04:02秒又写入7个字节,我们在抓包的时候发现重传并没有把这7个字节一起发送出去,说明这个7个字节被暂时缓存起来了,此时有可能是网络拥塞了,所以就不再向链路中发送数据避免加重网络拥塞,如果此时并不是网络拥塞,那么有可能这7个字节就在重传过程中被一起发送出去
tcp fast retransmission
-
快速重传,快速重传是当预测到链路可能存在拥塞的时候,连续收到了三次重复的Ack,快速重传之后进入快速恢复算法(拥塞避免,cwnd拥塞窗口线性增加)
tcp15.jpg
tcp previous segment not captured
- 这个是指前一个消息包还没有收到,后一个消息包先收到
上图,发生tcp previous segment not captured的消息包Sequence number: 1242674817,重传的消息包Sequence number: 1242674796,但是1242674817先到了,而1242674796后到,所以在收到1242674817的时候会提示有漏掉一个segment数据,1242674817会缓存起来并不会马上通知给应用层,直到1242674796到了之后一起通知给应用层
seq,ack,tsval,tsecr
- seq是消息的序号,ack是消息的确认号,确认号是确认上一条消息已经被收到,并且告知发送端下一次发过来的消息的序列号
- tsval是发送端发送这条消息的时间戳,tsecr是接收到对方消息的时间戳