寫在前面的話html
「聽見學生時代愛聽的歌,加上太累,回家路上一會兒想了好多,腳步慢了,眼眶溼了,不是感傷,而是生活呀,須要這麼多力量。過去那些跌跌撞撞忙碌的日子,怎麼說呢,多少有點像在逃避吧,聽起來不像是真的。」 以上這段話訴說了個人經歷,我也曾迷惘和無助過。也有不少朋友找到我,但願我作一些經驗分享和職業規劃指導。爲此我特意開辦了一個微信公衆號『easyserverdev』。若是有任何技術或者職業方面的問題須要我提供幫助,可經過這個公衆號與我取得聯繫,此公衆號不只分享高性能服務器開發經驗和故事,同時也免費爲廣大技術朋友提供技術答疑和職業解惑助,你有任何問題均可以在微信公衆號直接留言,我會盡快回復您。java
爲了能更好的排查網絡通訊問題,咱們須要熟悉操做系統提供的如下網絡接口函數,列表以下:linux
接口函數名稱 | 接口函數描述 | 接口函數簽名 |
---|---|---|
socket | 建立套接字 | int socket(int domain, int type, int protocol); |
connect | 鏈接一個服務器地址 | int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
send | 發送數據 | ssize_t send(int sockfd, const void *buf, size_t len, int flags); |
recv | 收取數據 | ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
accept | 接收鏈接 | int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags); |
shutdown | 關閉收發鏈路 | int shutdown(int sockfd, int how); |
close | 關閉套接字 | int close(int fd); |
setsockopt | 設置套接字選項 | int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); |
注意:這裏以bekeley提供的標準爲例,不包括特定操做系統上特有的接口函數(如Windows平臺的WSASend,linux的accept4),也不包括實際與網絡數據來往不相關的函數(如select、linux的epoll),這裏只討論與tcp相關的接口函數,像與udp相關的函數sendto/recvfrom等函數與此相似。 下面討論一下以上函數的一些使用注意事項:算法
以上函數若是調用出錯後,返回值均爲-1;可是返回值是-1,不必定表明出錯,這還得根據對應的套接字模式(阻塞與非阻塞模式)。編程
默認使用的socket函數建立的套接字是阻塞模式的,能夠調用相關接口函數將其設置爲非阻塞模式(Windows平臺可使用ioctlsocket函數,linux平臺可使用fcntl函數,具體設置方法能夠參考這裏。)。阻塞模式和非阻塞模式的套接字,對服務器的鏈接服務器和網絡數據的收發行爲影響很大。詳情以下:windows
send函數雖然名稱叫「send」,可是其並非將數據發送到網絡上去,只是將數據從應用層緩衝區中拷貝到協議棧內核緩衝區中,具體何時發送到網絡上去,與協議棧自己行爲有關係(socket選項nagle算法與這個有關係,下文介紹常見套接字選項時會介紹),這點須要特別注意,因此即便send函數返回一個大於0的值n,也不能代表已經有n個字節發送到網絡上去了。一樣的道理,recv函數也不是從網絡上收取數據,只是從協議棧內核緩衝區拷貝數據至應用層緩衝區,並非真正地從網絡上收數據,因此,調用recv時,操做系統的協議棧已經將數據從網絡上收到本身的內核緩衝區中了,recv僅僅是一次數據拷貝操做而已。數組
因爲套接字實現是收發全雙工的,收和發通道相互獨立,不會相互影響,shutdown函數是用來選擇關閉socket收發通道中某一路(固然,也能夠兩路都關閉),其how參數取值通常有三個:SHUT_RD/SHUT_WR/SHUT_RDWR,SHUT_RD表示關閉收消息鏈路,即該套接字不能再收取數據,同理SHUT_WR表示關閉套接字發消息鏈路,可是這裏有個問題,有時候咱們須要等待緩衝區中數據發送完後再關閉鏈接怎麼辦?這裏就要用到套接字選項LINGER,關於這個選項請參考下文常見的套接字選項介紹。最後,SHUT_RDWR同時關閉收消息鏈路和發消息鏈路。經過上面的分析,咱們得出結論,shutdown函數並不會要求操做系統底層回收套接字等資源,真正會回收資源是close函數,這個函數會要求操做系統回收相關套接字資源,並釋放對ip地址與端口號二元組的佔用,可是因爲tcp四次揮手最後一個階段有個TIME_WAIT狀態(關於這個狀態下文介紹tcp三次握手和四次回收時會詳細介紹),致使與該socket相關的端口號資源不會被當即釋放,有時候爲了達到釋放端口用來複用,咱們會設置套接字選項SOL_REUSEPORT(關於這個選項,下文會介紹)。綜合起來,咱們關閉一個套接字,通常會先調用shutdown函數再調用close函數,這就是所謂的優雅關閉:bash
SO_SNDTIMEO與SO_RCVTIMEO服務器
這兩個選項用於設置阻塞模式下套接字,SO_SNDTIMEO用於在send數據因爲對端tcp窗口過小,發不出去而最大的阻塞時長;SO_RCVTIMEO用於recv函數因接受緩衝區無數據而阻塞的最大阻塞時長。若是你須要獲取它們的默認值,請使用getsockopt函數。微信
TCP_NODELAY
操做系統底層協議棧默認有這樣一個機制,爲了減小網絡通訊次數,會將send等函數提交給tcp協議棧的多個小的數據包合併成一個大的數據包,最後再一次性發出去,也就是說,若是你調用send函數往內核協議棧緩衝區拷貝了一個數據,這個數據也許不會立刻發到網絡上去,而是要等到協議棧緩衝區積累到必定量的數據後纔會一次性發出去,咱們把這種機制叫作nagle算法。默認打開了這個機制,有時候咱們但願關閉這種機制,讓send的數據可以馬上發出去,咱們能夠選擇關閉這個算法,這就能夠經過設置套接字選項TCP_NODELAY,即關閉nagle算法。
SO_LINGER
linger這個單詞自己的意思,是「暫停、逗留」。這個選項的用處是用於解決,當須要關閉套接字時,協議棧發送緩衝區中尚有未發送出去的數據,等待這些數據發完的最長等待時間。
SO_REUSEADDR/SO_REUSEPORT
一個端口,尤爲是做爲服務器端端口在四次揮手的最後一步,有一個爲TIME_WAIT的狀態,這個狀態通常持續2MSL(MSL,maximum segment life, 最大生存週期,RFC上建議是2分鐘)。這個狀態存在緣由以下:1. 保證發出去的ack能被送達(超時會重發ack)2. 讓遲來的報文有足夠的時間被丟棄,反過來講,若是不存在這個狀態,那麼能夠馬上覆用這個地址和端口號,那麼可能會收到老的鏈接遲來的數據,這顯然是很差的。爲了當即回收複用端口號,咱們能夠經過開啓套接字SO_REUSEADDR/SO_REUSEPORT。
SO_KEEPALIVE
默認狀況下,當一個鏈接長時間沒有數據來往,會被系統防火牆之類的服務關閉。爲了不這種現象,尤爲是一些須要長鏈接的應用場景下,咱們須要使用心跳包機制,即定時從兩端定時發一點數據,這種行爲叫作「保活」。而tcp協議棧自己也提供了這種機制,那就是設置套接字SO_KEEPALIVE選項,開啓這個選項後,tcp協議棧會定時發送心跳包探針,可是這個默認時間比較長(2個小時),咱們能夠繼續經過相關選項改變這個默認值。
ping命令可用於測試網絡是否連通。
命令使用格式:
telnet ip或域名 port
複製代碼
例如:
telnet 120.55.94.78 8888
telnet www.baidu.com 80
複製代碼
結合ping和telnet命令咱們就能夠判斷一個服務器地址上的某個端口號是否能夠對外提供服務。
因爲咱們使用的開發機器以windows居多,默認狀況下,windows系統的telnet命令是沒有打開的,咱們能夠在【控制面板】- 【程序】-【程序和功能】- 【打開或關閉Windows功能】中打開telnet功能。
host 命令能夠解析域名獲得對應的ip地址。例如,咱們要獲得www.baidu.com這個域名的ip地址,能夠輸入:
獲得www.google.com的ip地址能夠輸入:
常見的選項有:
-a (all)顯示全部選項,netstat默認不顯示LISTEN相關
-t (tcp)僅顯示tcp相關選項
-u (udp)僅顯示udp相關選項
-n 拒絕顯示別名,能顯示數字的所有轉化成數字。(重要)
-l 僅列出有在 Listen (監聽) 的服務狀態
-p 顯示創建相關連接的程序名(macOS中表示協議 -p protocol)
-r 顯示路由信息,路由表
-e 顯示擴展信息,例如uid等
-s 按各個協議進行統計 (重要)
-c 每隔一個固定時間,執行該netstat命令。
複製代碼
lsof,即list opened filedescriptor,即列出當前操做系統中打開的全部文件描述符,socket也是一種file descriptor,常見的選項是:
-i 列出系統打開的socket fd
-P 不要顯示端口號別名
-n 不要顯示ip地址別名(如localhost會用127.0.0.1來代替)
+c w 程序列名稱最大能夠顯示到w個字符。
複製代碼
常見的選項組合爲lsof –i –Pn, 能夠看到列出了當前偵聽的socket,和鏈接socket的tcp狀態。以下圖所示:
嚴格意義上來講,這個不算網絡排查故障和調試命令,可是咱們能夠利用這個命令來查看某個進程的線程數量和線程調用堆棧是否運行正常。指令使用格式:
pstack pid
複製代碼
即, pstack 進程號,以下圖所示:
即netcat命令,這個工具在排查網絡故障時很是有用,於是被業績稱爲網絡界的「瑞士軍刀」。常見的用法以下:
nc –l 0.0.0.0 8888
複製代碼
nc 0.0.0.0 8888
複製代碼
咱們知道客戶端鏈接服務器通常都是操做系統隨機分配一個可用的端口號鏈接到服務器上去,這個指令甚至能夠指定使用哪一個端口號鏈接,如:
nc –p 12345 127.0.0.1 8888
複製代碼
客戶端使用端口12345去鏈接服務器127.0.0.1::8888。
使用nc命令發消息和發文件
客戶端
服務器
這個是linux系統自帶的抓包工具,功能很是強大,默認須要開啓root權限才能使用。
其常見的選項有:
-i 指定網卡
-X –XX 打印十六進制的網絡數據包
-n –nn 不顯示ip地址和端口的別名
-S 以絕對值顯示包的ISN號(包序列號)
複製代碼
經常使用的過濾條件有以下形式:
tcpdump –i any ‘port 8888’
tcpdump –i any ‘tcp port 8888’
tcpdump –i any ‘tcp src port 8888’
tcpdump –i any ‘tcp src port 8888 and udp dst port 9999’
tcpdump -i any 'src host 127.0.0.1 and tcp src port 12345' -XX -nn -vv
複製代碼
關於tcpdump命令接下來將會以對tcp三次握手和四次揮手的包數據進行抓包來分析。
熟練地掌握tcp三次握手和四次揮手過程的每個細節是咱們排查網絡問題的基礎。
下面咱們來經過tcpdump抓包能實戰一下三次握手的過程,假設個人服務器端的地址是 127.0.0.0.1:12345,使用nc命令建立一個服務器程序並在這個地址上進行偵聽:
nc –v -l 127.0.0.0 112345
複製代碼
而後在客戶端機器上開啓tcpdump工具:
而後在客戶端使用nc命令建立一個客戶端去鏈接服務器:
咱們抓到的包以下:
圖片看不清,能夠放大來看。上面咱們須要注意的是: 三次握手過程是客戶端先給服務器發送一個SYN,而後服務器應答一個SYN+ACK,應答的序列號是遞增1的,表示應答哪一個請求,即從4004096087遞增到4004096088,接着客戶端再應答一個ACK。這個時候,咱們發現發包序列號和應答序列號都變成1了,這是tcpdump使用相對序號,咱們加上-S選項後就變成絕對序列號了。
這是正常的tcp三次握手,假如咱們鏈接的服務器ip地址存在,但監聽端口號並不存在,咱們看下tcpdump抓包結果:
這個時候客戶端發送SYN,服務器應答ACK+RST:
這個應答包會致使客戶端的connect鏈接失敗。
還有一種狀況就是客戶端訪問一個很遙遠的ip,或者網絡繁忙,服務器對客戶端發送的網絡SYN報文沒有應答,會出現什麼狀況呢?
咱們先將防火牆的已有規則都清理掉: iptables -F 而後給防火牆的INPUT鏈上增長一個規則,丟棄本地網卡lo(也就是127.0.0.1這個迴環地址)上的全部SYN包:
iptables -I INPUT -p tcp --syn -i lo -j DROP
複製代碼
接着,咱們看到tcpdump抓到的數據包以下:
鏈接不上,一共重試了5次,重試的時間間隔是1秒,2秒,4秒,8秒,16秒,最後返回失敗。這個重試次數在/proc/sys/net/ipv4/tcp_syn_retries 內核參數中設置,默認爲6。
四次揮手與三次握手基本上相似,這裏就不貼出tcpdump抓包的詳情了。實際的網絡開發中,尤爲是高QPS的服務器程序,可能在在服務器程序所在的系統上留下大量非ESTABLISHED的中間狀態,如CLOSE_WAIT/TIME_WAIT,咱們可使用如下指令來統計這些狀態信息:
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
複製代碼
獲得結果可能相似:
讓咱們再貼一張tcp三次握手和四次揮手更清晰的圖吧。
下面看下通常比較關心的三種TCP狀態:
服務端收到創建鏈接的SYN沒有收到ACK包的時候處在SYN_RECV狀態。有兩個相關係統配置:
net.ipv4.tcp_synack_retries,整形,默認值是5
對於遠端的鏈接請求SYN,內核會發送SYN + ACK數據報,以確認收到上一個 SYN鏈接請求包。這是三次握手機制的第二個步驟。這裏決定內核在放棄鏈接以前所送出的 SYN+ACK 數目。不該該大於255,默認值是5,對應於180秒左右時間。一般咱們不對這個值進行修改,由於咱們但願TCP鏈接不要由於偶爾的丟包而沒法創建。
net.ipv4.tcp_syncookies
通常服務器都會設置net.ipv4.tcp_syncookies=1來防止SYN Flood攻擊。假設一個用戶向服務器發送了SYN報文後忽然死機或掉線,那麼服務器在發出SYN+ACK應答報文後是沒法收到客戶端的ACK報文的(第三次握手沒法完成),這種狀況下服務器端通常會重試(再次發送SYN+ACK給客戶端)並等待一段時間後丟棄這個未完成的鏈接,這段時間的長度咱們稱爲SYN Timeout,通常來講這個時間是分鐘的數量級(大約爲30秒-2分鐘)。這些處在SYNC_RECV的TCP鏈接稱爲半鏈接,並存儲在內核的半鏈接隊列中,在內核收到對端發送的ack包時會查找半鏈接隊列,並將符合的requst_sock信息存儲到完成三次握手的鏈接的隊列中,而後刪除此半鏈接。大量SYNC_RECV的TCP鏈接會致使半鏈接隊列溢出,這樣後續的鏈接創建請求會被內核直接丟棄,這就是SYN Flood攻擊。可以有效防範SYN Flood攻擊的手段之一,就是SYN Cookie。SYN Cookie原理由D. J. Bernstain和 Eric Schenk發明。SYN Cookie是對TCP服務器端的三次握手協議做一些修改,專門用來防範SYN Flood攻擊的一種手段。它的原理是,在TCP服務器收到SYN包並返回SYN+ACK包時,不分配一個專門的數據區,而是根據這個SYN包計算出一個cookie值。在收到ACK包時,TCP服務器在根據那個cookie值檢查這個TCP ACK包的合法性。若是合法,再分配專門的數據區進行處理將來的TCP鏈接。觀測服務上SYN_RECV鏈接個數爲:7314,對於一個高併發鏈接的通信服務器,這個數字比較正常。
CLOSE_WAIT
發起TCP鏈接關閉的一方稱爲client,被動關閉的一方稱爲server。被動關閉的server收到FIN後,但未發出ACK的TCP狀態是CLOSE_WAIT。出現這種情況通常都是因爲server端代碼的問題,若是你的服務器上出現大量CLOSE_WAIT,應該要考慮檢查代碼。
TIME_WAIT
根據三次握手斷開鏈接規定,發起socket主動關閉的一方 socket將進入TIME_WAIT狀態。TIME_WAIT狀態將持續2MSL。TIME_WAIT狀態下的socket不能被回收使用。 具體現象是對於一個處理大量短鏈接的服務器,若是是由服務器主動關閉客戶端的鏈接,將致使服務器端存在大量的處於TIME_WAIT狀態的socket, 甚至比處於Established狀態下的socket多的多,嚴重影響服務器的處理能力,甚至耗盡可用的socket,中止服務。TIME_WAIT是TCP協議用以保證被從新分配的socket不會受到以前殘留的延遲重發報文影響的機制,是必要的邏輯保證。和TIME_WAIT狀態有關的系統參數有通常由3個,本機設置以下:
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_fin_timeout,默認60s,減少fin_timeout,減小TIME_WAIT鏈接數量
net.ipv4.tcp_tw_reuse = 1表示開啓重用。容許將TIME-WAIT sockets從新用於新的TCP鏈接,默認爲0,表示關閉;
net.ipv4.tcp_tw_recycle = 1表示開啓TCP鏈接中TIME-WAIT sockets的快速回收,默認爲0,表示關閉。
複製代碼
咱們這裏總結一下這些與tcp狀態的選項:
net.ipv4.tcp_tw_recycle
net.ipv4.tcp_tw_reuse
複製代碼
在實際linux內核參數調優時並不建議開啓。緣由是關於這兩個選項會影響在NAT網絡中的,局域網服務器組之間通訊,而在非NAT網絡中不影響服務端與客戶端的通訊,因此在NAT網絡中不建議開啓。至於緣由,可參見:www.cnxct.com/coping-with…
如何在Java語言中去解析C++的網絡數據包,如何在C++中解析Java的網絡數據包,對於不少人來講是一件很困難的事情,因此只能變着法子使用第三方的庫。其實使用tcpdump工具能夠很容易解決與分析。 首先,咱們須要明確字節序列這樣一個概念,即咱們說的大端編碼(big endian)和小端編碼(little endian),x86和x64系列的cpu使用小端編碼,而數據在網絡上傳輸,以及Java語言中,使用的是大端編碼。那麼這是什麼意思呢? 咱們舉個例子,看一個x64機器上的32位數值在內存中的存儲方式:
i在內存中的地址序列是0x003CF7C4~ 0x003CF7C8,值爲40 e2 01 00。
十六進制0001e240正好等於10進制123456,也就是說小端編碼中權重高的的字節值存儲在內存地址高(地址值較大)的位置,權重值低的字節值存儲在內存地址低(地址值較小)的位置,也就是所謂的高高低低。 相反,大端編碼的規則應該是高低低高,也就是說權值高字節存儲在內存地址低的位置,權值低的字節存儲在內存地址高的位置。 因此,若是咱們一個C++程序的int32值123456不做轉換地傳給Java程序,那麼Java按照大端編碼的形式讀出來的值是:十六進制40E20100 = 十進制1088553216。 因此,咱們要麼在發送方將數據轉換成網絡字節序(大端編碼),要麼在接收端再進行轉換。
下面看一下若是C++端傳送一個以下數據結構,Java端該如何解析(因爲Java中是沒有指針的,也沒法操做內存地址,致使不少人無從下手),下面利用 tcpdump 來解決這個問題的思路。
咱們客戶端發送的數據包:
其結構體定義以下:
利用tcpdump抓到的包以下:
放大一點:
咱們白色標識出來就是咱們收到的數據包。這裏我想說明兩點:
若是咱們知道發送端發送的字節流,再比照接收端收到的字節流,咱們就能檢測數據包的完整性,或者利用這個來排查一些問題;
對於Java程序只要按照這個順序,先利用java.net.Socket的輸出流java.io.DataOutputStream對象readByte、readInt3二、readInt3二、readBytes、readBytes方法依次讀出一個char、int3二、int3二、16個字節的字節數組、63個字節數組便可,爲了還原像int32這樣的整形值,咱們須要作一些小端編碼向大端編碼的轉換。
/sbin/sysctl -p
複製代碼
當客戶端C鏈接服務器S成功後,若是服務器先關閉,客戶端C不關閉,服務器S將處於FIN_WAIT_2狀態,客戶端C處於CLOSE_WAIT狀態,服務器的FIN_WAIT_2狀態將在net.ipv4.tcp_fin_timeout後被回收,默認30秒,在這個期間不會被複用;客戶端C處於CLOSE_WAIT狀態將一直持續到進程結束或者操做系統重啓,不然操做系統不會回收CLOSE_WAIT狀態的鏈接,由於這個錯誤是能夠避免的,其根本緣由就是客戶端沒關閉鏈接致使,應該去檢查你的代碼。 一樣的道理,若是是客戶端C先關閉,服務器S未關閉,則客戶端C處於FIN_WAIT_2狀態,服務器器端處於CLOSE_WAIT狀態,與上面的狀況相似。可是,我這裏須要強調一點是:若是兩個處於相互鏈接狀態的端較遠,當中間的鏈路出現故障(如路由器斷電),且該鏈路是兩端的必經之路,那麼除非發送數據監測,不然兩端的tcp協議棧自己是監測不到這個鏈接斷開的問題,這個時候,咱們須要使用相似於「保活」機制的心跳包來監測,並及時發現這種「死鏈」,關閉套接字或者重連。
每一路鏈接以(源地址,源端口號,目標地址,目標端口號)這樣一個四元組惟一肯定,假設目標地址和目標端口號肯定的狀況下,由源地址+源端口號肯定,源地址通常能夠認爲是固定的,因此如今鏈接數量由可用端口號數量來肯定,這個參數由net.ipv4.ip_local_port_range肯定,默認值32768~61000,大約28000個左右。
當發生網絡故障時,咱們須要除了須要關注機器的內存、磁盤、線程棧等狀態外,還須要關注一下,服務上的鏈接狀態,確認是否存在不正常的tcp三次握手或者四次揮手的中間狀態(如CLOSE_WAIT和TIME_WAIT)狀態,另外就是查看下臨近的防火牆上來往的數據是否正常。在CentOS 7上咱們可使用iptables等命令查看和修改相關防火牆規則。
限於做者水平和經驗有限,文中若是不當的地方,歡迎提出意見。
全文完。
張小方寫於2018年3月29日
參考資料: