《Linux環境下C編程指南(第二版)》p260
轉載地址:http://www.cnblogs.com/biyeymyhjob/archive/2012/08/05/2623889.html
UNP總結 Chapter 4 基本TCP通訊端編程
1.socket函數
為了執行網路I/O,一個進程必須做的第一件事就是調用socket函數,指定期望的通訊協定類型
#include <sys/socket.h>int socket (int family, int type, int protocol);//返回:若成功則為非負描述符,若出錯則為-1
其中family指明協議族,type參數指明通訊端類型,protocol參數應該設為某個(見)協議類型常值,或者設為0,以選擇所給定family和type組合的系統預設值
socket函數的family常值
family |
說 明 |
AF_INET AF_INET AF_LOCAL AF_ROUTE AF_KEY |
IPv4協議 IPv6協議 Unix域協議 路由套介面 密鑰套介面 |
socket函數的type常值
type |
說 明 |
SOCK_STREAM SOCK_DGRAM SOCK_SEQPACKET SOCK_RAW |
位元組流套介面 資料報套介面 有序分組套介面 原始套介面 |
socket函數的protocol常值
protocol |
說 明 |
IPPROTO_TCP IPPROTO_UDP IPPROTO_SCTP |
TCP傳輸協議 UDP傳輸協議 SCTP傳輸協議 |
socket函數中family和type參數的組合
2.connect函數
TCP客戶用connect函數來建立與TCP伺服器的串連
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); //返回:若成功則為0,若出錯則為-1
sockfd是socket函數返回的通訊端描述符,剩下的2個參數分別是一個指向通訊端地址結構的指標和該結構的大小。connect函數將激發TCP的三向交握過程,而且僅在串連建立成功或出錯時才返回,其中出錯有如下幾種情況:
1).若TCP客戶沒有收到SYN包的響應,則返回ETIMEDOUT錯誤。如調用該函數時,核心發送一個SYN,若無響應則等待6s後再發一個,若仍無響應,則等待24s再發一個,若總共等了75s後仍未收到響應訊息則返回該錯誤(因核心而異)。
2).若響應時RST,表明該伺服器主機在我們指定的連接埠上沒有進程等待,客戶收到RST包後馬上返回ECONNREFUSED錯誤。
3).若客戶發出的SYN在中間的路由器上引發了一個“destination unreachable”的ICMP錯誤,則按第一種情形繼續發送SYN,若在規定的時間內沒有收到回應,則將ICMP錯誤作為EHOSTUNREACH或ENETUNREACH錯誤返回。
3.bind函數
bind函數把一個本地協議地址賦予一個通訊端。對於網際協議,協議地址是一個ip地址和一個連接埠號碼
#include <sys/socket.h>int bind (int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);//返回,成功為0,出錯為-1
參數sockfd是socket函數返回的通訊端描述符,myaddr是一個指向特定於協議的地址結構的指標,第三個參數是該地址結構的長度,對於TCP,調用bind函數可以指定一個連接埠,或者指定一個地址,也可以兩者都指定,還可以都不指定:
- 伺服器在啟動時候捆綁他們眾所周知的連接埠
- 進程可以把一個特定的IP地址捆綁到它的通訊端上,不過這個IP地址必須屬於其所在主機的網路介面之一
其中對於IPv4來說,通配地址常值INADDR_ANY來指定,其值一般為0,它通知核心選擇IP地址
4.listen函數
函數listen 僅被TCP伺服器調用,它做兩件事件:1).當函數socket建立一個套介面時,它被假設為一個主動套介面,也就是說,它是一個將調用connect發起串連的客戶套介面,函數listen將未串連的套介面轉換成被動套介面,指示核心應接受指向此套介面的串連請求,2).函數的第二個函數規定了核心為此套介面排隊的最大串連個數
#include <sys/socket.h>int listen (int sockfd, int backlog);//返回,成功為0,出錯-1
要理解backlog參數,我們要知道核心為任何一個給定的監聽通訊端維護2個隊列:
1).未完成串連隊列。客戶和伺服器之間的tcp三向交握並未完成。
2).已完成串連隊列。tcp的三向交握已經完成,處於ESTABLISHED狀態。
關於兩個隊列的處理:
- listen函數的backlog參數曾被規定為兩個隊列總和的最大值
- 源自Berkeley的實現給backlog增設了一個模糊因子,把它乘以1.5得到未處理隊列最大長度
- 不要把backlog定義為0,因為不同的實現對此有不同的解釋
- 在三路握手正常完成的前提下(也就是說沒有丟失分節,從而沒有重傳),未完成串連隊列的任何一項在其中的存留時間就是一個RTT,而RTT的值取決於特定的客戶與伺服器
- 當一個客戶SYN到達時,若這些隊列是滿的,TCP就忽略該分節,也就是不發送RST
- 在三路握手完成後,但在伺服器調用accept之前到達的資料應由伺服器TCP排列,最大資料量為相應已串連通訊端的接受緩衝區大小
5.accept函數
accept函數由TCP伺服器調用,用於從已完成串連隊列列頭返回下一個已完成串連,如果已完成串連隊列為空白,進程將被投入睡眠(如果通訊端為預設的阻塞方式)
#include <sys/socket.h>int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);//返回:若成功為非負描述符,出錯為-1
參數cliaddr和addrlen返回已串連的客戶的協議地址,如果對客戶的協議地址不感興趣,可以置為空白,參數addrlen在函數調用的時候是傳入的通訊端地址結構的大小,函數返回時它的值是核心存放在該通訊端地址結構中的確切位元組數。
如果accept成功,那麼其傳回值是由核心自動產生的一個全新描述符,代表與返回客戶的TCP串連,一般我們稱accept函數第一個參數為監聽通訊端描述符(由socket建立,隨後用作bind和listen的第一個參數的描述符),稱它的傳回值為已串連通訊端描述符
accept 函數最多返回三個值:一個既可能是新的通訊端描述符也可能是出錯指示的整數、客戶進程的協議地址(由cliaddr指標所指)、以及該地址的大小(由addrlen指標所指)。
6.fork和exec函數
fork函數(包括有些系統可能提供的它的各種變體)是Unix中派生新進程的唯一方法。
#include <unistd.h>pid_t fork(void);//返回:在子進程中為0,在父進程中為子進程的ID,若出錯為-1
理解fork的最難之處在於調用一次,它卻反回兩,傳回值本身告知當前進程是子進程還是父進程。
fork 在子進程中返回0,在父進程中返回子進程的ID號的原因在於:一個子進程只有一個父進程,而且在子進程中可以通過調用getppid擷取父進程ID。但是父進程可以有多個子進程,並且在父進程中沒有辦法擷取子進程的ID,如果父進程想跟蹤子進程,那麼它必須在fork返回後儲存子進程的ID。
fork函數的2個典型用法:
- 一個進程建立一個自身的副本,每個副本執行各自的操作。
- 一個進程想要執行另外一個程式,那麼它先調用fork函數建立一個自身的副本,然後調用exec函數把自身替換成新的程式。
下面exec函數之間的區別在於
a.待執行的程式是由檔案名稱還是由路徑名指定
b.新程式的參數是一一列出還是由一個指標數組來引用
c.把調用進程的環境傳遞給新程式還是給新城粗指定新的環境
#include <unistd.h>int execl (const char *pathname, const char *arg0, ... /* (char *) 0 */ );int execv (const char *pathname, char *const argv[]);int execle (const char *pathname, const char *arg0, .../* (char *) 0, char *const envp[] */ );int execve (const char *pathname, char *const argv[], char *const envp[]);int execlp (const char *filename, const char *arg0, ... /* (char *) 0 */ );int execvp (const char *filename, char *const argv[]);
另外進程在調用exec之前開啟著的描述符通常跨exec繼續保持開啟。
7.並發伺服器
下面僅給出accept到fork期間C/S的狀態
accept返回前客戶/伺服器的狀態
accept返回後客戶/伺服器的狀態
fork返回後客戶/伺服器的狀態
注意,此時listenfd和connfd這兩個描述符都在父進程和子進程共用
在下一步是由父進程關閉已串連通訊端,由子進程關閉監聽通訊端
父子進程關閉相應通訊端後客戶/伺服器的狀態
8.close函數
通常的Unix close函數也用來關閉通訊端,並終止TCP串連
#include <unistd.h>int close (int sockfd);//返回:若成功為0,出錯為-1
close 一個TCP通訊端的預設行為是把該通訊端設定成已關閉,然後立即返回到調用進程,在並發伺服器中,fork一個子進程會複製父進程在fork之前建立的所有描述符,複製完成後相應描述符的引用計數會增加1,調用close 會使描述符的引用計數減1,一旦描述符的引用計數為0,核心就會關閉該通訊端。調用close後通訊端的描述符引用計數仍然大於0的話,就不會引發TCP的終止序列。如果想在一個TCP串連上發送FIN
可以調用shutdown函數。
9.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.
需要這兩個函數,有如下的理由:
1).在一個沒有調用bind的TCP客戶上,connect成功返回後,getsockname用於返回核心賦予該串連的本地IP地址和本地連接埠號碼。
2).在以連接埠號碼0調用後,getsockname用於返回核心賦予的本地連接埠號碼
3).一旦串連建立,擷取客戶身份便可以調用getpeername。
4).在一個以通配IP地址調用bind的TCP伺服器上,與某個客戶的串連一旦建立(accept成功返回),getsockname就可以用於返回由核心賦予該串連的本地IP地址,在這樣的調用中,通訊端描述符參數必須是已串連通訊端的描述符,而不是監聽通訊端的描述符
5).當一個伺服器是由調用過accept的某個進程通過調用exec執行程式時,它能夠擷取客戶身份的唯一途徑便是調用getpeername
的inet派生就是一例,注意其中子進程是記憶體映像被替換成新的Telnet伺服器的程式檔案,也就是說包含對端地址的那個通訊端地址結構就此丟失,不過那個已串連通訊端描述符跨exec繼續保持開放(因為父子進程的拷貝作用?)
10.小結
1).所有的客戶服務器程式都是從調用socket函數開始。
2).客戶程式的調用順序一般是 socket --->connect ---->process user input;
3).伺服器的調用順序一般是:socket--->bind--->listen--->accept--->process user input;
4).並發伺服器為每個客戶串連建立一個進程或者線程來處理客戶的請求。
如: