網絡編程:使得計算機程序能夠在網絡中發送和接受數據,從而實現分布式系統和網絡服務的功能。
作用:使應用程序能夠通過網絡協議與其他計算機程序進行數據交換
基本概念
套接字(socket):
???????套接字是網絡通信的端點(端點指通信的兩個參與者中的每一個)。允許不同進程之間或者同一進程不同線程之間通過網絡交換數據。它是應用程序與網絡之間的接口(是應用層與TCP/IP協議族通信的中間軟件抽象層)。套接字允許應用程序使用網絡協議(TCP/UDP)進行數據傳輸。
? ? ? ? ? ? ? ? ? ?
套接字描述符:
套接字描述符是一個由操作系統分配的整數,用于唯一標識一個打開的套接字。通過套接字描述符,程序可以對套接字進行讀寫操作、設置屬性、管理連接等。通過socket()函數創建一個新的套接字,就會返回一個int類型的整數,這個整數就是套接字描述符。可以使用它進行連接、發送、接收數據等。實例connect(套接字描述符) ?close(套接字描述符)。套接字描述符的生命周期是在創建套接字時由系統分配,并在套接字關閉時釋放,關閉套接字后,描述符可以被重新分配給其他套接字。系統為每個運行的進程維護一張單獨的文件描述符表,當創建一個socket時,就將其對應的文件描述符寫入到上述的文件描述符表中,操作系統把該描述符作為索引訪問進程描述符表,通過指針找到保存該套接字文件所有信息的數據結構。
? ? ? ? ? ? ? ? ? ? ? ??
創建套接字后,套接字內部包含很多字段,但是系統創建套接字后,大多數字段沒有填寫。應用程序在創建完套接字后,還需要調用其他過程來填充這些字段
? ? ? ? ? ? ? ? ? ? ? ? ?
協議(protocol):
網絡協議是用于數據交換的規則和標準。例如,TCP(傳輸控制協議)和UDP(用戶數據報協議)是兩個常用的協議。TCP是面向連接的,保證數據可靠性,而UDP是無連接的,速度快但并不保證可靠性。
端口(port):
端口是用來標識特定進程或服務的邏輯地址。例如,HTTP協議通常使用端口80,FEP協議使用端口21
IP地址(IP Address):
是網絡中每臺計算機的唯一標識。它用于定位網絡中的設備并進行數據傳輸
客戶端-服務器模型(client-server model)
在這個模型中,客戶端發起請求,服務器提供響應。客戶端和服務器之間通過網絡進行通信
SYN
用于建立連接
FIN
用于請求終止連接
ACK
用于確認收到的請求連接/確認關閉連接請求
序列號(sequence number)
用于數據包排序
確認號(acknowledgement number)
用于確認收到的數據包
TCP和UDP的詳細介紹
TCP傳輸控制協議
特點
- 面向連接:在數據傳輸之前,TCP必須先建立連接。客戶端和服務器通過三 ???次握手過程來建立連接。
- 可靠性:TCP提供可靠的數據傳輸。它通過序列號、確認應答、重傳機制來確保數據的完整性和順序
- 數據流控制:TCP使用滑動窗口來控制數據流量,防止接收端因處理能力不足而溢出
- 擁塞控制:TCP會動態調整發送速度,以避免網絡擁塞。常用的算法包括慢啟動、擁塞避免、快重傳和快恢復
- 數據包順序:數據包在TCP中按發送順序到達接收端。TCP為每個數據包分配序列號,并對數據包進行排序
- 錯誤檢測和糾正:TCP包含校驗和字段,用于檢測數據包在傳輸過程中是否被損壞。如果檢測到錯誤,TCP會重新傳輸
優點
- 可靠性高:數據傳輸過程中發生的數據丟失或錯誤會被重傳,確保數據的完整性和正確性。
- 提供順序保證:數據包按順序到達接收端,適合需要數據順序的應用場景,如文件傳輸、網頁瀏覽等。
- 流量控制:防止網絡擁塞,提供穩定的傳輸性能。
- 擁塞控制:動態調整數據發送速率,減少網絡擁塞。
缺點
-
?開銷較大:由于需要建立連接、維護狀態、處理錯誤和重傳機制,TCP協議的開銷較大。
延遲較高:建立連接和數據傳輸過程中進行的可靠性檢查和流量控制增加了延遲,不適合實時性要求高的應用。
-
TCP三次握手和四次揮手
三次握手
TCP協議建立連接的過程。這個過程確保雙方都能準備好進行數據傳輸。
過程:
1.SYN:客戶端發送連接請求
????????客戶端向服務器發送一個帶有SYN(同步)標志的數據包,表示請求建立連接。
????????標志位:SYN=1
????????序列號:客戶端選擇一個初始序列號(ISN:這個序列號是客戶端用于標識其發送的數據
????????的起始編號)在數據包中發送。
????????客戶端 → 服務器 : SYN, Seq = x(ISN)
2.SYN-ACK:服務器確認請求
????????服務器接收到客戶端的SYN數據包后,發送一個帶有SYN和ACK(確認)標志的數據包,確認收到連接請求。
????????標志位:SYN=1,ACK=1
????????確認號:服務器確認客戶端的序列號(x+1),這里的x+1代表的意思是確認 ?收到客戶端的
????????SYN數據包(序列號為x),并期望接收下一個客戶端 ?發來的序列號是x+1。并選擇自己的初
????????始序列號y。
????????服務器 → 客戶端 : SYN, ACK, Seq = y, Ack = x + 1
3.ACK:客戶端確認連接建立
????????客戶端收到服務器的SYN-ACK數據包后,發送一個帶有ACK標志的數據包,確認服務器的序列號(y)和自己的序列號(x+1)。
????????標志位:ACK=1
????????確認號:客戶端確認服務器的序列號(y+1),這里的y+1代表的意思是確認 ?收到服務端的
????????SYN數據包(序列號為y),并期待接收下一個服務器 ?發來的序列號是y+1。
????????客戶端 → 服務器 : ACK, Seq = x + 1, Ack = y + 1
經過三次握手后:連接建立成功,客戶端和服務器都進入已連接的狀態,可以開始傳輸數據。
? ? ? ? ? ? ? ? ? ? ?
????????客戶端調用connect時,觸發了連接請求,向服務器發送了SYN J包,這時connect進入阻塞狀態;服務器監聽到連接請求,即收到SUN J包,調用accept函數接收請求向客戶端發送SYN K,ACK J+1,這時accept進入阻塞狀態;客戶端收到服務器的SUN K,ACK J+1之后,這時connect返回,并對SYN K進行確認;服務器收到ACK K+1時,accept返回,至此三次握手完畢。
上述三次握手過程我們可以通過網絡抓包來查看具體流程:
例如:服務器開啟了9502端口。使用tcpdump來抓包
Tcpdump -iany tcp port 9502
使用telnet連接
telnet 127.0.0.1 9502
示例:
14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378, win 32792, options [mss 16396,sackOK,TS val 255474104 ecr 0,nop,wscale 3], length 0(1)
14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379, win 32768, options [mss 16396,sackOK,TS val 255474104 ecr 255474104,nop,wscale 3], length 0(2)
14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val 255474104 ecr 255474104], length 0(3)
其中: [S] 表示這是一個SYN請求
? ? ? ? ? ? [S.] 表示這是一個SYN+ACK確認包:
? ? ? ? ? ? [.] 表示這是一個ACT確認包,
? ? ? ? ? ? (client)SYN->(server)SYN->(client)ACT 就是3次握手過程
(1)104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378
客戶端IP localhost.39870 (客戶端的端口一般是自動分配的) 向服務器localhost.9502發送syn包(syn=j)到服務器》
syn包(syn=j) : syn的seq= 2927179378 (j=2927179378)
(2)14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379,
收到請求并確認:服務器收到syn包,并必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包:
此時服務器主機自己的SYN:seq:y= syn seq 1721825043。
ACK為j+1 =(ack=j+1)=ack 2927179379
(3)14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1,
客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1)
客戶端和服務器進入ESTABLISHED狀態后,可以進行通信數據交互。此時和accept接口沒有關系,即使沒有accepte,也進行3次握手完成。
三次握手后建立連接后的數據傳輸過程
接上:
14:13:01.415407 IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7
14:13:01.415432 IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0
14:13:01.415747 IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18
14:13:01.415757 IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0
其中:[P.]表示這是一個數據推送,可以是從服務器向客戶端推送,也可以是從客服端向服務器推送
????????[.] 表示這是一個ACT確認包
????????Win 4099是指滑動窗口的大小
????????Length 18指數據包的大小
四次揮手
TCP協議用來終止連接的過程。這個過程確保雙方都能正常關閉連接,避免數據丟失。
過程:
1.FIN:客戶端請求終止連接
????????客戶端向服務器發送一個帶有FIN(結束)標志的數據包,表示請求關閉連接。
????????標志位:FIN=1
????????序列號:客戶端發送的序列號u
????????客戶端 → 服務器 : FIN, Seq = u
2.ACK服務器確認關閉連接請求
????????服務器收到客戶端的FIN數據包后,發送一個帶有ACK標志的數據包,確認收到關閉通知。
????????標志位:ACK=1
????????確認號:服務器確認客戶端的序列號(u+1)
????????服務器 → 客戶端 : ACK, Seq = v, Ack = u + 1
3.FIN:服務器請求關閉連接
????????服務器在完成數據傳輸后,向客戶端發送一個帶有FIN標志的數據包,表示請求關閉連接。
????????標志位:FIN=1
????????序列號:服務器發送的序列號v+1
????????服務器 → 客戶端 : FIN, Seq = v + 1
4.客戶端確認關閉的連接請求
????????客戶端收到服務器的FIN數據包后,發送一個帶有ACK標志的數據包,確認收到關閉請求。
????????標志位:ACK=1
????????確認號:客戶端確認服務器的序列號v+1(按照正常邏輯來說確認好應該是 ?v+2,但由于這已
????????經是最后一步應答,不會再有更多的數據包發送, 因此這里寫v+1即可,即只需指定接收到
????????的seq,而不指定接下來期 望接收的seq)。
????????客戶端 → 服務器 : ACK, Seq = u + 1, Ack = v + 1
經過四次揮手后,連接成功終止,客戶端和服務器都進入關閉狀態。客戶端和服務器可能會經歷一個TIME_WAIT狀態,確保所有的數據都能被正確處理。
由于TCP連接是全雙工的,因此每個方向都必須單獨進行關閉。這個原則是當一方完成它的數據發送任務后就能發送一個FIN來終止這個方向的連接。收到一個 FIN只意味著這一方向上沒有數據流動,一個TCP連接在收到一個FIN后仍能發送數據。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。
(1)客戶端A發送一個FIN,用來關閉客戶A到服務器B的數據傳送(報文段4)。
(2)服務器B收到這個FIN,它發回一個ACK,確認序號為收到的序號加1(報文段5)。和SYN一樣,一個FIN將占用一個序號。
(3)服務器B關閉與客戶端A的連接,發送一個FIN給客戶端A(報文段6)。
(4)客戶端A發回ACK報文確認,并將確認序號設置為收到序號加1(報文段7)。
????????????????
1.某個應用進程首先調用close主動關閉連接,這時TCP發送一個FIN M;
2.另一端接收到FIN M之后,執行被動關閉,對這個FIN進行確認。它的接收也作為文件結束符傳遞給應用進程,因為FIN的接收意味著應用進程在相應的連接上再也接收不到額外數據;
3.一段時間之后,接收到文件結束符的應用進程調用close關閉它的socket。這導致它的TCP也發送一個FIN N;
4.接收到這個FIN的源發送端TCP對它進行確認。
這樣每個方向上都有一個FIN和ACK。
注意:[F] 表示這是一個FIN包,是關閉連接操作,client/server都有可能發起
為什么建立連接協議是三次握手,而關閉連接確實四次握手呢?
????????這是因為服務端的LISTEN狀態下的SOCKET當收到SYN報文的建立連接請求后,它可以把ACK和SYN(ACK起應答作用,而SYN起同步作用)放在一個報文里來發送。但關閉連接時,當接收到對方的FIN報文通知時,它僅僅表示對方沒有數據發送給你了;但未必你所有的數據都全部發送給對方了,所喲你可以未必馬上關上socket,也即你可能還需要發送一些數據給對方后,再發送FIN報文給對方來表示你同意現在可以關閉連接了,所以斷開連接請求的ACK報文和FIN報文多數情況下都是分開發送的。
為什么TIME_WAIT狀態還需要等待2MSL后才能返回到CLOSE狀態?
????????這是因為雖然雙方都同意關閉連接了,而且握手的4個報文也都協調和發送完畢,按理可以直接回到CLOSED狀態(就好比從SYN_SEND狀態到ESTABLISH狀態那樣);但是因為我們必須要假想網絡是不可靠的,你無法保證你最后發送的ACK報文會一定被對方收到,因此對方處于LAST_ACK狀態下的SOCKET可能會因為超時未收到ACK報文,而重發FIN報文,所以這個TIME_WAIT狀態的作用就是用來重發可能丟失的ACK報文。
-
UDP用戶數據報協議
特點
-
無連接:UDP是無連接的,發送數據之前無需建立連接。每個數據報獨立處理。
不可靠:UDP不提供數據傳輸的可靠性保證。數據包可能丟失、重復或亂序,UDP不會進行重傳或順序保證。
沒有流量控制、擁塞控制:發送速度不受限,接收端可能會遭遇數據溢出不會調整發送速率,可能導致網絡阻塞的情況 頭部開銷小:頭部信息較少,占用帶寬少。
優點
低延遲:由于無連接和缺乏可靠性機制,UDP提供較低的傳輸延遲
開銷小:頭部開銷小,適合對傳輸速率要求高的應用
簡單:無需建立連接和維護連接,易于實現
支持廣播和組播:UDP支持將數據包發送到多個接受者,適合需要廣播或組播的應用場景。
缺點
不可靠:不能保證數據的到達、完整性和順序,適合對可靠性要求不高的場景。
沒有流量控制和擁塞控制:可能導致數據丟失或網絡擁塞,尤其是在高流量環境下。
TCP/IP(傳輸控制協議/互聯網協議)
????????是現代計算機通信的基礎,是一組協議的集合,用于在計算機中進行數據傳輸和通信。TCP/IP分為多個層次,每一個層次負責不同的通信功能。
分層(4層或5層):
1.應用層(application layer)
為應用程序提供網絡服務的接口,處理特定的應用協議。
協議:http用于網頁傳輸、FTP用于文件傳輸、SMTP用于電子郵件傳輸、DNS用于域名解析。
2.傳輸層(transport layers)
提供主機之間的數據傳輸服務,確保數據完整性和可靠性。
協議:TCP,UDP
示例:應用程序通過TCP/UDP進行數據傳輸
3.網絡層(network layer)
負責數據包的路由選擇和轉發,確保數據從源主機到達目標主機
協議:IPV4/IPV6
4.鏈路層(link layer)
處理物理網絡連接和數據幀的傳輸,負責在局部網絡中傳輸數據。
協議:Ethernet(用于局域網的標準協議)、wifi(用于無線網的標準協議)
5.物理層(physical layer)(可選)
處理物理媒介的傳輸,例如電纜、光纖、或無線信號。
Socket在TCP/IP中的位置與作用
位于應用層與傳輸層/網絡層之間,是應用層與TCP/IP協議族通信的中間軟件抽象層。
? ? ? ? ? ? ?
TCP客戶端與TCP服務器端通信的大致流程
????????服務器端先初始化一個socket,然后端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端連接。在這時如果有個客戶端初始化一個socket,然后連接服務器connect(),如果連接成功,這是客戶端與服務器的連接就建立成功了。客戶端發送數據請求,服務器接受請求并處理請求,然后把回應數據發送給客戶端,客戶端讀取數據,最后關閉連接,一次交互結束。
? ? ? ? ? ? ? ? ? ??
網絡編程的關鍵步驟
創建套接字(socket creation)
套接字是網絡通信的基礎。創建套接字時,需要指定協議族(如IPv4或IPv6)、套接字類型(如流式套接字或數據報套接字)以及協議(如TCP或UDP)
綁定(binding)
綁定將套接字與本地地址(IP地址和端口)關聯起來。這使得套接字能夠在指定的端口上接收數據。
監聽(listening)
監聽是服務端準備接受客戶端連接的過程。服務器通過監聽特定的端口來等待客戶端的連接請求。
接受連接(accepting connections)
在監聽狀態下,服務器接受來自客戶端的連接請求。一旦連接建立,服務器和客戶端可以進行數據交換
發送和接收數據(sending and receiving data)
一旦建立連接,數據可以在客戶端和服務器之間傳輸。這可以是請求和響應,或任何其他數據。
關閉套接字(closing socket)
數據傳輸完成后,套接字需要關閉,以釋放資源。
基本的socket函數
socket()
int socket(int protofamily,int type,int protocol);
返回sockfd套接字描述符,這個描述符唯一標識一個socket,會用這個sockfd進行后續的操作。
參數:
①protofamily協議域/族:決定了socket的地址類型,在通信中必須采用對應的地址。
AF_INET:要用ipv4地址(32位)與端口號(16位的)組合
AF_INET6:要用ipv6地址(128位)與端口號(16位的)組合
AF_UNIX/AF_LOCAL:要用一個絕對路徑名作為地址
②type:指定socket類型
SOCKET_STREAM:字節流套接字,適用于TCP
SOCKET_DGRAM:數據報套接字,適用于UDP
③protocol:指定協議
IPPROTO_TCP:TCP傳輸協議
IPPROTO_UDP
IPPROTO_SCTP
IPPROTO_TIPC
bind()
int bind(int sockfd,const struct *addr,socklen_t addrlen);
????????將一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。
????????通常服務器在啟動的時候都會綁定一個眾所周知的地址(如服務器+端口號),用于提供服務,客戶就可以通過它來連接服務器;而客戶端就不用指定,他會有一個系統自動分配的端口和ip的地址組合。這就是為什么通常服務器在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。
參數:
①sockfd:socket描述符
②addr:一個const struct sockaddr*指針,指向要綁定給sockfd的協議地址。這個根據socket()函數中的參數protofamily不同而不同
對于AF_INET(ipv4)來說結構體定義為:
對于AF_INET6(ipv6)來說結構體定義為:
③addrlen:對應的是地址的長度
補充:字節序
網絡字節序:TCP/IP中二進制整數在網絡中傳輸時的次序
以四個字節的32bit值傳輸次序為例:首先是0-7bit,其次是8-15bit,然后16-23bit,最后是24-31bit,即多字節的數據按照從高位到低位的順序發送。這種稱作大端字節序。所以在將一個地址綁定到socket的時候,先將主機字節序轉換為網絡字節序(即大端字節序)
字節序:計算機系統內部如何存儲多字節數據(如整數)的字節順序
- 大端字節序Big-Endian:高位字節存放在內存的低地址端,低位字節排放在內存 的高地址端。整數 0x12345678 會以 12 34 56 78 的順 序存儲在內存中。
- 小端字節序Little-Endian:高位字節存儲在內存的高位地址,低位字節存儲在內 ??存的低位地址。0x12345678 會以 78 56 34 12 的順序 ??存儲在內存中。
轉換函數:
在實際編程中,通常會使用系統提供的函數來進行字節序的轉換
htonl() 和 htons():將主機字節序轉換為網絡字節序(htonl 是 "host to network long" 的縮寫,htons 是 "host to network short" 的縮寫)。
ntohl() 和 ntohs():將網絡字節序轉換為主機字節序(ntohl 是 "network to host long" 的縮寫,ntohs 是 "network to host short" 的縮寫)。
inet_pton/inet_ntop? ??#include <arpa/inet.h> ?
支持ipv4和ipv6地址格式,這兩個函數確保數據在網絡上傳時能正確解釋和處理
inet_pton:將文本格式的IP地址轉換為網格格式的二進制地址
int inet_pton(int af, const char *src, void *dst);
成功返回1,失敗返回0/-1,并設置erron
參數:
af:地址族,AF_INET/AF_INET6
src:指向一個c字符串的指針,該字符串表示IP地址(文本格式)
dst:指向一個內存區域的指針,用于存儲轉換后的網絡格式地址。對于ipv4 指向一個struct in_addr結構體,對于ipv6指向一個struct in6_addr結構體。
inet_ntop:將網格格式的二進制地址轉換為文本格式的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
成功時返回dst指針,失敗時返回nullptr,并設置errno
參數:
af:地址族,AF_INET/AF_INET6
src:指向一個內存區域的指針,用于存儲轉換后的網絡格式地址。對于ipv4 指向一個struct in_addr結構體,對于ipv6指向一個struct in6_addr結構體。
dst:指向一個字符數組的指針,用于存儲轉換后的文本格式地址
size:dst數組的大小,以字節為單位
listen()? Connect()
????????作為一個服務器,在調用socket()、bind()之后就會調用listen()來監聽這個socket(服務器端的socket),如果客戶端這時調用connect()發出連接請求,服務器就會接收到這個請求。
int listen(int sockfd,int backlog);
參數:
①sockfd:要監聽的socket描述字(服務器端的)
②backlog:相應的socket可以排隊的最大連接個數
注意:socket()創建的socket默認是主動類型的,listen()將socket變為被動 ?? ??類型,等待客戶端的連接請求。
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
參數:
①sockfd:客戶端socket描述字
②addr:服務器的socket地址
③addrlen:socket地址的長度
注意:客戶端通過調用connect函數來建立與TCP服務器的連接。
accept()函數
????????TCP服務器依次調用socket()、bind()、listen()之后就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()之后就可以向TCP服務器發送一個連接請求,TCP服務器監聽到這個請求后就會調用accept()函數接受請求,如果accept成功返回,這樣連接就建立好了。之后可以通過對accept返回的套接字(connect_fd)的操作來完成與客戶的網絡I/O操作(類似于普通文件的讀寫I/O操作)。
int accept(int sockfd , struct sockaddr *addr , socklen_t *addrlen);
會返回一個連接的描述符,connect_fd
參數:
①sockfd:這是一個用于監聽的套接字描述符,即通過socket()、bind()、 listen()函數創建并設置的服務器端套接字,accept函數將 檢查這個套接字是否有來自于客戶端的連接請求。如果有他將接受這個連接請求,并返回一個新的套接字描述符 (connect_fd),用于與客戶端進行數據通信。
②addr:用于存儲客戶端的地址信息。這個地址描述了連接到服務器 ??的客戶端的網絡地址(ip和端口號).當accept()成功返回時,addr指向的內存區域將被填充為客戶端的地址信息(最開始 ??傳入的是一個空的)。若客戶端的地址信息不重要,可將這個參數設置為NULL。
③addlen:指定addr指向的地址的結構體的大小。如果不需要獲取客戶端地址信息,可以將這個參數設置為NULL 。
注意:accept默認會阻塞進程,直到有一個客戶建立連接后返回,它返回的是
個新可用的套接字,這個套接字是連接套接字connect_fd。
區分兩種套接字:
- 監聽套接字:如accept的參數sockfd他是監聽套接字,是服務器調用listen()之后生成的,由一個主動連接的套接字變身為一個監聽套接字。
- 連接套接字:accept()函數返回的已連接的socket描述字(一個連接套接字), 他代表著一個網絡已經存在的點點連接。
注意:服務器只會創建唯一一個監聽套接字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接都創建了一個已連接的socket描述字,當服務器完成了對某個客戶的服務,相應的已連接socket 描述字就會被關閉。即監聽套接字在一個服務器的生命周期中只能有一個,而連接套接字可以有多個。連接套接字沒有占用新的端口與客戶端通信,依然使用的是與監聽套接字socketfd一樣的端口號。
IO操作
在上述4步驟后,服務器端與客戶端已經建立連接。可以用網絡I/O進行讀寫操作了,即實現網絡中不同進程與線程之間的通信。
網絡I/O操作有下面常見的幾組函數:
read()/write()
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
????????read函數是負責從fd中讀取內容。當讀成功時,read返回實際所讀的字節數,如果返回的值是0表示已經讀到文件的結束了,小于0表示出現了錯誤。如果錯誤為EINTR說明讀是由中斷引起的,如果是ECONNREST表示網絡連接出了問題。
ssize_t write(int fd, const void *buf, size_t count);
????????write函數將buf中的nbytes字節內容寫入文件描述符fd.成功時返回寫的字節數。失敗時返回-1,并設置errno變量。 在網絡程序中,當我們向套接字文件描述符寫時有兩種可能。1)write的返回值大于0,表示寫了部分或者是全部的數據。2)返回的值小于0,此時出現了錯誤。我們要根據錯誤類型來處理。如果錯誤為EINTR表示在寫的時候出現了中斷錯誤。如果為EPIPE表示網絡連接出現了問題(對方已經關閉了連接)。
recv()/send()
#include <sys/types.h>
#include <sys/socket.h>
發送數據
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
參數:
Sockfd:套接字文件描述符
Buf:指向要發送的數據的緩沖區
Len:要發送的數據的字節數
Flags:通常設置為0,但可以指定其他標志,如MSG_OOB等
發送成功后返回發送的字節數;出錯時返回-1,并設置‘errno’以指示錯誤原因。
注意:#include <cerrno> 引入頭文件后就能使用’errno’,它是一個全局變量, ??它在出錯時被系統調用和庫函數設置,可以通過#include <cstring> ???strerror(errno)來將erron轉為字符串,方便打印。
errno有以下幾種
①EINVAL:參數無效
②ENGAIN/EWOULDBLOCK:資源暫時不可用(通產在非阻塞模式下)
③ECONNRESET:連接被對方重置
④ENOTCONN:套接字未連接(操作未連接的套接字)
接收數據
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
參數說明:
sockfd:套接字文件描述符,用于標識要接收數據的套接字。
buf:指向存放接收數據的緩沖區的指針。
len:指定緩沖區的大小(以字節為單位),即本次 recv 調用最多接收的字節數。
flags:通常設為 0,但可以指定其他標志,例如 MSG_OOB(接收帶外數據)等。
返回值:
成功時,返回實際接收到的字節數。
連接已關閉時,返回 0,表示對方已正常關閉連接。
失敗時,返回 -1,并設置 errno 以指示具體的錯誤原因。
recvmsg()/sendmsg()
從套接字發送數據
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
從套接字接收數據
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
參數:
Socket:套接字的文件描述符
Msg:指向struct msghdr結構的指針,該結構定義了要發送或接受的數據。
struct msghdr {void *msg_name; // 消息的目標地址socklen_t msg_namelen; // 地址長度struct iovec * msg_iov; // 指向數據緩沖區的指針size_t msg_iovlen; // 緩沖區數組中的元素數量void *msg_control; // 控制數據的指針(例如,用于傳輸文件描述符)size_t msg_controllen; // 控制數據的長度int msg_flags; // 消息標志
};
Flags:操作的標志位,通常為0或其他標志位(如MSG_DONTWAIT)。
recvfrom()/sendto()
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
參數說明:
sockfd:套接字文件描述符,用于標識發送數據的套接字。
buf:指向要發送數據的緩沖區的指針。
len:要發送的數據的字節數。
flags:通常設為 0,但可以指定其他標志,例如 MSG_CONFIRM(用于UDP防止ARP欺騙)等。
dest_addr:指向 sockaddr 結構體的指針,表示目標地址(IP 和端口)。
addrlen:dest_addr 結構體的大小,以字節為單位。
返回值:
成功時,返回實際發送的字節數。
失敗時,返回 -1,并設置 errno 以指示具體的錯誤原因
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
參數說明:
sockfd:套接字文件描述符,用于標識接收數據的套接字。
buf:指向存放接收數據的緩沖區的指針。
len:指定緩沖區的大小(以字節為單位),即本次 recvfrom 調用最多接收的字節數。
flags:通常設為 0,但可以指定其他標志,例如 MSG_WAITALL(等待完整數據)。
src_addr:指向 sockaddr 結構體的指針,存儲發送方的地址信息(IP 和端口)。如果不關心發送方地址,可以傳 NULL。
addrlen:指向 socklen_t 類型的指針,表示 src_addr 結構體的大小,調用后會被填充為實際地址長度。如果 src_addr 傳 NULL,則 addrlen 也可以傳 NULL。
返回值:
成功時,返回實際接收到的字節數。
連接已關閉時,返回 0,表示對方已正常關閉連接。
失敗時,返回 -1,并設置 errno 以指示具體的錯誤原因。
close()
????????在服務器與客戶端建立連接之后,會進行一些寫操作,完成了讀寫操作就要關閉相應的socket描述字。
int close(int fd);
①fd為某一connect后的已連接套接字符時:此時會終止與該特定用戶的連接。這個操作將使得服務器不再與這個客戶端進行數據交換。客戶端會收到一個連接終止的信號,從而知道連接已經被關閉。此時仍然接受新的客戶端建立連接。
②fd為監聽套接字符時:此時會停止服務器新的連接請求。對于已經建立的連接,服務器可以繼續與這些已經連接的客戶端進行數據交換,直到這些套接字被關閉(客戶端調用了close)
網絡編程入門實例
服務器端:一直監聽本機的8000號端口,如果收到連接請求,將接收請求并接收客戶端發來的消息,并向客戶端返回消息
#include <iostream>
#include <cstring> // memset、strerror
#include <cstdlib> // std::exit
#include <cerrno> // errno
#include <sys/types.h> // ssize_t
#include <sys/socket.h> // socket
#include <netinet/in.h> // sockaddr_in結構體
#include <unistd.h> // POSIX系統調用,fork、close
#include <fcntl.h> // 文件控制選項#define DEFAULT_PORT 8000 // 默認端口號
#define MAXLINE 4096 // 接收緩沖區的最大字節數int main(int argc, char** argv) {int socket_fd, connect_fd; // 套接字文件描述符struct sockaddr_in servaddr; // 服務器地址結構體char buff[MAXLINE]; // 用于存儲接收到的數據 ssize_t n; // 接收到的字節數// 初始化套接字socket_fd = socket(AF_INET, SOCK_STREAM, 0); // IPV4的流式套接字if (socket_fd == -1) {std::cerr << "創建套接字失敗: " << std::strerror(errno) << " 錯誤碼 " << errno << std::endl;std::exit(EXIT_FAILURE);}// 初始化服務器地址結構std::memset(&servaddr, 0, sizeof(servaddr)); // 將服務器地址結構清零servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址設置為INADDR_ANY,讓系統自動選擇本機IP地址servaddr.sin_port = htons(DEFAULT_PORT); // 設置端口號// 將本地地址綁定到創建的套接字上if (bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {std::cerr << "綁定套接字失敗: " << std::strerror(errno) << " 錯誤碼 " << errno << std::endl;std::exit(EXIT_FAILURE);}// 開始監聽客戶端的連接請求if (listen(socket_fd, 10) == -1) {std::cerr << "監聽套接字失敗: " << std::strerror(errno) << " 錯誤碼 " << errno << std::endl;std::exit(EXIT_FAILURE);}std::cout << "========等待客戶端連接=========" << std::endl;while (true) {// accept之前是阻塞的,直到客戶端連接connect_fd = accept(socket_fd, nullptr, nullptr); // 接收客戶端連接if (connect_fd == -1) {std::cerr << "接收客戶端連接失敗: " << std::strerror(errno) << std::endl;continue; // 繼續等待其他客戶端連接}// 連接成功后,接收客戶端發送的數據n = recv(connect_fd, buff, MAXLINE, 0); // 從客戶端接收數據if (n < 0) {std::cerr << "接收數據失敗: " << std::strerror(errno) << std::endl;close(connect_fd); // 關閉與此客戶端的連接套接字continue; // 繼續等待其他客戶端連接}// 向客戶端發送數據if (fork() == 0) { // 創建子進程來處理客戶端請求if (send(connect_fd, "hello connect successful", 26, 0) == -1) {std::cerr << "發送數據失敗: " << std::strerror(errno) << std::endl;}close(connect_fd);std::exit(EXIT_SUCCESS);}// 父進程繼續處理其他客戶連接buff[n] = '\0'; // 添加字符結束符std::cout << "接收到客戶消息: " << buff << '\n'; // 打印接收到的消息close(connect_fd);}close(socket_fd); // 關閉監聽套接字return 0;
}
客戶端
#include <iostream> // 標準輸入輸出
#include <cstring> // memset, strerror 等函數
#include <cstdlib> // exit 等函數
#include <cerrno> // errno 全局錯誤變量
#include <sys/types.h> // 數據類型,如 ssize_t
#include <sys/socket.h> // socket, connect 等函數
#include <netinet/in.h> // sockaddr_in 結構體
#include <arpa/inet.h> // inet_pton 函數:將IP地址文本轉成網絡字節序
#include <unistd.h> // close 函數#define MAXLINE 4096 // 接收和發送緩沖區的最大字節數int main(int argc, char** argv) {int sockfd; // 套接字描述符ssize_t n, rec_len; // n:發送字節數,rec_len:接收到的字節數char recvline[MAXLINE]; // 接收緩沖區char sendline[MAXLINE]; // 發送緩沖區char buf[MAXLINE]; // 臨時緩沖區,接收服務器響應struct sockaddr_in servaddr; // 服務器地址結構體// 檢查命令行參數是否為2(程序名 + IP地址)if (argc != 2) {std::cerr << "用法:./client <ipaddress>\n";std::exit(EXIT_FAILURE);}// 創建套接字:AF_INET(IPv4),SOCK_STREAM(TCP)sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {std::cerr << "創建套接字失敗: " << std::strerror(errno)<< " (錯誤碼: " << errno << ")\n";std::exit(EXIT_FAILURE);}// 初始化服務器地址結構體std::memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET; // 地址族 IPv4servaddr.sin_port = htons(8000); // 設置端口(網絡字節序)// 將IP地址從文本轉換為網絡字節序if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {std::cerr << "無法解析IP地址: " << std::strerror(errno)<< " (錯誤碼: " << errno << ")\n";std::exit(EXIT_FAILURE);}// 連接服務器if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {std::cerr << "連接失敗: " << std::strerror(errno)<< " (錯誤碼: " << errno << ")\n";std::exit(EXIT_FAILURE);}// 從標準輸入讀取要發送的消息std::cout << "請輸入要發送的消息:";std::cin.getline(sendline, MAXLINE);// 發送數據if (send(sockfd, sendline, std::strlen(sendline), 0) < 0) {std::cerr << "發送消息失敗: " << std::strerror(errno)<< " (錯誤碼: " << errno << ")\n";std::exit(EXIT_FAILURE);}// 接收服務器返回的數據rec_len = recv(sockfd, buf, MAXLINE, 0);if (rec_len < 0) {std::cerr << "接收數據失敗: " << std::strerror(errno)<< " (錯誤碼: " << errno << ")\n";std::exit(EXIT_FAILURE);}// 添加字符串終止符并打印接收到的消息buf[rec_len] = '\0';std::cout << "接收到的消息: " << buf << '\n';// 關閉連接close(sockfd);return 0;
}