Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Linux 網路程式設計 API 完整參考手冊

目錄

  1. Socket 基礎 API
  2. TCP 專用函數
  3. UDP 專用函數
  4. I/O 多工機制
  5. Socket 選項設定
  6. 資料收發函數
  7. 位址轉換函數
  8. 完整範例程式

Socket 基礎 API

核心函數總覽

函數功能TCPUDP說明
socket()建立 socket創建通訊端點
bind()綁定位址綁定本地 IP 和埠號
listen()監聽連線TCP 伺服器必須
accept()接受連線TCP 伺服器必須
connect()建立連線可選TCP 必須,UDP 可選
close()關閉 socket釋放資源
shutdown()關閉部分連線半關閉連線

1. socket() - 建立 Socket

函數原型

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

參數詳解

domain(協定家族)

說明用途
AF_INETIPv4 網路協定最常用
AF_INET6IPv6 網路協定IPv6 網路
AF_UNIX / AF_LOCALUnix domain socket本機行程間通訊
AF_PACKET原始封包介面網路監聽、封包分析

type(socket 類型)

說明特性協定
SOCK_STREAM串流 socket可靠、有序、雙向位元組流TCP
SOCK_DGRAM資料報 socket不可靠、無連線、訊息邊界UDP
SOCK_RAW原始 socket直接存取網路層ICMP、自訂協定
SOCK_SEQPACKET有序封包 socket可靠、有序、保留訊息邊界SCTP

type 可加入的標誌:

標誌說明
SOCK_NONBLOCK非阻塞模式(Linux 2.6.27+)
SOCK_CLOEXEC執行 exec 時關閉

protocol(協定)

說明
0自動選擇(最常用)
IPPROTO_TCP明確指定 TCP
IPPROTO_UDP明確指定 UDP
IPPROTO_ICMPICMP 協定
IPPROTO_RAW原始 IP 封包

返回值

  • 成功:socket 檔案描述符(非負整數)
  • 失敗:-1,並設定 errno

常見錯誤碼

errno說明
EACCES權限不足(如建立 raw socket 需要 root)
EMFILE行程打開檔案數達到上限
ENFILE系統打開檔案數達到上限
EPROTONOSUPPORT不支援的協定

使用範例

// TCP socket
int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_sock < 0) {
    perror("socket");
    exit(1);
}

// UDP socket
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);

// 非阻塞 TCP socket(Linux 2.6.27+)
int nonblock_sock = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);

// 原始 socket(需要 root 權限)
int raw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);

2. bind() - 綁定位址

函數原型

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

參數說明

參數說明
sockfdsocket 檔案描述符
addr要綁定的位址結構
addrlen位址結構的長度

sockaddr 結構家族

IPv4 位址結構

struct sockaddr_in {
    sa_family_t    sin_family;  // AF_INET
    in_port_t      sin_port;    // 埠號(網路位元組序)
    struct in_addr sin_addr;    // IP 位址
    char           sin_zero[8]; // 填充(必須為 0)
};

struct in_addr {
    uint32_t s_addr;  // IP 位址(網路位元組序)
};

IPv6 位址結構

struct sockaddr_in6 {
    sa_family_t     sin6_family;   // AF_INET6
    in_port_t       sin6_port;     // 埠號
    uint32_t        sin6_flowinfo; // IPv6 流量資訊
    struct in6_addr sin6_addr;     // IPv6 位址
    uint32_t        sin6_scope_id; // 範圍 ID
};

通用位址結構

struct sockaddr {
    sa_family_t sa_family;  // 位址家族
    char        sa_data[14]; // 位址資料
};

特殊 IP 位址

IP 位址巨集說明用途
0.0.0.0INADDR_ANY任意位址伺服器監聽所有網卡
127.0.0.1INADDR_LOOPBACK回環位址本機測試
255.255.255.255INADDR_BROADCAST廣播位址UDP 廣播

埠號說明

埠號範圍類型說明
0-1023知名埠需要特權(root)
1024-49151註冊埠常用服務埠
49152-65535動態埠客戶端臨時埠
0自動分配系統自動選擇可用埠

返回值

  • 成功:0
  • 失敗:-1,並設定 errno

常見錯誤碼

errno說明解決方法
EADDRINUSE位址已被使用設定 SO_REUSEADDR 或等待 TIME_WAIT
EACCES權限不足使用 >1024 的埠或提升權限
EINVALsocket 已綁定每個 socket 只能 bind 一次
EADDRNOTAVAIL位址不可用檢查 IP 位址是否正確

使用範例

// TCP 伺服器綁定
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

// 設定 SO_REUSEADDR(重要!)
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);           // 埠號 8080
addr.sin_addr.s_addr = INADDR_ANY;     // 監聽所有網卡

if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
    perror("bind");
    exit(1);
}

// 綁定特定 IP
addr.sin_addr.s_addr = inet_addr("192.168.1.100");
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

// UDP 伺服器綁定(與 TCP 相同)
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
bind(udp_sock, (struct sockaddr*)&addr, sizeof(addr));

// 客戶端不綁定(自動分配)
// TCP/UDP 客戶端通常不需要 bind,connect() 或第一次 sendto() 會自動綁定

3. listen() - 監聽連線(TCP 專用)

函數原型

#include <sys/socket.h>

int listen(int sockfd, int backlog);

參數說明

參數說明
sockfd已綁定的 socket
backlog連線佇列長度

backlog 詳解

