Socket 是網絡編程的核心抽象,Linux 將其視為一種特殊的文件描述符,統一了本地進程通信與網絡通信的接口。本文系統講解 Socket 概念、類型、核心 API、地址結構、字節序轉換、連接建立流程,並提供完整的 TCP 客戶端/服務端範例。
| 屬性 | 說明 |
|---|---|
| 本質 | 內核中一個數據結構,用戶空間通過整數描述符訪問 |
| 類比 | 類似文件描述符,支持 read/write/close 等系統調用 |
| 標識一個連接的五元組 | 協議、本地 IP、本地端口、遠端 IP、遠端端口 |
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:AF_INET (IPv4)、AF_INET6 (IPv6)、AF_UNIX (本地)。type:SOCK_STREAM / SOCK_DGRAM / SOCK_RAW。protocol:通常為 0(自動選擇默認協議)。errno。int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
backlog:已完成連接隊列最大長度(內核會按 somaxconn 截取)。int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv/send 支持 MSG_PEEK、MSG_OOB 等標志,功能更豐富。close(fd):引用計數歸零時發送 FIN,雙向關閉。shutdown(fd, SHUT_RD/WR/RDWR):單向關閉,不影響引用計數。struct sockaddr {
sa_family_t sa_family; // AF_INET, AF_INET6, AF_UNIX...
char sa_data[14]; // 協議地址(不透明)
};
struct sockaddr_in {
sa_family_t sin_family; // AF_INET
in_port_t sin_port; // 端口(網絡字節序)
struct in_addr sin_addr; // IP 地址
unsigned char sin_zero[8]; // 填充,對齊到 sizeof(sockaddr)
};
struct in_addr {
in_addr_t s_addr; // 32 位 IP(網絡字節序)
};
struct sockaddr_in6 {
sa_family_t sin6_family; // AF_INET6
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr; // 128 位 IP
uint32_t sin6_scope_id;
};
網絡傳輸統一使用大端序,x86 等小端機器需轉換:
| 函數 | 含義 |
|---|---|
| htons() | Host TO Network Short (16 位) |
| htonl() | Host TO Network Long (32 位) |
| ntohs() | Network TO Host Short |
| ntohl() | Network TO Host Long |
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 已是網絡序
// 或
inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);
addr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0,綁定所有本地接口
addr.sin_port = 0; // 內核自動分配空閒端口
綁定後可用 getsockname() 獲取實際分配的端口。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8080
#define BACKLOG 5
#define BUF_SIZE 1024
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) { perror("socket"); exit(1); }
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 避免 TIME_WAIT 佔用端口
struct sockaddr_in serv_addr = {0};
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("bind"); exit(1);
}
if (listen(listen_fd, BACKLOG) < 0) {
perror("listen"); exit(1);
}
printf("Server listening on port %d...\n", PORT);
while (1) {
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len);
if (conn_fd < 0) { perror("accept"); continue; }
char cli_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cli_addr.sin_addr, cli_ip, sizeof(cli_ip));
printf("Accepted connection from %s:%d\n", cli_ip, ntohs(cli_addr.sin_port));
char buf[BUF_SIZE];
ssize_t n;
while ((n = read(conn_fd, buf, BUF_SIZE)) > 0) {
write(conn_fd, buf, n); // 回射
}
if (n < 0) perror("read");
close(conn_fd);
printf("Connection closed\n");
}
close(listen_fd);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUF_SIZE 1024
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) { perror("socket"); exit(1); }
struct sockaddr_in serv_addr = {0};
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
perror("inet_pton"); exit(1);
}
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connect"); exit(1);
}
printf("Connected to server\n");
char buf[BUF_SIZE];
while (fgets(buf, BUF_SIZE, stdin)) {
write(sockfd, buf, strlen(buf));
ssize_t n = read(sockfd, buf, BUF_SIZE);
if (n > 0) {
buf[n] = '\0';
printf("Echo: %s", buf);
}
}
close(sockfd);
return 0;
}
gcc server.c -o server
gcc client.c -o client
# 終端1
./server
# 終端2
./client
# 輸入任意文本,服務端原樣返回
| 問題 | 原因 | 對策 |
|---|---|---|
| 端口佔用 / Address already in use | 服務端重啟時前一實例處於 TIME_WAIT | setsockopt(SO_REUSEADDR);或等待 2MSL |
| read 返回 0 | 對端調用 close/shutdown(SHUT_WR),連接正常關閉 | 作為 EOF 處理,退出讀循環 |
| write 返回 EPIPE / SIGPIPE | 對端已關閉,本端繼續寫入 | 忽略 SIGPIPE (signal(SIGPIPE, SIG_IGN)),檢查 write 返回值 |
| 粘包/拆包導致協議解析錯誤 | TCP 字節流無邊界 | 應用層定義幀格式:長度前綴 / 分隔符 / 固定長度 |
| accept 阻塞無法處理並發 | 單線程循環 | 多進程/多線程/IO 多路復用(select/poll/epoll) |
| 大量 TIME_WAIT 耗盡端口 | 高併發短連接主動關閉 | SO_REUSEADDR、啟用 tcp_tw_reuse/tcp_tw_recycle(內核參數)、長連接+心跳 |
select/poll/epoll 實現單線程高併發(Nginx/Redis 模型)。fcntl(fd, F_SETFL, O_NONBLOCK) 配合 epoll ET 模式。sendfile、splice、mmap 減少用戶/內核態拷貝。SSL_CTX/SSL 封裝 Socket 實現加密傳輸。AF_UNIX 本地進程通信,性能優於循環回 TCP。Socket 編程是 Linux 網絡開發的基石。熟練掌握 socket/bind/listen/accept/connect/read/write/close 生命週期、地址結構填充、字節序轉換、TCP 流式特性帶來的粘包問題及常見錯誤處理,即可構建穩健高效的網絡應用。進階掌握 epoll、非阻塞、零拷貝、TLS 等技術,可邁向高性能服務端開發領域。
安裝 Linux 前必懂的六大核心問題:發行版選型、軟體生態、桌面環境與雙系統策略
Linux Sendmail + Cyrus-IMAPD 郵件系統搭建完整指南
Linux防火牆iptables配置精要:開放埠、服務管理與持久化
Linux find 刪除 N 天前檔案指令詳解:mtime、類型過濾、stat/touch 時間操作
ARM Linux 啟動流程深度剖析:從連結腳本到 start_kernel
Nagios 網卡流量監控超時故障排查:SNMP 服務異常導致 CHECK_NRPE Socket Timeout