1. 程式人生 > >UNIX網路程式設計——基本TCP套接字程式設計 【轉貼】

UNIX網路程式設計——基本TCP套接字程式設計 【轉貼】

一、基於TCP協議的網路程式

下圖是基於TCP協議的客戶端/伺服器程式的一般流程:

                                       

                 

伺服器呼叫socket()、bind()、listen()完成初始化後,呼叫accept()阻塞等待,處於監聽埠的狀態,客戶端呼叫socket()初始化後,呼叫connect()發出SYN段並阻塞等待伺服器應答,伺服器應答一個SYN-ACK段,客戶端收到後從connect()返回,同時應答一個ACK段,伺服器收到後從accept()返回。


資料傳輸的過程:

建立連線後,TCP協議提供全雙工的通訊服務,但是一般的客戶端/伺服器程式的流程是由客戶端主動發起請求,伺服器被動處理請求,一問一答的方式。因此,伺服器從accept()返回後立刻呼叫read(),讀socket就像讀管道一樣,如果沒有資料到達就阻塞等待,這時客戶端呼叫write()傳送請求給伺服器,伺服器收到後從read()返回,對客戶端的請求進行處理,在此期間客戶端呼叫read()阻塞等待伺服器的應答,伺服器呼叫write()將處理結果發回給客戶端,再次呼叫read()阻塞等待下一條請求,客戶端收到後從read()返回,傳送下一條請求,如此迴圈下去。


如果客戶端沒有更多的請求了,就呼叫close()關閉連線,就像寫端關閉的管道一樣,伺服器的read()返回0,這樣伺服器就知道客戶端關閉了連線,也呼叫close()關閉連線。注意,任何一方呼叫close()後,連線的兩個傳輸方向都關閉,不能再發送資料了。如果一方呼叫shutdown()則連線處於半關閉狀態,仍可接收對方發來的資料。


在學習socket API時要注意應用程式和TCP協議層是如何互動的: 

*應用程式呼叫某個socket函式時TCP協議層完成什麼動作,比如呼叫connect()會發出SYN段

 *應用程式如何知道TCP協議層的狀態變化,比如從某個阻塞的socket函式返回就表明TCP協議收到了某些段,再比如read()返回0就表明收到了FIN段


補充一下,其實TCP 共有11種狀態,上圖沒有出現的CLOSING 狀態,當雙方同時關閉連線時會出現此狀態,替換掉FIN_WAIT2狀態。


二、基本socket函式

1、socket函式

包含標頭檔案<sys/socket.h>
功能:建立一個套接字用於通訊
原型:

[cpp] view plain copy print ?
  1. int socket(int domain, int type, int protocol);  
int socket(int domain, int type, int protocol);

引數
domain :指定通訊協議族(protocol family),AF_INET、AF_INET6、AF_UNIX等
type:指定socket型別,流式套接字SOCK_STREAM,資料報套接字SOCK_DGRAM,原始套接字SOCK_RAW
protocol :協議型別,IPPROTO_TCP等;一般由前兩個引數就決定了協議型別,設定為0即可。
返回值:成功返回非負整數, 它與檔案描述符類似,我們把它稱為套介面描述字,簡稱套接字。失敗返回-1


2、bind函式

包含標頭檔案<sys/socket.h>
功能:繫結一個本地地址到套接字
原型:

[cpp] view plain copy print ?
  1. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
引數
sockfd:socket函式返回的套接字
addr:要繫結的地址
addrlen:地址長度
返回值:成功返回0,失敗返回-1

     如果一個TCP客戶或者伺服器未曾呼叫bind捆綁一個埠,當呼叫connect或listen時,核心就要為相應的套接字選擇一個臨時埠。讓核心來選擇臨時埠對於TCP客戶來說是正常的,除非應該需要一個預留埠然而對於TCP伺服器來說卻極為罕見,因為伺服器是通過它們的眾所周知埠被大家認識的。
     呼叫bind可以指定IP地址或埠,可以兩者都指定,也可以都不指定。

                      
     如果指定埠號為0,那麼核心就在bind被呼叫時選擇一個臨時埠。然而如果指定IP地址為通配地址,那麼核心將等到套接字已連線(TCP)或已在套接字上發出資料報(UDP)時才選擇一個本地IP地址
     對於IPv4來說,統配地址由常值INADDR_ANY來指定,其值一般為0.