說明建議
小值(1-10)低並發場景簡單服務
中值(128-512)一般 Web 服務推薦 128
大值(1024+)高並發場景大型服務
SOMAXCONN系統最大值通常 128(可調整)

注意backlog已完成三次握手但未被 accept() 的連線數,不是總連線數。

系統限制

# 查看系統最大值
cat /proc/sys/net/core/somaxconn

# 修改系統最大值
sudo sysctl -w net.core.somaxconn=1024

返回值

  • 成功:0
  • 失敗:-1

使用範例

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;

bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

// 開始監聽
if (listen(sockfd, 128) < 0) {
    perror("listen");
    exit(1);
}

printf("伺服器正在監聽埠 8080...\n");

// 使用系統最大值
listen(sockfd, SOMAXCONN);

TCP 連線佇列

TCP 有兩個佇列:

佇列說明狀態
SYN 佇列(半連線)收到 SYN,未完成三次握手SYN_RCVD
ACCEPT 佇列(全連線)完成三次握手,等待 accept()ESTABLISHED

backlog 參數控制 ACCEPT 佇列的長度。


4. accept() - 接受連線(TCP 專用)

函數原型

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

參數說明

參數說明
sockfd監聽 socket
addr客戶端位址(輸出參數,可為 NULL)
addrlen位址結構長度(輸入輸出參數)

返回值

  • 成功:新的連線 socket 檔案描述符
  • 失敗:-1

重要概念

兩個 socket 的區別:

Socket功能生命週期
監聽 socket接受新連線伺服器整個生命週期
連線 socket與客戶端通訊單次連線

常見錯誤碼

errno說明
EAGAIN/EWOULDBLOCK非阻塞模式下無連線
EINTR被信號中斷
EMFILE行程檔案描述符用盡
ENFILE系統檔案描述符用盡

accept4() - Linux 擴展

int accept4(int sockfd, struct sockaddr *addr, 
            socklen_t *addrlen, int flags);

flags 可用值:

標誌說明
SOCK_NONBLOCK返回的 socket 為非阻塞
SOCK_CLOEXEC執行 exec 時關閉

使用範例

// 1. 基本用法
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);

int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &addr_len);
if (connfd < 0) {
    perror("accept");
    exit(1);
}

// 取得客戶端資訊
char *client_ip = inet_ntoa(client_addr.sin_addr);
int client_port = ntohs(client_addr.sin_port);
printf("客戶端連線: %s:%d\n", client_ip, client_port);

// 2. 不關心客戶端位址
int connfd = accept(listenfd, NULL, NULL);

// 3. 非阻塞模式處理
while (1) {
    int connfd = accept(listenfd, NULL, NULL);
    if (connfd < 0) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            break;  // 沒有新連線
        }
        perror("accept");
        break;
    }
    // 處理新連線
}

// 4. 使用 accept4(Linux)
int connfd = accept4(listenfd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);

5. connect() - 建立連線

函數原型

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

參數說明

參數說明
sockfdsocket 檔案描述符
addr伺服器位址
addrlen位址結構長度

TCP vs UDP

協定是否必須行為
TCP✅ 必須發起三次握手,阻塞直到連線建立
UDP❌ 可選不發送封包,只記錄對端位址

返回值

  • 成功:0
  • 失敗:-1

常見錯誤碼

errno說明原因
ECONNREFUSED連線被拒絕伺服器未監聽該埠
ETIMEDOUT連線超時網路不通或防火牆阻擋
ENETUNREACH網路不可達路由問題
EINPROGRESS連線進行中非阻塞模式正常情況
EISCONN已經連線socket 已連線,不能重複

非阻塞 connect 處理

// 1. 設定非阻塞
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

// 2. 發起連線
if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
    if (errno == EINPROGRESS) {
        // 正常:連線正在進行中
        
        // 使用 select/poll/epoll 等待連線完成
        fd_set wset;
        FD_ZERO(&wset);
        FD_SET(sockfd, &wset);
        
        struct timeval timeout = {5, 0};  // 5 秒超時
        int ret = select(sockfd + 1, NULL, &wset, NULL, &timeout);
        
        if (ret > 0) {
            // 檢查連線是否成功
            int error;
            socklen_t len = sizeof(error);
            getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
            
            if (error == 0) {
                printf("連線成功\n");
            } else {
                printf("連線失敗: %s\n", strerror(error));
            }
        } else if (ret == 0) {
            printf("連線超時\n");
        }
    } else {
        perror("connect");
    }
}

使用範例

// TCP 客戶端連線
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");

if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    perror("connect");
    exit(1);
}

printf("已連線到伺服器\n");

// UDP「連線」(可選)
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
connect(udp_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
// UDP connect 後可以使用 send/recv 而不是 sendto/recvfrom

6. close() 和 shutdown() - 關閉連線

close() - 完全關閉

函數原型

#include <unistd.h>

int close(int fd);

行為說明

特性說明
引用計數fd 引用計數減 1,為 0 時才真正關閉
雙向關閉同時關閉讀和寫
資料處理根據 SO_LINGER 決定

返回值

  • 成功:0
  • 失敗:-1

shutdown() - 半關閉

函數原型

#include <sys/socket.h>

int shutdown(int sockfd, int how);

how 參數

說明效果
SHUT_RD (0)關閉讀端不能再 recv,但對端仍可 send
SHUT_WR (1)關閉寫端不能再 send,發送 FIN 給對端
SHUT_RDWR (2)關閉讀寫等同 close,但不減少引用計數

close() vs shutdown() 對比

特性close()shutdown()
影響範圍只影響本行程的 fd影響所有引用該 socket 的行程
引用計數減少引用計數不影響引用計數
半關閉不支援支援(SHUT_RD/SHUT_WR)
發送 FIN引用計數為 0 時發送SHUT_WR 立即發送

使用範例

// 1. 正常關閉
close(sockfd);

// 2. 優雅關閉(半關閉)
shutdown(sockfd, SHUT_WR);  // 告訴對方:我不再發送資料
// 此時還可以繼續接收對方資料
while (recv(sockfd, buffer, sizeof(buffer), 0) > 0) {
    // 接收剩餘資料
}
close(sockfd);

// 3. 立即關閉(丟棄未發送資料)
struct linger lng = {1, 0};
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lng, sizeof(lng));
close(sockfd);  // 發送 RST 而非 FIN

// 4. 父子行程共享 socket
int sockfd = accept(listenfd, NULL, NULL);
pid_t pid = fork();
if (pid == 0) {
    // 子行程
    close(listenfd);  // 關閉監聽 socket
    // 處理 sockfd
    close(sockfd);
    exit(0);
} else {
    // 父行程
    close(sockfd);  // 父行程不處理此連線
}

TCP 專用函數

TCP 狀態機

客戶端狀態                    伺服器狀態
                            LISTEN
CLOSED     ─── SYN ──>     SYN_RCVD
SYN_SENT   <── SYN+ACK ─   
ESTABLISHED ─── ACK ──>    ESTABLISHED
    ↓                           ↓
    ↓       [資料傳輸]          ↓
    ↓                           ↓
FIN_WAIT_1  ─── FIN ──>    CLOSE_WAIT
FIN_WAIT_2  <── ACK ───    CLOSE_WAIT
TIME_WAIT   <── FIN ───    LAST_ACK
TIME_WAIT   ─── ACK ──>    CLOSED
CLOSED (2MSL 後)

TCP 函數呼叫流程

伺服器端

socket() → bind() → listen() → accept() → recv()/send() → close()

客戶端

socket() → connect() → send()/recv() → close()

UDP 專用函數

sendto() - UDP 發送

函數原型

#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

參數說明

參數說明
sockfdUDP socket
buf要發送的資料
len資料長度
flags發送標誌(通常為 0)
dest_addr目標位址
addrlen位址長度

返回值

  • 成功:實際發送的位元組數
  • 失敗:-1

recvfrom() - UDP 接收

函數原型

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

參數說明

參數說明
sockfdUDP socket
buf接收緩衝區
len緩衝區大小
flags接收標誌(通常為 0)
src_addr來源位址(輸出參數)
addrlen位址長度(輸入輸出參數)

返回值

  • 成功:實際接收的位元組數
  • 失敗:-1

UDP 完整範例

// ============ UDP 伺服器 ============
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;

bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

char buffer[1024];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);

while (1) {
    // 接收資料
    int n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                     (struct sockaddr*)&client_addr, &addr_len);
    if (n > 0) {
        buffer[n] = '\0';
        printf("收到來自 %s:%d 的資料: %s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);
        
        // 回傳資料
        sendto(sockfd, buffer, n, 0,
               (struct sockaddr*)&client_addr, addr_len);
    }
}

// ============ UDP 客戶端 ============
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

char *msg = "Hello, UDP!";
sendto(sockfd, msg, strlen(msg), 0,
       (struct sockaddr*)&server_addr, sizeof(server_addr));

// 接收回應
char buffer[1024];
struct sockaddr_in recv_addr;
socklen_t addr_len = sizeof(recv_addr);
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                 (struct sockaddr*)&recv_addr, &addr_len);
buffer[n] = '\0';
printf("伺服器回應: %s\n", buffer);

close(sockfd);

UDP 使用「連線」模式

// UDP 也可以使用 connect(),好處:
// 1. 可以使用 send()/recv() 而非 sendto()/recvfrom()
// 2. 可以接收 ICMP 錯誤(如 ECONNREFUSED)

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

// 「連線」到伺服器
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

// 現在可以使用 send/recv
send(sockfd, "Hello", 5, 0);
char buffer[1024];
recv(sockfd, buffer, sizeof(buffer), 0);

close(sockfd);

I/O 多工機制

Socket 阻塞與非阻塞模式

在討論 I/O 多工之前,需要先理解 socket 本身就有阻塞和非阻塞兩種模式

1. 阻塞模式 (Blocking I/O) - 預設模式

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 預設就是阻塞模式

// accept() 會阻塞等待,直到有連線進來
int connfd = accept(sockfd, NULL, NULL);

// recv() 會阻塞等待,直到收到資料
recv(connfd, buffer, sizeof(buffer), 0);

2. 非阻塞模式 (Non-blocking I/O)

// 設定非阻塞
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

// 或建立時直接設定 (Linux 2.6.27+)
int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);

// accept() 立即返回,沒連線就返回 -1 (errno=EAGAIN)
int connfd = accept(sockfd, NULL, NULL);

// recv() 立即返回,沒資料就返回 -1 (errno=EAGAIN)
recv(connfd, buffer, sizeof(buffer), 0);

為什麼需要 select/poll/epoll?

問題場景:

  • 你有 100 個 socket 需要監控
  • 不知道哪個 socket 有資料可讀

❌ 沒有 I/O 多工的解決方案

