歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~nginx
下圖是TCP客戶端與服務器之間交互的一系列典型事件時間表:編程
爲了執行網絡I/O,一個進程(不管是服務端仍是客戶端)必須作的第一件事情就是調用socket
函數。服務器
#include <sys/socket.h> /* basic socket definitions */
int socket(int family, int type, int protocol);/* 返回:非負描述字——成功,-1——出錯 */
複製代碼
family
——協議族族 | 解釋 |
---|---|
AF_INET |
IPv4協議 |
AF_INET6 |
IPv6協議 |
AF_LOCAL |
Unix域協議 |
AF_ROUTE |
路由套接口 |
AF_KEY |
密鑰套接口 |
type
——套接口類型類型 | 解釋 |
---|---|
SOCK_STREAM |
字節流套接口 |
SOCK_DGRAM |
數據報套接口 |
SOCK_RAW |
原始套接口 |
下面是有效的family
和type
組合(簡略版):網絡
AF_INET |
AF_INET6 |
|
---|---|---|
SOCK_STREAM |
TCP | TCP |
SOCK_DGRAM |
UDP | UDP |
SOCK_RAW |
IPv4 | IPv6 |
socket
函數返回一個套接口描述字,簡稱套接字(sockfd
)。獲取套接字無需指定地址,只須要指定協議族和套接口類型(如上表中的組合)。併發
TCP客戶用connect
函數來創建一個與TCP服務器的鏈接。框架
#include <sys/socket.h> /* basic socket definitions */
int connect(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);/* 返回:0——成功,-1——出錯 */
複製代碼
sockfd
即是socket
函數返回的套接口描述字。servaddr
必須包含服務器的IP地址和端口號。bind
函數),內核會選擇源IP和一個臨時端口。connect
函數會觸發TCP三次握手。有可能出現下面的錯誤狀況:1.客戶端未收到SYN
分節的響應機器學習
第一次發出未收到,間隔6s再發一次,再沒收到,隔24秒再發一次,總共等待75s還沒收到則返回錯誤( ETIMEDOUT
)。能夠用時間日期程序驗證一下:socket
查看本地網絡信息:tcp
JACKIELUO-MC0:intro jackieluo$ ifconfig
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
ether f4:0f:24:2a:72:a6
inet6 fe80::1830:dbd:1b29:2989%en0 prefixlen 64 secured scopeid 0x6
inet 192.168.0.101 netmask 0xffffff00 broadcast 192.168.0.255
nd6 options=201<PERFORMNUD,DAD>
media: autoselect
status: active
複製代碼
將程序指向本地地址192.168.0.101
(確保時間日期服務器程序已運行),成功:
JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.101
Sat Oct 6 17:06:55 2018
複製代碼
將程序指向本地子網地址192.168.0.102
,其主機ID(102)不存在,等待幾分鐘後超時返回:
JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.102
connect error: Operation timed out
複製代碼
2.收到RST
即服務器主機在指定端口上沒有等待鏈接的進程,這稱爲「hard error」,客戶端一接收到RST
,立刻返回錯誤(ECONNREFUSED
)。驗證:
關閉以前本機運行的daytimetcpsrv
進程
將程序指向本地地址192.168.0.101
:
JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.101
connect error: Connection refused
複製代碼
3.發出的SYN
在路由器上引起了目的不可達ICMP
錯誤
這個錯誤被稱爲「soft error」,最終返回EHOSTUNREACH
或者ENETUNREACH
。
函數bind
爲套接口分配一個本地協議地址,包括IP地址和端口號。
#include <sys/socket.h> /* basic socket definitions */
int bind(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);/* 返回:0——成功,-1——出錯 */
複製代碼
bind
函數綁定ip地址和端口,供客戶端調用。一個例外是RPC(遠程過程調用)服務器,它由內核爲其選擇臨時端口。而後經過RPC端口映射器進行註冊,客戶端與該服務器鏈接以前,先經過端口映射器獲取服務器的端口。SYN
所在分組的目的IP地址做爲服務器的源IP地址。(即服務器收到SYN
的IP)給函數bind
指定用於捆綁的IP地址和/或端口號的結果:
IP地址 | 端口 | 結果 |
---|---|---|
0 | 內核選擇IP地址和端口 | |
非0 | 內核選擇IP地址,進程指定端口 | |
本地IP地址 | 0 | 進程選擇IP地址,內核指定端口 |
本地IP地址 | 非0 | 進程選擇IP地址和端口 |
函數listen
僅被TCP服務器調用。
#include <sys/socket.h> /* basic socket definitions */
int listen(int sockfd, int backlog);/* 返回:0——成功,-1——出錯 */
複製代碼
調用函數socket
函數建立的套接口,默認是主動方,下一步應是調用connect
,CLOSED
的下一個狀態是SYN_SENT
(見TCP狀態轉換圖)。而函數listen
將套接口轉換成被動方,告訴內核,應接受指向此套接口的鏈接請求,CLOSED
狀態變成LISTEN
。
函數listen
的第二個參數backlog
表示內核爲此套接口排隊的最大鏈接數。對於給定的監聽套接口,內核會維護兩個隊列:
未完成鏈接隊列(incomplete connection queue) SYN分節已由客戶發出,到達服務器,正在進行TCP的三路握手。此時這些套接口處於SYN_RCVD
狀態。
已完成鏈接隊列(completed connection queue) SYN分節已由客戶發出,到達服務器,而且已完成三路握手。此時這些套接口處於ESTABLISHED
狀態。
當來自客戶的SYN到達時,TCP在未完成鏈接隊列中建立一個新條目,直到三路握手中,第三個分節(客戶對服務SYN的ACK)到達,這個條目移到已完成鏈接隊列的隊尾。
當進程調用accept
函數時,已完成鏈接隊列的頭部條目返回給進程。
兩個隊列之和不能超過backlog
當一個客戶SYN到達時,若這兩個隊列都是滿的,TCP就忽略此分節,且不發送RST。客戶TCP將重發SYN,指望不久就能在隊列中找到空閒位置。
TCP爲監聽套接口維護的兩個隊列函數accept
由TCP服務器調用,從已完成鏈接隊列頭部返回下一個已完成鏈接,若該隊列爲空,則進程睡眠(假定套接口爲默認的阻塞方式)。
#include <sys/socket.h> /* basic socket definitions */
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);/* 返回:非負描述字——成功,-1——出錯 */
複製代碼
函數accept
的第一個參數和返回值都是套接口描述字。其中,
socket
返回,也用於bind
,listen
的第一個參數。一般一個服務器,只生成一個監聽套接口描述字,直到其關閉。而內核爲每一個被接受的客戶鏈接,建立一個已鏈接套接口,當客戶鏈接完成時,關閉該已鏈接套接口。
注意到intro/daytimetcpsrv.c
中,後兩個參數傳的都是空指針,這是由於咱們不關注客戶的身份,無需知道客戶的協議地址。
connfd = Accept(listenfd, (SA *) NULL, NULL);
複製代碼
稍做修改,再也不傳入空指針,見intro/daytimetcpsrv1.c
:
socklen_t len;
struct sockaddr_in servaddr, cliaddr;
...
connfd = Accept(listenfd, (SA *) &cliaddr, &len);
printf("connection from %s, port %d\n",
Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
ntohs(cliaddr.sin_port));
複製代碼
kill掉以前的daytimetcpsrv
進程:
$ sudo lsof -i -P | grep -i "listen"
daytimetc 80986 root 3u IPv4 0xae12d925e4528793 0t0 TCP *:13 (LISTEN)
$ sudo kill -9 80986
複製代碼
編譯運行新的服務端程序:
$ make daytimetcpsrv1.c daytimetcpsrv1
$ ./daytimetcpsrv1
複製代碼
重複執行客戶端程序,發幾個請求:
$ ./daytimetcpcli 127.0.0.1
Wed Sep 26 14:11:20 2018
$ ./daytimetcpcli 127.0.0.1
Wed Sep 26 14:17:06 2018
複製代碼
查看服務端打印:
connection from 127.0.0.1, port 58201
connection from 127.0.0.1, port 58342
複製代碼
注意到,因爲客戶端程序沒有調用bind
函數,內核爲它的協議地址選擇了源ip做爲IP地址,臨時端口號也發生了變化。
#include <unistd.h>
pid_t fork(void);/* 返回:在子進程中爲0,在父進程中爲子進程ID,-1——出錯 */
複製代碼
fork
函數調用一次,卻返回兩次。
getppid
來獲得父進程的ID經過返回值能夠判斷當前進程是子進程仍是父進程。
父進程在調用fork
以前打開的全部描述字在函數fork
返回後都是共享的。網絡服務器會利用這一特性:
accept
。fork
,已鏈接套接口就在父進程與子進程間共享。(通常來講就是子進程讀、寫已鏈接套接口,而父進程關閉已鏈接套接口)。fork
有兩個典型應用:
fork
生成一個拷貝,利用子進程調用exec
來執行新的程序。典型應用是shell。以文件形式存儲在硬盤上的可執行程序若要被執行,須要由一個現有進程調用exec
函數。咱們將調用exec
的進程稱爲調用進程,新程序的進程ID並不改變,仍處於當前進程。
客戶和服務器,從調用socket
開始,返回一個套接口描述字。客戶調用connect
,服務器調用bind
、listen
、accept
。最後套接口由close
關閉。
多數TCP服務器是調用fork
來實現併發處理多客戶請求的。多數UDP服務器則是迭代的。
相關閱讀
此文已由做者受權騰訊雲+社區發佈,更多原文請點擊
搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社區!