1、基於TCP協議的網絡程序linux
下圖是基於TCP協議的客戶端/服務器程序的通常流程:ubuntu
服務器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化後,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到後從connect()返回,同時應答一個ACK段,服務器收到後從accept()返回。服務器
數據傳輸的過程:
網絡
創建鏈接後,TCP協議提供全雙工的通訊服務,可是通常的客戶端/服務器程序的流程是由客戶端主動發起請求,服務器被動處理請求,一問一答的方式。所以,服務器從accept()返回後馬上調用read(),讀socket就像讀管道同樣,若是沒有數據到達就阻塞等待,這時客戶端調用write()發送請求給服務器,服務器收到後從read()返回,對客戶端的請求進行處理,在此期間客戶端調用read()阻塞等待服務器的應答,服務器調用write()將處理結果發回給客戶端,再次調用read()阻塞等待下一條請求,客戶端收到後從read()返回,發送下一條請求,如此循環下去。dom
若是客戶端沒有更多的請求了,就調用close()關閉鏈接,就像寫端關閉的管道同樣,服務器的read()返回0,這樣服務器就知道客戶端關閉了鏈接,也調用close()關閉鏈接。注意,任何一方調用close()後,鏈接的兩個傳輸方向都關閉,不能再發送數據了。若是一方調用shutdown()則鏈接處於半關閉狀態,仍可接收對方發來的數據。socket
在學習socket API時要注意應用程序和TCP協議層是如何交互的: tcp
*應用程序調用某個socket函數時TCP協議層完成什麼動做,好比調用connect()會發出SYN段函數
*應用程序如何知道TCP協議層的狀態變化,好比從某個阻塞的socket函數返回就代表TCP協議收到了某些段,再好比read()返回0就代表收到了FIN段
學習
補充一下,其實TCP 共有11種狀態,上圖沒有出現的CLOSING 狀態,當雙方同時關閉鏈接時會出現此狀態,替換掉FIN_WAIT2狀態。spa
2、基本socket函數
一、socket函數
包含頭文件<sys/socket.h>
功能:建立一個套接字用於通訊
原型:
int socket(int domain, int type, int protocol);
參數
domain :指定通訊協議族(protocol family),AF_INET、AF_INET六、AF_UNIX等
type:指定socket類型,流式套接字SOCK_STREAM,數據報套接字SOCK_DGRAM,原始套接字SOCK_RAW
protocol :協議類型,IPPROTO_TCP等;通常由前兩個參數就決定了協議類型,設置爲0便可。
返回值:成功返回非負整數, 它與文件描述符相似,咱們把它稱爲套接口描述字,簡稱套接字。失敗返回-1
二、bind函數
包含頭文件<sys/socket.h>
功能:綁定一個本地地址到套接字
原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);參數
若是一個TCP客戶或者服務器不曾調用bind捆綁一個端口,當調用connect或listen時,內核就要爲相應的套接字選擇一個臨時端口。讓內核來選擇臨時端口對於TCP客戶來講是正常的,除非應該須要一個預留端口然而對於TCP服務器來講卻極爲罕見,由於服務器是經過它們的衆所周知端口被你們認識的。
調用bind能夠指定IP地址或端口,能夠二者都指定,也能夠都不指定。
若是指定端口號爲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並不是必需。
三、listen函數
包含頭文件<sys/socket.h>
功能:將套接字用於監聽進入的鏈接
原型:
int listen(int sockfd, int backlog);參數
返回值:成功返回0,失敗返回-1
通常來講,listen函數應該在調用socket和bind函數以後,調用函數accept以前調用。
listen函數把一個未鏈接的套接字轉換成一個被動套接字,指示內核應接受指向該套接字的鏈接請求,調用listen致使套接字從CLOSE狀態轉換到LISTEN狀態。
爲了理解其中的backlog參數,對於給定的監聽套接字,內核要維護兩個隊列:
以下圖所示:
服務器處於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函數的返回值)。
四、accept函數
包含頭文件<sys/socket.h>
功能:從已完成鏈接隊列返回第一個鏈接,若是已完成鏈接隊列爲空,則阻塞。
原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);參數
若是accept成功,那麼其返回值是由內核自動生成的一個全新描述符,表明與返回客戶的TCP鏈接。在accept函數的第一個參數爲監聽套接字描述符,稱爲它的返回值爲已鏈接套接字描述符。
區分這兩個套接字很是重要,一個服務器一般僅僅建立一個監聽套接字,它在該服務器的生命期內一直存在。內核爲每一個由服務器進程接受的客戶鏈接建立一個已鏈接套接字。當服務器完成對某個給定客戶的服務時,相應的已鏈接套接字就被關閉。
若是服務器調用accept而且當前沒有鏈接請求,服務器會阻塞直到一個請求到來。若是sockfd處於非阻塞模式,accept會返回-1並將errno設置爲EAGAIN或EWOULDBLOCK。
五、connect函數
包含頭文件<sys/socket.h>
功能:創建一個鏈接至addr所指定的套接字
原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);參數