方案 1:阻塞模式 - 無法同時監控多個 socket
// 只能一個一個檢查,會卡在第一個 recv
recv(sock1, buf, size, 0);  // 卡在這裡
recv(sock2, buf, size, 0);  // 永遠執行不到
方案 2:非阻塞模式 - CPU 空轉浪費資源
// 輪詢 (polling) - 不斷檢查每個 socket
while (1) {
    for (int i = 0; i < 100; i++) {
        int n = recv(socks[i], buf, size, 0);
        if (n > 0) {
            // 處理資料
        }
    }
    // CPU 空轉,100% 使用率!
}
方案 3:多執行緒 - 資源浪費
// 為每個 socket 開一個執行緒
for (int i = 0; i < 100; i++) {
    pthread_create(&thread, NULL, handle_client, &socks[i]);
}
// 100 個連線 = 100 個執行緒!記憶體、context switch 開銷大

✅ 有 select/poll/epoll 的解決方案

int epfd = epoll_create1(0);

// 註冊 100 個 socket
for (int i = 0; i < 100; i++) {
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = socks[i];
    epoll_ctl(epfd, EPOLL_CTL_ADD, socks[i], &ev);
}

// 一次監控所有 socket,只有就緒的會被通知
struct epoll_event events[100];
while (1) {
    // 這裡會阻塞等待,但只要有任何一個 socket 就緒就會返回
    int nfds = epoll_wait(epfd, events, 100, -1);

    for (int i = 0; i < nfds; i++) {
        int fd = events[i].data.fd;
        recv(fd, buffer, size, 0);  // 這裡保證有資料,不會阻塞
    }
}

核心優勢:

  1. 不需要輪詢 - 系統會通知你哪些 socket 就緒
  2. 不浪費 CPU - 沒有事件時進程休眠
  3. 可擴展性好 - 輕鬆處理成千上萬的連線

I/O 模型總結對比

模式優點缺點使用場景
阻塞 I/O簡單一次只能處理一個連線單一連線、簡單應用
非阻塞 I/O (輪詢)可處理多個連線CPU 空轉浪費不建議單獨使用
多執行緒/行程可處理多個連線資源開銷大連線數少 (<100)
select/poll/epoll高效監控多個連線程式碼複雜度高高並發 (>100 連線)

實際使用建議:

連線數推薦方案範例
< 10阻塞 I/O + 多執行緒pthread_create(&tid, NULL, handle_client, &connfd);
100-1000poll + 非阻塞poll(fds, nfds, -1);
> 1000epoll + 非阻塞 + 邊緣觸發epoll_wait(epfd, events, MAX_EVENTS, -1);

核心結論:

  1. Socket 本身可以阻塞/非阻塞 - 這是 socket 的屬性
  2. select/poll/epoll 用於監控多個 socket - 解決「不知道哪個 socket 就緒」的問題
  3. 搭配使用才高效 - epoll + 非阻塞是高並發伺服器的標準做法

select、poll、epoll 比較表

特性selectpollepoll
監控數量限制1024(FD_SETSIZE)無限制無限制
時間複雜度O(n)O(n)O(1)
記憶體複製每次都要複製每次都要複製只需註冊一次
跨平台✅ 所有 Unix/Linux/Windows✅ 所有 Unix/Linux❌ 僅 Linux
事件通知水平觸發水平觸發水平/邊緣觸發
適用場景<100 連線100-1000 連線>1000 連線

select() - POSIX 標準

函數原型

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

參數說明

參數說明
nfds最大 fd + 1
readfds監控可讀的 fd 集合
writefds監控可寫的 fd 集合
exceptfds監控異常的 fd 集合
timeout超時時間

fd_set 操作巨集

巨集功能
FD_ZERO(fd_set *set)清空集合
FD_SET(int fd, fd_set *set)加入 fd
FD_CLR(int fd, fd_set *set)移除 fd
FD_ISSET(int fd, fd_set *set)檢查 fd 是否就緒

使用範例

fd_set readfds;
struct timeval timeout;

while (1) {
    FD_ZERO(&readfds);
    FD_SET(sockfd, &readfds);
    
    timeout.tv_sec = 5;
    timeout.tv_usec = 0;
    
    int ret = select(sockfd + 1, &readfds, NULL, NULL, &timeout);
    
    if (ret > 0 && FD_ISSET(sockfd, &readfds)) {
        // sockfd 可讀
        recv(sockfd, buffer, sizeof(buffer), 0);
    }
}

poll() - System V 標準

函數原型

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
    int   fd;       // 檔案描述符
    short events;   // 要監控的事件
    short revents;  // 實際發生的事件
};

events 標誌

標誌說明
POLLIN有資料可讀
POLLOUT可以寫入資料
POLLERR發生錯誤
POLLHUP連線掛斷
POLLNVAL無效的 fd

使用範例

struct pollfd fds[2];
fds[0].fd = sockfd;
fds[0].events = POLLIN | POLLOUT;

while (1) {
    int ret = poll(fds, 1, 5000);  // 5 秒超時
    
    if (ret > 0) {
        if (fds[0].revents & POLLIN) {
            recv(sockfd, buffer, sizeof(buffer), 0);
        }
        if (fds[0].revents & POLLOUT) {
            send(sockfd, data, len, 0);
        }
    }
}

epoll() - Linux 專用高效能方案

三個核心函數

1. epoll_create1() - 建立 epoll 實例
int epoll_create1(int flags);
參數說明
flags0 或 EPOLL_CLOEXEC
返回值epoll 檔案描述符
2. epoll_ctl() - 控制 epoll
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
參數說明
epfdepoll 實例
op操作(ADD/MOD/DEL)
fd要操作的檔案描述符
event事件結構

