進(jìn)程通信的概念最初來源于單機(jī)系統(tǒng)。由于每個(gè)進(jìn)程都在自己的地址范圍內(nèi)運(yùn)行,為保證兩個(gè)相互通信的進(jìn)程之間既互不干擾又協(xié)調(diào)一致工作,操作系統(tǒng)為進(jìn)程通信提供了相應(yīng)設(shè)施,如:
UNIX BSD有:管道(pipe)、命名管道(named pipe)軟中斷信號(hào)(signal)
UNIX system V有:消息(message)、共享存儲(chǔ)區(qū)(shared memory)和信號(hào)量(semaphore)等.1
他們都僅限于用在本機(jī)進(jìn)程之間通信。網(wǎng)間進(jìn)程通信要解決的是不同主機(jī)進(jìn)程間的相互通信問題(可把同機(jī)進(jìn)程通信看作是其中的特例)。為此,首先要解決的是網(wǎng)間進(jìn)程標(biāo)識(shí)問題。同一主機(jī)上,不同進(jìn)程可用進(jìn)程號(hào)(process ID)唯一標(biāo)識(shí)。但在網(wǎng)絡(luò)環(huán)境下,各主機(jī)獨(dú)立分配的進(jìn)程號(hào)不能唯一標(biāo)識(shí)該進(jìn)程。例如,主機(jī)A賦于某進(jìn)程號(hào)5,在B機(jī)中也可以存在5號(hào)進(jìn)程,因此,“5號(hào)進(jìn)程”這句話就沒有意義了。 其次,操作系統(tǒng)支持的網(wǎng)絡(luò)協(xié)議眾多,不同協(xié)議的工作方式不同,地址格式也不同。因此,網(wǎng)間進(jìn)程通信還要解決多重協(xié)議的識(shí)別問題。
其實(shí)TCP/IP協(xié)議族已經(jīng)幫我們解決了這個(gè)問題,網(wǎng)絡(luò)層的“ip地址”可以唯一標(biāo)識(shí)網(wǎng)絡(luò)中的主機(jī),而傳輸層的“協(xié)議+端口”可以唯一標(biāo)識(shí)主機(jī)中的應(yīng)用程序(進(jìn)程)。這樣利用三元組(ip地址,協(xié)議,端口)就可以標(biāo)識(shí)網(wǎng)絡(luò)的進(jìn)程了,網(wǎng)絡(luò)中的進(jìn)程通信就可以利用這個(gè)標(biāo)志與其它進(jìn)程進(jìn)行交互。
使用TCP/IP協(xié)議的應(yīng)用程序通常采用應(yīng)用編程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已經(jīng)被淘汰),來實(shí)現(xiàn)網(wǎng)絡(luò)進(jìn)程之間的通信。
就目前而言,幾乎所有的應(yīng)用程序都是采用socket,而現(xiàn)在又是網(wǎng)絡(luò)時(shí)代,網(wǎng)絡(luò)中進(jìn)程通信是無處不在,這就是我為什么說“一切皆socket”。
套接字
Socket是進(jìn)程間通信的一種抽象,提供了一套API接口,對(duì)網(wǎng)絡(luò)傳輸層一套具體的進(jìn)程提供了抽象接口的調(diào)用
socket起源于Unix,而Unix/Linux基本哲學(xué)之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關(guān)閉close”模式來操作。Socket就是該模式的一個(gè)實(shí)現(xiàn), socket即是一種特殊的文件,一些socket函數(shù)就是對(duì)其進(jìn)行的操作(讀/寫IO、打開、關(guān)閉).
說白了Socket是應(yīng)用層與TCP/IP協(xié)議族通信的中間軟件抽象層,它是一組接口。在設(shè)計(jì)模式中,Socket其實(shí)就是一個(gè)門面模式,它把復(fù)雜的TCP/IP協(xié)議族隱藏在Socket接口后面,對(duì)用戶來說,一組簡單的接口就是全部,讓Socket去組織數(shù)據(jù),以符合指定的協(xié)議。
注意:其實(shí)socket也沒有層的概念,它只是一個(gè)facade設(shè)計(jì)模式的應(yīng)用,讓編程變的更簡單。是一個(gè)軟件抽象層。在網(wǎng)絡(luò)編程中,我們大量用的都是通過socket實(shí)現(xiàn)的。
進(jìn)程間通信機(jī)制( s o c k e t I P C ) 進(jìn)程間通信機(jī)制(socket IPC) 進(jìn)程間通信機(jī)制(socketIPC)
openEuler通過Socket接口為用戶程序提供網(wǎng)絡(luò)服務(wù)。本節(jié)通過Socket介紹數(shù)據(jù)傳輸?shù)幕灸P? 2
套接字存在于進(jìn)程與協(xié)議棧之間,為應(yīng)用程序提供API調(diào)用、遠(yuǎn)程TCP、IP通信同住機(jī)內(nèi)Unix通信、應(yīng)用程序與內(nèi)核之間的netlink之類的,一個(gè)socket對(duì)象。
簡介
套接字(Socket)是計(jì)算機(jī)網(wǎng)絡(luò)中進(jìn)行數(shù)據(jù)通信的端點(diǎn),它提供了一種在不同計(jì)算機(jī)或同一臺(tái)計(jì)算機(jī)的不同進(jìn)程之間進(jìn)行數(shù)據(jù)交換的機(jī)制。在操作系統(tǒng)中,套接字是網(wǎng)絡(luò)通信的基礎(chǔ),通過套接字可以實(shí)現(xiàn)進(jìn)程間的通信(IPC, Inter-Process Communication)以及不同計(jì)算機(jī)之間的網(wǎng)絡(luò)通信。
數(shù)據(jù)傳輸基本模型
套接字通?;趦煞N基本的數(shù)據(jù)傳輸模型:面向連接的傳輸(TCP,Transmission Control Protocol)和無連接的傳輸(UDP,User Datagram Protocol)。
1.TCP/IP模型
TCP/IP模型是一個(gè)四層模型,它包括應(yīng)用層、傳輸層、網(wǎng)絡(luò)層和鏈路層。
-
應(yīng)用層:負(fù)責(zé)處理用戶的應(yīng)用程序,如Web瀏覽器、FTP客戶端等。應(yīng)用層協(xié)議定義了應(yīng)用程序如何通過套接字接口使用網(wǎng)絡(luò)服務(wù)。常見的應(yīng)用層協(xié)議有HTTP、FTP、SMTP等。
-
傳輸層:負(fù)責(zé)數(shù)據(jù)的可靠傳輸。TCP是傳輸層的主要協(xié)議,它提供了面向連接的、可靠的、基于字節(jié)流的傳輸服務(wù)。TCP通過三次握手建立連接,并通過序列號(hào)確保數(shù)據(jù)的順序性和可靠性。
-
網(wǎng)絡(luò)層:負(fù)責(zé)將數(shù)據(jù)包從源地址路由到目的地址。IP(Internet Protocol)是網(wǎng)絡(luò)層的主要協(xié)議,它定義了數(shù)據(jù)包的結(jié)構(gòu),并提供了地址和路由功能。
-
鏈路層:負(fù)責(zé)數(shù)據(jù)在物理介質(zhì)(如以太網(wǎng)、Wi-Fi等)上的傳輸。鏈路層協(xié)議(如Ethernet、PPP等)定義了如何在物理網(wǎng)絡(luò)上發(fā)送和接收數(shù)據(jù)。
2.UDP模型
UDP是另一種重要的傳輸層協(xié)議,與TCP不同,UDP提供的是無連接的、不可靠的數(shù)據(jù)傳輸服務(wù)。UDP數(shù)據(jù)包包含應(yīng)用程序數(shù)據(jù)、源端口、目的端口和UDP長度等信息。由于UDP不提供可靠性保證,它通常用于對(duì)實(shí)時(shí)性要求較高或能夠容忍偶爾丟包的場景,如視頻流、VoIP等。
套接字類型
在openEuler或其他類Unix系統(tǒng)中,套接字通常分為三種類型:
-
流套接字(SOCK_STREAM):提供面向連接的、可靠的、基于字節(jié)流的傳輸服務(wù),通常用于TCP協(xié)議。
-
數(shù)據(jù)報(bào)套接字(SOCK_DGRAM):提供無連接的、不可靠的、基于數(shù)據(jù)報(bào)的傳輸服務(wù),通常用于UDP協(xié)議。
-
原始套接字(SOCK_RAW):允許直接訪問底層協(xié)議,通常用于開發(fā)新的網(wǎng)絡(luò)協(xié)議或進(jìn)行網(wǎng)絡(luò)調(diào)試。
套接字(Socket)編程
套接字編程通常涉及以下幾個(gè)步驟:
-
創(chuàng)建套接字:使用
socket()
函數(shù)創(chuàng)建一個(gè)套接字,并指定協(xié)議族(如AF_INET用于IPv4協(xié)議)、套接字類型和協(xié)議。
創(chuàng)建Socket:
- 使用系統(tǒng)調(diào)用(如`socket()`函數(shù))創(chuàng)建一個(gè)新的Socket描述符。
- 指定協(xié)議族(如`AF_INET`表示IPv4協(xié)議)、Socket類型(如`SOCK_STREAM`表示流式Socket)以及協(xié)議(通常為0,表示使用默認(rèn)協(xié)議)。
-
綁定地址和端口:使用
bind()
函數(shù)將套接字綁定到一個(gè)本地地址和端口上,以便其他進(jìn)程或計(jì)算機(jī)可以通過該地址和端口訪問該套接字。
綁定地址:
- 使用`bind()`函數(shù)將Socket與本地地址和端口號(hào)綁定。
- 這樣,當(dāng)遠(yuǎn)程主機(jī)嘗試連接時(shí),系統(tǒng)就知道將連接路由到這個(gè)Socket。
-
監(jiān)聽和接受連接(對(duì)于服務(wù)器端):使用
listen()
函數(shù)使套接字進(jìn)入監(jiān)聽狀態(tài),并使用accept()
函數(shù)接受客戶端的連接請(qǐng)求。
監(jiān)聽連接:
- 對(duì)于服務(wù)端Socket,使用`listen()`函數(shù)將其置于監(jiān)聽狀態(tài)。
- 這將允許遠(yuǎn)程主機(jī)發(fā)起連接請(qǐng)求。
-
連接和發(fā)送/接收數(shù)據(jù)(對(duì)于客戶端):使用
connect()
函數(shù)連接到服務(wù)器端的套接字,并使用send()
或write()
函數(shù)發(fā)送數(shù)據(jù),使用recv()
或read()
函數(shù)接收數(shù)據(jù)。
接受連接:
- 當(dāng)有遠(yuǎn)程主機(jī)發(fā)起連接請(qǐng)求時(shí),服務(wù)端使用`accept()`函數(shù)接受連接。
- `accept()`會(huì)創(chuàng)建一個(gè)新的Socket描述符,用于與遠(yuǎn)程主機(jī)通信。
- 原來的Socket(監(jiān)聽Socket)繼續(xù)處于監(jiān)聽狀態(tài),等待新的連接請(qǐng)求。
-
關(guān)閉套接字:使用
close()
或shutdown()
函數(shù)關(guān)閉套接字,釋放相關(guān)資源。
通過這些步驟,可以在openEuler或其他類Unix系統(tǒng)中進(jìn)行基于套接字的網(wǎng)絡(luò)通信編程。
Socket 的連接
更準(zhǔn)確來說是,流式Socket連接的相關(guān)內(nèi)容
1.連接概述
基本概念
在系統(tǒng)場景中系統(tǒng)一般提供三種類型的Socket:也就是
-
流式Socket(Stream Socket):
- 基于TCP(Transmission Control Protocol,傳輸控制協(xié)議)的Socket。
- 提供面向連接的、可靠的、基于字節(jié)流的傳輸服務(wù)。
- 數(shù)據(jù)在發(fā)送和接收時(shí)保持順序,無重復(fù),并且無丟失。
- 需要三次握手建立連接,四次揮手?jǐn)嚅_連接。
-
數(shù)據(jù)報(bào)Socket(Datagram Socket):
- 基于UDP(User Datagram Protocol,用戶數(shù)據(jù)報(bào)協(xié)議)的Socket。
- 提供無連接的、不可靠的、基于數(shù)據(jù)報(bào)的服務(wù)。
- 數(shù)據(jù)在發(fā)送和接收時(shí)可能不保持順序,也可能出現(xiàn)重復(fù)或丟失。
- 不需要建立和維護(hù)連接,適用于對(duì)實(shí)時(shí)性要求較高,但數(shù)據(jù)可靠性要求不高的場景。
-
原始Socket(Raw Socket):
- 允許應(yīng)用程序直接操作底層協(xié)議,繞過內(nèi)核協(xié)議棧。
- 開發(fā)者可以自定義協(xié)議頭,直接構(gòu)造數(shù)據(jù)包。
- 這類Socket在常規(guī)應(yīng)用中較少使用,因?yàn)樗枰獙?duì)網(wǎng)絡(luò)協(xié)議有深入的了解。
- 流式Socket(Stream Socket)基于TCP,要三次握手的那個(gè),可靠的字節(jié)流。
- 數(shù)據(jù)報(bào)Socket(Dategram Socket)基于UDP,基于數(shù)據(jù)報(bào)的非可靠數(shù)據(jù)傳輸服務(wù)
- 原始Socket(Raw Socket)繞過內(nèi)核協(xié)議棧,填充各級(jí)協(xié)議頭直接構(gòu)造數(shù)據(jù)包,常規(guī)應(yīng)用不使用。
TCP通信需要先建立虛擬鏈路(通信雙方的一個(gè)連接,connection),TCP/IP通訊下,Socket采用四元組(源IP、源端口、目的IP、目的端口)標(biāo)識(shí)(identity)
i
d
e
n
t
i
t
y
identity
identity
(1)基本概念
同一時(shí)間只能處理一個(gè)連接
openEuler提供了兩種列緩存連接請(qǐng)求,分別為半連接隊(duì)列和連接隊(duì)列,當(dāng)服務(wù)端建立具體的請(qǐng)求時(shí),半連接隊(duì)列和連接隊(duì)列在其中起緩存作用。
第一次的握手,
(2)連接狀態(tài)
連接狀態(tài)
//源文件:include/net/tcp_states.h
enum{
? TCP_ESTABLISHED = 1;
? TCP_SYN_SENT,
? TCP_SYN_RECV,
? TCP_FIN_WAIT1,
? TCP_FIN_WAIT2,
? TCP_CLOSE,
? TCP_CLOSE_WAIT,
? TCP_LAST_ACK,
? TCP_LISENCE,
? ...
};
//連接狀態(tài)
TCP連接狀態(tài)由tcp_states.h中定義的枚舉類型表示。以下是一些常見的TCP連接狀態(tài):
#TCP_ESTABLISHED:連接已經(jīng)建立,數(shù)據(jù)可以傳輸。
#TCP_SYN_SENT:連接正在建立中,服務(wù)器已發(fā)送SYN報(bào)文,等待客戶端的SYN-ACK報(bào)文。
TCP_SYN_RECV:連接正在建立中,服務(wù)器已收到客戶端的SYN報(bào)文,并發(fā)送了SYN-ACK報(bào)文,等待客戶端的ACK報(bào)文。
#TCP_FIN_WAIT1:連接正在關(guān)閉中,應(yīng)用程序已調(diào)用close()函數(shù),等待對(duì)方發(fā)送FIN報(bào)文。
TCP_FIN_WAIT2:連接正在關(guān)閉中,已收到對(duì)方的FIN報(bào)文,并發(fā)送了ACK報(bào)文,等待對(duì)方確認(rèn)。
TCP_CLOSE:連接已關(guān)閉,無數(shù)據(jù)傳輸。
#TCP_CLOSE_WAIT:連接正在關(guān)閉中,對(duì)方已發(fā)送FIN報(bào)文,等待應(yīng)用程序關(guān)閉連接。
TCP_LAST_ACK:連接正在關(guān)閉中,已發(fā)送最后的ACK報(bào)文,等待對(duì)方確認(rèn)。
(3)連接隊(duì)列
2.建立連接
-
數(shù)據(jù)傳輸:
- 一旦連接建立,就可以使用
send()
或write()
函數(shù)發(fā)送數(shù)據(jù),以及使用recv()
或read()
函數(shù)接收數(shù)據(jù)。 - 數(shù)據(jù)傳輸是雙向的,可以在兩個(gè)Socket之間自由流動(dòng)。
- 一旦連接建立,就可以使用
TCP連接的建立通常通過三次握手來完成:
1. **SYN(SYN=1, seq=x)**:客戶端發(fā)送一個(gè)SYN報(bào)文給服務(wù)器,并進(jìn)入SYN_SENT狀態(tài),等待服務(wù)器的確認(rèn)。
2. **SYN-ACK(SYN=1, ACK=1, seq=y, ack=x+1)**:服務(wù)器收到SYN報(bào)文后,發(fā)送一個(gè)SYN-ACK報(bào)文給客戶端,并進(jìn)入SYN_RECV狀態(tài),等待客戶端的確認(rèn)。
3. **ACK(ACK=1, seq=x+1, ack=y+1)**:客戶端收到SYN-ACK報(bào)文后,發(fā)送一個(gè)ACK報(bào)文給服務(wù)器,并進(jìn)入ESTABLISHED狀態(tài),表示連接已建立。服務(wù)器收到ACK報(bào)文后,也進(jìn)入ESTABLISHED狀態(tài)。
3.關(guān)閉連接
-
關(guān)閉連接:
- 當(dāng)數(shù)據(jù)傳輸完成后,使用
close()
函數(shù)關(guān)閉Socket。 - 對(duì)于服務(wù)端,可能需要顯式關(guān)閉監(jiān)聽Socket以停止接受新的連接。
- 當(dāng)數(shù)據(jù)傳輸完成后,使用
TCP連接的關(guān)閉通常通過四次揮手來完成:
1. **FIN(FIN=1, seq=u)**:應(yīng)用程序調(diào)用`close()`函數(shù)關(guān)閉連接,客戶端發(fā)送一個(gè)FIN報(bào)文給服務(wù)器,并進(jìn)入FIN_WAIT_1狀態(tài),等待服務(wù)器的確認(rèn)。
2. **ACK(ACK=1, seq=v, ack=u+1)**:服務(wù)器收到FIN報(bào)文后,發(fā)送一個(gè)ACK報(bào)文給客戶端,并進(jìn)入CLOSE_WAIT狀態(tài),表示已知道客戶端要關(guān)閉連接。
3. **FIN(FIN=1, seq=w)**:當(dāng)服務(wù)器準(zhǔn)備好關(guān)閉連接時(shí),發(fā)送一個(gè)FIN報(bào)文給客戶端,并進(jìn)入LAST_ACK狀態(tài),等待客戶端的確認(rèn)。
4. **ACK(ACK=1, seq=u+1, ack=w+1)**:客戶端收到FIN報(bào)文后,發(fā)送一個(gè)ACK報(bào)文給服務(wù)器,并進(jìn)入TIME_WAIT狀態(tài),等待足夠的時(shí)間以確保服務(wù)器收到ACK報(bào)文。服務(wù)器收到ACK報(bào)文后,關(guān)閉連接。
上圖為四次揮手的示意圖
由于TCP連接是全雙工的,因此每個(gè)方向都必須單獨(dú)進(jìn)行關(guān)閉。這個(gè)原則是當(dāng)一方完成它的數(shù)據(jù)發(fā)送任務(wù)后就能發(fā)送一個(gè)FIN來終止這個(gè)方向的連接。收到一個(gè) FIN只意味著這一方向上沒有數(shù)據(jù)流動(dòng),一個(gè)TCP連接在收到一個(gè)FIN后仍能發(fā)送數(shù)據(jù)。首先進(jìn)行關(guān)閉的一方將執(zhí)行主動(dòng)關(guān)閉,而另一方執(zhí)行被動(dòng)關(guān)閉。
(1)客戶端A發(fā)送一個(gè)FIN,用來關(guān)閉客戶A到服務(wù)器B的數(shù)據(jù)傳送(報(bào)文段4)。
(2)服務(wù)器B收到這個(gè)FIN,它發(fā)回一個(gè)ACK,確認(rèn)序號(hào)為收到的序號(hào)加1(報(bào)文段5)。和SYN一樣,一個(gè)FIN將占用一個(gè)序號(hào)。
(3)服務(wù)器B關(guān)閉與客戶端A的連接,發(fā)送一個(gè)FIN給客戶端A(報(bào)文段6)。
(4)客戶端A發(fā)回ACK報(bào)文確認(rèn),并將確認(rèn)序號(hào)設(shè)置為收到序號(hào)加1(報(bào)文段7)。
對(duì)應(yīng)函數(shù)接口如圖:
socket 編程接口介紹
下面介紹socket 編程中使用到的一些接口函數(shù)。使用 socket 接口需要在我們的應(yīng)用程序
代碼中包含兩個(gè)頭文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
1. socket()函數(shù)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()函數(shù)類似于 open()函數(shù),它用于創(chuàng)建一個(gè)網(wǎng)絡(luò)通信端點(diǎn)(打開一個(gè)網(wǎng)絡(luò)通信),如果成功則返回一個(gè)網(wǎng)絡(luò)文件描述符,通常把這個(gè)文件描述符稱為 socket 描述符(socket descriptor),這個(gè) socket 描述符跟文件描述符一樣,后續(xù)的操作都有用到它,把它作為參數(shù),通過它來進(jìn)行一些讀寫操作。
該函數(shù)包括 3 個(gè)參數(shù),如下所示:
d
o
m
a
i
n
domain
domain
參數(shù) domain 用于指定一個(gè)通信域;這將選擇將用于通信的協(xié)議族。
對(duì)于 TCP/IP 協(xié)議來說,通常選擇 AF_INET 就可以了,當(dāng)然如果你的 IP 協(xié)議的版本支持 IPv6,那么可以選擇 AF_INET6。
t
y
p
e
type
type
參數(shù) type 指定套接字的類型,當(dāng)前支持的類型有:
p
r
o
t
o
c
o
l
protocol
protocol
參數(shù) protocol 通常設(shè)置為 0
,表示為給定的通信域和套接字類型選擇默認(rèn)協(xié)議。當(dāng)對(duì)同一域和套接字類型支持多個(gè)協(xié)議時(shí),可以使用 protocol 參數(shù)選擇一個(gè)特定協(xié)議。在 AF_INET 通信域中,套接字類型為 SOCK_STREAM 的默認(rèn)協(xié)議是傳輸控制協(xié)議 (Transmission Control Protocol,TCP 協(xié)議)
在 AF_INET 通信域中,套接字類型為 SOCK_DGRAM 的默認(rèn)協(xié)議時(shí) UDP。
調(diào)用 socket()
與調(diào)用 open()
函數(shù)很類似,調(diào)用成功情況下,均會(huì)返回用于文件 I/O 的文件描述符,只不過對(duì)于 socket()來說,其返回的文件描述符一般稱為 socket 描述符。當(dāng)不再需要該文件描述符時(shí),可調(diào)用close()
函數(shù)來關(guān)閉套接字,釋放相應(yīng)的資源。
如果 socket() 函數(shù)調(diào)用失敗,則會(huì)返回-1,并且會(huì)設(shè)置 errno 變量以指示錯(cuò)誤類型。
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);//打開套接字
if (0 > socket_fd) {
perror("socket error");
exit(-1);
}
......
......
close(socket_fd); //關(guān)閉套接字
2. bind()函數(shù)
bind()函數(shù)原型如下所示:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()函數(shù)用于將一個(gè) IP 地址或端口號(hào)與一個(gè)套接字進(jìn)行綁定(將套接字與地址進(jìn)行關(guān)聯(lián))。將一個(gè)客戶端的套接字關(guān)聯(lián)上一個(gè)地址沒有多少新意,可以讓系統(tǒng)選一個(gè)默認(rèn)的地址。一般來講,會(huì)將一個(gè)服務(wù)器的套接字綁定到一個(gè)眾所周知的地址—即一個(gè)固定的與服務(wù)器進(jìn)行通信的客戶端應(yīng)用程序提前就知道的地址(注意這里說的地址包括 IP 地址和端口號(hào))。因?yàn)閷?duì)于客戶端來說,它與服務(wù)器進(jìn)行通信,首先需要知道服務(wù)器的 IP 地址以及對(duì)應(yīng)的端口號(hào),所以通常服務(wù)器的 IP 地址以及端口號(hào)都是眾所周知的。
調(diào)用 bind()函數(shù)將參數(shù) sockfd 指定的套接字與一個(gè)地址 addr 進(jìn)行綁定,成功返回 0,失敗情況下返回-1,并設(shè)置 errno 以提示錯(cuò)誤原因。
參數(shù) addr 是一個(gè)指針,指向一個(gè) struct sockaddr 類型變量,如下所示:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
第二個(gè)成員 sa_data 是一個(gè) char 類型數(shù)組,一共 14 個(gè)字節(jié),在這 14 個(gè)字節(jié)中就包括了 IP 地址、端口號(hào)等信息,這個(gè)結(jié)構(gòu)對(duì)用戶并不友好,它把這些信息都封裝在了 sa_data 數(shù)組中,這樣使得用戶是無法對(duì)sa_data 數(shù)組進(jìn)行賦值。事實(shí)上,這是一個(gè)通用的 socket 地址結(jié)構(gòu)體。
一般我們?cè)谑褂玫臅r(shí)候都會(huì)使用 struct sockaddr_in 結(jié)構(gòu)體,sockaddr_in 和 sockaddr 是并列的結(jié)構(gòu)(占用的空間是一樣的),指向 sockaddr_in 的結(jié)構(gòu)體的指針也可以指向 sockadd 的結(jié)構(gòu)體,并代替它,而且sockaddr_in 結(jié)構(gòu)對(duì)用戶將更加友好,在使用的時(shí)候進(jìn)行類型轉(zhuǎn)換就可以了。該結(jié)構(gòu)體內(nèi)容如下所示:
struct sockaddr_in {
sa_family_t sin_family; /* 協(xié)議族 */
in_port_t sin_port; /* 端口號(hào) */
struct in_addr sin_addr; /* IP 地址 */
unsigned char sin_zero[8];
};
這個(gè)結(jié)構(gòu)體的第一個(gè)字段是與 sockaddr 結(jié)構(gòu)體是一致的,而剩下的字段就是 sa_data 數(shù)組連續(xù)的 14 字節(jié)信息里面的內(nèi)容,只不過從新定義了成員變量而已,sin_port 字段是我們需要填寫的端口號(hào)信息,sin_addr字段是我們需要填寫的 IP 地址信息,剩下 sin_zero 區(qū)域的 8 字節(jié)保留未用。
最后一個(gè)參數(shù) addrlen 指定了 addr 所指向的結(jié)構(gòu)體對(duì)應(yīng)的字節(jié)長度。
使用示例
struct sockaddr_in socket_addr;
memset(&socket_addr, 0x0, sizeof(socket_addr)); //清零
//填充變量
socket_addr.sin_family = AF_INET;
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
socket_addr.sin_port = htons(5555);
//將地址與套接字進(jìn)行關(guān)聯(lián)、綁定
bind(socket_fd, (struct sockaddr *)&socket_addr, sizeof(socket_addr));
Tips: 代碼中的 htons 和 htonl 并不是函數(shù),只是一個(gè)宏定義,主要的作用在于為了避免大小端的問題,需要這些宏需要在我們的應(yīng)用程序代碼中包含頭文件<netinet/in.h>。
Tips:bind()函數(shù)并不是總是需要調(diào)用的,只有用戶進(jìn)程想與一個(gè)具體的 IP 地址或端口號(hào)相關(guān)聯(lián)的時(shí)候才需要調(diào)用這個(gè)函數(shù)。如果用戶進(jìn)程沒有這個(gè)必要,那么程序可以依賴內(nèi)核的自動(dòng)的選址機(jī)制來完成自動(dòng)地址選擇,通常在客戶端應(yīng)用程序中會(huì)這樣做。
3. listen()函數(shù)
listen()函數(shù)只能在服務(wù)器進(jìn)程中使用,讓服務(wù)器進(jìn)程進(jìn)入監(jiān)聽狀態(tài),等待客戶端的連接請(qǐng)求,listen()函數(shù)在一般在 bind()函數(shù)之后調(diào)用,在 accept()函數(shù)之前調(diào)用,它的函數(shù)原型是:
int listen(int sockfd, int backlog);
無法在一個(gè)已經(jīng)連接的套接字(即已經(jīng)成功執(zhí)行 connect()的套接字或由 accept()調(diào)用返回的套接字)上執(zhí)行 listen()。
參數(shù) backlog 用來描述 sockfd 的等待連接隊(duì)列能夠達(dá)到的最大值。在服務(wù)器進(jìn)程正處理客戶端連接請(qǐng)求的時(shí)候,可能還存在其它的客戶端請(qǐng)求建立連接,因?yàn)?TCP 連接是一個(gè)過程,由于同時(shí)嘗試連接的用戶過多,使得服務(wù)器進(jìn)程無法快速地完成所有的連接請(qǐng)求,那怎么辦呢?直接丟掉其他客戶端的連接肯定不是一個(gè)很好的解決方法。因此內(nèi)核會(huì)在自己的進(jìn)程空間里維護(hù)一個(gè)隊(duì)列,這些連接請(qǐng)求就會(huì)被放入一個(gè)隊(duì)列中,服務(wù)器進(jìn)程會(huì)按照先來后到的順序去處理這些連接請(qǐng)求,這樣的一個(gè)隊(duì)列內(nèi)核不可能讓其任意大,所以必須有一個(gè)大小的上限,這個(gè) backlog 參數(shù)告訴內(nèi)核使用這個(gè)數(shù)值作為隊(duì)列的上限。而當(dāng)一個(gè)客戶端的連接請(qǐng)求到達(dá)并且該隊(duì)列為滿時(shí),客戶端可能會(huì)收到一個(gè)表示連接失敗的錯(cuò)誤,本次請(qǐng)求會(huì)被丟棄不作處理。
- accept()函數(shù)
服務(wù)器調(diào)用 listen()函數(shù)之后,就會(huì)進(jìn)入到監(jiān)聽狀態(tài),等待客戶端的連接請(qǐng)求,使用 accept()函數(shù)獲取客戶端的連接請(qǐng)求并建立連接。函數(shù)原型如下所示:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
為了能夠正常讓客戶端能正常連接到服務(wù)器,服務(wù)器必須遵循以下處理流程:
① 調(diào)用 socket()函數(shù)打開套接字;
② 調(diào)用 bind()函數(shù)將套接字與一個(gè)端口號(hào)以及 IP 地址進(jìn)行綁定;
③ 調(diào)用 listen()函數(shù)讓服務(wù)器進(jìn)程進(jìn)入監(jiān)聽狀態(tài),監(jiān)聽客戶端的連接請(qǐng)求;
④ 調(diào)用 accept()函數(shù)處理到來的連接請(qǐng)求。
accept()函數(shù)通常只用于服務(wù)器應(yīng)用程序中,如果調(diào)用 accept()函數(shù)時(shí),并沒有客戶端請(qǐng)求連接(等待連接隊(duì)列中也沒有等待連接的請(qǐng)求),此時(shí) accept()會(huì)進(jìn)入阻塞狀態(tài),直到有客戶端連接請(qǐng)求到達(dá)為止。當(dāng)有客戶端連接請(qǐng)求到達(dá)時(shí),accept()函數(shù)與遠(yuǎn)程客戶端之間建立連接,accept()函數(shù)返回一個(gè)新的套接字。這個(gè)套接字與 socket()函數(shù)返回的套接字并不同,socket()函數(shù)返回的是服務(wù)器的套接字(以服務(wù)器為例),而accept()函數(shù)返回的套接字連接到調(diào)用 connect()的客戶端,服務(wù)器通過該套接字與客戶端進(jìn)行數(shù)據(jù)交互,譬如向客戶端發(fā)送數(shù)據(jù)、或從客戶端接收數(shù)據(jù)。
所以,理解 accept()函數(shù)的關(guān)鍵點(diǎn)在于它會(huì)創(chuàng)建一個(gè)新的套接字,其實(shí)這個(gè)新的套接字就是與執(zhí)行
connect()(客戶端調(diào)用 connect()向服務(wù)器發(fā)起連接請(qǐng)求)的客戶端之間建立了連接,這個(gè)套接字代表了服務(wù)器與客戶端的一個(gè)連接。如果 accept()函數(shù)執(zhí)行出錯(cuò),將會(huì)返回-1,并會(huì)設(shè)置 errno 以指示錯(cuò)誤原因。
參數(shù) addr 是一個(gè)傳出參數(shù),參數(shù) addr 用來返回已連接的客戶端的 IP 地址與端口號(hào)等這些信息。
參數(shù)addrlen 應(yīng)設(shè)置為 addr 所指向的對(duì)象的字節(jié)長度,如果我們對(duì)客戶端的 IP 地址與端口號(hào)這些信息不感興趣,可以把 arrd 和 addrlen 均置為空指針 NULL。
5. connect()函數(shù)
connect()函數(shù)原型如下所示:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
該函數(shù)用于客戶端應(yīng)用程序中,客戶端調(diào)用 connect()函數(shù)將套接字 sockfd 與遠(yuǎn)程服務(wù)器進(jìn)行連接,參數(shù) addr 指定了待連接的服務(wù)器的 IP 地址以及端口號(hào)等信息,參數(shù) addrlen 指定了 addr 指向的 struct sockaddr對(duì)象的字節(jié)大小。
客戶端通過 connect()函數(shù)請(qǐng)求與服務(wù)器建立連接,對(duì)于 TCP 連接來說,調(diào)用該函數(shù)將發(fā)生 TCP 連接的握手過程,并最終建立一個(gè) TCP 連接,而對(duì)于 UDP 協(xié)議來說,調(diào)用這個(gè)函數(shù)只是在 sockfd 中記錄服務(wù)器IP 地址與端口號(hào),而不發(fā)送任何數(shù)據(jù)。
函數(shù)調(diào)用成功則返回 0,失敗返回-1,并設(shè)置 errno 以指示錯(cuò)誤原因。
- 發(fā)送和接收函數(shù)
一旦客戶端與服務(wù)器建立好連接之后,我們就可以通過套接字描述符來收發(fā)數(shù)據(jù)了(對(duì)于客戶端使用socket()返回的套接字描述符,而對(duì)于服務(wù)器來說,需要使用 accept()返回的套接字描述符),這與我們讀寫普通文件是差不多的操作,譬如可以調(diào)用 read()或 recv()函數(shù)讀取網(wǎng)絡(luò)數(shù)據(jù),調(diào)用 write()或 send()函數(shù)發(fā)送數(shù)據(jù)。
read()函數(shù)
read()函數(shù)大家都很熟悉了,通過 read()函數(shù)從一個(gè)文件描述符中讀取指定字節(jié)大小的數(shù)據(jù)并放入到指定的緩沖區(qū)中,read()調(diào)用成功將返回讀取到的字節(jié)數(shù),此返回值受文件剩余字節(jié)數(shù)限制,當(dāng)返回值小于指定的字節(jié)數(shù)時(shí)并不意味著錯(cuò)誤;這可能是因?yàn)楫?dāng)前可讀取的字節(jié)數(shù)小于指定的字節(jié)數(shù)(比如已經(jīng)接近文件結(jié)尾,或者正在從管道或者終端讀取數(shù)據(jù),或者 read()函數(shù)被信號(hào)中斷等),出錯(cuò)返回-1 并設(shè)置 errno,如果在調(diào) read 之前已到達(dá)文件末尾,則這次 read 返回 0。
套接字描述符也是文件描述符,所以使用 read()函數(shù)讀取網(wǎng)絡(luò)數(shù)據(jù)時(shí),read()函數(shù)的參數(shù) fd 就是對(duì)應(yīng)的套接字描述符。
recv()函數(shù)
recv()函數(shù)原型如下所示:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
不論是客戶端還是服務(wù)器都可以通過 revc()函數(shù)讀取網(wǎng)絡(luò)數(shù)據(jù),它與 read()函數(shù)的功能是相似的。參數(shù)sockfd 指定套接字描述符,參數(shù) buf 指向了一個(gè)數(shù)據(jù)接收緩沖區(qū),參數(shù) len 指定了讀取數(shù)據(jù)的字節(jié)大小,參數(shù) flags 可以指定一些標(biāo)志用于控制如何接收數(shù)據(jù)。
函數(shù) recv()與 read()很相似,但是 recv()可以通過指定 flags 標(biāo)志來控制如何接收數(shù)據(jù)3
數(shù)據(jù)的傳輸
在網(wǎng)絡(luò)編程中,數(shù)據(jù)的傳輸是核心功能之一。數(shù)據(jù)傳輸涉及到許多概念和技術(shù),包括阻塞
與非阻塞I/O、I/O復(fù)用
等。這些概念和技術(shù)對(duì)于理解和優(yōu)化網(wǎng)絡(luò)性能至關(guān)重要。
1. 阻塞與非阻塞
阻塞I/O:在阻塞I/O模型中,當(dāng)應(yīng)用程序發(fā)起一個(gè)讀或?qū)懻?qǐng)求時(shí),如果數(shù)據(jù)還沒有準(zhǔn)備好,應(yīng)用程序?qū)⒈蛔枞?,直到?shù)據(jù)準(zhǔn)備好為止。在這個(gè)過程中,應(yīng)用程序無法執(zhí)行其他任務(wù)。這種模型簡單易懂,但對(duì)于需要同時(shí)處理多個(gè)I/O操作的應(yīng)用來說,效率較低。
非阻塞I/O:在非阻塞I/O模型中,應(yīng)用程序發(fā)起讀或?qū)懻?qǐng)求時(shí),如果數(shù)據(jù)沒有準(zhǔn)備好,操作會(huì)立即返回一個(gè)錯(cuò)誤,而不是阻塞應(yīng)用程序。這樣,應(yīng)用程序可以繼續(xù)執(zhí)行其他任務(wù),直到數(shù)據(jù)準(zhǔn)備好為止。非阻塞I/O提高了應(yīng)用程序的響應(yīng)性,但需要開發(fā)者自己管理多個(gè)I/O操作的輪詢和調(diào)度,增加了編程的復(fù)雜性。
2. I/O復(fù)用
I/O復(fù)用:I/O復(fù)用技術(shù)允許單個(gè)線程同時(shí)處理多個(gè)I/O操作。通過復(fù)用描述符集合,線程可以在多個(gè)描述符之間進(jìn)行切換,從而實(shí)現(xiàn)對(duì)多個(gè)I/O操作的并行處理。I/O復(fù)用技術(shù)包括select、poll和epoll等。
select:是最早的I/O復(fù)用技術(shù)之一。它允許應(yīng)用程序監(jiān)視多個(gè)文件描述符的狀態(tài)變化。當(dāng)某個(gè)文件描述符的狀態(tài)發(fā)生變化時(shí),select會(huì)通知應(yīng)用程序,從而進(jìn)行相應(yīng)的讀或?qū)懖僮鳌5?,select在處理大量文件描述符時(shí)性能較低,因?yàn)樗捎幂喸兊姆绞絹頇z查每個(gè)文件描述符的狀態(tài)。
poll:poll是select的改進(jìn)版本,它解決了select在處理大量文件描述符時(shí)性能低下的問題。poll使用一個(gè)鏈表來存儲(chǔ)文件描述符,而不是使用位圖,這使得它在處理大量文件描述符時(shí)更加高效。
epoll:epoll是Linux特有的I/O復(fù)用技術(shù),它采用事件驅(qū)動(dòng)的方式來實(shí)現(xiàn)對(duì)多個(gè)I/O操作的并行處理。epoll通過一個(gè)紅黑樹來管理文件描述符,并使用一個(gè)事件表來存儲(chǔ)觸發(fā)的事件。當(dāng)某個(gè)文件描述符的狀態(tài)發(fā)生變化時(shí),epoll會(huì)通知應(yīng)用程序,并將事件添加到事件表中。這樣,應(yīng)用程序可以一次性處理多個(gè)觸發(fā)的事件,提高了處理效率。
在實(shí)際應(yīng)用中,開發(fā)者需要根據(jù)具體場景和需求選擇合適的I/O模型和復(fù)用技術(shù)。對(duì)于需要處理大量并發(fā)連接的應(yīng)用,非阻塞I/O和I/O復(fù)用技術(shù)通常是更好的選擇。而對(duì)于簡單的、不需要處理大量并發(fā)連接的應(yīng)用,阻塞I/O模型可能更加適合。
數(shù)據(jù)的傳輸路徑
數(shù)據(jù)報(bào)文收發(fā)的總體流程
在計(jì)算機(jī)網(wǎng)絡(luò)中,數(shù)據(jù)的傳輸路徑涉及多個(gè)層次和組件,從物理層到應(yīng)用層。數(shù)據(jù)報(bào)文(或稱數(shù)據(jù)包)的收發(fā)遵循一個(gè)總體流程,包括發(fā)送報(bào)文和接收?qǐng)?bào)文兩個(gè)主要階段。
1. 發(fā)送報(bào)文
發(fā)送報(bào)文的過程通常涉及以下步驟:
應(yīng)用層處理:
- 應(yīng)用程序生成要發(fā)送的數(shù)據(jù)。
- 應(yīng)用程序使用適當(dāng)?shù)膮f(xié)議(如HTTP、FTP、SMTP等)對(duì)數(shù)據(jù)進(jìn)行封裝,添加必要的頭部信息(如目標(biāo)地址、端口號(hào)等)。
傳輸層處理:
- 傳輸層(通常是TCP或UDP)接收來自應(yīng)用層的數(shù)據(jù)段,并添加傳輸層頭部信息(如序列號(hào)、窗口大小、校驗(yàn)和等)。
- 如果是TCP連接,傳輸層負(fù)責(zé)將數(shù)據(jù)分割成適當(dāng)大小的數(shù)據(jù)段,并處理流量控制和擁塞控制。
網(wǎng)絡(luò)層處理:
- 網(wǎng)絡(luò)層(通常是IP層)接收來自傳輸層的數(shù)據(jù)包,并添加網(wǎng)絡(luò)層頭部信息(如源IP地址、目標(biāo)IP地址等)。
- 路由器根據(jù)數(shù)據(jù)包中的IP地址信息進(jìn)行路由選擇,將數(shù)據(jù)包轉(zhuǎn)發(fā)到下一個(gè)目標(biāo)地址。
數(shù)據(jù)鏈路層處理:
- 數(shù)據(jù)鏈路層(如以太網(wǎng))將網(wǎng)絡(luò)層傳來的數(shù)據(jù)包封裝成幀,添加幀頭部和尾部(如MAC地址、幀類型等)。
- 幀通過物理介質(zhì)(如網(wǎng)線、無線信號(hào)等)傳輸?shù)较噜彽墓?jié)點(diǎn)。
物理層傳輸:
- 物理層負(fù)責(zé)在物理介質(zhì)上傳輸比特流。這涉及到電信號(hào)、光信號(hào)或無線信號(hào)的傳輸。
2. 接收?qǐng)?bào)文
接收?qǐng)?bào)文的過程是發(fā)送過程的逆向操作,通常涉及以下步驟:
物理層接收:
- 物理層接收到來自物理介質(zhì)的比特流,并將其轉(zhuǎn)換為數(shù)據(jù)鏈路層可以理解的信號(hào)。
數(shù)據(jù)鏈路層處理:
- 數(shù)據(jù)鏈路層從接收到的信號(hào)中提取幀,驗(yàn)證幀的完整性(如CRC校驗(yàn))。
- 如果幀有效,數(shù)據(jù)鏈路層將其傳遞給網(wǎng)絡(luò)層。
網(wǎng)絡(luò)層處理:
- 網(wǎng)絡(luò)層從幀中提取數(shù)據(jù)包,并根據(jù)數(shù)據(jù)包中的IP地址信息進(jìn)行路由處理。
- 如果數(shù)據(jù)包的目標(biāo)地址與本地節(jié)點(diǎn)匹配,網(wǎng)絡(luò)層將其傳遞給傳輸層。
傳輸層處理:
- 傳輸層(TCP或UDP)接收數(shù)據(jù)包,并驗(yàn)證其完整性和正確性(如序列號(hào)、校驗(yàn)和等)。
- 如果是TCP連接,傳輸層負(fù)責(zé)重新排序收到的數(shù)據(jù)段,并進(jìn)行流量控制和擁塞控制。
應(yīng)用層處理:
- 應(yīng)用層從傳輸層接收數(shù)據(jù),并去除協(xié)議頭部信息。
- 應(yīng)用程序處理接收到的數(shù)據(jù),并根據(jù)需要執(zhí)行相應(yīng)的操作(如顯示網(wǎng)頁、保存文件等)。
在實(shí)際的網(wǎng)絡(luò)通信中,發(fā)送和接收?qǐng)?bào)文的過程可能涉及多個(gè)中間節(jié)點(diǎn)(如路由器、交換機(jī)等)的轉(zhuǎn)發(fā)和處理。此外,為了保證數(shù)據(jù)傳輸?shù)目煽啃院托?,網(wǎng)絡(luò)協(xié)議棧中的各個(gè)層次通常會(huì)使用各種算法和機(jī)制(如差錯(cuò)控制、流量控制、擁塞控制等)來優(yōu)化數(shù)據(jù)傳輸過程。4
整理工具:
- 繪圖工具:VISO
- 截圖工具:Photor
- 文本工具:typora
- 書簽工具:pocket
參考文獻(xiàn):
-
Linux 的 SOCKET 編程詳解,hguisu ??
-
《openEuler操作系統(tǒng)(第2版)》,任炬、張堯?qū)W ??
-
Socket 編程基礎(chǔ),比特冬哥 ??文章來源:http://www.zghlxwxcb.cn/news/detail-831008.html
-
Socket 編程詳解:從基本概念到實(shí)例應(yīng)用(TCP|UDP C語言實(shí)例詳解) ,二進(jìn)制coder ??文章來源地址http://www.zghlxwxcb.cn/news/detail-831008.html
到了這里,關(guān)于DP讀書:《openEuler操作系統(tǒng)》(十)套接字 Socket 數(shù)據(jù)傳輸?shù)幕灸P偷奈恼戮徒榻B完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!