一、基於TCP協議的網絡程序
下圖是基於TCP協議的客戶端/服務器程序的一般流程:
服務器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化後,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到後從connect()返回,同時應答一個ACK段,服務器收到後從accept()返回。
查看本欄目更多精彩內容:http://www.bianceng.cn/OS/unix/
數據傳輸的過程:
建立連接後,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>
功能:創建一個套接字用於通信
原型:
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>
功能:綁定一個本地地址到套接字
原型:
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地址或端口,可以兩者都指定,也可以都不指定。
查看本欄目更多精彩內容:http://www.bianceng.cn/OS/unix/
如果指定端口號為0,那麼內核就在bind被調用時選擇一個臨時端口。然而如果指定IP地址為通配地址,那麼內核將等到套接字已連接(TCP)或已在套接字上發出數據報(UDP)時才選擇一個本地IP地址。
對於IPv4來說,統配地址由常值INADDR_ANY來指定,其值一般為0.
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>
功能:將套接字用於監聽進入的連接
原型:
int listen(int sockfd, int backlog);
參數
sockfd:socket函數返回的套接字
backlog:規定內核為此套接字排隊的最大連接個數
返回值:成功返回0,失敗返回-1
一般來說,listen函數應該在調用socket和bind函數之後,調用函數accept之前調用。
listen函數把一個未連接的套接字轉換成一個被動套接字,指示內核應接受指向該套接字的連接請求,調用listen導致套接字從CLOSE狀態轉換到LISTEN狀態。
為了理解其中的backlog參數,對於給定的監聽套接字,內核要維護兩個隊列:
未完成連接隊列:已由客戶發出並到達服務器,服務器正在等待完成相應的TCP三路握手過程
已完成連接的隊列:每個已完成TCP三次握手過程的客戶。
如下圖所示:
查看本欄目更多精彩內容:http://www.bianceng.cn/OS/unix/
服務器處於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
huangcheng@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_max_syn_backlog
512
每當有一個客戶端connect了,listen的隊列中就加入一個連接,每當服務器端accept了,就從listen的隊列中取出一個連接,轉成一個專門用來傳輸數據的socket(accept函數的返回值)。
4、accept函數
包含頭文件<sys/socket.h>
功能:從已完成連接隊列返回第一個連接,如果已完成連接隊列為空,則阻塞。
原型:
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所指定的套接字
原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
參數
sockfd:未連接套接字
addr:要連接的套接字地址
addrlen:第二個參數addr長度
返回值:成功返回0,失敗返回-1
如果套接字描述符處於非阻塞模式下,那麼在連接不能馬上建立時,connect將會返回-1,並且將errno設為特殊的錯誤碼EINPROGRESS。
作者:csdn博客 ctthuangcheng