op 操作類型:

說明
EPOLL_CTL_ADD加入新的監控
EPOLL_CTL_MOD修改已有的監控
EPOLL_CTL_DEL刪除監控
3. epoll_wait() - 等待事件
int epoll_wait(int epfd, struct epoll_event *events, 
               int maxevents, int timeout);

epoll_event 結構

struct epoll_event {
    uint32_t     events;   // 事件類型
    epoll_data_t data;     // 使用者資料
};

typedef union epoll_data {
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

events 標誌

標誌說明
EPOLLIN可讀
EPOLLOUT可寫
EPOLLET邊緣觸發模式
EPOLLONESHOT只觸發一次
EPOLLRDHUP對端關閉連線

觸發模式對比

模式說明優缺點
水平觸發(LT)只要有資料就通知簡單,但可能重複通知
邊緣觸發(ET)狀態變化時才通知一次高效能,但必須一次讀完

完整範例

// 1. 建立 epoll
int epfd = epoll_create1(0);

// 2. 註冊監聽 socket
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

// 3. 事件迴圈
struct epoll_event events[MAX_EVENTS];
while (1) {
    int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
    
    for (int i = 0; i < nfds; i++) {
        int fd = events[i].data.fd;
        
        if (fd == listenfd) {
            // 處理新連線
            int connfd = accept(listenfd, NULL, NULL);
            setnonblocking(connfd);
            
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = connfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
        } else {
            // 處理資料
            if (events[i].events & EPOLLIN) {
                // 邊緣觸發必須迴圈讀取
                while (1) {
                    int n = recv(fd, buffer, sizeof(buffer), 0);
                    if (n > 0) {
                        // 處理資料
                    } else if (n == 0 || errno != EAGAIN) {
                        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                        close(fd);
                        break;
                    } else {
                        break;  // 資料讀完
                    }
                }
            }
        }
    }
}

Socket 選項設定

getsockopt() / setsockopt()

函數原型

#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname,
               void *optval, socklen_t *optlen);

int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen);

SOL_SOCKET 層級選項

選項資料型別說明預設值
SO_REUSEADDRint允許重用本地位址0(關閉)
SO_REUSEPORTint允許多個 socket 綁定同一埠0(關閉)
SO_KEEPALIVEint啟用 TCP keepalive0(關閉)
SO_RCVBUFint接收緩衝區大小(位元組)系統預設
SO_SNDBUFint傳送緩衝區大小(位元組)系統預設
SO_RCVTIMEOstruct timeval接收超時無限等待
SO_SNDTIMEOstruct timeval傳送超時無限等待
SO_LINGERstruct linger關閉時行為優雅關閉
SO_BROADCASTint允許廣播(UDP)0(關閉)
SO_ERRORint取得錯誤狀態(唯讀)-
SO_TYPEint取得 socket 類型(唯讀)-

IPPROTO_TCP 層級選項

選項資料型別說明預設值
TCP_NODELAYint禁用 Nagle 演算法0(啟用 Nagle)
TCP_KEEPIDLEintKeepalive 開始時間(秒)7200
TCP_KEEPINTVLintKeepalive 間隔(秒)75
TCP_KEEPCNTintKeepalive 重試次數9
TCP_CORKint暫停資料傳送(攢夠再發)0(關閉)
TCP_DEFER_ACCEPTint延遲 accept(有資料才喚醒)0(關閉)
TCP_QUICKACKint快速確認(禁用延遲 ACK)0(啟用延遲)

IPPROTO_IP 層級選項

選項資料型別說明
IP_TOSint設定 Type of Service
IP_TTLint設定 Time to Live
IP_MULTICAST_LOOPint多播封包是否回環
IP_MULTICAST_TTLint多播封包 TTL
IP_ADD_MEMBERSHIPstruct ip_mreq加入多播群組
IP_DROP_MEMBERSHIPstruct ip_mreq離開多播群組

常用範例

// 1. SO_REUSEADDR(伺服器必備)
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

// 2. SO_REUSEPORT(負載均衡)
int reuse_port = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &reuse_port, sizeof(reuse_port));

// 3. TCP_NODELAY(低延遲)
int nodelay = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));

// 4. SO_KEEPALIVE + 參數設定
int keepalive = 1;
int idle = 60;        // 60 秒後開始
int interval = 5;     // 每 5 秒一次
int count = 3;        // 失敗 3 次斷線

setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count));

// 5. SO_LINGER(控制 close 行為)
struct linger lng;
lng.l_onoff = 1;
lng.l_linger = 0;  // 立即關閉,發送 RST
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lng, sizeof(lng));

// 6. SO_RCVTIMEO(接收超時)
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

// 7. SO_RCVBUF / SO_SNDBUF(緩衝區大小)
int buf_size = 64 * 1024;  // 64KB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(buf_size));
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &buf_size, sizeof(buf_size));

// 8. 取得 socket 類型
int type;
socklen_t len = sizeof(type);
getsockopt(sockfd, SOL_SOCKET, SO_TYPE, &type, &len);
if (type == SOCK_STREAM) {
    printf("TCP socket\n");
} else if (type == SOCK_DGRAM) {
    printf("UDP socket\n");
}

// 9. 取得錯誤狀態(非阻塞 connect 後使用)
int error;
socklen_t len = sizeof(error);
getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
if (error == 0) {
    printf("連線成功\n");
}

