Socket 介面 (The Sockets Interface)
Overview Table
| 主題 | 核心重點 | 書頁 |
|---|---|---|
| Socket 介面總覽 | 與 Unix I/O 搭配建構網路應用的函式集,源自 Berkeley(4.2BSD) | p.968 |
| Socket address 結構 | sockaddr_in(16 bytes,network byte order)+ 泛型 sockaddr 強制轉型 |
p.969 |
socket |
建立 socket descriptor;回傳的 fd「部分開啟」尚不能讀寫 | p.970 |
connect |
client 主動建立連線,blocking;成功後由 socket pair 標識連線 | p.970-971 |
bind |
server 將 socket address 與 descriptor 綁定 | p.971 |
listen |
把 active socket 轉為 listening socket;backlog 為佇列提示 |
p.971 |
accept |
等待連線請求,回傳全新的 connected descriptor | p.972-973 |
getaddrinfo / getnameinfo |
主機/服務字串 ↔ socket address 雙向轉換,reentrant、protocol-independent | p.973-977 |
| Helper functions | open_clientfd / open_listenfd 封裝樣板碼 |
p.978-981 |
| Echo client/server | 迭代式(iterative)伺服器範例;EOF 語意 | p.980-984 |
Socket 介面是一組與 Unix I/O 函式搭配使用的函式,用來建構網路應用程式,已實作於所有 Unix 變種、Windows 與 Macintosh 系統 (p.968)。整體 client-server 互動流程如下(Figure 11.12):
Client Server
getaddrinfo getaddrinfo
| |
socket socket ┐
| | │
| bind │ open_listenfd
┐ | | │
│ | listen ┘
│ | Connection |
│ connect ----- request --------> accept
┘ | |
rio_writen ------------------> rio_readlineb
| | ┌ Await connection
rio_readlineb <----------------- rio_writen │ request from
| | │ next client
close -------- EOF -------> rio_readlineb │
| │
close --------┘
(open_clientfd = socket + connect)
Socket 介面由 UC Berkeley 研究者於 1980 年代初開發,故常稱 Berkeley sockets;設計上可搭配任何底層協定,第一個實作是 TCP/IP,隨 Unix 4.2BSD kernel 散布,是 Internet 史上的重要事件。
11.4.1 Socket Address 結構 (p.969)
從 Linux kernel 的角度,socket 是通訊的端點 (end point);從 Linux 程式的角度,socket 是一個有對應 descriptor 的開啟檔案。Internet socket address 存放於 16-byte 的 sockaddr_in 結構中,IP address 與 port number 一律以 network byte order(big-endian)儲存。
/* IP socket address structure */
struct sockaddr_in {
uint16_t sin_family; /* Protocol family (always AF_INET) */
uint16_t sin_port; /* Port number in network byte order */
struct in_addr sin_addr; /* IP address in network byte order */
unsigned char sin_zero[8]; /* Pad to sizeof(struct sockaddr) */
};
/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr {
uint16_t sa_family; /* Protocol family */
char sa_data[14]; /* Address data */
};
- 泛型指標問題:
connect、bind、accept需要接受「任意種類」的 socket address。設計當時 C 尚無void *,解法是讓函式收泛型struct sockaddr *,應用程式須把 protocol-specific 結構指標強制轉型過去。 - 依 Stevens 慣例定義
typedef struct sockaddr SA;簡化轉型寫法。 _in字尾意為 internet,不是 input (p.969 Aside)。
11.4.2 socket 函式 (p.970)
Client 與 server 都用 socket 建立 socket descriptor。
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
/* Returns: nonnegative descriptor if OK, -1 on error */
- 硬編碼寫法:
clientfd = Socket(AF_INET, SOCK_STREAM, 0);— AF_INET 表示使用 32-bit IP address,SOCK_STREAM 表示此 socket 是連線的端點。 - 最佳實務:改用
getaddrinfo(11.4.7)自動產生這些參數,讓程式 protocol-independent。 - 回傳的 descriptor 只是部分開啟 (partially opened),還不能讀寫;如何完成開啟取決於身為 client 或 server。
11.4.3 connect 函式 (p.970-971)
Client 呼叫 connect 與 server 建立連線。
#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
/* Returns: 0 if OK, -1 on error */
- 嘗試與 socket address
addr上的 server 建立 Internet 連線,addrlen為sizeof(sockaddr_in)。 - 會 block 直到連線成功建立或發生錯誤;成功後
clientfd即可讀寫。 - 成功建立的連線由 socket pair 標識:
(x:y, addr.sin_addr:addr.sin_port),其中x是 client 的 IP address、y是唯一標識 client host 上 client process 的 ephemeral port。
11.4.4 bind 函式 (p.971)
bind、listen、accept 是 server 端用來與 client 建立連線的函式。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* Returns: 0 if OK, -1 on error */
- 要求 kernel 把
addr中的 server socket address 與 socket descriptorsockfd關聯起來;addrlen為sizeof(sockaddr_in)。 - 同樣建議由
getaddrinfo供應參數。
11.4.5 listen 函式 (p.971)
Client 是主動 (active) 發起連線請求的實體;server 是被動 (passive) 等待請求的實體。Kernel 預設把 socket 建立的 descriptor 視為 active socket(連線的 client 端),server 必須呼叫 listen 告訴 kernel 這個 descriptor 是給 server 用的。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
/* Returns: 0 if OK, -1 on error */
- 把
sockfd從 active socket 轉換為 listening socket,可接受 client 的連線請求。 backlog是給 kernel 的提示 (hint):開始拒絕請求之前應排隊的未完成連線請求數;通常設為大值如 1024。
11.4.6 accept 函式 (p.972-973)
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
/* Returns: nonnegative connected descriptor if OK, -1 on error */
accept 等待連線請求抵達 listenfd,把 client 的 socket address 填入 addr,並回傳一個 connected descriptor,可用 Unix I/O 函式與 client 通訊。
| Listening descriptor | Connected descriptor | |
|---|---|---|
| 角色 | client 連線請求的端點 | client-server 之間已建立連線的端點 |
| 建立時機 | 通常只建立一次 | 每次 server 接受連線請求時建立 |
| 存活期間 | server 的整個生命週期 | 只在服務該 client 期間存在 |
連線建立三步驟(Figure 11.14;假設 listenfd=3,因為 descriptor 0-2 保留給標準檔案):
Step 1: Server 在 accept 中 block,等待 listenfd 上的連線請求
Client Server
clientfd listenfd(3)
Step 2: Client 呼叫 connect(送出連線請求後 block)
Client ---- Connection request ----> Server
clientfd listenfd(3)
Step 3: accept 開啟新的 connfd(4) 並回傳;client 從 connect 返回
Client <====== connection ======> Server
clientfd listenfd(3)
connfd(4)
乍看是多餘的複雜化,實則讓我們能建構並行伺服器 (concurrent server):每當 listening descriptor 收到連線請求,可以 fork 一個新 process 透過 connected descriptor 與該 client 通訊,同時處理多個連線。詳見 12-Concurrent-Programming/01-Process-and-Event-Based-Concurrency。
11.4.7 主機與服務轉換 (p.973-977)
Linux 提供 getaddrinfo 與 getnameinfo,在二進位 socket address 結構與字串表示(hostname、host address、service name、port number)之間雙向轉換;與 socket 介面併用時可寫出不依賴特定 IP 版本的網路程式。
getaddrinfo:字串 → socket address
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *host, const char *service,
const struct addrinfo *hints, struct addrinfo **result);
/* Returns: 0 if OK, nonzero error code on error */
void freeaddrinfo(struct addrinfo *result);
const char *gai_strerror(int errcode);
- 是過時的
gethostbyname/getservbyname的現代替代品;與它們不同,它是 reentrant(見 12.7.2)且適用於任何協定。 - 回傳指向 addrinfo 結構 linked list 的
result,每個節點指向一個對應 host+service 的 socket address 結構:
result --> [addrinfo] --ai_addr--> [socket addr struct]
| ai_canonname --> 官方主機名 (需 AI_CANONNAME)
| ai_next
v
[addrinfo] --ai_addr--> [socket addr struct]
| ai_next
v
[addrinfo] --ai_addr--> [socket addr struct]
| ai_next = NULL
- Client 走訪此串列,逐一嘗試直到
socket與connect成功;server 則嘗試到socket與bind成功為止。 - 用畢必須呼叫
freeaddrinfo釋放串列,避免 memory leak;非零錯誤碼可用gai_strerror轉為訊息字串(不是strerror)。 host可為 domain name 或數字位址(dotted-decimal);service可為服務名(如http)或十進位 port number。兩者可其一設為NULL,但至少要指定一個。
struct addrinfo {
int ai_flags; /* Hints argument flags */
int ai_family; /* First arg to socket function */
int ai_socktype; /* Second arg to socket function */
int ai_protocol; /* Third arg to socket function */
char *ai_canonname; /* Canonical hostname */
size_t ai_addrlen; /* Size of ai_addr struct */
struct sockaddr *ai_addr; /* Ptr to socket address structure */
struct addrinfo *ai_next; /* Ptr to next item in linked list */
};
hints 參數(可選)提供對回傳串列的細部控制:只能設定 ai_family、ai_socktype、ai_protocol、ai_flags 四個欄位,其餘必須為 0/NULL(實務上先 memset 清零再設定):
- 預設可同時回傳 IPv4 與 IPv6 位址;
ai_family = AF_INET限制為 IPv4,AF_INET6限制為 IPv6。 - 預設每個 unique address 最多回傳 3 個 addrinfo(connection、datagram、raw socket 各一);設
ai_socktype = SOCK_STREAM則每個位址最多一個(連線端點用)。 ai_flags為 bit mask,以 OR 組合:
| Flag | 效果 |
|---|---|
AI_ADDRCONFIG |
建議搭配連線使用:僅在本機有設定 IPv4 時才回傳 IPv4 位址(IPv6 同理) |
AI_CANONNAME |
令第一個 addrinfo 的 ai_canonname 指向 host 的正式 (canonical) 名稱(預設為 NULL) |
AI_NUMERICSERV |
強制 service 參數必須是 port number |
AI_PASSIVE |
回傳可供 server 作 listening socket 用的位址(預設是給 client connect 用的 active socket);此時 host 應為 NULL,位址欄位為 wildcard address,告訴 kernel 此 server 接受發往本主機任何 IP address 的請求 |
getaddrinfo 填好的欄位可直接傳給 socket 介面函式而不需再處理:ai_family, ai_socktype, ai_protocol → socket();ai_addr, ai_addrlen → connect() / bind()。這使 client/server 程式碼與 IP 版本完全解耦。
getnameinfo:socket address → 字串
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *service, size_t servlen, int flags);
/* Returns: 0 if OK, nonzero error code on error */
- 是
getaddrinfo的反函式;取代過時的gethostbyaddr/getservbyport,同樣 reentrant 且 protocol-independent。 - 把
sa轉成 host 與 service 名稱字串並複製到呼叫者提供的緩衝區。不要 hostname 可設host=NULL, hostlen=0(service 同理),但兩者至少要設一個。 flagsbit mask:NI_NUMERICHOST回傳數字位址字串而非 domain name;NI_NUMERICSERV跳過/etc/services查詢,直接回傳 port number。
hostinfo 範例程式 (p.977):類似 nslookup,hints 設 AF_INET + SOCK_STREAM、service=NULL 呼叫 getaddrinfo,再走訪串列以 getnameinfo(NI_NUMERICHOST) 將每個位址印成 dotted-decimal 字串,最後 freeaddrinfo。執行 ./hostinfo twitter.com 得到 4 個 IP address,與 11.3.2 的 nslookup 結果一致 (p.978)。Practice Problem 11.4 指出 getaddrinfo/getnameinfo 涵蓋了 inet_pton/inet_ntop 的功能且抽象層級更高、與位址格式無關。
11.4.8 Socket 介面 Helper 函式 (p.978-981)
CSAPP 以更高階的 open_clientfd 與 open_listenfd 封裝 getaddrinfo + socket 介面的樣板碼,兩者皆 reentrant 且 protocol-independent。
open_clientfd(client 端)
#include "csapp.h"
int open_clientfd(char *hostname, char *port);
/* Returns: descriptor if OK, -1 on error */
流程(Figure 11.18, code/src/csapp.c):
memset(&hints, 0, ...)
hints.ai_socktype = SOCK_STREAM
hints.ai_flags = AI_NUMERICSERV | AI_ADDRCONFIG
getaddrinfo(hostname, port, &hints, &listp)
|
v
for each p in listp: ┌─ socket 失敗 → continue(試下一個)
socket(p->ai_family, │
p->ai_socktype, │
p->ai_protocol) ──────────┘
connect(clientfd, p->ai_addr, p->ai_addrlen)
├─ 成功 → break
└─ 失敗 → Close(clientfd) 再試下一個 ← 必須先關閉!
|
v
freeaddrinfo(listp)
p == NULL ? return -1 : return clientfd
- 回傳的 descriptor 已可立即用 Unix I/O 讀寫。
connect失敗時務必先close該 descriptor 再嘗試下一個節點。- 程式碼中沒有任何對特定 IP 版本的依賴——
socket與connect的參數全由getaddrinfo自動產生。
open_listenfd(server 端)
#include "csapp.h"
int open_listenfd(char *port);
/* Returns: descriptor if OK, -1 on error */
流程(Figure 11.19):hints 設 SOCK_STREAM + AI_PASSIVE | AI_ADDRCONFIG | AI_NUMERICSERV,以 host=NULL 呼叫 getaddrinfo(因此位址為 wildcard address),走訪串列直到 socket 與 bind 成功,失敗一樣先 Close 再試下一個;最後呼叫 listen(listenfd, LISTENQ) 轉為 listening descriptor,若 listen 失敗須關閉 descriptor 避免洩漏。
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, ...):消除 bind 的 "Address already in use" 錯誤,讓 server 可被終止、重啟後立刻接受連線請求;否則重啟的 server 預設會拒絕 client 連線約 30 秒,嚴重妨礙除錯 (p.980)。
| open_clientfd | open_listenfd | |
|---|---|---|
| getaddrinfo 參數 | (hostname, port, ...) |
(NULL, port, ...) → wildcard address |
| hints flags | AI_NUMERICSERV | AI_ADDRCONFIG |
AI_PASSIVE | AI_ADDRCONFIG | AI_NUMERICSERV |
| 走訪串列直到 | socket + connect 成功 |
socket + bind 成功 |
| 額外步驟 | 無 | setsockopt(SO_REUSEADDR)、最後 listen |
| 回傳 | 可讀寫的 connected descriptor | listening descriptor |
11.4.9 Echo Client 與 Server 範例 (p.980-984)
Echo client(Figure 11.20):Open_clientfd 建立連線後,以 Rio_readinitb 初始化,進入迴圈——Fgets 從 stdin 讀一行文字、Rio_writen 送給 server、Rio_readlineb 讀回 echo 行、Fputs 印到 stdout。迴圈在 fgets 於 stdin 遇到 EOF 時終止(使用者按 Ctrl+D,或重導向的輸入檔讀完)。之後 client Close(clientfd)——這會使 EOF 通知送往 server(server 的 rio_readlineb 回傳 0 時偵測到)。process 終止時 kernel 會自動關閉所有開啟的 descriptor,故該 close 非必要,但明確關閉自己開啟的 descriptor 是良好的程式習慣。
Echo server(Figure 11.21,iterative):Open_listenfd 後進入無窮迴圈,每輪 Accept 等待連線、Getnameinfo 印出連上的 client 的 domain name 與 port、呼叫 echo(connfd) 服務該 client,返回後 Close(connfd)。雙方都關閉各自 descriptor 後,連線即終止。
clientaddr宣告為struct sockaddr_storage而非sockaddr_in:依定義,sockaddr_storage大到足以容納任何型別的 socket address,保持程式碼 protocol-independent (p.982)。- 此 server 一次只能服務一個 client,逐一輪流處理的 server 稱為 iterative server;Chapter 12 將介紹能同時服務多個 client 的 concurrent server。
echo 函式(Figure 11.22):Rio_readinitb 後,while ((n = Rio_readlineb(...)) != 0) 反覆讀一行、印出收到的 byte 數、Rio_writen 原樣寫回,直到 rio_readlineb 遇到 EOF。Rio 套件詳見 10-System-IO/02-Rio-Package-and-File-Metadata。
沒有所謂的 EOF 字元。EOF 是 kernel 偵測到的一種條件 (condition),應用程式在 read 回傳 0 時得知。對磁碟檔案,EOF 發生在目前檔案位置超過檔案長度時;對 Internet 連線,EOF 發生在某一端 process 關閉它那端的連線時——另一端在試圖讀取串流最後一個 byte 之後的資料時偵測到 EOF。
- 「
addrlen = sizeof(sockaddr_in)」僅適用於 IPv4;protocol-independent 程式應使用getaddrinfo填好的ai_addrlen,server 端接收位址則用sizeof(struct sockaddr_storage)。 - 「client 不需要 bind」是常態,但 kernel 會自動為 client 指派 ephemeral port——並非 client 沒有位址。
- 「close 非必要(kernel 會代關)」只在 process 即將終止時成立;長壽命 process(如迴圈中的 server)不 close 會造成 descriptor 洩漏。
Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
|---|---|
| socket 對 kernel / 對程式分別是什麼? | kernel:通訊端點;程式:有 descriptor 的開啟檔案 |
sin_port、sin_addr 的 byte order? |
一律 network byte order(big-endian) |
_in 字尾的意思? |
internet(不是 input) |
為何 socket 函式收 struct sockaddr *? |
當年 C 沒有 void *,以泛型結構 + 應用端強制轉型 (SA) 達成 |
socket() 回傳的 fd 能馬上讀寫嗎? |
不能,只是部分開啟;client 需 connect,server 需 bind+listen+accept |
connect 成功後連線由什麼標識? |
socket pair (x:y, addr.sin_addr:addr.sin_port),y 為 ephemeral port |
listen 的作用? |
把 kernel 預設的 active socket 轉為 listening socket;backlog 是排隊數提示(常設 1024) |
| listening vs connected descriptor? | 前者建立一次、活整個 server 生命期;後者每次 accept 建立、只活服務期間;區分是為了支援 concurrent server |
| server 的 listenfd 通常是 3,為什麼? | descriptor 0-2 保留給 stdin/stdout/stderr |
getaddrinfo 取代了誰?優點? |
取代 gethostbyname/getservbyname;reentrant、protocol-independent(getnameinfo 取代 gethostbyaddr/getservbyport) |
getaddrinfo 回傳什麼?用完要做什麼? |
addrinfo 結構的 linked list;必須 freeaddrinfo 否則 memory leak;錯誤用 gai_strerror 轉字串 |
| client / server 各走訪串列到何時停? | client:socket+connect 成功;server:socket+bind 成功 |
AI_PASSIVE + host=NULL 的效果? |
回傳 server 用的 listening 位址,位址欄為 wildcard address(接受本機任一 IP 的請求) |
| 每個 unique address 預設回傳幾個 addrinfo? | 最多 3 個(connection/datagram/raw);SOCK_STREAM 限制為最多 1 個 |
SO_REUSEADDR 的目的? |
消除 "Address already in use",否則重啟的 server 約 30 秒拒絕連線 |
為何 echo server 用 sockaddr_storage? |
大到能容納任何型別的 socket address,保持 protocol-independent |
| 連線上的 EOF 何時發生?如何偵測? | 對端 process 關閉其連線端時;read/rio_readlineb 回傳 0 |
| 一次服務一個 client 的 server 叫什麼? | iterative server(concurrent server 見 Ch12) |
inet_ntop vs getnameinfo? |
後者抽象層級更高、與位址格式無關(Practice Problem 11.4) |