全文閱讀大致3分鍾,學習本文可以掌握以下知識:
- netcat、ss、lsof命令的使用
- tcp協議的三次握手和四次揮手
- udp協議的基本表現過程以及icmp報文發送的原因
- tcpdump、nc命令的使用
- 三道關于TCP/IP協議的面試題答案
1、從查看系統端口監聽說起
在平時的開發中,出現listen EADDRINUSE: address already in use :::3000這種錯誤的頻率很高,尤其在windows系統下,殺死個進程都殺不徹底。當遇到這種問題的時候,我們第一反應就是查看系統是哪個進程也在監聽同樣的端口。于是引出了我們要介紹的以下三個命令。
以下三個命令只在類UNI\系統上,系統之間的命令參數有一些細微差異,以系統提示爲准,下面說的都是指在linux系統上*
1.1、netstat
netstat命令提供了一些關于網絡連接的信息,可以用它來羅列所有監聽的TCP端口或UDP端口,以及對應的套接字狀態,如下:
netstat -tunlp
- -t 顯示TCP端口
- -u 顯示UDP端口
- -n 顯示IP地址而不是域名
- -l 只顯示正在監聽的端口
- -p 顯示監聽端口的進程ID
輸出大致如下:
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:27017 0.0.0.0:* LISTEN 1889/mongod
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 786/nginx -g daemon
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 884/sshd
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 786/nginx -g daemon
tcp6 0 0 :::8080 :::* LISTEN 23087/node
tcp6 0 0 :::10000 :::* LISTEN 4988/node
tcp6 0 0 :::80 :::* LISTEN 786/nginx -g daemon
tcp6 0 0 :::8054 :::* LISTEN 11915/node
udp 0 0 172.16.179.237:123 0.0.0.0:* 750/ntpd
udp 0 0 127.0.0.1:123 0.0.0.0:* 750/ntpd
udp 0 0 0.0.0.0:123 0.0.0.0:* 750/ntpd
udp6 0 0 :::123 :::* 750/ntpd
netstat命令如今已經過時了,因爲有新的命令替換-ss。
1.2、ss
ss命令沒有了netstat的一些特性,不過它暴露出更多的TCP狀態並且它更加輕量快速。該命令的選項和netstat大致一樣,所以很容易上手:
ss -tunlp
輸出大致如下:
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
udp UNCONN 0 0 172.16.179.237:123 *:* users:(("ntpd",pid=750,fd=19))
udp UNCONN 0 0 127.0.0.1:123 *:* users:(("ntpd",pid=750,fd=18))
udp UNCONN 0 0 *:123 *:* users:(("ntpd",pid=750,fd=17))
udp UNCONN 0 0 :::123 :::* users:(("ntpd",pid=750,fd=16))
tcp LISTEN 0 128 *:27017 *:* users:(("mongod",pid=1889,fd=7))
tcp LISTEN 0 128 *:80 *:* users:(("nginx",pid=11173,fd=10),("nginx",pid=786,fd=10))
tcp LISTEN 0 128 *:22 *:* users:(("sshd",pid=884,fd=3))
tcp LISTEN 0 128 *:443 *:* users:(("nginx",pid=11173,fd=9),("nginx",pid=786,fd=9))
tcp LISTEN 0 128 :::8080 :::* users:(("node",pid=23087,fd=10))
tcp LISTEN 0 128 :::10000 :::* users:(("node",pid=4988,fd=10))
tcp LISTEN 0 128 :::80 :::* users:(("nginx",pid=11173,fd=11),("nginx",pid=786,fd=11))
tcp LISTEN 0 128 :::8054 :::* users:(("node",pid=11915,fd=12))
1.3、lsof
lsof是一個強大的命令行工具,提供了進程打開的文件的一些信息。因爲在Linux,一切皆文件。所以一個打開的套接字也可以認爲是一個文件。
羅列所有監聽的TCP端口:
lsof -nP -iTCP -sTCP:LISTEN
- -n 不要轉換端口號爲端口名稱
- -p 不要解析域名,顯示其IP地址
- -iTCP -sTCP:LISTEN 顯示TCP狀態爲LISTEN的網絡文件
輸出如下:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 786 root 9u IPv4 13574 0t0 TCP *:443 (LISTEN)
nginx 786 root 10u IPv4 13575 0t0 TCP *:80 (LISTEN)
nginx 786 root 11u IPv6 13576 0t0 TCP *:80 (LISTEN)
sshd 884 root 3u IPv4 14458 0t0 TCP *:22 (LISTEN)
mongod 1889 root 7u IPv4 21178 0t0 TCP *:27017 (LISTEN)
node 4988 root 10u IPv6 40123 0t0 TCP *:10000 (LISTEN)
nginx 11173 www-data 9u IPv4 13574 0t0 TCP *:443 (LISTEN)
nginx 11173 www-data 10u IPv4 13575 0t0 TCP *:80 (LISTEN)
nginx 11173 www-data 11u IPv6 13576 0t0 TCP *:80 (LISTEN)
node 11915 root 12u IPv6 7200966 0t0 TCP *:8054 (LISTEN)
node 23087 root 10u IPv6 5497007 0t0 TCP *:8080 (LISTEN)
查找指定端口可以這樣:lsof -nP -iTCP:8054 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 11915 root 12u IPv6 7200966 0t0 TCP *:8054 (LISTEN)
好了,三個命令介紹到此爲止。這個時候問一下大家一個問題:
上述過濾的狀態都是LISTEN,那麽TCP有多少種狀態?狀態與狀態之間的變化是怎樣的?你能從某個狀態中就能推斷出當前TCP連接處于什麽階段嗎?
這個問題你自己心中有數的話,可以跳過下一小節
2、TCP狀態的轉移
下圖是從wiki上引用的TCP狀態轉移圖:
看著有點複雜,我們將其拆分成最熱門的兩個步驟:三次握手、四次揮手。後面附贈面試題答案哦~
2.1、三次握手
- 客戶端向服務器發送TCP連接請求數據包,客戶端狀態從CLOSED變爲SYN_SENT,其中包含主機A的初始序列號seq(A)=x。(其中報文中同步標志位SYN=1,ACK=0,表示這是一個TCP連接請求數據報文;序號seq=x,表明傳輸數據時的第一個數據字節的序號是x);
- 服務端收到請求後,會發回連接確認數據包。服務端狀態從LISTEN變爲SYN_RECEIVED,(其中確認報文段中,標識位SYN=1,ACK=1,表示這是一個TCP連接響應數據報文,並含服務端的初始序列號seq(B)=y,以及服務端對客戶端初始序列號的確認號ack(B)=seq(A)+1=x+1)
- 客戶端收到服務端的確認報文後,還需作出Ack(此時這個數據包可以攜帶數據報文了),即發送一個序列號seq(A)=x+1;確認號爲ack(A)=y+1的報文,此時客戶端狀態轉爲ESTABLISHED,服務端收到這個ACK後,狀態也轉爲ESTABLISHED;
2.2、面試題:爲什麽需要三次握手?
此題需要從兩個點回答:
- 首要原因是爲了解決客戶端多次發起請求的問題,你想想看,在網絡狀況不好的情況下,客戶端發起一個連接請求沒收到響應的話會繼續發送請求,如果最先發送的請求到服務端了,在用兩次握手的前提下,服務端就會用這個已經過期的請求的序列號建立連接,而客戶端卻認爲這個序列號是過期的,就會忽略掉,這樣雙方造成了很大的誤解。而如果用三次握手的話,客戶端就還有機會告訴服務端你的這個響應是過期的還是正常的,如果是過期的就可以發送RST消息告訴服務端斷掉這個連接,如果不是的話,就返回ACK建立連接。
- 第二個原因是爲了同步雙方的序列號,兩次握手是做不到同步雙方的序列號的。
關于第一個原因可以參考下圖(截圖自RFC793的3.4節):
2.3、四次揮手
- 第一次揮手(FIN=1,seq=x) 假設客戶端想要關閉連接,客戶端發送一個FIN標志位置爲1的包,表示自己已經沒有數據可以發送了,但是仍然可以接受數據。發送完畢後,客戶端進入FIN_WAIT_1狀態
- 第二次揮手(ACK=1,ACKnum=x+1) 服務器端確認客戶端的FIN包,發送一個確認包,表明自己接受到了客戶端關閉連接的請求,但還沒有准備好關閉連接。 發送完畢後,服務器端進入CLOSE_WAIT狀態,客戶端接收到這個確認包之後,進入FIN_WAIT_2狀態,等待服務器端關閉連接。
- 第三次揮手(FIN=1,seq=y) 服務器端准備好關閉連接時,向客戶端發送結束連接請求,FIN置爲1。發送完畢後,服務器端進入LAST_ACK狀態,等待來自客戶端的最後一個ACK。
- 第四次揮手(ACK=1,ACKnum=y+1) 客戶端接收到來自服務器端的關閉請求,發送一個確認包,並進入TIME_WAIT狀態,等待可能出現的要求重傳的ACK包。 服務器端接收到這個確認包之後,關閉連接,進入CLOSED狀態。 客戶端等待了某個固定時間(兩個最大段生命周期,2MSL,2 Maximum Segment Lifetime)之後,沒有收到服務器端的ACK,認爲服務器端已經正常關閉連接,于是自己也關閉連接,進入CLOSED狀態。
爲什麽是2MSL?因爲TCP/IP協議規定了超過這個時間的數據包都是會被廢棄掉的,也就是一個數據包在網絡中存活的最大時間
2.4、面試題:爲什麽需要四次揮手?
答:第二次和第三次無法整合起來變成三次揮手是因爲服務端接收到FIN報文之後,手上可能還有數據需要發送給客戶端,所以ACK和FIN不能同時發送。
3、UDP協議探析
探究UDP我們使用netcat這個工具,我們先用netcat來新建一個UDP服務器:
nc -u -l 0.0.0.0 3000
然後使用nc來新建一個客戶端:
nc -u -p 3001 localhost 3000
- -u 指定udp協議
- -l 指定監聽的端口和ip
- -p 指定客戶端的源端口
我們還需要使用tcpdump工具來dump數據包,或者可以使用wireshark來抓包:
─$ sudo tcpdump -ni lo0 'udp port 3001 or icmp' 1 ↵
Password:
tcpdump: data link type PKTAP
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on pktap, link-type PKTAP (Apple DLT_PKTAP), capture size 262144 bytes
- -n 不用將Ip地址解析爲域名
- -i 指定抓包的網卡,我們這裏指定抓的是回環口
因爲UDP是無連接的,所以這會看不到任何的數據包
但是真的完全沒有“連接”嗎?其實並不是完全正確的,至少在客戶端這邊有這麽一個連接存在,
我們使用上面提到的命令lsof:
╰─$ lsof -nP -iUDP | grep 3000
nc 60738 linxiaowu 3u IPv4 0x8ea59d14b13d38bf 0t0 UDP *:3000
nc 60744 linxiaowu 6u IPv4 0x8ea59d14b13d208f 0t0 UDP 127.0.0.1:3001->127.0.0.1:3000
從上面可以看出,客戶端已經有了連接的概念,服務端還沒有意識有這麽一個連接存在。接著我們從客戶端發送一條消息:hi,此時我們再使用lsof可以看到服務端也有此連接了:
❯ lsof -nP -iUDP | grep 3000
nc 60738 linxiaowu 3u IPv4 0x8ea59d14b13d38bf 0t0 UDP 127.0.0.1:3000->127.0.0.1:3001
nc 60744 linxiaowu 6u IPv4 0x8ea59d14b13d208f 0t0 UDP 127.0.0.1:3001->127.0.0.1:3000
所以從這裏可以看到UDP的連接完全建立是在第一個數據包發送之後。tcpdump可以看到數據包:
17:18:17.419352 IP 127.0.0.1.3001 > 127.0.0.1.3000: UDP, length 3
這個時候我們關掉服務器,如果是TCP,那麽會有一系列的協商報文發送出去,而UDP就不會,再看端口:
lsof -nP -iUDP | grep 3000
nc 60744 linxiaowu 6u IPv4 0x8ea59d14b13d208f 0t0 UDP 127.0.0.1:3001->127.0.0.1:3000
客戶端此時並不知道服務器down掉了,接著我們從客戶端發送消息hi?,此時netcat命令會自動退出,這個時候,它才知道連接斷開了,並且我們發現有個ICMP報文從服務端發送出來:
ICMP報文提示端口不可達,也就是服務端的端口關掉監聽了。
根據TCP/IP協議的規定,如果對應的服務不可用,那麽系統內核根據協議類型發送對應的響應報文,對于UDP應該發送一個“端口不可達”的ICMP報文,對于TCP應該發送一個TCP RST消息
所以UDP的連接斷開會延遲到其中一方發送報文收到端口不可達的時候:
17:22:05.710012 IP 127.0.0.1.3001 > 127.0.0.1.3000: UDP, length 4
17:22:05.710047 IP 127.0.0.1 > 127.0.0.1: ICMP 127.0.0.1 udp port 3000 unreachable, length 36
3.1、面試題:爲什麽DNS使用UDP協議?
這個問題其實是個僞命題。使用udp協議是以前舊有的規範定義的,現在的RFC是將TCP協議也一起寫進去的。因爲以前的網絡帶寬不高,使用UDP協議會比TCP協議的數據包小很多,並且以前的DNS包體一般都很小,很少超過512字節的,但是現在的DNS支持Ipv6、https,包體也變大了,這個時候如果還是使用udp協議,很容易因爲mtu之類的限制導致傳輸失敗,因爲tcp可以分包傳輸,所以對于大的包體,就大部分都是使用tcp協議。
原文:豆米播客
喜歡小編的可以點個贊關注小編哦,小編每天都會給大家分享文章。
我自己是一名從事了多年的前端老程序員,小編爲大家准備了新出的前端編程學習資料,免費分享給大家!
如果你也想學習前端,那麽幫忙轉發一下然後再關注小編後私信【1】可以得到我整理的這些前端資料了(私信方法:點擊我頭像進我主頁有個上面有個私信按鈕)