資料收發函數

函數總覽

函數TCPUDP說明
recv()接收資料
send()傳送資料
recvfrom()接收資料(含來源位址)
sendto()傳送資料(指定目標位址)
read()POSIX 讀取(無標誌)
write()POSIX 寫入(無標誌)
recvmsg()高階接收(支援控制訊息)
sendmsg()高階傳送(支援控制訊息)

recv() - 接收資料

函數原型

#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

flags 標誌

標誌說明
0預設行為(最常用)
MSG_PEEK偷看資料但不移除
MSG_WAITALL等到收滿 len 位元組
MSG_DONTWAIT非阻塞接收(單次)
MSG_OOB接收帶外資料
MSG_TRUNC返回真實長度(即使被截斷)

返回值說明

返回值說明
> 0實際接收的位元組數
0對方關閉連線(TCP)
-1錯誤(檢查 errno)

常見 errno

errno說明處理方式
EAGAIN / EWOULDBLOCK非阻塞模式無資料稍後重試
EINTR被信號中斷重新呼叫
ECONNRESET連線被重置關閉 socket
ETIMEDOUT接收超時關閉 socket

使用範例

// 1. 基本用法
char buffer[1024];
int n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n > 0) {
    printf("收到 %d 位元組\n", n);
} else if (n == 0) {
    printf("對方關閉連線\n");
} else {
    perror("recv");
}

// 2. 偷看資料(不移除)
int n = recv(sockfd, buffer, sizeof(buffer), MSG_PEEK);
// 資料還在緩衝區,下次 recv 還會讀到

// 3. 非阻塞處理
int n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n == -1) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // 無資料,稍後再試
    } else {
        perror("recv");
    }
}

// 4. 邊緣觸發必須迴圈讀取
while (1) {
    int n = recv(sockfd, buffer, sizeof(buffer), 0);
    if (n > 0) {
        // 處理資料
    } else if (n == 0) {
        break;  // 對方關閉
    } else {
        if (errno == EAGAIN) {
            break;  // 讀完了
        }
        perror("recv");
        break;
    }
}

// 5. 確保接收指定長度
int total = 0;
int len = 1024;
while (total < len) {
    int n = recv(sockfd, buffer + total, len - total, 0);
    if (n > 0) {
        total += n;
    } else if (n == 0) {
        printf("連線關閉,只收到 %d 位元組\n", total);
        break;
    } else {
        perror("recv");
        break;
    }
}

send() - 傳送資料

函數原型

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

flags 標誌

標誌說明
0預設行為
MSG_NOSIGNAL不產生 SIGPIPE 信號
MSG_DONTWAIT非阻塞發送
MSG_MORE還有更多資料(減少封包數)
MSG_OOB發送帶外資料

返回值

返回值說明
> 0實際傳送的位元組數(可能小於 len)
-1錯誤

常見 errno

errno說明
EAGAIN / EWOULDBLOCK傳送緩衝區滿
EPIPE連線已斷(會觸發 SIGPIPE)
ECONNRESET連線被重置

使用範例

// 1. 基本用法
char *data = "Hello, World!";
int n = send(sockfd, data, strlen(data), 0);

// 2. 避免 SIGPIPE
send(sockfd, data, len, MSG_NOSIGNAL);

// 3. 確保全部發送
int total = 0;
int len = strlen(data);
while (total < len) {
    int n = send(sockfd, data + total, len - total, MSG_NOSIGNAL);
    if (n > 0) {
        total += n;
    } else if (n == -1) {
        if (errno == EAGAIN) {
            continue;  // 緩衝區滿,稍後再試
        }
        perror("send");
        break;
    }
}

// 4. MSG_MORE 優化(減少封包數)
send(sockfd, header, header_len, MSG_MORE | MSG_NOSIGNAL);
send(sockfd, body, body_len, MSG_NOSIGNAL);  // 最後一次不加 MSG_MORE

recvfrom() / sendto() - UDP 專用

見前面 UDP 專用函數章節。

read() / write() - POSIX 標準

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

與 recv/send 的區別:

  • read/write 不支援 flags 參數
  • read/write 是通用 I/O 函數,不限於 socket
  • recv/send 是專門為 socket 設計的

位址轉換函數

字串 ↔ 二進位

inet_pton() / inet_ntop() - 推薦使用

#include <arpa/inet.h>

// 字串 → 二進位
int inet_pton(int af, const char *src, void *dst);

// 二進位 → 字串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
參數說明
afAF_INETAF_INET6
src來源位址
dst目標緩衝區
size緩衝區大小

使用範例:

// IPv4 字串 → 二進位
struct sockaddr_in addr;
inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);

// IPv4 二進位 → 字串
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr.sin_addr, ip_str, sizeof(ip_str));
printf("IP: %s\n", ip_str);

// IPv6
struct sockaddr_in6 addr6;
inet_pton(AF_INET6, "::1", &addr6.sin6_addr);

char ip6_str[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &addr6.sin6_addr, ip6_str, sizeof(ip6_str));

inet_addr() / inet_ntoa() - 舊版 API

// 字串 → 二進位(僅 IPv4)
in_addr_t inet_addr(const char *cp);

// 二進位 → 字串(僅 IPv4,不可重入)
char *inet_ntoa(struct in_addr in);

注意inet_ntoa() 不是執行緒安全的!

主機名稱解析

getaddrinfo() / getnameinfo() - 現代方法

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *node, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **res);

