????????套接字(Socket)是計算機網絡數據通信的基本概念和編程接口,允許不同主機上的進程(運行中的程序)通過網絡進行數據交換。它為應用層軟件提供了發送和接收數據的能力,使得開發者可以在不用深入了解底層網絡細節的情況下進行網絡編程,屏蔽了應用程序對底層協議的操作,使得應用程序使用網絡進行數據傳輸變得更簡便,使代碼更容易維護。socket英文直譯為“插座”,可以理解為應用層調用網絡服務的接口。
????????套接字主要由以下三個屬性組成:
????????網絡地址:通常是IP地址,用于標識網絡上的設備。
????????端口號:用于標識設備上的特定應用或進程。端口號是一個16位的數字,范圍從0到65535。
????????協議:如TCP(傳輸控制協議)或UDP(用戶數據報協議),定義了數據傳輸的規則和格式。
????????根據數據傳輸方式的不同,主要有兩種類型的套接字:
????????流套接字(Stream Sockets):基于TCP協議,提供面向連接、可靠的數據傳輸服務。數據像流水一樣連續傳輸,接收方按發送順序接收數據,適用于需要準確無誤傳輸數據的應用,如網頁服務器。
????????數據報套接字(Datagram Sockets):基于UDP協議,提供無連接的數據傳輸服務。每個報文段獨立傳輸,可能會丟失或無法保證順序,適用于對傳輸速度要求高但可以容忍一定丟包率的應用,如在線視頻會議。
????????套接字通過封裝TCP/IP協議細節,提供了一組API,允許應用程序創建套接字、綁定地址和端口、監聽連接、接受連接、發送和接收數據等。在網絡通信中,通常一個套接字負責監聽和接受外部連接(服務器套接字),另一個套接字負責發起連接(客戶端套接字)。
????????套接字的引入極大地簡化了網絡編程的復雜度,使得開發者可以專注于應用邏輯的實現,而無需深入了解網絡協議棧的內部工作原理。通過使用套接字,可以輕松實現不同計算機之間的數據交換,支持構建分布式系統和多種網絡應用。
網絡字節序和主機字節序
????????在網絡編程中,特別是在跨平臺和網絡通信時,字節序(Byte Order)是非常重要的概念。字節序指的是多字節數據在內存中的存儲順序。主要有兩種字節序:
????????大端字節序(Big-Endian):高位字節存儲在內存的低地址處,低位字節存儲在高地址處。這種字節序遵循自然數字的書寫習慣,也被稱為網絡字節序(Network Byte Order)或網絡標準字節序,因為它在網絡通信中被廣泛采用,如IP協議就要求使用大端字節序。
????????小端字節序(Little-Endian):低位字節存儲在內存的低地址處,高位字節存儲在高地址處。這是Intel x86-64架構以及其他一些現代處理器普遍采用的字節序,稱為主機字節序(Host Byte Order)。
????????在網絡通信中,為了讓不同字節序的主機能夠相互理解對方的數據,常常需要進行字節序轉換。例如,發送數據前需要將主機字節序轉換為網絡字節序,接收數據后則需要將網絡字節序轉換為主機字節序。例如,如果你有一個IP地址或端口號(通常存儲為整數)需要在網絡上傳輸,就需要先使用htons()或htonl()將其轉換為網絡字節序,然后在網絡另一端接收時,使用ntohs()或ntohl()將其轉換回主機字節序。這樣可以確保數據在網絡中的傳輸不受不同主機字節序的影響。
????????htol函數
? ? ? ? 作用是將無符號整數 hostlong 從主機字節順序(h)轉換為網絡字節順序(n),一般用于ip地址的轉換。
? ? ? ? 函數原型為uint32_t htonl(uint32_t hostlong);
????????htos函數
????????作用是將無符號短整數 hostshort 從主機字節順序(h)轉換為網絡字節順序(n)。一般用于端口號的轉換。
????????函數原型為uint16_t htons(uint16_t hostshort);
? ? ? ? ntohl函數
? ? ? ? 作用是將無符號整數 netlong 從網絡字節順序(n)轉換為主機字節順序(h)。
????????函數原型為uint32_t ntohl(uint32_t netlong);
? ? ? ? ntohs函數
? ? ? ? 作用是將無符號短整數 netshort 從網絡字節順序(n)轉換為主機字節順序(h)。
? ? ? ? 函數原型為uint16_t ntohs(uint16_t netshort);
? ? ? ? 用于將ip地址轉換為網絡字節序的函數推薦以下幾種
? ? ? ? inet_aton函數
? ? ? ? 函數原型為int inet_aton(const char *cp, struct in_addr *inp);
? ? ? ? 作用是將來自 IPv4 點分十進制表示法的 Internet 主機地址 cp 轉換為二進制形式(以網絡字節順序)并將其存儲在 inp 指向的結構體中。
????????return int 成功返回 1;失敗 返回 0
????????inet_pton函數
? ? ? ? 函數原型為int inet_pton(int af, const char *src, void *dst);
? ? ? ? 作用是字符串格式轉換為sockaddr_in格式
????????int af: 通常為 AF_INET 用于IPv4地址,或 AF_INET6 用于IPv6地址
????????char *src: 包含IP地址字符串的字符數組,如果是IPv4地址,格式為點分十進制(如 "192.168.1.1");如果是IPv6地址,格式為冒號分隔的十六進制表示(如 "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
????????void *dst:指向一個足夠大的緩沖區(對于IPv4是一個struct in_addr結構體,對于IPv6是一個struct in6_addr結構體),用于存儲轉換后的二進制IP地址
????????return int : 成功轉換返回0; 輸入地址錯誤返回1;發生錯誤返回-1
????????inet_ntoa函數
? ? ? ? 函數原型為char *inet_ntoa(struct in_addr in);
? ? ? ? 作用是將主機字節序存儲的ip地址轉換為字符型
????????in 將以網絡字節順序給出的 Internet 主機地址 in 轉換為 IPv4 點分十進制表示法的字符串。字符串存儲在靜態分配的緩沖區中,后續調用將覆蓋該緩沖區。
????????return char* 緩沖區指針
TCP協議開發常用函數
socket函數
? ? ? ? 函數原型為int socket(int domain, int type, int protocol);
? ? ? ? 作用是在通信域中創建一個未綁定的socket,并返回一個文件描述符,該描述符可以在后續對socket進行操作的函數調用中使用
????????domain用于指定要創建套接字的通信域。一般使用以下三種。
????????AF_UNIX:本地通信,通常用于 UNIX 系統間的進程間通信。
????????AF_INET:IPv4 互聯網協議。
????????AF_INET6:IPv6 互聯網協議。
????????type 用于指定要創建的socket類型,一般使用以下兩種
????????SOCK_STREAM:提供序列化、可靠的、雙向的、基于連接的字節流。可以支持帶外數據傳輸機制。
????????SOCK_DGRAM:支持數據報(無連接、不可靠的固定最大長度的消息)。
? ? ? ? 使用TCP協議就選第一個,UDP協議選第二個
? ? ? ? 這個參數還可以和以下兩種配合使用,可以同時設置
????????SOCK_NONBLOCK:在新文件描述符引用的打開文件描述符上設置 O_NONBLOCK 文件狀態標志(參見 open(2))。使用此標志可以節省調用 fcntl(2) 來實現相同結果的額外調用。
????????SOCK_CLOEXEC:在新文件描述符上設置關閉時執行(FD_CLOEXEC)標志。
????????protocol 指定要與socket一起使用的特定協議。指定協議為 0 會導致 socket() 使用適用于所請求的socket類型的未指定的默認協議,一般設置為0即可,
? ? ? ? 成功創建則返回文件描述符,失敗返回-1
bind函數
? ? ? ? 函數原型為int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
? ? ? ? 作用是當使用 socket創建套接字時,它存在于一個名稱空間(地址族)中,但沒有為其分配地址。bind() 將由 addr 指定的地址(ip地址和端口號)分配給文件描述符 sockfd 所引用的套接字。addrlen 指定了 addr 指向的地址結構的大小(以字節為單位)。傳統上,這個操作被稱為“給套接字分配一個名稱”
????????ockfd 套接字文件描述符
????????addr 指定的地址。地址的長度和格式取決于socket的地址族
????????addrlen addr 指向的地址結構的大小(以字節為單位)。填sizeof獲取struct sockaddr類型變量的大小
????????return int 成功 0 失敗 -1
地址族的概念
????????在網絡編程中,地址族(Address Family)指定了套接字(socket)使用的網絡協議類型以及地址的格式。簡而言之,地址族決定了網絡通信的范圍和方式,比如是在同一臺機器上的進程間通信,還是在網絡上不同主機間的通信。每種地址族都支持特定類型的通信協議和地址格式。下面是一些常見的地址族:
① AF_INET
代表IPv4網絡協議的地址族,使用32位地址。
主要用于互聯網上的通信。
地址格式通常為點分十進制,如192.168.1.1。
② AF_INET6
代表IPv6網絡協議的地址族,使用128位地址。
是IPv4的后繼,旨在解決IPv4地址耗盡問題,并提供更多的功能。
地址格式為冒號分隔的十六進制,如2001:0db8:85a3:0000:0000:8a2e:0370:7334。
③ AF_UNIX (或 AF_LOCAL)
用于同一臺機器上的進程間通信(IPC)。
使用文件系統路徑名作為地址。
這種方式不通過網絡層進行數據傳輸,而是在操作系統內部完成,因此效率較高。
? ? ? ? 上述函數都使用了struct sockaddr結構體,他存儲了地址族和ip地址,端口等信息,聲明如下
struct sockaddr {
? ? sa_family_t sa_family; // 地址家族,如 AF_INET、AF_INET6、AF_UNIX 等
? ? char ? ? ? ?sa_data[14]; // 用于存儲具體地址數據的數組,其布局取決于地址
}
? ? ? ? 如果我們使用常見的IPV4,IPV6,這樣將ip地址,端口號一起放在一個char數組中,很難對其進行讀取或者賦值,所以對于IPV4協議,在設置時使用struct sockaddr_in結構體結合pton函數直接轉換為網絡字節序更加方便,在調用函數時直接強轉為struct sockaddr型即可
struct sockaddr_in {
? ? sa_family_t ? ?sin_family; /* 地址族:AF_INET */
? ? in_port_t ? ? ?sin_port; ? /* 端口號,網絡字節順序 */
? ? struct in_addr sin_addr; ? /* 互聯網地址 */
};
sin_family 總是設置為 AF_INET。
sin_port 包含端口號,以網絡字節順序表示。低于 1024 的端口號稱為特權端口(或有時稱為:保留端口)。只有特權進程(在 Linux 中:具有 CAP_NET_BIND_SERVICE 用戶命名空間中的權限,控制其網絡命名空間)可以綁定到這些套接字。
例如
struct sockaddr_in server_addr
inet_pton(AF_INET,"0.0.0.0",&server_addr.sin_addr); //本機ip地址可以表示為0.0.0.0
server_addr.sin_port = htons(6666);
listen函數
? ? ? ? 函數原型為int listen(int sockfd, int backlog);
? ? ? ? 作用是將 sockfd 指定的套接字標記為被動套接字,即將用于使用 accept接受傳入的連接請求。由服務端調用
????????sockfd 監聽連接請求的套接字文件描述符,也就是客戶端的套接字文件描述符,要求該描述符已經通過bind函數綁定到一個本地地址。
????????backlog 未被及時響應的連接可以被放入隊列等待連接,該參數指定等待隊列可以容納的最大連接數
????????return int 成功 0 失敗 -1
accept函數
? ? ? ? 函數原型為int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
????????作用是從監聽套接字 sockfd 的待處理連接隊列中提取第一個連接請求,創建一個新的連接套接字,并返回指向該套接字的新文件描述符。返回的是客戶端套接字的文件描述符,(是返回的可以和客戶端通信的文件描述符,也可以理解為是客戶端的文件描述符,可以通過此來和客戶端進行通信)新創建的套接字不處于監聽狀態,就與之后主動連接的客戶端的套接字形成了一對一的關系,原始套接字 sockfd 不受此調用的影響。如果調用之后,沒有客戶端來連接,就會掛起等待(阻塞),等到接收到為止。
????????sockfd 一個使用 socket(2) 創建、使用 bind(2) 綁定到本地地址,并在 listen(2) 后監聽連接的套接字。
????????addr 要么是一個空指針,要么是一個指向 sockaddr 結構的指針,用于返回連接socket的地址,即客戶端的ip地址和端口號信息
?????????addrlen 如果 address 不是空指針,則為一個指向 socklen_t 對象的指針,該對象在調用前指定提供的 sockaddr 結構的長度,并在調用后指定存儲地址的長度。先定義一個socklen_t 類型的變量,用sizeof接收大小,然后傳入指針
????????return int 返回一個新的套接字文件描述符,用于與客戶端通信,所以可以看做是客戶端的文件描述符,如果失敗返回-1,并設置errno來表示錯誤原因
connect函數
? ? ? ? 函數原型為int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
? ? ? ? 由客戶端調用,作用是與服務端建立連接。
????????sockfd 客戶端套接字的文件描述符
????????addr 指向sockaddr結構體的指針,包含目的地地址信息
????????addrlen 指定addr指向的結構體的大小
????????return int 成功 0,失敗 -1,并設置errno以指示錯誤原因
send函數
? ? ? ? 函數原型為ssize_t send(int sockfd, const void *buf, size_t len, int flags);
????????用于向另一個套接字傳輸消息。默認會阻塞,需要不阻塞的需要設置flags
????????sockfd 發送套接字的文件描述符。
????????buf 發送緩沖區,并非操作系統分配為服務端和客戶端分配的緩沖區,而是用戶為了發送數據,自己維護的字節序列。const修飾表名它是“只讀”的,即send函數不會修改這塊內存的內容。
????????len 要發送的數據的字節長度。它決定了從buf指向的緩沖區中將發送多少數據。
????????flags flags 對于大多數應用,這個參數被設置為0,表示不使用任何特殊行為。
????????MSG_DONTWAIT 啟用非阻塞操作;如果操作會阻塞,則返回 EAGAIN 或 EWOULDBLOCK。這提供了類似于設置 O_NONBLOCK 標志(通過 fcntl(2) F_SETFL 操作)的行為,但不同之處在于 MSG_DONTWAIT 是一個每次調用的選項,而 O_NONBLOCK 是對打開文件描述符(參見 open(2))的設置,將影響調用進程中的所有線程以及持有引用相同打開文件描述符的其他進程。
????????return ssize_t成功發送的字節數。如果出現錯誤,它將返回-1,并設置errno以指示錯誤的具體原因。
recv函數
? ? ? ? 函數原型為ssize_t recv(int sockfd, void *buf, size_t len, int flags);
? ? ? ? 作用是從套接字關聯的連接中接收數據。默認會阻塞,設置不阻塞需要設置flags
????????sockfd 套接字文件描述符。
????????buf 接收緩沖區,同樣地,此處也并非內核維護的緩沖區。
????????len 緩沖區長度,即buf可以接收的最大字節數。
????????flags flags 參數是以下標志之一或多個的按位或。對于大多數應用,這個參數被設置為0,表示不使用任何特殊行為。
????????MSG_DONTWAIT 啟用非阻塞操作;如果操作會阻塞,則調用失敗
????????MSG_ERRQUEUE 此標志指定應該從套接字錯誤隊列中接收排隊的錯誤。
????????MSG_OOB 此標志請求接收在正常數據流中不會接收到的帶外數據。
????????MSG_PEEK 此標志導致接收操作從接收隊列的開頭返回數據,而不從隊列中刪除該數據。因此,后續的接收調用將返回相同的數據。
????????MSG_TRUNC 對于原始(AF_PACKET)、Internet 數據報、netlink和 UNIX 數據報套接字:返回報文段或數據報的實際長度,即使它比傳遞的緩沖區長。
????????MSG_WAITALL 此標志請求操作阻塞,直到滿足完整的請求。
????????return ssize_t 返回接收到的字節數,如果連接已經正常關閉,返回值將是0。如果出現錯誤,返回-1,并且errno變量將被設置為指示錯誤的具體原因。
shutdown函數
? ? ? ? 函數原型為int shutdown(int sockfd, int how);
? ? ? ? 作用是關閉套接字的一部分或全部連接
????????sockfd 套接字文件描述符
????????how 指定關閉的類型。其取值如下:
????????SHUT_RD:關閉讀。之后,該套接字不再接收數據。任何當前阻塞在recv調用上的操作都將返回0。只會影響本端,不會觸發揮手。從數據角度來看,套接字上接收緩沖區已有的數據將被丟棄,如果再有新的數據流到達,會對數據進行 ACK,然后悄悄地丟棄。也就是說,對端還是會接收到 ACK,在這種情況下根本不知道數據已經被丟棄了。此操作只影響本端,不會觸發揮手操作
????????SHUT_WR:關閉寫。之后,試圖通過該套接字發送數據將導致錯誤。如果使用此選項,TCP連接將發送一個FIN包給連接的對端,表明此方向上的數據傳輸已經完成。此時對端的recv調用將接收到0。
????????關閉連接的“寫”這個方向,這就是常被稱為”半關閉“的連接。此時,不管套接字引用計數的值是多少,都會直接關閉連接的寫方向。套接字上發送緩沖區已有的數據將被立即發送出去,并發送一個 FIN 報文給對端。應用程序如果對該套接字進行寫操作會報錯。
????????SHUT_RDWR:關閉讀寫。同時關閉套接字的讀取和寫入部分,等同于分別調用SHUT_RD和SHUT_WR。之后,該套接字既不能接收數據也不能發送數據。相當于 SHUT_RD 和 SHUT_WR 操作各一次,關閉套接字的讀和寫兩個方向。
????????return int 成功 0 失敗 -1,并設置errno變量以指示具體的錯誤原因。
close函數
? ? ? ? 函數原型為int close(int __fd);
????????用于關閉一個之前通過open()、socket()等函數打開的文件描述符
????????close會對套接字引用計數減一,一旦發現套接字引用計數到 0,就會對套接字進行徹底釋放,并且會關閉 TCP 兩個方向的數據流。但是close調用之后,引用計數并不一定會降到0,如果沒有降到0就不會觸發四次揮手。但是在當前進程內一定無法使用該套接字進行讀/寫。
????????在輸入方向,系統內核會將該套接字設置為不可讀,任何讀操作都會返回異常。
????????在輸出方向,系統內核嘗試將發送緩沖區的數據發送給對端,并最后向對端發送一個 FIN 報文,接下來如果再對該套接字進行寫操作會返回異常。
????????如果對端沒有檢測到套接字已關閉,還繼續發送報文,就會收到一個 RST 報文,告訴對端:“Hi, 我已經關閉了,別再給我發數據了。
? ? int __fd: 這是一個整數值,表示要關閉的文件描述符
? ? return: 成功關閉文件描述符時,close()函數返回0,失敗返回-1,例如試圖關閉一個已經關閉的文件描述符或系統資源不足,close()會返回-1
shutdown和close函數的區別
????????TCP通信中,套接字也是通過文件描述符操控的,底層同樣存儲在struct file結構體中,socket相關的數據存在該類型結構體實例的私有數據字段,因此,我們通過close()關閉套接字,效果和關閉文件是類似的,都是使得底層文件描述的引用計數減一,若引用計數減為0則釋放套接字相關的資源。
????????1、close 會關閉當前進程下的連接,并釋放所有連接對應的資源,而 shutdown 并不會釋放掉套接字和所有的資源。
????????2、close 存在引用計數的概念,并不一定導致該套接字不可用;shutdown 則不管引用計數,直接使得該套接字不可用,如果有別的進程企圖使用該套接字,將會受到影響。
????????3、close 的引用計數導致不一定會發出 FIN 結束報文,而 shutdown 則總是會發出 FIN 結束報文,這在我們打算關閉連接通知對端的時候,是非常重要的
????????4、close 函數并不能幫助我們關閉連接的一個方向, shutdown 函數才可以。
UDP協議開發常用函數
????????UDP通訊也使用socket,與TCP的主要差別在于使用流程,接收和發送的函數與TCP不一樣。由于UDP是無連接的傳輸方式,不存在握手這一步驟,所以在綁定地址之后,服務端不需要listen,客戶端也不需要connect,服務端同樣不需要accept,只要服務端綁定以后,就可以相互發消息了,由于沒有握手過程,兩端都不能確定對方是否收到消息,這也是UDP協議不如TCP協議可靠的地方。
? ? ? ? 同樣的在使用UDP協議的過程時,如果使用IPV4,那么也可以使用struct sockaddr_in結構體來方便我們設置IP地址和端口號,在調用相關函數時再強轉為struct sockaddr型。
recvfrom函數
? ? ? ? 函數原型為ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
????????????????????????????????????????????????????????struct sockaddr *src_addr, socklen_t *addrlen);
? ? ? ? 作用是將接收到的消息放入緩沖區 buf 中。默認是阻塞的
????????sockfd 套接字文件描述符 自己的套接字
????????buf 緩沖區指針
????????len 緩沖區大小
????????flags 通信標簽,詳見recv方法說明
????????src_addr 可以填NULL,如果 src_addr 不是 NULL,并且底層協議提供了消息的源地址,則該源地址將被放置在 src_addr 指向的緩沖區中。從而獲取數據來源的地址信息,用于之后的發送
????????addrlen 如果src_addr不為NULL,它應初始化為與 src_addr 關聯的緩沖區的大小。返回時,addrlen 被更新為包含實際源地址的大小。如果提供的緩沖區太小,則返回的地址將被截斷;在這種情況下,addrlen 將返回一個大于調用時提供的值。
????????return ssize_t 實際收到消息的大小。如果接收失敗,返回-1
sendto函數
? ? ? ? 函數原型為ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
????????????????????????????????????????????????const struct sockaddr *dest_addr, socklen_t addrlen);
? ? ? ? 作用是向指定地址發送緩沖區中的數據(一般用于UDP模式)
????????sockfd 套接字文件描述符 也是自己的套接字
????????buf 緩沖區指針
????????len 緩沖區大小
????????flags 通信標簽,詳細見send方法說明,同樣一般為0
????????dest_addr 目標地址。如果用于連接模式,該參數會被忽略
????????addrlen 目標地址長度
????????return ssize_t 發送的消息大小。發送失敗返回-1