socket函數
為了執行網絡I/O,一個進程必須做到第一件事情就是調用socket函數,指定期望的通信協議類型(使用IPv4的TCP、使用IPv6的UDP、Unix域字節流協議等)
#include<sys/socket.h> int socket(int family,int type,int protocol); //返回:若成功則為非負描述符,若出錯則為-1
其中family參數指明協議族,它是圖4-2中所示的某個常值。該參數也往往被稱為協議域。type參數指明套接字類型,它是圖4-3中所示的某個常值。protocol參數應設為圖4-4所示的某個協議類型常值,或者設為0,以選擇所給定family和type組合的系統默認值。
並非所有套接字family與type的組合都是有效的,圖4-5給出了一些有效的組合和對應的真正協議。其中標為 “是”的項也是有效的,但還沒有找到便捷的縮略詞,而空白項則是無效組合。
4-1基本TCP客戶/服務程序的套接字
socket函數在成功返回一個小的非負整數值,它與文件描述符類似,我們把它稱為套接字描述符(socket descriptor),簡稱sockfd。為了得到這個套接字描述符,我們只是指定了協議族(IPv4、IPv6或Unix)和套接字類型(字節流、數據報或原始套接字)。我們並沒有指定本地協議地址或遠程協議地址。
對比AF_xxx和PF_xxx
AF_前綴表示地址族,PF_前綴表示協議族。
在POSIX規范指定socket函數的第一個參數為PF_值,而AF_值用於套接字地址結構,然而它在addrinfo結構中卻只定義一個族值,既用於調用socket函數,也用於套接字地址結構中
connect函數
TCP客戶用connect函數來建立與TCP服務的連接。
#include<sys/socket.h> int connect(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen); //返回:若成功則為0,若出錯則為-1
sockfd是由socket函數返回的套接字描述符,第二個、第三個參數分別是一個指向套接字地址結構的指針和該結構的大小,套接字地址結構必須含有服務器的IP地址和端口號。
客戶在調用函數connect前不必非得調用bind函數,因為如果需要的話,內核會確定源IP地址,並選擇一個臨時端口號作為源端口。
如果是TCP套接字,調用connect函數將激發TCP的三路握手過程,而且僅在連接建立成功或出錯時才返回,其中出錯返回可能有以下幾種情況。
(1)若TCP客戶沒有收到SYN分節的響應,則返回ETIMEDOUT錯誤,舉例來說,調用connect函數時,4.4BSD內核發送一個SYN,若無響應則等待6s后再發送一個,若仍無響應則等待24s后再法一個,若總共等了75s后仍未收到響應則返回本錯誤。
有些系統提供對超時值的管理性控制。
(2)若對客戶的SYN的響應是RST(表示復位),則表明該服務器主機在我們制定的端口上沒有進程在等待與之連接(例如服務器進程也許沒在運行)。這是一種硬錯誤(hard error),客戶一接受到RST就馬上返回ECONNREFUSED錯誤。
RST是TCP在發生錯誤時發送的一種TCP分節。產生RST的三個條件是:目的地為某端口的SYN到達,然而該端口上沒有正在監聽的服務器:TCP想取消一個已有連接:TCP接收到一個根本不存在的連接上的分節。
(3)若客戶發出的SYN在中間的某個路由器上引發一個“destination unreachable”(目的地不可達)ICMP錯誤,則認為是一種軟錯誤(soft error)。客戶主機內核保存該信息,並按第一種情況中所述的時間間隔繼續發送SYN,若在某個規定時間后仍未收到響應,則把保存的消息(即ICMP錯誤)作為EHOSTUNREACH或ENETUNREACH錯誤返回給進程。以下兩種情形也是由可能的:一種是按照本地系統的轉發表,根本沒有到達遠程系統的路徑;二是connect調用根本不等待就返回。
bind函數
bind函數把一個本地協議地址賦予一個套接字。對於網際網協議,協議地址是32位的IPv4地址或128位的IPv6地址與16位的TCP或UDP端口號的組合。
#include<sys/socket.h> int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen); //返回:若成功則為0,若出錯則為-1
第二個參數是一個指向特定於協議的地址結構的指針,第三個參數是該地址結構的長度。對於TCP,調用bind函數可以指定一個端口號,或指定一個IP地址,也可以兩者都指定,還可以都不指定。
listen函數
listen函數僅由TCP服務器調用,它做兩件事。
(1)當socket函數創建一個套接字時,它被假設為一個主動套接字,也就是說,它是一個將調用connect發起連接的客戶套接字。listen函數把一個未連接的套接字轉換成一個被動套接字,指示內核應接受指向該套接字的連接請求。調用listen導致套接字從CLOSED狀態換到LISTEN狀態。
(2)本函數的第二個參數規定了內核應該為相應套接字排隊的最大連接個數。
#include<sys/socket.h> int listen(int sockfd,int backlog); //返回:若成功則為0,若出錯則為-1
本函數通常應該在調用socket和bind這兩個函數之后,並在調用accept函數之前調用。
為了理解其中的backlog參數,我們必須認識到內核為任何一個給定的監聽套接字維護兩個隊列;
(1)未完成連接隊列(incomplete connection queue),每個這樣的SYN分節對應其中一項:已由某個客戶發出並發到服務器,而服務器正在等待完成相應的TCP三路握手過程。這些套接字處於SYN_RCVD狀態。
(2)已完成連接隊列(completed connection queue),每個已完成TCP三路握手過程的客戶對應其中的一項。這些套接字處於ESTABLISHED狀態
每當在未完成連接隊列中創建一項時,來自監聽套接字的參數就復制到即將建立的連接中。連接的創建機制是完全自動的,無需服務器進程插手。
accpet函數
accpet函數由TCP服務器調用,用於從已完成連接隊列隊頭返回下一個已完成連接。如果已完成隊列為空,那么進程被投入睡眠(假定套接字為默認的阻塞方式)
#include<sys/socket.h> int accept(int aockfd,struct sockaddr *cliaddr,socklen_t *addrlen); //返回:若成功則為非負描述符,若出錯則為-1
參數cliaddr和addrlen用來返回已連接的對端進程(客戶)的協議地址。addrlen是值-結果參數,調用前,我們將由*addrlen所引用的整數值置為由cliaddr所指的套接字地址結構的長度,返回時,該整數值即為由內核存放在該套接字地址結構內的確切字節數。
如果accpet成功,那么其返回值是由內核自動生成的一個全新描述符,代表與所返回客戶的TCP連接。在討論accpet函數時,我們稱它的第一個參數為監聽套接字(listen sicket)描述符(由socket創建,隨后用作bind和listen的第一個參數的描述符),稱它的返回值為已連接套接字(connected socket)描述符。區分這兩個套接字非常重要。一個服務器通常僅僅創建一個監聽套接字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建一個已連接套接字(也就是說對於它的TCP三路握手過程已經完成)。當服務器完成對某個給定客戶的服務時,相應的已連接套接字就被關閉。
本函數最多返回三個值:一個既可能是新套接字描述符也可能是出錯指示的整數、客戶進程的協議地址(cliaddr指針所指)以及該地址的大小(由addrlen指針所指)。如果我們對返回客戶協議地址不感興趣,那么可以把cliaddr和addrlen均值為空指針。
fork和exec函數
#include<unistd.h> pid_t fork(void); //返回:在子進程中為0,在父進程中為子進程ID,若出錯則為-1
如果你以前從未接觸過該函數,那么理解fork最困難之處在於調用它一次,它卻返回兩次。它在調用進程(稱為父進程)中返回一次,返回值是新派生進程(稱為子進程)的進程ID號;在子進程又返回一次,返回值為0.因此,返回值本身告知當前進程是子進程還是父進程。
fork在子進程返回0而不是父進程的進程ID的原因在於:任何子進程只有一個父進程,而且子進程總是可以調用getppid取得父進程的ID。相反,父進程可以有許多子進程,而且無法獲取各個子進程的 進程ID。如果父進程想要跟蹤所有子進程的進程ID,那么它必須記錄每次調用fork的返回值。
父進程中調用fork之前打開的所有描述符在fork返回之后由子進程分享。我們將看到網絡服務器利用了這個特性:父進程調用accpet之后調用fork。所接受的已連接套接字隨后就在父進程與子進程之間共享。通常情況下,子進程接着讀寫這個已連接套接字,父進程則關閉這個已連接套接字。
fork有兩個典型用法。
(1)一個進程創建一個自身的副本,這樣每個副本都可以在另一個副本執行其他任務的同時處理各自的某個操作。這是網絡服務器的典型用法 。
(2)一個進程想要執行另一個程序。既然創建新進程的唯一方法是調用fork,該進程於是首先調用fork創建一個自身的副本,然后創建新進程的唯一方法是調用fork,該進程於是首先調用fork創建一個自身的副本,然后其中一個副本(通常為子進程)調用exec(接下去介紹)把自身替換成新的程序。這是諸如shell之類程序的典型用法。
存放在硬盤上的可執行程序文件能夠被Unix執行的唯一方法是:由一個現有進程調用六個exec函數中的某一個(當這6個函數中是哪一個被調用並不重要時,我們往往把他們統稱為exec函數。)exec把當前進程映像替換成新的程序文件,而且該新程序通常從main函數開始執行。進程ID並不變。我們稱調用exec的進程為調用進程,稱新執行的程序為新程序。
這6個exec函數之間的區別在於(a)待執行的程序文件是由文件名還是由路徑名指定;(b)新程序的參數是一一列出還是由一個指針數組來引用;(c)把調用進程的環境傳遞給新程序還是給新程序指定新的環境。
close函數
通常的Unix close函數也常用來關閉套接字,並終止TCP連接。
#include<unistd.h> int close(int sockfd); //返回:若成功則為0,若出錯則為-1
close一個TCP套接字的默認行為是把該套接字標記成已關閉,然后立即返回到調用進程。該套接字描述符不能再由調用進程使用,也就是說它不能再作為read或write的第一個參數。然而TCP將嘗試發送已排隊等待發送到對端的任何數據,發送完畢后發生的是正常的TCP連接終止序列。
getsockname和getpeername函數
這兩個函數或者返回與某個套接字關聯的本地協議地址(getsockname),或者返回與某個套接字關聯的外地協議地址(getpeername)
#include<sys/socket.h> int getsockname(int sockfd,struct sockaddr *localaddr,socklen_t *addrlen); int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addrlen); //均返回:若成功則為0,若出錯則為-1