套接字API詳解

weixin_45673259 2022-01-07 21:33:00 阅读数:907

套接 api
#include<sys/socket.h>
int socket(int family, int type,int protocol);
返回:若成功則為非負描述符,若出錯則為-1
family
AF_INET IPv4協議
AF_INET6 IPv6協議
AF_LOCAL Unix域協議
AF_ROUTE 路由套接字
AF_KEY 密匙套接字
type
SOCK_STREAM 字節流套接字
SOCK_DGRAM 數據報套接字
SOCK_SEQPACKET 有序分組套接字
SOCK_RAW 原始套接字
protocol
IPPROTO_TCP TCP傳輸協議
IPPROTO_UDP UDP傳輸協議
IPPROTO_SCTP SCTP傳輸協議
#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr,socklen_t addrlen);
返回:若成功則為0,若出錯則為-1

客戶在調用函數connect之前不必非得調用bind函數,因為如果需要的話,內核會確定源IP地址,並選擇一個臨時端口作為源端口。
如果是TCP套接字,調用connect函數將激發TCP的三路握手過程,而且僅在連接建立成功或出錯時才返回,其中出錯返回可能有以下幾種情况。

  1. 若TCP客戶沒有收到SYN分節的響應,則返回ETIMEOUT錯誤。即連接超時。
  2. 若對客戶的SYN響應是RST(錶示複比特),則錶明該服務器主機在我們指定的端口沒有進程在等待與之連接(例如服務器進程也許沒在運行)。這是一種硬錯誤,客戶一接收到RST就馬上返回ECONNREFUSED錯誤。 RST是TCP在發生錯誤時發送的一種TCP分節。產生RST的三個條件是:目的地為某端口的SYN到達,然而該端口上沒有正在監聽的服務器;TCP想取消一個已有連接;TCP接收到一個根本不存在的連接上的分節。
  3. 若對客戶發出的SYN在中間的某個路由上引發一個“destination unreachable”(目的地不可達)ICMP錯誤,則認為是一種軟錯誤。客戶主機內核保存該信息,並按第一種情况中所述的時間間隔繼續發送SYN。若在某個規定的時間(4.4BSD規定75s)後仍未收到響應,則把保存的消息(即ICMP錯誤)作為EHOSTUNREACH或ENETUNREACH錯誤返回給進程。以下兩種情况也是有可能的:一是按照本地系統的轉發錶,根本沒有到達遠程系統的路徑;二是connect調用根本不等待就返回。

非阻塞套接字connect:此時無論是否成功connect都立即返回,如果返回-1,錯誤碼為EINPROGRESS,則錶示正在嘗試連接,否則錶示出錯。在Windows上可以調用select函數,在指定時間內判斷該socket是否可寫,如果可寫,則說明連接成功,反之認為連接失敗。在Linux上不僅要調用select檢測是否可寫,還要調用getsockopt檢測此時socket是否出錯,通過錯誤碼來檢測和確定是否連接上,錯誤碼為0時錶示連接上,反之則是未連接上。

int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
返回:若成功則為0,若出錯則為-1
通配地址由常值INADDR_ANY來指定,其值一般為0

對於TCP調用bind函數可以指定一個端口號,或指定一個IP地址,也可以兩者都指定,還可以都不指定。
如果一個TCP客戶或服務器未曾調用bind捆綁一個端口,那麼當調用connect或linsten時,內核就要為套接字選擇一個臨時端口。
從bind函數返回的一個常見錯誤是EADDRINUSE(地址已使用)。

int listen(int sockfd, int backlog);
返回:若成功則為0,若出錯則為-1

內核為任何一個給定的監聽套接字維護兩個隊列:
(1)未完成連接隊列,每個這樣的SYN分節對應其中一項:已由某個客戶發出並到達服務器,而服務器正在等待完成相應的TCP三路握手過程,這些套接字都處於SYN_RCVD狀態。
(2)已完成連接隊列,每個已完成TCP三路握手過程的客戶對應其中一項,這些套接字處於ESTABLISHED狀態。
監聽套接字的兩個隊列
每當在未完成連接隊列中創建一項時,來自監聽套接字的參數就複制到即將建立的連接中。連接的創建機制是完全自動的,無需服務器進程插手。
在這裏插入圖片描述
當來自客戶的SYN到達時,TCP在未完成連接隊列中創建一個新項,然後響應以三路握手的第二個分節:服務器的SYN響應,其中捎帶對客戶的SYN的ACK。這一項一直保存在未連接隊列中,直到三路握手的第三個分節(客戶對服務器SYN的ACK)到達或者該項超時為止。(源自Berkeley的實現為這些未完成連接的項設置的超時值為75s)如果三路握手正常完成,該項就從未完成連接隊列移到已完成連接隊列的隊尾。當進程調用accept時( 該函數在下一節講解),已完成連接隊列中的隊頭項將返回給進程,或者如果該隊列為空,那麼進程將被置於休眠狀態,直到TCP在該隊列中放入一項才喚醒它。
當一個客戶SYN到達時,若這些隊列是滿的,TCP就忽略該分節,也就是不發送RST。
在三路握手完成之後,但在服務器調用accept之前到達的數據應由服務器TCP排隊,最大數據量為相應已連接套接字的接收緩沖區大小。

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
返回:若成功則為非負描述符,若出錯則為-1

