首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

Linux Socket 網路程式設計完整指南:從基礎 API 到 TCP 連線實戰

     編輯:關於C語言

Linux Socket 網路程式設計完整指南

我們每天瀏覽網頁、使用即時通訊軟體,背後都仰賴網路進程間的通訊。瀏覽器進程如何與 Web 伺服器溝通?QQ 進程如何傳遞訊息給好友?核心關鍵在於 Socket。本文將系統性介紹 Socket 概念、類型、核心 API、TCP 三次握手與四次揮手機制,並提供完整的客戶端/伺服器端範例代碼。

1. 網路進程如何通訊?

本地進程間通訊(IPC)有管道、訊息佇列、共享記憶體、訊號量等方式,但網路環境下,如何唯一標識一個進程是首要問題。本地可用 PID,但網路中無效。TCP/IP 協議族解決了這個問題:

  • 網路層的 IP 位址 唯一標識主機
  • 傳輸層的 協議 + 連接埠 唯一標識主機上的應用程式(進程)

利用三元組 (IP 位址, 協議, 連接埠) 即可標識網路進程,實現跨主機通訊。現代應用幾乎皆採用 UNIX BSD Socket API,故有「一切皆 Socket」之說。

2. 什麼是 Socket?

Socket 起源於 Unix,呼應「一切皆檔案」哲學:以 open → read/write → close 模式操作。Socket 是一種特殊檔案,Socket 函數即對其進行開啟、讀寫、關閉等操作。歷史上,Socket 一詞首見於 1970 年 IETF RFC 33,早於 BSD Socket 定義約 12 年。

3. Socket 基本操作(以 TCP 為例)

3.1 socket() 函數:建立 Socket 描述符

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

對應檔案的 open,返回唯一識別 Socket 的描述符。三個參數:

  • domain(協議族):AF_INET (IPv4)、AF_INET6 (IPv6)、AF_LOCAL/AF_UNIX (本地通訊) 等,決定位址類型。
  • type(Socket 類型):SOCK_STREAM (串流/TCP)、SOCK_DGRAM (資料報/UDP)、SOCK_RAW 等。
  • protocol(協議):IPPROTO_TCP、IPPROTO_UDP 等。通常設 0 讓系統依 type 自動選預設協議。

注意:type 與 protocol 不可任意組合(如 SOCK_STREAM 不能搭配 IPPROTO_UDP)。新建 Socket 無具體位址,需 bind() 指定,否則 connect()/listen() 時由系統隨機分配。

3.2 bind() 函數:綁定位址

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

將特定位址(IP + 連接埠)指派給 Socket。sockaddr 結構隨協議族不同:

  • IPv4:struct sockaddr_in(含 sin_family, sin_port, sin_addr)
  • IPv6:struct sockaddr_in6
  • Unix 域:struct sockaddr_un(路徑名稱)

關鍵:網路位元組序。主機位元組序(大端/小端)因 CPU 而異,網路傳輸統一用大端(Network Byte Order)。綁定前務必用 htons(), htonl() 轉換,否則會引發難以除錯的錯誤。

3.3 listen() 與 connect():監聽與發起連線

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • listen() 將主動 Socket 轉為被動監聽狀態,backlog 指定佇列最大連線數。
  • connect() 由客戶端呼叫,向伺服器發起 TCP 連線請求。

3.4 accept() 函數:接受連線

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

伺服器調用 accept() 取得客戶端連線。關鍵區別:

  • 第一個參數:監聽 Socket 描述符(socket() 建立,生命週期贯穿伺服器)
  • 返回值:已連線 Socket 描述符(每個客戶連線對應一個,服務完成後關閉)

3.5 網路 I/O 函數:read/write, recv/send 等

連線建立後進行資料收發。常用函數組:

read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()

推薦使用 recvmsg()/sendmsg(),功能最完整,可替代其他函數。read() 返回 0 表示對端關閉,<0 表示錯誤(EINTR 中斷、ECONNRESET 連線重置)。write() 返回值 >0 為寫入位元組數,<0 為錯誤(EPIPE 對端已關閉)。

3.6 close() 函數:關閉 Socket

結束通訊時關閉描述符,釋放資源,類似 fclose()。

4. TCP 三次握手建立連線詳解

TCP 三次握手示意圖

  1. 客戶端發送 SYN (seq=x),進入 SYN_SENT 狀態
  2. 伺服器回覆 SYN+ACK (seq=y, ack=x+1),進入 SYN_RCVD
  3. 客戶端發送 ACK (ack=y+1),雙方進入 ESTABLISHED

5. TCP 四次揮手釋放連線詳解

TCP 四次揮手示意圖

  1. 主動關閉端發送 FIN (seq=u),進入 FIN_WAIT_1
  2. 被動端回覆 ACK (ack=u+1),進入 CLOSE_WAIT;主動端進入 FIN_WAIT_2
  3. 被動端發送 FIN (seq=v),進入 LAST_ACK
  4. 主動端回覆 ACK (ack=v+1),進入 TIME_WAIT(等待 2MSL 後關閉)

6. 實戰範例:簡易 TCP 回聲伺服器與客戶端

伺服器端 (迭代模式,單執行緒)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXLINE 4096

int main() {
    int listenfd, connfd;
    struct sockaddr_in servaddr;
    char buff[MAXLINE];
    int n;

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("create socket error");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(6666);

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
        perror("bind socket error");
        exit(EXIT_FAILURE);
    }

    if (listen(listenfd, 10) == -1) {
        perror("listen socket error");
        exit(EXIT_FAILURE);
    }

    printf("====== waiting for client's request ======\n");
    while (1) {
        if ((connfd = accept(listenfd, NULL, NULL)) == -1) {
            perror("accept socket error");
            continue;
        }
        n = recv(connfd, buff, MAXLINE, 0);
        buff[n] = '\0';
        printf("recv msg from client: %s\n", buff);
        close(connfd);
    }
    close(listenfd);
    return 0;
}

客戶端

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

#define MAXLINE 4096

int main(int argc, char** argv) {
    if (argc != 2) {
        fprintf(stderr, "usage: %s \n", argv[0]);
        exit(EXIT_FAILURE);
    }

    int sockfd;
    char sendline[MAXLINE];
    struct sockaddr_in servaddr;

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("create socket error");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
        perror("inet_pton error");
        exit(EXIT_FAILURE);
    }

    if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
        perror("connect error");
        exit(EXIT_FAILURE);
    }

    printf("send msg to server: \n");
    fgets(sendline, MAXLINE, stdin);
    if (send(sockfd, sendline, strlen(sendline), 0) < 0) {
        perror("send msg error");
        exit(EXIT_FAILURE);
    }
    close(sockfd);
    return 0;
}

提示:此為迭代伺服器,一次只能處理一個請求。生產環境需使用 fork()、執行緒池或 I/O 多路複用(select/poll/epoll)實現並發。

7. 思考題

請嘗試編寫:伺服器監聽 192.168.100.168,接收來自 192.168.100.2 的訊息;若收到 "Client Query" 則印出 "Receive Query"。客戶端依序發送 "Client Query test"、"Client Query"、"Client Query Quit" 後退出。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved