主目錄見: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個字節的長度

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四次揮手
和三次握手比較類似,主機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算法
這個是未用Nagle算法的抓包信息
滑動窗口
對于發送方來說:
- 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秒,具體抓包信息如下:
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.
應用層將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是接收到對方消息的時間戳