[cpp] view plain copy print ?
  1. struct sockaddr_in servaddr;  
  2. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
     其實無論是網路位元組序還是主機位元組序,INADDR_ANY的值(為0)都是一樣的,因此使用htonl並非必需。
     為了得到核心選擇的臨時埠值,必須呼叫函式getsockname來返回協議地址。
     從bind函式返回的一個常見錯誤時 EADDRINUSE(“Address already in use",地址已使用),後面的部落格會討論SO_REUSEADDR和SO_REUSEPORT這兩個套接字選項。
      注意:埠號必須不小於1024,除非該程序具有相應的特權(即為超級使用者)。


3、listen函式

包含標頭檔案<sys/socket.h>
功能:將套接字用於監聽進入的連線
原型:

[cpp] view plain copy print ?
  1. int listen(int sockfd, int backlog);  
int listen(int sockfd, int backlog);
引數
sockfd:socket函式返回的套接字
backlog:規定核心為此套接字排隊的最大連線個數

返回值:成功返回0,失敗返回-1


     一般來說,listen函式應該在呼叫socket和bind函式之後,呼叫函式accept之前呼叫。

     listen函式把一個未連線的套接字轉換成一個被動套接字,指示核心應接受指向該套接字的連線請求,呼叫listen導致套接字從CLOSE狀態轉換到LISTEN狀態。

     為了理解其中的backlog引數,對於給定的監聽套接字,核心要維護兩個佇列:

  • 未完成連線佇列:已由客戶發出併到達伺服器,伺服器正在等待完成相應的TCP三路握手過程
  • 已完成連線的佇列:每個已完成TCP三次握手過程的客戶。

如下圖所示:

                              

               

     伺服器處於listen狀態時收到客戶端syn 分節(connect)時在未完成佇列中建立一個新的條目,然後用三路握手的第二個分節即伺服器的syn 響應及對客戶端syn的ack,此條目在第三個分節到達前(客戶端對伺服器syn的ack)一直保留在未完成連線佇列中,如果三路握手完成,該條目將從未完成連線佇列搬到已完成連線佇列尾部當程序呼叫accept時,從已完成佇列中的頭部取出一個條目給程序,當已完成佇列為空時程序將睡眠,直到有條目在已完成連線佇列中才喚醒。
     backlog被規定為兩個佇列總和的最大值,大多數實現預設值為5
     一旦佇列滿,系統會拒絕多餘連線請求,所以backlog的值應該基於伺服器期望負載和接受連線請求與啟動服務的處理能力來選擇。
     當客戶端發起connect而導致傳送syn分節給伺服器端握手,如果這時兩個佇列都是滿的,tcp就忽略此分節,並且不發RST,這將導致客戶端TCP重發SYN(超時),伺服器端忽略syn而不發RST響應的原因是如果發RST ,客戶端connect將立即返回錯誤,強制客戶端程序處理這種情況,而不是讓tcp的正常重傳機制來處理。實際上所有源自Berkeley的實現都是忽略新的SYN分節。
     還有,backlog為0 時在linux上表明允許不受限制的連線數,這是一個缺陷,因為它可能會導致SYN Flooding(拒絕服務型攻擊)。
     linux 系統tcp /ip協議棧有個選項可以設定未連結佇列大小tcp_max_syn_backlog

[cpp] view plain copy print ?
  1. [email protected]:~$ cat /proc/sys/net/ipv4/tcp_max_syn_backlog  
  2. 512  
[email protected]:~$ cat /proc/sys/net/ipv4/tcp_max_syn_backlog
512

     每當有一個客戶端connect了,listen的佇列中就加入一個連線,每當伺服器端accept了,就從listen的佇列中取出一個連線,轉成一個專門用來傳輸資料的socket(accept函式的返回值)。


4、accept函式

包含標頭檔案<sys/socket.h>
功能:從已完成連線佇列返回第一個連線,如果已完成連線佇列為空,則阻塞。
原型:

[cpp] view plain copy print ?
  1. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);  
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
引數
sockfd:伺服器套接字
addr:將返回對等方的套接字地址
addrlen:返回對等方的套接字地址長度
返回值:成功返回非負整數,失敗返回-1

     如果accept成功,那麼其返回值是由核心自動生成的一個全新描述符,代表與返回客戶的TCP連線。在accept函式的第一個引數為監聽套接字描述符,稱為它的返回值已連線套接字描述符
     區分這兩個套接字非常重要,一個伺服器通常僅僅建立一個監聽套接字,它在該伺服器的生命期內一直存在。核心為每個由伺服器程序接受的客戶連線建立一個已連線套接字。當伺服器完成對某個給定客戶的服務時,相應的已連線套接字就被關閉。
     如果伺服器呼叫accept並且當前沒有連線請求,伺服器會阻塞直到一個請求到來。如果sockfd處於非阻塞模式,accept會返回-1並將errno設定為EAGAIN或EWOULDBLOCK。



5、connect函式

包含標頭檔案<sys/socket.h>
功能:建立一個連線至addr所指定的套接字
原型:

[cpp] view plain copy print ?
  1. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
引數
sockfd:未連線套接字
addr:要連線的套接字地址
addrlen:第二個引數addr長度
返回值:成功返回0,失敗返回-1

如果套接字描述符處於非阻塞模式下,那麼在連線不能馬上建立時,connect將會返回-1,並且將errno設為特殊的錯誤碼EINPROGRESS,不過已經發起的TCP三次握手還是繼續

 

摘自:http://blog.csdn.net/ctthuangcheng/article/details/9408541