如果accept成功,那麼其返回值是由內核自動生成的一個全新描述符,代錶與所返回客戶的TCP連接。在討論accept函數時,稱它的第一個參數為監聽套接字描述符,它的返回值為已連接套接字描述符。
如果我們對返回的客戶協議地址不感興趣,那麼可以把cliaddr和addrlen均置為空指針。

int recv(int sockfd, void* buf, int len,int flags);
返回值:若成功返回接收的字節數,若出錯返回-1,連接斷開返回0

返回值等於0:收到對端關閉的FIN包
大於0:接收成功的字節數。
等於-1:errno=EWOULDBLOCK 非阻塞IO,讀緩沖為空
errno=EINTR 系統調用被中斷打斷,重新接收即可
errno=ETIMEDOUT tcp探活包超時

int send(int sockfd, void *buf, int len, int flags);
返回值:成功返回接收的字節數,出錯返回-1,連接斷開返回0

返回值等於0:收到對端關閉的FIN包
大於0:發送成功的字節數。
等於-1:errno=EWOULDBLOCK 非阻塞IO,寫緩沖滿
errno=EINTR 系統調用被中斷打斷,重新發送即可
errno=EPIPE 寫通道關閉
注意:send發送0字節時也會返回0,此時不會向對端發送任何數據,而recv只有在對端關閉連接時才會返回0,對端發送0字節數據,本端的recv函數是不會接收到0字節數據的。
在這裏插入圖片描述

int close(int sockfd);
返回:若成功則為0,若出錯則為-1

close一個TCP套接字的默認行為是把該套接字標記成已關閉,然後立即返回到調用進程。該套接字描述符不能再由調用進程使用,也就是說它不能再作為read或write的第一個參數。然而TCP將嘗試發送已排隊等待發送到對端的任何數據,發送完畢後發生的是正常的TCP連接終止序列(2.6節)。
並發服務器中父進程關閉已連接套接字只是導致相應描述符的引用計數值减1。既然引用計數值仍大於0,這個close調用並不引發TCP的四分組連接終止序列。對於父進程與子進程共享已連接套接字的並發服務器來說,這正是所期望的。
如果我們確實想在某個TCP連接上發送一個FIN, 那麼可以改用shutdown函數(6.6節)以代替close。

int getsockname(int sockfd,struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd,struct sockaddr *peeraddr, socklen_t *addrlen);
均返回:若成功則為0,若出錯則為-1

這兩個函數的最後一個參數都是值-結果參數。
這兩個函數都得裝填由localaddr或peeraddr指針所指的套接字地址結構。

使用這兩個函數的理由如下:
(1)在一個沒有調用bind的TCP客戶上,connect成功返回後,getsockname用於返回由內核賦予該連接的本地IP地址和本地端口號。
(2)在以端口號0調用bind後,getsockname用於返回由內核賦予的本地端口號。
(3)getsockname可用於獲取某個套接字的地址族。
(4)在一個以通配IP地址調用bind的TCP服務器上, 與某個客戶的連接一旦建立(accept成功返回),getsockname就可 以用於返回由內核賦子該連接的本地IP地址。在這樣的調用中,套接字描述符參數必須是已連接套接字的描述符,而不是監聽套接字的描述符。
(5)當一個服務器是由調用過accept的某個進程通過調用exec執行程序時,它能够獲取客戶身份的唯一途 徑便是調用getpeername。

處理被中斷的系統調用

術語“慢系統調用”:是可能永遠阻塞的系統調用。
適用於慢系統調用的基本規則是:當阻塞於某個慢系統調用的一個進程捕獲某個信號且相應信號處理函數返回時,該系統調用可能返回一個EINTR錯誤。有些內核會自動重啟某些被中斷的系統調用。不過為了移植,應該主動捕獲錯誤重啟該系統調用。如accept()函數。

accept返回前連接中止**

這裏,三路握手完成從而連接建立之後,客戶TCP卻發送了一個RST(複比特)。在服務端看來,就在該連接已由TCP排隊,等著服務器進程調用accept的時候RST到達。稍後,服務器進程調用accept。
處理這種中止的連接依賴於不同的實現。源自於Berkeley的實現完全在內核中處理中止的連接,服務器進程根本看不到。然而大多數SVR4實現返回一個錯誤給服務器進程,作為accept的返回結果,不過錯誤本身取决於實現。這些SVR4實現返回一個EPROTO(“protocol error”,協議錯誤)errno值,而POSIX指出返回的errno值必須是ECONNABORTED。POSIX做出的修改的理由在於:流子系統中發生某些致命的協議相關事件時,也會返回EPROTO。要是對於由客戶引起的一個已建立連接的非致命中止也返回同樣的錯誤,那麼服務器就不知道該再次調用accept還是不該了。 換成ECONNABORTED錯誤,服務器就可以忽略它,再次調用accept就可以了。