void freeaddrinfo(struct addrinfo *res);

int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,
                char *host, socklen_t hostlen,
                char *serv, socklen_t servlen, int flags);

使用範例:

// 解析主機名稱
struct addrinfo hints, *res;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;     // IPv4 或 IPv6
hints.ai_socktype = SOCK_STREAM; // TCP

int ret = getaddrinfo("www.google.com", "80", &hints, &res);
if (ret == 0) {
    // 使用 res
    int sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    connect(sockfd, res->ai_addr, res->ai_addrlen);
    
    freeaddrinfo(res);  // 釋放記憶體
}

// 反向解析(位址 → 主機名稱)
struct sockaddr_in addr;
char host[NI_MAXHOST];
char serv[NI_MAXSERV];

getnameinfo((struct sockaddr*)&addr, sizeof(addr),
            host, sizeof(host),
            serv, sizeof(serv),
            NI_NUMERICSERV);
printf("主機: %s, 埠: %s\n", host, serv);

位元組序轉換

函數說明
htons()Host to Network Short(16 位元)
htonl()Host to Network Long(32 位元)
ntohs()Network to Host Short(16 位元)
ntohl()Network to Host Long(32 位元)
// 埠號必須轉換為網路位元組序
uint16_t port = 8080;
addr.sin_port = htons(port);

// IP 位址也要轉換
uint32_t ip = 0xC0A80164;  // 192.168.1.100
addr.sin_addr.s_addr = htonl(ip);

// 讀取時轉回主機位元組序
uint16_t local_port = ntohs(addr.sin_port);

完整範例程式

TCP Echo 伺服器(epoll + 非阻塞 + ET)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define MAX_EVENTS 1024
#define BUFFER_SIZE 4096

// 設定非阻塞
void setnonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    // 1. 建立 socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        perror("socket");
        exit(1);
    }
    
    // 2. 設定 SO_REUSEADDR
    int reuse = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    
    // 3. 綁定位址
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    
    if (bind(listenfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        exit(1);
    }
    
    // 4. 開始監聽
    if (listen(listenfd, 128) < 0) {
        perror("listen");
        exit(1);
    }
    
    printf("伺服器正在監聽埠 %d...\n", PORT);
    
    // 5. 設定非阻塞
    setnonblocking(listenfd);
    
    // 6. 建立 epoll
    int epfd = epoll_create1(0);
    if (epfd < 0) {
        perror("epoll_create1");
        exit(1);
    }
    
    // 7. 註冊監聽 socket(邊緣觸發)
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = listenfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
    
    // 8. 事件迴圈
    struct epoll_event events[MAX_EVENTS];
    char buffer[BUFFER_SIZE];
    
    while (1) {
        int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (nfds < 0) {
            perror("epoll_wait");
            continue;
        }
        
        for (int i = 0; i < nfds; i++) {
            int fd = events[i].data.fd;
            
            // 新連線
            if (fd == listenfd) {
                while (1) {
                    struct sockaddr_in client_addr;
                    socklen_t len = sizeof(client_addr);
                    int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &len);
                    
                    if (connfd < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;  // 沒有新連線了
                        }
                        perror("accept");
                        break;
                    }
                    
                    printf("新連線: %s:%d (fd=%d)\n",
                           inet_ntoa(client_addr.sin_addr),
                           ntohs(client_addr.sin_port),
                           connfd);
                    
                    // 設定非阻塞
                    setnonblocking(connfd);
                    
                    // 加入 epoll
                    ev.events = EPOLLIN | EPOLLET;
                    ev.data.fd = connfd;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
                }
            }
            // 客戶端資料
            else {
                if (events[i].events & EPOLLIN) {
                    // 邊緣觸發必須迴圈讀取
                    while (1) {
                        int n = recv(fd, buffer, sizeof(buffer), 0);
                        
                        if (n > 0) {
                            printf("收到 %d 位元組 (fd=%d)\n", n, fd);
                            
                            // Echo 回去
                            int sent = 0;
                            while (sent < n) {
                                int s = send(fd, buffer + sent, n - sent, MSG_NOSIGNAL);
                                if (s > 0) {
                                    sent += s;
                                } else if (s == -1) {
                                    if (errno != EAGAIN) {
                                        perror("send");
                                        goto close_conn;
                                    }
                                    break;
                                }
                            }
                        } else if (n == 0) {
                            printf("客戶端關閉連線 (fd=%d)\n", fd);
                            goto close_conn;
                        } else {
                            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                                break;  // 資料讀完了
                            }
                            perror("recv");
                            goto close_conn;
                        }
                    }
                }
                
                if (events[i].events & (EPOLLERR | EPOLLHUP)) {
                    printf("連線錯誤或掛斷 (fd=%d)\n", fd);
                    goto close_conn;
                }
                
                continue;
                
close_conn:
                epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                close(fd);
            }
        }
    }
    
    close(epfd);
    close(listenfd);
    return 0;
}

TCP 客戶端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080

