值得收藏的TCP套接口編程文章

歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~nginx

本文由jackieluo發表於雲+社區專欄shell

TCP客戶端-服務器典型事件

下圖是TCP客戶端與服務器之間交互的一系列典型事件時間表:編程

  1. 首先啓動服務器,等待客戶端鏈接
  2. 啓動客戶端,鏈接到服務器
  3. 客戶端發送一個請求給服務器,服務器處理請求,響應客戶端
  4. 循環步驟3
  5. 客戶端給服務器發一個文件結束符,關閉客戶端鏈接
  6. 服務器也關閉鏈接

img
基本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 原始套接口

下面是有效的familytype組合(簡略版):網絡

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)。能夠用時間日期程序驗證一下: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函數

函數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函數建立的套接口,默認是主動方,下一步應是調用connectCLOSED的下一個狀態是SYN_SENT(見TCP狀態轉換圖)。而函數listen將套接口轉換成被動方,告訴內核,應接受指向此套接口的鏈接請求,CLOSED狀態變成LISTEN

函數listen的第二個參數backlog表示內核爲此套接口排隊的最大鏈接數。對於給定的監聽套接口,內核會維護兩個隊列:

  1. 未完成鏈接隊列(incomplete connection queue) SYN分節已由客戶發出,到達服務器,正在進行TCP的三路握手。此時這些套接口處於SYN_RCVD狀態。

  2. 已完成鏈接隊列(completed connection queue) SYN分節已由客戶發出,到達服務器,而且已完成三路握手。此時這些套接口處於ESTABLISHED狀態。

  3. 當來自客戶的SYN到達時,TCP在未完成鏈接隊列中建立一個新條目,直到三路握手中,第三個分節(客戶對服務SYN的ACK)到達,這個條目移到已完成鏈接隊列的隊尾。

  4. 當進程調用accept函數時,已完成鏈接隊列的頭部條目返回給進程。

  5. 兩個隊列之和不能超過backlog

  6. 當一個客戶SYN到達時,若這兩個隊列都是滿的,TCP就忽略此分節,且不發送RST。客戶TCP將重發SYN,指望不久就能在隊列中找到空閒位置。

    img
    TCP爲監聽套接口維護的兩個隊列

accept函數

函數accept由TCP服務器調用,從已完成鏈接隊列頭部返回下一個已完成鏈接,若該隊列爲空,則進程睡眠(假定套接口爲默認的阻塞方式)。

#include <sys/socket.h> /* basic socket definitions */
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);/* 返回:非負描述字——成功,-1——出錯 */
複製代碼

函數accept的第一個參數和返回值都是套接口描述字。其中,

  1. 第一個參數,稱爲監聽套接口描述字,即由函數socket返回,也用於bindlisten的第一個參數。
  2. 返回值,稱爲已鏈接套接口描述字。

一般一個服務器,只生成一個監聽套接口描述字,直到其關閉。而內核爲每一個被接受的客戶鏈接,建立一個已鏈接套接口,當客戶鏈接完成時,關閉該已鏈接套接口。

注意到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函數調用一次,卻返回兩次。

  1. 在調用它的進程(即父進程),它返回一次,返回值是派生出來的子進程的進程ID。 父進程可能有不少子進程,必須經過返回值跟蹤記錄子進程ID。
  2. 在子進程,它還返回一次,返回值爲0。 子進程只有一個父進程,總能夠經過getppid來獲得父進程的ID

經過返回值能夠判斷當前進程是子進程仍是父進程。

父進程在調用fork以前打開的全部描述字在函數fork返回後都是共享的。網絡服務器會利用這一特性:

  1. 父進程調用accept
  2. 父進程調用fork,已鏈接套接口就在父進程與子進程間共享。(通常來講就是子進程讀、寫已鏈接套接口,而父進程關閉已鏈接套接口)。

fork有兩個典型應用:

  1. 一個進程爲本身派生一個拷貝,併發執行任務,這也是典型的併發網絡服務器模型。
  2. 一個進程想執行其餘的程序,因而調用fork生成一個拷貝,利用子進程調用exec來執行新的程序。典型應用是shell。

以文件形式存儲在硬盤上的可執行程序若要被執行,須要由一個現有進程調用exec函數。咱們將調用exec的進程稱爲調用進程,新程序的進程ID並不改變,仍處於當前進程。

小結

客戶和服務器,從調用socket開始,返回一個套接口描述字。客戶調用connect,服務器調用bindlistenaccept。最後套接口由close關閉。

多數TCP服務器是調用fork來實現併發處理多客戶請求的。多數UDP服務器則是迭代的。

相關閱讀

系統重啓後nginx reload不生效緣由分析

SRS開源直播服務 - StateThreads微線程框架學習

高性能網絡編程3----TCP消息的接收

【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識

此文已由做者受權騰訊雲+社區發佈,更多原文請點擊

搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!

海量技術實踐經驗,盡在雲加社區

相關文章
相關標籤/搜索