值得收藏的TCP套接口編程文章
本文由jackieluo發表於雲+社區專欄
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 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函數
函數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 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地址,臨時端口號也發生了變化。
fork和exec函數
#include <unistd.h>
pid_t fork(void);/* 返回:在子進程中為0,在父進程中為子進程ID,-1——出錯 */
fork
函數調用一次,卻返回兩次。
- 在調用它的進程(即父進程),它返回一次,返回值是派生出來的子進程的進程ID。 父進程可能有很多子進程,必須通過返回值跟蹤記錄子進程ID。
- 在子進程,它還返回一次,返回值為0。 子進程只有一個父進程,總可以通過
getppid
來得到父進程的ID
通過返回值可以判斷當前進程是子進程還是父進程。
父進程在調用fork
之前打開的所有描述字在函數fork
返回後都是共享的。網絡服務器會利用這一特性:
- 父進程調用
accept
。 - 父進程調用
fork
,已連接套接口就在父進程與子進程間共享。(一般來說就是子進程讀、寫已連接套接口,而父進程關閉已連接套接口)。
fork
有兩個典型應用:
- 一個進程為自己派生一個拷貝,並發執行任務,這也是典型的並發網絡服務器模型。
- 一個進程想執行其他的程序,於是調用
fork
生成一個拷貝,利用子進程調用exec
來執行新的程序。典型應用是shell。
以文件形式存儲在硬盤上的可執行程序若要被執行,需要由一個現有進程調用exec
函數。我們將調用exec
的進程稱為調用進程,新程序的進程ID並不改變,仍處於當前進程。
小結
客戶和服務器,從調用socket
開始,返回一個套接口描述字。客戶調用connect
,服務器調用bind
、listen
、accept
。最後套接口由close
關閉。
多數TCP服務器是調用fork
來實現並發處理多客戶請求的。多數UDP服務器則是叠代的。
相關閱讀
系統重啟後nginx reload不生效原因分析
SRS開源直播服務 - StateThreads微線程框架學習
高性能網絡編程3----TCP消息的接收
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識
此文已由作者授權騰訊雲+社區發布,更多原文請點擊
搜索關註公眾號「雲加社區」,第一時間獲取技術幹貨,關註後回復1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社區!
值得收藏的TCP套接口編程文章