服務器進程中止

客戶端已經連接服務端後,服務器進程中止,客戶TCP接收來自服務器TCP的FIN並響應以一個ACK。但是客戶進程看不到這個RST,因為它調用writen後馬上readline,並由於接收到的FIN,所調用的readline立即返回0(錶示EOF)。
當一個進程向某個已收到RST套接字執行寫操作時,內核向該進程發送一個SIGPIPE信號,該信號的默認行為是終止進程,因此進程必須捕獲它以免不情願地被終止。
不論進程是捕獲了該信號並從其信號處理函數返回,還是簡單地忽略該信號,寫操作都將返回EPIPE錯誤。
第一次寫操作會引發RST,第二次寫會引發SIGPIPE信號。寫一個已接收了FIN的套接字不成問題,但是寫一個已接收了RST的套接字則是一個錯誤,會將引發SIGPIPE信號殺死自身進程。

服務器主機崩潰

當服務器主機崩潰時,已有的網絡連接上不發出任何東西。這裏假設的是主機崩潰,而不是由操作員執行命令關機。
此時我們在客戶上鍵入一行文本,它由writen 寫入內核,再由客戶TCP作為個數據分節送出。客戶隨後阻塞於readline調用,等待回射的應答。
如果我們用tcpdump觀察網絡就會發現,客戶TCP持續重傳數據分 節,試圖從服務器上接收一個ACK。TCPv2的25.11節給出了TCP重傳的一個典型模式:源自Berkeley的實現重傳該數據分節12次,共等待約9分鐘才放弃重傳。當客戶TCP最後終於放弃時(假設在這段時間內,服務器主機沒有重新啟動,或者如果是服務器主機未崩潰但是從網絡上不可達,那麼假設主機仍然不可達),給客戶進程返回一個錯誤。既然客戶阻塞在readline調用上,該調用將返回一個錯誤。假設服務器主機已崩潰,從而對客戶的數據分節根本沒有響應,那麼所返回的錯誤是ETIMEDOUT。然而如果某個中間路由器判定服務器主機已不可達,從而響應以一個“destinationunreachable" (目的地不可達) ICMP消息,那麼所返回的錯誤是EHOSTUNREACH或ENETUNREACHD盡管我們的客戶最終還是會發現對端主機已崩潰或不可達,不過有時候我們需要比不得不等待9分鐘更快地檢測出這種情况。所用方法就是對readline調用設置一個超時。

我們剛剛討論的情形只有在我們向服務器主機發送數據時才能檢測出它已經崩潰。如果我們不主動向它發送數據也想檢測出服務器主機的崩潰,那麼需要采用另外一個技術, 也就是我們將在7.5節討論的KEEPALIVE套接字選項。

服務器主機崩潰後重啟

我們先在客戶和服務器之間建立連接,然後假設服務器崩潰重啟。
當服務器崩潰重啟時,它的TCP丟失崩潰前的所有連接信息,因此服務器TCP對於所收到的來自客戶的數據分節響應一個RST。
當客戶TCP收到該RST,當客戶正阻塞於readline調用,導致該調用返回ECONNRESET錯誤。

服務器主機關機

Unix系統關機時,init進程通常會給所有進程發送SIGTERM信號(該信號可被捕獲),等待一段固定時間(往往在5-20秒),然後給所有仍在運行的進程發送SIGKILL信號(該信號不能被捕獲)。這麼做是留給所有運行的進程一小段時間來清除和終止。如果我們不捕獲SIGTERM信號並終止,我們的服務器將由SIGKILL信號終止。當服務器子進程終止時,它的所有打開的描述符都被關閉,隨後的發生的步驟和服務器進程終止的一樣。

數據格式

在客戶和服務器之間傳輸文本串,不用考慮客戶與主機的字節序如何。
在客戶與服務器之間傳遞二進制結構時,需要考慮客戶與服務器之間的字節序問題。
解决這種數據格式問題有兩種解决辦法:
(1)把所有的數值數據作為文本串來傳遞。需要假設客戶和服務器具有相同的字符集。
(2)顯示定義所支持的數據類型的二進制格式(比特數、大端或小端字節序),並以這樣的格式在客戶和服務器之間傳遞所有數據。

來自Unix網絡編程卷一

版权声明:本文为[weixin_45673259]所创,转载请带上原文链接,感谢。 https://gsmany.com/2022/01/202201072133003460.html