做網(wǎng)站頁面該建多大的畫布以圖搜圖百度識圖
? ? ? ? 上一節(jié),我們學(xué)習(xí)了TCP協(xié)議的服務(wù)器-客戶端的編程流程以及對中間的過程進(jìn)行了詳細(xì)的討論,那么,這一節(jié),我們對于TCP協(xié)議的特點進(jìn)行進(jìn)一步的分析,這也是面試的重點和難點。
目錄
一、TCP 協(xié)議特點
1.1??連接的建立與斷開
1.1.1 面試題
1.2? TCP 狀態(tài)轉(zhuǎn)移(面試題)
1.3?流式服務(wù)特點
1.4?應(yīng)答確認(rèn)與超時重傳
1.5?滑動窗口
二、多進(jìn)程、多線程處理并發(fā)?
三、UDP協(xié)議
3.1 UDP協(xié)議編程流程
3.2 UDP 協(xié)議特點
3.3 應(yīng)用場景
四、面試題
4.1 TCP和UDP的區(qū)別
4.2 同一個端口可不可以被一個 TCP 和一個 UDP 的應(yīng)用程序同時使用?
4.3 同一個應(yīng)用程序可以創(chuàng)建多個套接字嗎?
一、TCP 協(xié)議特點
? ? ? ?通過前面的學(xué)習(xí),我們知道:TCP 協(xié)議提供的是:面向連接、可靠的、字節(jié)流服務(wù)。
1.1??連接的建立與斷開
? ? ? ? 使用 TCP 協(xié)議通信的雙發(fā)必須先建立連接(三次握手),然后才能開始數(shù)據(jù)的讀寫。雙方都必須為該連接分配必要的內(nèi)核資源,以管理連接的狀態(tài)和連接上數(shù)據(jù)的傳輸。TCP 連接是全雙工的,雙方的數(shù)據(jù)可以通過一個連接進(jìn)行讀寫。完成數(shù)據(jù)交換之后,通信雙方都必須斷開連接以釋放系統(tǒng)資源(四次揮手)。 使用 tcpdump?抓包命令可以抓包觀察 TCP 連接的建立與關(guān)閉。該命令需要管理員權(quán)限,格式如下(假設(shè)兩個測試用的主機 IP 地址為 192.168.43.214 和 192.168.43.160 ) :
三次握手發(fā)生在客戶端執(zhí)行 connect()的時候,該方法返回成功,則說明三次握手已經(jīng)建 立。三次握手示例圖如下:
客戶端執(zhí)行connect()會給服務(wù)器發(fā)送一個tcp報文,此時SYN標(biāo)志有效,還會發(fā)送一個序列號;服務(wù)器收到報文,會發(fā)送報文回復(fù),此時SYN有效,發(fā)送一個序列號,還回復(fù)會一個確認(rèn)號是客服端發(fā)送的序列號+1;客服端收到服務(wù)器的回復(fù),也會再次回復(fù)服務(wù)器,此時會發(fā)送確認(rèn)號是剛剛客戶端發(fā)送的序列號+1? ;
四次揮手發(fā)生在客戶端或服務(wù)端執(zhí)行 close()關(guān)閉連接的時候,示例圖如下:
當(dāng)一端要進(jìn)行close(),會給對方發(fā)送一個報文,此時FIN標(biāo)志有效,還有一個序列號,然后對方收到報文,會回復(fù)對方已經(jīng)收到了,發(fā)送一個確認(rèn)號ACK,是剛剛發(fā)送的序列號+1;然后另一端也要close()關(guān)閉,也會給對方發(fā)送報文告訴對方字節(jié)要關(guān)閉了,FIN 序列號,對方收到報文了,會回復(fù)對方已經(jīng)收到了,也發(fā)送一個確認(rèn)號ACK,確認(rèn)號是剛剛發(fā)送的序列號+1。
1.1.1 面試題
1、四次揮手的過程可以用三次完成嗎??
可以,四次揮手可以演化成三次揮手 。
? ? ? ? 當(dāng)一端close 發(fā)送報文過來,此時我也要close了,回復(fù)報文,和通知對方關(guān)閉的報文一起發(fā)送。
- 第一次揮手(FIN): 客戶端發(fā)送一個FIN報文,表示它要關(guān)閉到服務(wù)器的數(shù)據(jù)傳送。
- 第二次揮手(FIN): 服務(wù)器收到FIN后,直接發(fā)送一個FIN報文,表示它也要關(guān)閉到客戶端的數(shù)據(jù)傳送。
- 第三次揮手(ACK): 客戶端收到FIN后,發(fā)送一個ACK報文,確認(rèn)收到關(guān)閉請求,連接關(guān)閉。
2、揮手時,可能受到什么樣的攻擊?
FIN Flood 攻擊:
- 原理:攻擊者發(fā)送大量的FIN包到目標(biāo)服務(wù)器。這些包會讓服務(wù)器嘗試關(guān)閉大量的連接,耗費資源處理這些無效的連接終止請求。
- 影響:服務(wù)器資源被耗盡,可能導(dǎo)致拒絕服務(wù)(DoS攻擊)。
3、為什么是三次握手,可不可以是兩次為什么?
? ? ? 握手只能是三次:例如客戶端連接服務(wù)器后然后關(guān)閉了,服務(wù)器收到了并回復(fù)客戶端,此時服務(wù)器就認(rèn)為和客戶端建立了鏈接,這個鏈接就一直保持著,但是客戶端已經(jīng)沒了,所以還需要客戶端第三次進(jìn)行確認(rèn)回復(fù),來確保雙方都保持鏈接。
4、三次握手時可能出現(xiàn)什么攻擊?
SYN Flood 攻擊:
- 原理:攻擊者發(fā)送大量的SYN請求包到目標(biāo)服務(wù)器,但不完成后續(xù)的握手步驟(即不發(fā)送ACK包)。目標(biāo)服務(wù)器會為每個SYN請求分配資源并等待ACK回應(yīng),這樣會導(dǎo)致服務(wù)器資源耗盡,無法處理合法用戶的請求。(當(dāng)有一個鏈接進(jìn)來就會先放到未完成三次握手隊列中,如果在短時間內(nèi)有人連續(xù)發(fā)送鏈接就會把未完成三次握手隊列塞滿,使真正要進(jìn)行鏈接的客戶端連接不上。)
- 影響:造成服務(wù)器拒絕服務(wù)(DoS攻擊)。
SYN ACK Flood 攻擊:
- 原理:攻擊者在沒有發(fā)送初始SYN包的情況下,發(fā)送大量的SYN-ACK包到目標(biāo)服務(wù)器。服務(wù)器會浪費資源去回復(fù)ACK,等待建立連接,導(dǎo)致資源耗盡。
- 影響:和SYN Flood類似,可能導(dǎo)致拒絕服務(wù)。
1.2? TCP 狀態(tài)轉(zhuǎn)移(面試題)
? ? ? 下圖是 TCP 連接從建立到關(guān)閉整個過程中通信兩端狀態(tài)的變化。tcp狀態(tài)的改變是在建立連接和斷開連接的基礎(chǔ)上的 ,其中 CLOSED 是假想的起始點,并不是一個實際的狀態(tài)。這種狀態(tài)變化就好比我們打電話通話處于不同的狀態(tài),但是只要雙方撥通了電話,那么就一直是通話中。只有在撥打電話和掛斷電話時,狀態(tài)會發(fā)生變化。
? ? ? ? 上圖中,TIME_WAIT 狀態(tài)一般情況下是主動關(guān)閉的一端才會出現(xiàn)的狀態(tài)。該狀態(tài)出現(xiàn)后,會維持一段長為 2MSL(Maximum Segment Life)的時間,才能完全關(guān)閉。MSL 是 TCP 報文 段在網(wǎng)絡(luò)中的最大生存時間,標(biāo)準(zhǔn)文檔 RFC1122 的建議值是 2min。?在 Linux 系統(tǒng)上,一個 TCP 端口不能被同時打開多次(兩次及以上)。當(dāng)一個 TCP 連接 處于 TIME_WAIT 狀態(tài)時,我們將無法立即使用該連接占用著的端口來建立一個新連接,必須要等待這兩分鐘過去,才能繼續(xù)使用這個端口。
雙方同時關(guān)閉:
? ? ?雙方都執(zhí)行close(),都像對方發(fā)送FIN,雙發(fā)都變成FIN_WAIT_1狀態(tài),等到雙方都收到各自都收到對方發(fā)出的FIN,并發(fā)出ACK之后就會變成CLOSING狀態(tài),在等到雙方都收到對方的ACK之后就會變成TIME_WAIT狀態(tài)。
四次揮手演化成三次揮手:
? ? ? ? 主動關(guān)閉端執(zhí)行close(),發(fā)FIN,被動關(guān)閉端收到FIN,但此時被動關(guān)閉端也要關(guān)閉了,就把ACK和FIN一起發(fā)送給主動關(guān)閉端
connect()三次握手:
? ? ? 客戶端執(zhí)行connect()后進(jìn)行第一次進(jìn)行握手發(fā)出SYN狀態(tài)就變成了SYN_SENT狀態(tài),這個狀態(tài)非常短暫,會觀察不到,瞬間就沒了。
? ? ? 服務(wù)器收到SYN后,又給客戶端發(fā)出SYN,ACK后變成,SYN_RCVD狀態(tài)。
服務(wù)器和客戶端都完成三次握手狀態(tài)就會變成,ESTABLISHED。
close()四次揮手:
? ? ? ? 無論哪一方主動執(zhí)行close()端,先發(fā)送FIN,然后主動關(guān)閉端就會變成FIN_WAIT_1狀態(tài),然后對方收到FIN,再發(fā)ACK就會變成CLOSE_WAIT狀態(tài),主動關(guān)閉端收到對方的回復(fù),就變成了FIN_WAIT_2狀態(tài)。此時兩次揮手結(jié)束。被動關(guān)閉端執(zhí)行close(),會給主動關(guān)閉端發(fā)送FIN,會變成LAST_ACK狀態(tài),主動關(guān)閉端收到FIN并發(fā)送ACK,主動關(guān)閉端狀態(tài)就變成了TIME_WAIT,然后被動關(guān)閉段收到ACK,然后就消失了。
? ? ? ?TIME_WAIT會持續(xù)大概兩分鐘的時間。
如上圖所示:服務(wù)器會跟很多客戶端有連接,每個連接都有自己的狀態(tài)。每一個連接都會有自己的接收緩沖區(qū)和發(fā)送緩沖區(qū)。
使用命令:netstat -natp可以查看連接的狀態(tài)
面試題:
為什么TIME_WAIT狀態(tài)要持續(xù)一段時間?
1.可靠地終止TCP的連接。
2.保證讓遲來的TCP報文段有足夠的時間被識別并丟棄。
- 被動關(guān)閉端關(guān)閉發(fā)FIN,主動關(guān)閉端收到FIN,發(fā)ACK,變成TIME_WAIT,有可能被動關(guān)閉端沒收到這個ACK,這個ACK在路上丟失了,過一會被動關(guān)閉端沒收到主動關(guān)閉端的ACK就會再次發(fā)FIN,如果TIME_WAIT狀態(tài)不持續(xù)直接關(guān)閉,那最后假如ACK丟失,被動關(guān)閉端在發(fā)送FIN就沒人管它了。
- 在通訊的過程中,有一些數(shù)據(jù)正在發(fā)送,但還沒發(fā)送到,數(shù)據(jù)正在從A端到B端但還沒到,此時斷開接收端和發(fā)送端的連接,之后這個延遲的數(shù)據(jù)包到達(dá)了,但此時連接已經(jīng)斷開了,就會出現(xiàn)一些問題尤其是服務(wù)器。如果沒有TIME_WAIT狀態(tài),我們就可以立刻重新啟動服務(wù)端,這樣延遲的數(shù)據(jù)包就會陸陸續(xù)續(xù)發(fā)到我們這個新啟動的服務(wù)器里,雖然我們新啟動的服務(wù)器用的是這個ip這個端口,延遲的數(shù)據(jù)包用的也是這個ip和端口,但是這些數(shù)據(jù)包是發(fā)給上個已經(jīng)結(jié)束的進(jìn)程的,不是發(fā)給我們這個新進(jìn)程的。因此就會讓TIME_WAIT狀態(tài)等待大概2分鐘,這倆分鐘是一個報文生存期最長時間的倆倍,這樣就會把我們網(wǎng)絡(luò)中延遲的數(shù)據(jù)包耗死,我們把這些延遲的數(shù)據(jù)一收延后丟掉,倆分鐘后網(wǎng)絡(luò)中就干凈了。
題目:
? ? ? ? 一個局域網(wǎng)內(nèi),有一個客戶端一個服務(wù)器,他們都已完成三次握手狀態(tài),沒有發(fā)送數(shù)據(jù),此時拔掉網(wǎng)線,服務(wù)器再close(),重新運行服務(wù)器,運行之后在插上網(wǎng)線,問此時客戶端跟服務(wù)器的狀態(tài)。
? ? ? ?網(wǎng)線拔掉之后,不進(jìn)行收發(fā)送數(shù)據(jù),雙方是不知道的,由于拔掉網(wǎng)線,關(guān)閉服務(wù)器,服務(wù)器會發(fā)送FIN,但是客戶端收不到,也不會回復(fù),服務(wù)器就等了倆分鐘后就關(guān)閉了,再重新啟動服務(wù)器,此時服務(wù)器就是LISTEN狀態(tài)等待連接,客戶端還是完成三次握手狀態(tài)。
1.3?流式服務(wù)特點
? ? TCP 字節(jié)流的特點,發(fā)送端執(zhí)行的寫操作次數(shù)和接收端執(zhí)行的讀操作次數(shù)之間沒有任何數(shù)量關(guān)系,應(yīng)用程序?qū)?shù)據(jù)的發(fā)送和接收是沒有邊界限制的。多次發(fā)送的數(shù)據(jù)會被對方一次接受,或者一次發(fā)送的數(shù)據(jù)被對方,分多次接受。
netstat -natp命令?可查看端口是否被占用 也能查看接收緩沖區(qū)和發(fā)送緩沖區(qū)有多少數(shù)據(jù)
- bind()會失敗的原因 :端口被占用或者把這個程序運行了,又運行了一個,端口已經(jīng)分給第一個運行的程序。
- recv()返回值為0是唯一判斷對方客戶端關(guān)閉鏈接的條件。
- connect()鏈接失敗原因:沒有運行服務(wù)器,客戶端連接就會失敗。網(wǎng)斷了也鏈接不上。
修改循環(huán)收發(fā)的服務(wù)器端的代碼如下:
char buff[128]={0};
recv(sockfd,buff,1,0);
/*一次只接收一個字符*/
? ? ? ?客戶端發(fā)個hello,服務(wù)器將接收字符個數(shù)改成1,出現(xiàn)的結(jié)果是:循環(huán)5次把hello打印完,直到把buff里的數(shù)據(jù)打印完??头四抢飼淮问盏?個ok。
? ? ? ? 這是因為服務(wù)器和客戶端都有一個接收緩沖區(qū)和發(fā)送緩沖區(qū),一端send()發(fā)送數(shù)據(jù),先把數(shù)據(jù)寫到發(fā)送緩沖區(qū)里,再通過底層協(xié)議把發(fā)送緩沖區(qū)的數(shù)據(jù)挪到對方的接受緩沖區(qū)中,然后對方再通過recv()把接收緩沖區(qū)中的數(shù)據(jù)讀出來。recv()發(fā)送成功只能說明成功將數(shù)據(jù)發(fā)達(dá)發(fā)送緩沖區(qū),對方并沒有收到。有可能會多次從接收緩沖區(qū)一次讀取,也有可能分多次讀取,就像我們購物從菜鳥驛站取快遞,我們?nèi)〕龅目爝f件也可能一次取完,也有可能還沒到菜鳥驛站,我們就需要分多次取。這就是TCP 粘包:連續(xù)多次send()發(fā)送的數(shù)據(jù),被對方recv()一次性收到。發(fā)送數(shù)據(jù)的次數(shù)跟接收數(shù)據(jù)的次數(shù)是不對應(yīng)的。所以會出現(xiàn)粘包。如何解決呢?(面試題)
解決粘包問題的常見方法有以下幾種:(面試題)
1. 使用定長消息
? ? ? ? ?通過規(guī)定每條消息的長度,接收方可以按照固定長度讀取數(shù)據(jù)。例如,如果消息長度固定為100字節(jié),接收方每次讀取100字節(jié)的數(shù)據(jù),就可以避免粘包問題。
2. 使用特殊分隔符
? ? ? ? 在每條消息的末尾添加特定的分隔符(如換行符、特殊字符等),接收方可以通過檢測分隔符來區(qū)分消息邊界。
3. 使用消息頭(長度前綴)
? ? ? ?在每條消息前添加一個消息頭,用于存儲消息的長度,接收方先讀取消息頭中的長度信息,再根據(jù)長度讀取具體的消息內(nèi)容。
1.4?應(yīng)答確認(rèn)與超時重傳
? ? ? ?TCP 發(fā)送的報文段是交給 IP 層傳送的。但 IP 層只能提供盡最大努力的服務(wù),也就是說,TCP 下面的網(wǎng)絡(luò)所提供的是不可靠的傳輸。因此,TCP 必須采用適當(dāng)?shù)拇胧┎拍苁箖蓚€運輸層之間的通信變得可靠。TCP 的可靠傳輸是通過使用應(yīng)答確認(rèn)和超時重傳來完成。下圖是通過 netstat 命令抓包看到的信息:
面試題:?
tcp的可靠性體現(xiàn)在:應(yīng)答確認(rèn)、超時重傳、去重、亂序重排、進(jìn)行流量控制滑動窗口
- 應(yīng)答確認(rèn):給對方send()發(fā)送一個數(shù)據(jù),對方收到了,在底層會回復(fù)發(fā)送方表明收到數(shù)據(jù)了,A端給B端發(fā)送數(shù)據(jù),表面只能看到倆次交互,實際有四次,另外兩次我們看不到,但可以用tcpdump抓包命令看到。
- 超時重傳:給對方發(fā)送數(shù)據(jù)收,等了一段事件后沒有收到對方的回復(fù),就認(rèn)為這個數(shù)據(jù)丟失了,就會再重新發(fā)送一份數(shù)據(jù)給對方。
- 去重:給對方發(fā)送數(shù)據(jù),對方收到了,回復(fù)確認(rèn)收到信息,但回復(fù)這個信息丟失了,發(fā)送段沒收到,就會認(rèn)為發(fā)送的數(shù)據(jù)在路上丟失了,就會重新發(fā),然后接收端就會有倆個一樣的數(shù)據(jù),重復(fù)了就會去重。
- 亂序重排:后發(fā)送的數(shù)據(jù)比先發(fā)送的數(shù)據(jù)先到達(dá),這樣順序就會亂,但在接收到數(shù)據(jù)后,會對數(shù)據(jù)的順序進(jìn)行檢查。
- 滑動窗口:?給對方發(fā)送數(shù)據(jù),一個字節(jié)一個字節(jié)發(fā)效率不高,就會有一個窗口,窗口左邊是已發(fā)送對方回復(fù)確認(rèn)的數(shù)據(jù),窗口內(nèi)是有已發(fā)送未收到確認(rèn)的和未發(fā)送的數(shù)據(jù),窗口右邊是超過窗口范圍內(nèi)外就不能發(fā)送的,窗口內(nèi)比如能夠發(fā)送100字節(jié),我們20字節(jié)一個包,這樣發(fā)送,發(fā)送20字節(jié),沒收到對方回復(fù),我們還能繼續(xù)發(fā)送,直到把這滑動窗口內(nèi)的100字節(jié)數(shù)據(jù)全部發(fā)送完了,還沒收到對方回復(fù)收到的信號,就不能再發(fā)送了,如果前面20字節(jié),對方回復(fù)收到了,這個窗口就向后移動,確保窗口內(nèi)數(shù)據(jù)有100個字節(jié),然后新到窗口內(nèi)的數(shù)據(jù)就能發(fā)送了。因為如果你光發(fā)送數(shù)據(jù),也不知道對方收沒收就到一直發(fā);或者就是對方一次性只能接受多少數(shù)據(jù),發(fā)太多也沒用。
下圖是無差錯時,數(shù)據(jù)交互的流程:發(fā)送端發(fā)送數(shù)據(jù) m1 給接收端,接收端收到數(shù)據(jù)后會給發(fā)送端一個確認(rèn)信息,以表明數(shù)據(jù)已經(jīng)被成功收到。在發(fā)送方未收到確認(rèn)信息前,M1 應(yīng)繼續(xù)被保留,直到確認(rèn)信息到達(dá)才能丟棄。
下圖是出現(xiàn)差錯時,數(shù)據(jù)交互的流程:
1.5?滑動窗口
? ? ? ?TCP 協(xié)議是利用滑動窗口實現(xiàn)流量控制的。一般來說,我們總是希望數(shù)據(jù)傳輸?shù)酶煲恍?#xff0c;不會一次只發(fā)一個字節(jié)。但是如果發(fā)送方把數(shù)據(jù)發(fā)得過快,接受方就可能來不及接收, 這就會造成數(shù)據(jù)的丟失。所謂流量控制就是讓發(fā)送方的發(fā)送速率不要太快,要讓接收方來的及接收。在 TCP 的報頭中有一個字段叫做接收通告窗口,這個字段由接收端填充,是接收端告訴發(fā)送端自己還有多少緩沖區(qū)可以接收數(shù)據(jù)。于是發(fā)送端就可以根據(jù)這個接收端的處理能力來發(fā)送數(shù)據(jù),而不會導(dǎo)致接收端處理不過來。所以發(fā)送端就會有一個發(fā)送窗口,這個發(fā)送窗口的大小是由接收端填充的接收通告窗口的大小決定的,并且窗口的位置會隨著發(fā)送端數(shù)據(jù)的發(fā)送和接收到接收端對數(shù)據(jù)的確認(rèn)而不斷的向右滑動,將之稱為滑動窗口。發(fā)送方的滑動窗口示意圖如下:
當(dāng)收到 36 的 ack,并發(fā)出 46-51 的字節(jié)后,窗口滑動的示意圖如下:
二、多進(jìn)程、多線程處理并發(fā)?
? ? ? 如下圖所示, 當(dāng)一個客戶端與服務(wù)器建立連接以后,服務(wù)器端 accept()返回,進(jìn)而準(zhǔn)備循環(huán)接收客戶端發(fā)過來的數(shù)據(jù)。如果客戶端暫時沒發(fā)數(shù)據(jù),服務(wù)端會在第 40 行的 recv()阻 塞。此時,其他客戶端向服務(wù)器發(fā)起連接后,由于服務(wù)器阻塞了,無法執(zhí)行 accept()接受連 接,也就是其他客戶端發(fā)送的數(shù)據(jù),服務(wù)器無法讀取。服務(wù)器也就無法并發(fā)同時處理多個客戶端。?
? ? ? 這個問題可以通過引入多線程和多進(jìn)程來解決。服務(wù)端接受一個客戶端的連接后,創(chuàng)建 一個線程或者進(jìn)程,然后在新創(chuàng)建的線程或進(jìn)程中循環(huán)處理數(shù)據(jù)。主線程(父進(jìn)程)只負(fù)責(zé)監(jiān)聽客戶端的連接,并使用 accept()接受連接,不進(jìn)行數(shù)據(jù)的處理。如下圖所示:
多線程處理并發(fā)的服務(wù)器端示例代碼 MultiThread.c 如下:主線程負(fù)責(zé)監(jiān)聽端口和接受客戶端連接,每接受到一個客戶端連接后,就創(chuàng)建一個新線程來處理該客戶端的通信。每個子線程會循環(huán)接收客戶端發(fā)送的數(shù)據(jù),并回復(fù)一個確認(rèn)消息"ok"。當(dāng)客戶端斷開連接時,子線程會關(guān)閉相應(yīng)的客戶端套接字并退出。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>// 線程函數(shù),用來處理單個客戶端的收發(fā)數(shù)據(jù)
void* fun(void * arg)
{int c = (int)arg; // 將傳入的參數(shù)轉(zhuǎn)換為整數(shù)類型的客戶端套接字描述符while( 1 ){char buff[128] = {0}; // 用于接收數(shù)據(jù)的緩沖區(qū)// 接收客戶端發(fā)送的數(shù)據(jù),如果接收失敗或連接關(guān)閉,則退出循環(huán)if ( recv(c, buff, 127, 0) <= 0 ){break;}printf("recv(%d)=%s\n", c, buff); // 打印接收到的數(shù)據(jù)send(c, "ok", 2, 0); // 發(fā)送確認(rèn)消息給客戶端}printf("one client over(%d)\n", c); // 打印客戶端連接結(jié)束的消息close(c); // 關(guān)閉客戶端連接
}int main()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 創(chuàng)建套接字assert(sockfd != -1); // 確認(rèn)套接字創(chuàng)建成功struct sockaddr_in saddr, caddr; // 定義服務(wù)器和客戶端的地址結(jié)構(gòu)memset(&saddr, 0, sizeof(saddr)); // 將服務(wù)器地址結(jié)構(gòu)清零saddr.sin_family = AF_INET; // 設(shè)置地址族為AF_INETsaddr.sin_port = htons(6000); // 設(shè)置端口號為6000,并轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 設(shè)置IP地址為127.0.0.1int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 綁定套接字到指定的IP地址和端口assert(res != -1); // 確認(rèn)綁定成功listen(sockfd, 5); // 開始監(jiān)聽,最大連接數(shù)為5while( 1 ){int len = sizeof(caddr); // 客戶端地址結(jié)構(gòu)長度// 接受客戶端連接請求,返回客戶端套接字描述符int c = accept(sockfd, (struct sockaddr*)&caddr, &len);if ( c < 0 ){continue; // 如果接受失敗,繼續(xù)等待下一個連接}printf("accept c = %d\n", c); // 打印接受到的客戶端套接字描述符pthread_t id; // 定義線程id// 創(chuàng)建子線程處理客戶端連接,傳入客戶端套接字描述符作為參數(shù)pthread_create(&id, NULL, fun, (void*)c);}close(sockfd); // 關(guān)閉服務(wù)器套接字exit(0); // 退出程序
}
?多進(jìn)程處理并發(fā)的服務(wù)器端示例代碼 MultiProcess.c 如下:主進(jìn)程負(fù)責(zé)監(jiān)聽端口和接受客戶端連接,每接受到一個客戶端連接后,創(chuàng)建一個子進(jìn)程來處理該客戶端的通信。子進(jìn)程會循環(huán)接收客戶端發(fā)送的數(shù)據(jù),并回復(fù)一個確認(rèn)消息"OK"。當(dāng)客戶端斷開連接時,子進(jìn)程會關(guān)閉相應(yīng)的客戶端套接字并退出。主進(jìn)程通過捕捉SIGCHLD信號來處理子進(jìn)程退出,防止產(chǎn)生僵尸進(jìn)程。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>// 處理客戶端連接的函數(shù)
void DealClientLink(int c, struct sockaddr_in caddr)
{while (1){char buff[128] = {0}; // 用于接收數(shù)據(jù)的緩沖區(qū)int n = recv(c, buff, 127, 0); // 接收客戶端發(fā)送的數(shù)據(jù)if (n <= 0) // 如果接收失敗或客戶端關(guān)閉連接,則退出循環(huán){break;}// 打印客戶端發(fā)送的數(shù)據(jù),包括客戶端的IP地址和端口號printf("%s:%d %s\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port), buff);send(c, "OK", 2, 0); // 發(fā)送確認(rèn)消息給客戶端}printf("one client unlink\n"); // 打印客戶端斷開連接的消息close(c); // 關(guān)閉客戶端連接
}// 信號處理函數(shù),用于處理子進(jìn)程退出時的SIGCHLD信號
void sigfun(int sign)
{wait(NULL); // 等待子進(jìn)程結(jié)束,防止僵尸進(jìn)程
}int main()
{signal(SIGCHLD, sigfun); // 注冊SIGCHLD信號處理函數(shù),處理僵尸進(jìn)程int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 創(chuàng)建套接字assert(-1 != sockfd); // 確認(rèn)套接字創(chuàng)建成功struct sockaddr_in saddr; // 定義服務(wù)器的地址結(jié)構(gòu)memset(&saddr, 0, sizeof(saddr)); // 將服務(wù)器地址結(jié)構(gòu)清零saddr.sin_family = AF_INET; // 設(shè)置地址族為AF_INETsaddr.sin_port = htons(6000); // 設(shè)置端口號為6000,并轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 設(shè)置IP地址為127.0.0.1int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 綁定套接字到指定的IP地址和端口assert(-1 != res); // 確認(rèn)綁定成功listen(sockfd, 5); // 開始監(jiān)聽,最大連接數(shù)為5while (1){struct sockaddr_in caddr; // 定義客戶端的地址結(jié)構(gòu)int len = sizeof(caddr); // 客戶端地址結(jié)構(gòu)長度int c = accept(sockfd, (struct sockaddr*)&caddr, &len); // 接受客戶端連接請求,返回客戶端套接字描述符assert(-1 != c); // 確認(rèn)接受成功// 打印接受到的客戶端連接成功的消息,包括客戶端的IP地址和端口號printf("%s:%d Link Success\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));pid_t pid = fork(); // 創(chuàng)建子進(jìn)程assert(-1 != pid); // 確認(rèn)子進(jìn)程創(chuàng)建成功if (0 == pid){DealClientLink(c, caddr); // 子進(jìn)程處理客戶端連接exit(0); // 必須結(jié)束子進(jìn)程,否則會有多個進(jìn)程調(diào)用accept}else{close(c); // 父進(jìn)程關(guān)閉客戶端連接描述符}}close(sockfd); // 關(guān)閉服務(wù)器套接字exit(0); // 退出程序
}
客戶端代碼 TcpClient.c 如下:客戶端首先創(chuàng)建一個套接字,然后連接到指定IP地址和端口號的服務(wù)器。連接成功后,客戶端進(jìn)入一個循環(huán),從標(biāo)準(zhǔn)輸入獲取用戶輸入的數(shù)據(jù),并將其發(fā)送到服務(wù)器。隨后,客戶端接收服務(wù)器的響應(yīng)并打印出來。如果用戶輸入"end",客戶端會退出循環(huán),關(guān)閉套接字并結(jié)束程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 創(chuàng)建套接字assert(sockfd != -1); // 確認(rèn)套接字創(chuàng)建成功struct sockaddr_in saddr; // 定義服務(wù)器的地址結(jié)構(gòu)memset(&saddr, 0, sizeof(saddr)); // 將服務(wù)器地址結(jié)構(gòu)清零saddr.sin_family = AF_INET; // 設(shè)置地址族為AF_INETsaddr.sin_port = htons(6000); // 設(shè)置端口號為6000,并轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 設(shè)置IP地址為127.0.0.1int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 連接到服務(wù)器assert(res != -1); // 確認(rèn)連接成功while (1){char buff[128] = {0}; // 用于存儲用戶輸入的緩沖區(qū)printf("input:\n"); // 提示用戶輸入fgets(buff, 128, stdin); // 從標(biāo)準(zhǔn)輸入獲取用戶輸入if (strncmp(buff, "end", 3) == 0) // 如果用戶輸入"end",則退出循環(huán){break;}send(sockfd, buff, strlen(buff), 0); // 發(fā)送用戶輸入的數(shù)據(jù)到服務(wù)器memset(buff, 0, 128); // 清空緩沖區(qū)recv(sockfd, buff, 127, 0); // 接收服務(wù)器的響應(yīng)printf("buff=%s\n", buff); // 打印服務(wù)器的響應(yīng)}close(sockfd); // 關(guān)閉套接字exit(0); // 退出程序
}
三、UDP協(xié)議
3.1 UDP協(xié)議編程流程
? ? ?UDP 提供的是無連接、不可靠的、數(shù)據(jù)報服務(wù)。可以通俗的將TCP理解成打電話,UDP理解成發(fā)短信。
? ? ? ? socket()用來創(chuàng)建套接字,使用 udp 協(xié)議時,選擇數(shù)據(jù)報服務(wù) SOCK_DGRAM。sendto() 用來發(fā)送數(shù)據(jù),由于 UDP 是無連接的,每次發(fā)送數(shù)據(jù)都需要指定對端的地址(IP 和端 口)。recvfrom()接收數(shù)據(jù),每次都需要傳給該方法一個地址結(jié)構(gòu)來存放發(fā)送端的地址。 recvfrom()可以接收所有客戶端發(fā)送給當(dāng)前應(yīng)用程序的數(shù)據(jù),并不是只能接收某一個客戶端的數(shù)據(jù)。
UDP 服務(wù)端編程示例代碼:
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <unistd.h>
4. #include <string.h>
5. #include <assert.h>
6. #include <sys/socket.h>
7. #include <netinet/in.h>
8. #include <arpa/inet.h>
9.
10. int main()
11. {
12. int sockfd = socket(AF_INET,SOCK_DGRAM,0);
13. assert( sockfd != -1 );
14.
15. struct sockaddr_in saddr,caddr;
16. memset(&saddr,0,sizeof(saddr));
17. saddr.sin_family = AF_INET;
18. saddr.sin_port = htons(6000);
19. saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
20.
21. int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
22. assert( res != -1 );
23.
24. while( 1 )
25. {
26. int len = sizeof(caddr);
27. char buff[128] = {0};
28. recvfrom(sockfd,buff,127,0,(struct sockaddr*)&caddr,&len);
29. printf("ip:%s,port:%d,buff=%s\n",inet_ntoa(caddr.sin_addr), ntohs(caddr.si
n_port),buff );
30.
31. sendto(sockfd,"ok",2,0,(struct sockaddr*)&caddr,sizeof(caddr));
32. }
33.
34. close(sockfd);
35. }
UDP 客戶端編程示例代碼:
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <unistd.h>
4. #include <string.h>
5. #include <assert.h>
6. #include <sys/socket.h>
7. #include <netinet/in.h>
8. #include <arpa/inet.h>
9.
10. int main()
11. {
12. int sockfd = socket(AF_INET,SOCK_DGRAM,0);
13. assert( sockfd != -1 );
14.
15. struct sockaddr_in saddr;
16. memset(&saddr,0,sizeof(saddr));
17. saddr.sin_family = AF_INET;
18. saddr.sin_port = htons(6000);
19. saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
20.
21. while( 1 )
22. {
23. char buff[128] = {0};
24. printf("input:\n");
25.
26. fgets(buff,128,stdin);
27.
28. if ( strncmp(buff,"end",3) == 0 )
29. {
30. break;
31. }
32.
33. sendto(sockfd,buff,strlen(buff),0,(struct sockaddr*)&saddr,sizeof(saddr));
34. memset(buff,0,128);
35.
36. int len = sizeof(saddr);
37. recvfrom(sockfd,buff,127,0,(struct sockaddr*)&saddr,&len);
38.
39. printf("buff=%s\n",buff);
40. }
41.
42. close(sockfd);
43. }
啟動服務(wù)端和客戶端,再關(guān)掉服務(wù)端,還能再發(fā)送數(shù)據(jù)嘛?可以 因為udp是無連接的,只要服務(wù)端啟動,有人發(fā)數(shù)據(jù)就接受。關(guān)掉服務(wù)端對客戶端來說,絲毫沒有影響 ?
3.2 UDP 協(xié)議特點
? ? ? UDP 數(shù)據(jù)報服務(wù)特點:發(fā)送端應(yīng)用程序每執(zhí)行一次寫操作,UDP 模塊就將其封裝成一 個 UDP 數(shù)據(jù)報發(fā)送。接收端必須及時針對每一個 UDP 數(shù)據(jù)報執(zhí)行讀操作,否則就會丟包,因此它不會出現(xiàn)粘包現(xiàn)象。 并且,如果用戶沒有指定足夠的應(yīng)用程序緩沖區(qū)來讀取 UDP 數(shù)據(jù),則 UDP 數(shù)據(jù)將被截斷。
3.3 應(yīng)用場景
? ? ?tcp和udp應(yīng)用分場景,例如下載一個文件,肯定是要完整的下載下來,數(shù)據(jù)不能丟失。如果實時通話視頻時,那就用udp,因為只是要看當(dāng)下的你,如果視頻過程中網(wǎng)不好,數(shù)據(jù)沒發(fā)出去,再重新發(fā),這樣慢慢的就會變成錄屏,因為tcp有接收緩沖區(qū),重新發(fā)的數(shù)據(jù)都會被對方,接收到接受緩沖區(qū),對方要全部讀完,所以這一幀數(shù)據(jù)沒發(fā)送成功就不要了。
四、面試題
4.1 TCP和UDP的區(qū)別
tcp是面向連接的可靠的流式服務(wù),udp是無連接不可靠的數(shù)據(jù)報服務(wù) 。
- tcp建立連接要進(jìn)行三次握手,而udp不需要建立連接直接指定地址發(fā)數(shù)據(jù)就行
- tcp在發(fā)送數(shù)據(jù)時有應(yīng)答確認(rèn),超時重傳機制,而udp發(fā)送數(shù)據(jù)成功就成功,失敗了也不會重發(fā)。
- tcp會出現(xiàn)粘包,udp不會出現(xiàn)粘包。
4.2 同一個端口可不可以被一個 TCP 和一個 UDP 的應(yīng)用程序同時使用?
? ? ? ?是的,可以,同一個端口可以同時被一個 TCP 應(yīng)用程序和一個 UDP 應(yīng)用程序使用。TCP 和 UDP 是兩個不同的傳輸層協(xié)議,它們的連接和數(shù)據(jù)傳輸方式不同,因此它們可以在相同的端口號上共存。操作系統(tǒng)和網(wǎng)絡(luò)棧通過區(qū)分傳輸層協(xié)議(TCP 或 UDP)來將數(shù)據(jù)包正確地交付給對應(yīng)的應(yīng)用程序。例如,假設(shè)你有一個 TCP 服務(wù)在端口 8080 上運行,同時你也可以在相同的端口 8080 上運行一個 UDP 服務(wù)。這兩個服務(wù)不會互相干擾,因為操作系統(tǒng)能夠根據(jù)協(xié)議類型將到達(dá)端口 8080 的 TCP 數(shù)據(jù)包和 UDP 數(shù)據(jù)包區(qū)分開來并分別處理。
4.3 同一個應(yīng)用程序可以創(chuàng)建多個套接字嗎?
同一個應(yīng)用程序可以創(chuàng)建多個套接字。套接字是網(wǎng)絡(luò)通信的基礎(chǔ),它允許程序發(fā)送和接收數(shù)據(jù)。應(yīng)用程序創(chuàng)建多個套接字的原因有很多,包括但不限于以下幾個方面:
多協(xié)議支持:一個應(yīng)用程序可能需要同時支持多種協(xié)議,例如同時使用 TCP 和 UDP,這時它需要分別為 TCP 和 UDP 創(chuàng)建不同的套接字。
多端口監(jiān)聽:一個服務(wù)器應(yīng)用程序可能需要監(jiān)聽多個端口,以便提供不同的服務(wù)或支持不同的協(xié)議版本。例如,一個應(yīng)用程序可以同時監(jiān)聽 80 端口(HTTP)和 443 端口(HTTPS)。
客戶端連接管理:對于一個 TCP 服務(wù)器,每當(dāng)一個客戶端連接到服務(wù)器時,服務(wù)器通常會為每個客戶端連接創(chuàng)建一個新的套接字。這允許服務(wù)器同時處理多個客戶端連接。
多線程或多進(jìn)程通信:應(yīng)用程序可能使用多個套接字來實現(xiàn)多線程或多進(jìn)程間的通信。例如,一個線程或進(jìn)程負(fù)責(zé)監(jiān)聽網(wǎng)絡(luò)連接,另一個線程或進(jìn)程負(fù)責(zé)處理數(shù)據(jù)。
至此,已經(jīng)講解完畢!篇幅較長,慢慢消化,以上就是全部內(nèi)容!請務(wù)必掌握,創(chuàng)作不易,歡迎大家點贊加關(guān)注評論,您的支持是我前進(jìn)最大的動力!下期再見!