這次我們介紹 accept
函數,它是 TCP 服務器用來接受客戶端連接請求的核心系統調用。
1. 函數介紹
accept
是一個 Linux 系統調用,專門用于TCP 服務器(使用 SOCK_STREAM
套接字)。它的主要功能是從監聽套接字(通過 listen
設置的套接字)的未決連接隊列(pending connection queue)中取出第一個連接請求,并為這個新連接創建一個全新的、獨立的套接字文件描述符。
你可以把 accept
想象成總機接線員:
- 有很多電話(客戶端連接請求)打進來,響鈴并排隊在總機(監聽套接字)那里。
- 接線員(
accept
調用)拿起一個響鈴的電話。 - 接線員把這條線路接到一個新的、專用的電話線(新的套接字文件描述符)上。
- 接線員可以繼續去接下一個電話(下一次
accept
調用),而第一個通話(與第一個客戶端的通信)則通過那條專用線路進行,互不干擾。
這個新創建的套接字文件描述符專門用于與那一個特定的客戶端進行雙向數據通信。原始的監聽套接字則繼續保持監聽狀態,等待并接受更多的連接請求。
2. 函數原型
#include <sys/socket.h> // 必需// 標準形式
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);// 帶有標志的變體 (Linux 2.6.28+)
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
3. 功能
- 從隊列中取出連接: 從監聽套接字
sockfd
維護的未決連接隊列中提取第一個已完成或正在完成的連接請求。 - 創建新套接字: 為這個新連接創建一個新的、非監聽狀態的套接字文件描述符。
- 返回通信端點: 返回這個新的套接字文件描述符,服務器程序可以使用它來與特定的客戶端進行數據交換(
read
/write
)。 - 獲取客戶端信息: 如果
addr
和addrlen
參數不為 NULL,則將連接到服務器的客戶端的地址信息(IP 地址和端口號)填充到addr
指向的緩沖區中。
4. 參數
-
int sockfd
: 這是監聽套接字的文件描述符。它必須是:- 通過
socket()
成功創建的。 - 通過
bind()
綁定了本地地址(IP 和端口)的。 - 通過
listen()
進入監聽狀態的。
- 通過
-
struct sockaddr *addr
: 這是一個指向套接字地址結構的指針,用于接收客戶端的地址信息。- 如果你不關心客戶端是誰,可以傳入
NULL
。 - 如果傳入非
NULL
值,則它通常指向一個struct sockaddr_in
(IPv4) 或struct sockaddr_in6
(IPv6) 類型的變量。 - 該結構體在
accept
返回后會被填入客戶端的地址信息。
- 如果你不關心客戶端是誰,可以傳入
-
socklen_t *addrlen
: 這是一個指向socklen_t
類型變量的指針。- 輸入: 在調用
accept
時,這個變量必須被初始化為addr
指向的緩沖區的大小(以字節為單位)。例如,如果addr
指向struct sockaddr_in
,則*addrlen
應初始化為sizeof(struct sockaddr_in)
。 - 輸出:
accept
返回時,這個變量會被更新為實際存儲在addr
中的地址結構的大小。這對于處理不同大小的地址結構(如 IPv4 和 IPv6)很有用。
- 輸入: 在調用
-
int flags
(accept4
特有): 這個參數允許在創建新套接字時設置一些屬性,類似于socket()
的type
參數可以使用的修飾符。SOCK_NONBLOCK
: 將新創建的套接字設置為非阻塞模式。SOCK_CLOEXEC
: 在調用exec()
時自動關閉該套接字。
5. 返回值
- 成功時: 返回一個新的、非負的整數,即為新連接創建的套接字文件描述符。服務器應使用這個返回的文件描述符與客戶端進行后續的數據通信。
- 失敗時: 返回 -1,并設置全局變量
errno
來指示具體的錯誤原因(例如EAGAIN
或EWOULDBLOCK
套接字被標記為非阻塞且沒有未決連接,EBADF
sockfd
無效,EINVAL
套接字未監聽,EMFILE
進程打開的文件描述符已達上限等)。
阻塞與非阻塞:
- 阻塞套接字(默認):如果監聽隊列中沒有待處理的連接,
accept
調用會阻塞(掛起)當前進程,直到有新的連接到達。 - 非阻塞套接字(如果監聽套接字被設置為非阻塞):如果監聽隊列中沒有待處理的連接,
accept
會立即返回 -1,并將errno
設置為EAGAIN
或EWOULDBLOCK
。
6. 相似函數,或關聯函數
socket
: 用于創建原始的監聽套接字。bind
: 將監聽套接字綁定到本地地址。listen
: 使套接字進入監聽狀態,開始接收連接請求。connect
: 客戶端使用此函數向服務器發起連接。close
: 服務器在與客戶端通信結束后,需要關閉accept
返回的那個套接字文件描述符。通常也需要關閉原始的監聽套接字(在服務器退出時)。fork
/ 多線程: 服務器通常在accept
之后調用fork
或創建新線程來處理與客戶端的通信,以便主服務器進程可以繼續調用accept
接受新的連接。
7. 示例代碼
示例 1:基本的 TCP 服務器 accept
循環
這個例子演示了一個典型的、順序處理的 TCP 服務器如何使用 accept
循環來接受和處理客戶端連接。
// sequential_tcp_server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h> // inet_ntoa (注意:不是線程安全的)#define PORT 8080
#define BACKLOG 10void handle_client(int client_fd, struct sockaddr_in *client_addr) {char buffer[1024];ssize_t bytes_read;printf("Handling client %s:%d (fd: %d)\n",inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port), client_fd);// 讀取客戶端發送的數據while ((bytes_read = read(client_fd, buffer, sizeof(buffer) - 1)) > 0) {buffer[bytes_read] = '\0'; // 確保字符串結束printf("Received from client: %s", buffer); // buffer 可能已包含 \n// 將收到的數據回顯給客戶端if (write(client_fd, buffer, bytes_read) != bytes_read) {perror("write to client failed");break;}}if (bytes_read < 0) {perror("read from client failed");} else {printf("Client %s:%d disconnected (fd: %d)\n",inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port), client_fd);}close(client_fd); // 關閉與該客戶端的連接
}int main() {int server_fd, client_fd;struct sockaddr_in address, client_address;socklen_t client_addr_len = sizeof(client_address);int opt = 1;// 1. 創建套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 2. 設置套接字選項if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {perror("setsockopt failed");close(server_fd);exit(EXIT_FAILURE);}// 3. 配置服務器地址address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);// 4. 綁定套接字if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");close(server_fd);exit(EXIT_FAILURE);}// 5. 監聽連接if (listen(server_fd, BACKLOG) < 0) {perror("listen failed");close(server_fd);exit(EXIT_FAILURE);}printf("Server listening on port %d\n", PORT);// 6. 主循環:接受并處理連接while (1) {printf("Waiting for a connection...\n");// 7. 接受連接 (阻塞調用)client_fd = accept(server_fd, (struct sockaddr *)&client_address, &client_addr_len);if (client_fd < 0) {perror("accept failed");continue; // 或 exit(EXIT_FAILURE);}printf("New connection accepted.\n");// 8. 處理客戶端 (順序處理,同一時間只能處理一個)handle_client(client_fd, &client_address);// 處理完一個客戶端后,循環繼續 accept 下一個}// 注意:在實際程序中,需要有退出機制和清理代碼// close(server_fd); // 不會執行到這里return 0;
}
代碼解釋:
- 創建、綁定、監聽服務器套接字,這部分與之前
socket
,bind
,listen
的例子相同。 - 進入一個無限的
while(1)
循環。 - 在循環內部,調用
accept(server_fd, (struct sockaddr *)&client_address, &client_addr_len)
。server_fd
: 監聽套接字。&client_address
: 指向sockaddr_in
結構的指針,用于接收客戶端地址。&client_addr_len
: 指向socklen_t
變量的指針,該變量在調用前被初始化為sizeof(client_address)
。
accept
是一個阻塞調用。如果沒有客戶端連接,程序會在此處掛起等待。- 當有客戶端連接到達時,
accept
返回一個新的文件描述符client_fd
。 - 調用
handle_client
函數處理與該客戶端的通信。這個函數會讀取客戶端數據并回顯回去。 handle_client
函數結束時(客戶端斷開或出錯),會調用close(client_fd)
關閉這個連接。- 主循環繼續,再次調用
accept
等待下一個客戶端。
缺點: 這種順序處理的方式效率很低。服務器在處理一個客戶端時,無法接受其他客戶端的連接,直到當前客戶端處理完畢。
示例 2:并發 TCP 服務器 (使用 fork
)
這個例子演示了如何使用 fork
創建子進程來并發處理多個客戶端連接。
// concurrent_tcp_server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/wait.h> // waitpid#define PORT 8080
#define BACKLOG 10void handle_client(int client_fd, struct sockaddr_in *client_addr) {char buffer[1024];ssize_t bytes_read;printf("Child %d: Handling client %s:%d (fd: %d)\n",getpid(), inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port), client_fd);while ((bytes_read = read(client_fd, buffer, sizeof(buffer) - 1)) > 0) {buffer[bytes_read] = '\0';printf("Child %d: Received from client: %s", getpid(), buffer);if (write(client_fd, buffer, bytes_read) != bytes_read) {perror("Child: write to client failed");break;}}if (bytes_read < 0) {perror("Child: read from client failed");} else {printf("Child %d: Client %s:%d disconnected.\n",getpid(), inet_ntoa(client_addr->sin_addr), ntohs(client_addr->sin_port));}close(client_fd);printf("Child %d: Connection closed. Exiting.\n", getpid());_exit(EXIT_SUCCESS); // 子進程使用 _exit 退出
}int main() {int server_fd, client_fd;struct sockaddr_in address, client_address;socklen_t client_addr_len = sizeof(client_address);int opt = 1;pid_t pid;if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {perror("setsockopt failed");close(server_fd);exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");close(server_fd);exit(EXIT_FAILURE);}if (listen(server_fd, BACKLOG) < 0) {perror("listen failed");close(server_fd);exit(EXIT_FAILURE);}printf("Concurrent Server (PID: %d) listening on port %d\n", getpid(), PORT);while (1) {client_fd = accept(server_fd, (struct sockaddr *)&client_address, &client_addr_len);if (client_fd < 0) {perror("accept failed");continue;}printf("Main process (PID: %d): New connection accepted.\n", getpid());// Fork a new process to handle the clientpid = fork();if (pid < 0) {perror("fork failed");close(client_fd); // Important: close the client fd on fork failure} else if (pid == 0) {// --- Child process ---close(server_fd); // Child doesn't need the listening sockethandle_client(client_fd, &client_address);// handle_client calls close(client_fd) and _exit()// so nothing more needed here} else {// --- Parent process ---close(client_fd); // Parent doesn't need the client-specific socketprintf("Main process (PID: %d): Forked child process (PID: %d) to handle client.\n", getpid(), pid);// Optional: Clean up any finished child processes (non-blocking)// This prevents zombie processes if children finish quicklypid_t wpid;int status;while ((wpid = waitpid(-1, &status, WNOHANG)) > 0) {printf("Main process (PID: %d): Reaped child process (PID: %d)\n", getpid(), wpid);}}}close(server_fd);return 0;
}
代碼解釋:
- 服務器設置部分與順序服務器相同。
- 在
accept
成功返回后,立即調用fork()
。 fork
返回后:- 在子進程 (
pid == 0
):- 關閉不需要的監聽套接字
server_fd
。 - 調用
handle_client(client_fd, ...)
處理客戶端。 handle_client
處理完畢后會關閉client_fd
并調用_exit()
退出。
- 關閉不需要的監聽套接字
- 在父進程 (
pid > 0
):- 關閉不需要的客戶端套接字
client_fd
(因為子進程在處理它)。 - 打印信息,表明已派生子進程處理客戶端。
- 可選地調用
waitpid(-1, &status, WNOHANG)
來非阻塞地清理已經結束的子進程(回收僵尸進程)。如果省略這一步,結束的子進程會變成僵尸進程,直到父進程退出。
- 關閉不需要的客戶端套接字
- 在子進程 (
- 父進程繼續循環,調用
accept
等待下一個客戶端連接。
示例 3:使用 accept4
設置非阻塞客戶端套接字
這個例子演示了如何使用 accept4
函數在創建新連接套接字的同時就將其設置為非阻塞模式。
// accept4_example.c
#define _GNU_SOURCE // 必須定義以使用 accept4
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h> // F_GETFL, F_SETFL, O_NONBLOCK#define PORT 8080
#define BACKLOG 10int main() {int server_fd, client_fd;struct sockaddr_in address, client_address;socklen_t client_addr_len = sizeof(client_address);int opt = 1;int flags;if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {perror("setsockopt failed");close(server_fd);exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");close(server_fd);exit(EXIT_FAILURE);}if (listen(server_fd, BACKLOG) < 0) {perror("listen failed");close(server_fd);exit(EXIT_FAILURE);}printf("Server listening on port %d. Accepting connections...\n", PORT);while (1) {printf("Waiting for a connection...\n");// 使用 accept4 直接創建非阻塞的客戶端套接字client_fd = accept4(server_fd, (struct sockaddr *)&client_address, &client_addr_len, SOCK_NONBLOCK);if (client_fd < 0) {perror("accept4 failed");continue;}printf("New connection accepted (fd: %d). Checking if it's non-blocking...\n", client_fd);// 驗證套接字是否確實是非阻塞的flags = fcntl(client_fd, F_GETFL, 0);if (flags == -1) {perror("fcntl F_GETFL failed");close(client_fd);continue;}if (flags & O_NONBLOCK) {printf("Confirmed: Client socket (fd: %d) is non-blocking.\n", client_fd);} else {printf("Warning: Client socket (fd: %d) is NOT non-blocking.\n", client_fd);}// --- 在這里,你可以對非阻塞的 client_fd 進行 read/write/select/poll 操作 ---// 例如,將其添加到 epoll 或 select 的監視集合中// 為了演示,我們簡單地關閉它printf("Closing client socket (fd: %d).\n", client_fd);close(client_fd);}close(server_fd);return 0;
}
代碼解釋:
- 服務器設置部分與之前相同。
- 在調用
accept4
時,傳入了SOCK_NONBLOCK
標志作為第四個參數。 - 如果
accept4
成功,返回的client_fd
就已經被設置為非阻塞模式。 - 代碼通過
fcntl(client_fd, F_GETFL, 0)
獲取套接字標志,并檢查O_NONBLOCK
位是否被設置,以驗證accept4
的效果。 - 在實際應用中,得到非阻塞的
client_fd
后,通常會將其加入到select
、poll
或epoll
的監視集合中,以便高效地管理多個并發連接。
重要提示與注意事項:
- 返回新的文件描述符:
accept
返回的文件描述符與原始監聽套接字sockfd
完全不同。原始套接字繼續用于監聽,新套接字用于與特定客戶端通信。 - 必須關閉: 服務器在與客戶端通信結束后,必須調用
close()
關閉accept
返回的那個文件描述符,以釋放資源。 - 獲取客戶端地址: 利用
addr
和addrlen
參數獲取客戶端的 IP 和端口對于日志記錄、訪問控制、調試等非常有用。 - 并發處理: 對于需要同時處理多個客戶端的服務器,必須使用
fork
、多線程或 I/O 多路復用(select
/poll
/epoll
)等技術。簡單的順序處理無法滿足實際需求。 - 錯誤處理: 始終檢查
accept
的返回值。在繁忙的服務器上,非阻塞accept
可能會因為沒有連接而返回EAGAIN
。 accept4
的優勢:accept4
可以在原子操作中設置新套接字的屬性,避免了先accept
再fcntl
的兩步操作,理論上更高效且沒有競態條件。
總結:
accept
是 TCP 服務器模型的核心。它使得服務器能夠從監聽狀態進入與客戶端的實際數據交換狀態。理解其阻塞/非阻塞行為、返回值含義以及如何與并發處理技術(如 fork
)結合使用,是構建健壯網絡服務器的基礎。accept4
則為需要精細控制新連接套接字屬性的場景提供了便利。