int main() {
    // 1. 建立 socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }
    
    // 2. 連線到伺服器
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
    
    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        exit(1);
    }
    
    printf("已連線到伺服器 %s:%d\n", SERVER_IP, SERVER_PORT);
    
    // 3. 發送和接收資料
    char buffer[1024];
    while (1) {
        printf("輸入訊息: ");
        fgets(buffer, sizeof(buffer), stdin);
        
        // 移除換行符
        buffer[strcspn(buffer, "\n")] = 0;
        
        if (strlen(buffer) == 0) {
            continue;
        }
        
        if (strcmp(buffer, "quit") == 0) {
            break;
        }
        
        // 發送資料
        int len = strlen(buffer);
        int sent = 0;
        while (sent < len) {
            int n = send(sockfd, buffer + sent, len - sent, 0);
            if (n > 0) {
                sent += n;
            } else {
                perror("send");
                goto cleanup;
            }
        }
        
        // 接收回應
        int n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0) {
            buffer[n] = '\0';
            printf("伺服器回應: %s\n", buffer);
        } else if (n == 0) {
            printf("伺服器關閉連線\n");
            break;
        } else {
            perror("recv");
            break;
        }
    }
    
cleanup:
    close(sockfd);
    return 0;
}

UDP Echo 伺服器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    // 1. 建立 UDP socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }
    
    // 2. 綁定位址
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    
    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        exit(1);
    }
    
    printf("UDP 伺服器正在監聽埠 %d...\n", PORT);
    
    // 3. 接收和發送資料
    char buffer[BUFFER_SIZE];
    struct sockaddr_in client_addr;
    socklen_t addr_len;
    
    while (1) {
        addr_len = sizeof(client_addr);
        
        // 接收資料
        int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
                        (struct sockaddr*)&client_addr, &addr_len);
        
        if (n > 0) {
            buffer[n] = '\0';
            printf("收到來自 %s:%d 的資料: %s\n",
                   inet_ntoa(client_addr.sin_addr),
                   ntohs(client_addr.sin_port),
                   buffer);
            
            // Echo 回去
            sendto(sockfd, buffer, n, 0,
                   (struct sockaddr*)&client_addr, addr_len);
        } else if (n < 0) {
            perror("recvfrom");
        }
    }
    
    close(sockfd);
    return 0;
}

UDP 客戶端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080

int main() {
    // 1. 建立 UDP socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }
    
    // 2. 設定伺服器位址
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
    
    // 3. 發送和接收資料
    char buffer[1024];
    while (1) {
        printf("輸入訊息: ");
        fgets(buffer, sizeof(buffer), stdin);
        buffer[strcspn(buffer, "\n")] = 0;
        
        if (strlen(buffer) == 0) {
            continue;
        }
        
        if (strcmp(buffer, "quit") == 0) {
            break;
        }
        
        // 發送資料
        sendto(sockfd, buffer, strlen(buffer), 0,
               (struct sockaddr*)&server_addr, sizeof(server_addr));
        
        // 接收回應
        struct sockaddr_in recv_addr;
        socklen_t addr_len = sizeof(recv_addr);
        int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
                        (struct sockaddr*)&recv_addr, &addr_len);
        
        if (n > 0) {
            buffer[n] = '\0';
            printf("伺服器回應: %s\n", buffer);
        }
    }
    
    close(sockfd);
    return 0;
}

編譯和執行

編譯命令

# TCP 伺服器
gcc -o tcp_server tcp_server.c -Wall -O2

# TCP 客戶端
gcc -o tcp_client tcp_client.c -Wall -O2

# UDP 伺服器
gcc -o udp_server udp_server.c -Wall -O2

# UDP 客戶端
gcc -o udp_client udp_client.c -Wall -O2

執行

# 終端機 1:啟動伺服器
./tcp_server

# 終端機 2:啟動客戶端
./tcp_client

最佳實踐建議

1. Socket 選項設定

場景必要設定
TCP 伺服器SO_REUSEADDR
低延遲應用TCP_NODELAY
長連線SO_KEEPALIVE + 參數
高並發epoll + 非阻塞 + 邊緣觸發

2. 錯誤處理

// EAGAIN/EWOULDBLOCK - 重試
if (errno == EAGAIN || errno == EWOULDBLOCK) {
    // 正常,稍後重試
}

// EINTR - 重新呼叫
if (errno == EINTR) {
    // 被信號中斷,重新呼叫
}

// EPIPE/ECONNRESET - 關閉連線
if (errno == EPIPE || errno == ECONNRESET) {
    close(sockfd);
}

3. I/O 模型選擇

連線數推薦方案
< 100select 或阻塞 I/O
100-1000poll
> 1000epoll(Linux)

4. 記憶體管理

// 使用完後釋放
freeaddrinfo(res);

// 關閉 fd
close(sockfd);

// epoll 刪除監控
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);

參考資源

線上手冊

man 2 socket
man 2 bind
man 7 tcp
man 7 udp
man 7 ip
man 7 epoll

重要標頭檔

#include <sys/socket.h>    // socket, bind, listen, accept, connect
#include <netinet/in.h>    // sockaddr_in, INADDR_ANY
#include <arpa/inet.h>     // inet_pton, inet_ntop
#include <sys/epoll.h>     // epoll_create1, epoll_ctl, epoll_wait
#include <fcntl.h>         // fcntl, O_NONBLOCK
#include <unistd.h>        // close, read, write

結語

這份文件涵蓋了 Linux 網路程式設計的核心 API,包括:

  • ✅ Socket 基礎函數(socket, bind, listen, accept, connect)
  • ✅ TCP/UDP 專用函數
  • ✅ I/O 多工機制(select, poll, epoll)
  • ✅ Socket 選項設定(setsockopt)
  • ✅ 資料收發函數(send, recv, sendto, recvfrom)
  • ✅ 位址轉換函數
  • ✅ 完整的範例程式

希望這份參考手冊對你的網路程式設計有所幫助!