值得收藏的TCP套介面程式設計文章
歡迎大家前往 ofollow,noindex" target="_blank">騰訊雲+社群 ,獲取更多騰訊海量技術實踐乾貨哦~
TCP客戶端-伺服器典型事件
下圖是TCP客戶端與伺服器之間互動的一系列典型事件時間表:
- 首先啟動伺服器,等待客戶端連線
- 啟動客戶端,連線到伺服器
- 客戶端傳送一個請求給伺服器,伺服器處理請求,響應客戶端
- 迴圈步驟3
- 客戶端給伺服器發一個檔案結束符,關閉客戶端連線
- 伺服器也關閉連線
基本TCP客戶-伺服器程式的套介面函式
套介面程式設計基本函式
socket 函式
為了執行網路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
)。獲取套接字無需指定地址,只需要指定協議族和套介面型別(如上表中的組合)。
connect函式
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
)。可以用時間日期程式驗證一下:
檢視本地網路資訊:
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 Oct6 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函式
函式 bind
為套介面分配一個本地協議地址,包括IP地址和埠號。
#include <sys/socket.h> /* basic socket definitions */ int bind(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);/* 返回:0——成功,-1——出錯 */
- 客戶端可以不呼叫這個函式,由核心選擇一個本地ip的臨時埠就好。
- 伺服器一般都會呼叫
bind
函式繫結ip地址和埠,供客戶端呼叫。一個例外是RPC(遠端過程呼叫)伺服器,它由核心為其選擇臨時埠。然後通過RPC埠對映器進行註冊,客戶端與該伺服器連線之前,先通過埠對映器獲取伺服器的埠。 - 程序可以把一個特定的IP地址捆綁到它的套介面上。對於客戶端,它傳送的請求,源IP地址就是這個地址;對於伺服器,如果綁定了IP地址,則只接受目的地為此IP地址的客戶連線。
- 如果伺服器不把IP地址繫結到套介面上,那麼核心把客戶端傳送
SYN
所在分組的目的IP地址作為伺服器的源IP地址。(即伺服器收到SYN
的IP)
給函式 bind
指定用於捆綁的IP地址和/或埠號的結果:
| IP地址 | 埠 | 結果 | | ---------- | ---- | ---------------------------- | | | 0 | 核心選擇IP地址和埠 | | | 非0 | 核心選擇IP地址,程序指定埠 | | 本地IP地址 | 0 | 程序選擇IP地址,核心指定埠 | | 本地IP地址 | 非0 | 程序選擇IP地址和埠 |
listen函式
函式 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函式
函式 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 80986root3uIPv4 0xae12d925e45287930t0TCP *: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地址,臨時埠號也發生了變化。
fork和exec函式
#include <unistd.h> pid_t fork(void);/* 返回:在子程序中為0,在父程序中為子程序ID,-1——出錯 */
fork
函式呼叫一次,卻返回兩次。
getppid
通過返回值可以判斷當前程序是子程序還是父程序。
父程序在呼叫 fork
之前開啟的所有描述字在函式 fork
返回後都是共享的。網路伺服器會利用這一特性:
accept fork
fork
有兩個典型應用:
- 一個程序為自己派生一個拷貝,併發執行任務,這也是典型的併發網路伺服器模型。
- 一個程序想執行其他的程式,於是呼叫
fork
生成一個拷貝,利用子程序呼叫exec
來執行新的程式。典型應用是shell。
以檔案形式儲存在硬碟上的可執行程式若要被執行,需要由一個現有程序呼叫 exec
函式。我們將呼叫 exec
的程序稱為呼叫程序,新程式的程序ID並不改變,仍處於當前程序。
小結
客戶和伺服器,從呼叫 socket
開始,返回一個套介面描述字。客戶呼叫 connect
,伺服器呼叫 bind
、 listen
、 accept
。最後套介面由 close
關閉。
多數TCP伺服器是呼叫 fork
來實現併發處理多客戶請求的。多數UDP伺服器則是迭代的。