目录
- 网络编程基础
- OSI 七层模型与 TCP/IP 四层模型
- 套接字:网络编程的基石
- 网络字节序与主机字节序
- 核心 Socket API
- 创建套接字 (
socket) - 绑定地址 (
bind) - 监听连接 (
listen) - 接受连接 (
accept) - 连接服务器 (
connect) - 数据传输 (
send,recv) - 关闭套接字 (
close)
- 创建套接字 (
- 一个简单的 TCP 服务器/客户端示例
- 回显服务器
- 回显客户端
- 进阶主题
- 多路复用:
select,poll,epoll - 阻塞 vs. 非阻塞 I/O
- 高级 I/O 模型简介
- 多路复用:
- 常用工具与调试
netstat,sstelnet,nc(netcat)wireshark
网络编程基础
OSI 七层模型与 TCP/IP 四层模型
理解网络模型有助于我们明白数据是如何封装和传输的。

| OSI 七层模型 | TCP/IP 四层模型 | 主要功能和协议 |
|---|---|---|
| 应用层 | 应用层 | 为应用程序提供服务 (HTTP, FTP, SMTP, DNS) |
| 表示层 | 数据格式转换、加密解密、压缩 | |
| 会话层 | 建立、管理和终止会话 | |
| 传输层 | 传输层 | 提供端到端的、可靠的或不可靠的数据传输 (TCP, UDP) |
| 网络层 | 网络层 | 负责逻辑寻址和路由选择 (IP, ICMP) |
| 数据链路层 | 网络接口层 | 负责物理寻址(MAC地址)和错误检测 (Ethernet, Wi-Fi) |
| 物理层 | 传输二进制比特流 (网线、光纤、无线电波) |
Linux C 网络编程主要关注传输层和应用层。
套接字
套接字是网络编程的“文件描述符”,是操作系统提供给应用程序进行网络 I/O 操作的接口,它就像一个“门”,应用程序通过这个门发送和接收数据。
- 流式套接字 (
SOCK_STREAM):使用 TCP 协议。- 特点:面向连接、可靠、有序、不丢包。
- 场景:文件传输、网页浏览、邮件发送等对数据准确性要求高的场景。
- 数据报套接字 (
SOCK_DGRAM):使用 UDP 协议。- 特点:无连接、不可靠、可能丢包、可能重复。
- 场景:视频会议、在线游戏、DNS查询等对实时性要求高、能容忍少量丢包的场景。
网络字节序与主机字节序
- 主机字节序:CPU 存储多字节数据的顺序。
- 大端序 (Big-Endian):高位字节存储在低地址,低位字节存储在高地址。(网络标准)
- 小端序:低位字节存储在低地址,高位字节存储在高地址。(x86/x64 架构)
- 网络字节序:网络传输中统一使用 大端序。
关键:在发送数据前,必须将主机字节序转换为网络字节序;在接收数据后,必须将网络字节序转换回主机字节序。
常用函数:

htons()(host to network short): 16位短整型转换htonl()(host to network long): 32位长整型转换ntohs()(network to host short): 16位短整型转换ntohl()(network to host long): 32位长整型转换
核心 Socket API
以下是 TCP 编程中最常用的一些函数。
TCP 服务器端流程
socket() -> bind() -> listen() -> accept() -> send()/recv() -> close()
TCP 客户端流程
socket() -> connect() -> send()/recv() -> close()
| 函数 | 原型 | 描述 |
|---|---|---|
socket() |
int socket(int domain, int type, int protocol); |
创建一个套接字。domain 通常是 AF_INET (IPv4) 或 AF_INET6 (IPv6)。type 是 SOCK_STREAM (TCP) 或 SOCK_DGRAM (UDP),成功返回套接字文件描述符,失败返回 -1。 |
bind() |
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
将套接字与一个特定的 IP 地址和端口号绑定。sockaddr 是一个通用地址结构,实际使用时会用 sockaddr_in (IPv4) 或 sockaddr_in6 (IPv6) 并进行类型转换。 |
listen() |
int listen(int sockfd, int backlog); |
将套接字转换为被动套接字,用于接受连接请求。backlog 是等待连接队列的最大长度。 |
accept() |
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
从已完成连接的队列中取出一个连接,创建一个新的套接字用于与客户端通信。sockfd 是监听套接字,返回的是新的通信套接字。addr 和 addrlen 用来获取客户端的地址信息。 |
connect() |
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
客户端调用,用于主动连接到服务器。sockfd 是客户端的套接字,addr 是服务器的地址。 |
send() |
ssize_t send(int sockfd, const void *buf, size_t len, int flags); |
通过已连接的套接字发送数据。flags 通常为 0。 |
recv() |
ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
通过已连接的套接字接收数据。flags 通常为 0。 |
close() |
int close(int fd); |
关闭套接字,释放资源。 |
struct sockaddr_in 结构体 (IPv4)

这是 bind, connect 等函数最常用的地址结构体。
#include <netinet/in.h>
struct sockaddr_in {
short sin_family; // 地址族,必须是 AF_INET
unsigned short sin_port; // 端口号,必须用 htons() 转换
struct in_addr sin_addr; // IP 地址
unsigned char sin_zero[8]; // 填充字节,必须为 0
};
// in_addr 结构体
struct in_addr {
unsigned long s_addr; // IP 地址,必须用 inet_addr() 或 inet_pton() 转换
};
一个简单的 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> #include <errno.h>
回显服务器代码 (server.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 1. 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项,允许地址重用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用网络接口
address.sin_port = htons(PORT);
// 2. 绑定地址和端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 3. 开始监听
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 接受客户端连接
if ((client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("Client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 5. 循环接收和发送数据
while (1) {
int valread = read(client_fd, buffer, BUFFER_SIZE);
if (valread <= 0) {
// valread == 0 表示客户端关闭了连接
// valread < 0 表示发生错误
if (valread == 0) {
printf("Client disconnected.\n");
} else {
perror("read");
}
break;
}
printf("Received: %s", buffer);
// 6. 回显数据
send(client_fd, buffer, valread, 0);
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
}
// 7. 关闭套接字
close(client_fd);
close(server_fd);
return 0;
}
回显客户端代码 (client.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define PORT 8080
#define SERVER_IP "127.0.0.1" // 本地回环地址
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
char message[BUFFER_SIZE];
// 1. 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将 IP 地址从文本转换为二进制形式
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 2. 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
printf("Connected to server %s:%d\n", SERVER_IP, PORT);
// 3. 循环发送和接收数据
while (1) {
printf("Enter message to send (or 'exit' to quit): ");
fgets(message, BUFFER_SIZE, stdin);
if (strncmp(message, "exit", 4) == 0) {
break;
}
send(sock, message, strlen(message), 0);
int valread = read(sock, buffer, BUFFER_SIZE);
if (valread <= 0) {
printf("Server disconnected or error occurred.\n");
break;
}
printf("Server echoed: %s", buffer);
memset(buffer, 0, BUFFER_SIZE);
memset(message, 0, BUFFER_SIZE);
}
// 4. 关闭套接字
close(sock);
return 0;
}
如何编译和运行
-
编译:
gcc server.c -o server gcc client.c -o client
-
运行:
- 终端1 (启动服务器):
./server # 输出: Server listening on port 8080...
- 终端2 (启动客户端):
./client # 输出: Connected to server 127.0.0.1:8080 # 然后输入任意消息,如 "hello" # 输出: Enter message to send (or 'exit' to quit): hello # 服务器终端会显示: Received: hello # 客户端终端会显示: Server echoed: hello
- 在客户端输入
exit即可退出程序。
- 终端1 (启动服务器):
进阶主题
阻塞 vs. 非阻塞 I/O
-
阻塞 I/O (Blocking I/O):默认模式,当一个进程调用
recv()但没有数据可读时,该进程会被挂起(进入睡眠状态),直到数据到达或发生错误,同样,accept()在没有连接请求时也会阻塞。- 优点:编程简单。
- 缺点:效率低,一个进程只能同时处理一个客户端连接。
-
非阻塞 I/O (Non-blocking I/O):通过
fcntl()或ioctl()设置套接字为非阻塞模式,当调用recv()但没有数据时,它会立即返回一个错误码EWOULDBLOCK或EAGAIN,而不会让进程挂起。- 优点:进程不会被挂起,可以去执行其他任务。
- 缺点:需要不断轮询检查,消耗大量 CPU 资源,效率也不高。
多路复用
为了解决非阻塞 I/O 轮询效率低的问题,引入了 I/O 多路复用技术,它允许你同时监视多个套接字,任何一个套接字就绪(可读、可写、出错)时,select/poll/epoll 函数才会返回。
-
select()- 原理:创建一个文件描述符集合(
fd_set),通过位图来表示哪些 fd 需要监视,调用select会阻塞,直到集合中任何一个 fd 就绪。 - 缺点:
- 单个进程能监视的 fd 数量有限(通常为 1024)。
- 每次调用都需要从用户空间将
fd_set复制到内核空间,返回时再复制回来,开销大。 select返回后,需要遍历整个fd_set来找到就绪的 fd。
- 原理:创建一个文件描述符集合(
-
poll()- 原理:使用
pollfd结构体数组代替fd_set,没有 fd 数量限制。 - 缺点:和
select一样,每次都需要大量数据在内核和用户空间之间复制,效率仍然不高。
- 原理:使用
-
epoll()(Linux 特有,性能最高)- 原理:它将文件描述符的管理放在内核中,你只需要在
epoll_ctl时向内核注册 fd,后续只需调用epoll_wait即可。 - 优点:
- 没有 fd 数量限制。
- 只需在
epoll_ctl时复制数据,epoll_wait时几乎不复制。 epoll_wait返回的是就绪 fd 的列表,无需遍历,效率极高。- 支持
EPOLLET(边缘触发) 模式,性能比select/poll的水平触发模式更高。
- 原理:它将文件描述符的管理放在内核中,你只需要在
epoll 是构建高性能网络服务器的首选。
常用工具与调试
-
netstat/ss:查看网络连接状态。netstat -tulpn:显示所有 TCP/UDP 端口以及进程ID。ss -tulpn:netstat的现代替代品,速度更快,信息更全。
-
telnet/nc(netcat):测试网络服务的简单工具。telnet <ip> <port>:尝试连接到指定 IP 和端口,如果成功,说明服务正在监听。nc <ip> <port>:功能更强大的网络工具,可以连接、监听、发送和接收数据,常用于快速调试。
-
wireshark/tcpdump:网络抓包分析工具。当你的程序出现奇怪的网络问题时(如数据包丢失、顺序错乱),使用它们可以捕获底层的网络数据包,查看 TCP 握手、挥手过程,以及数据传输的详细情况,是定位问题的利器。
Linux C 语言网络编程的核心是 Socket API,从简单的 TCP 回显服务器开始,理解 socket, bind, listen, accept, connect, send, recv 的流程和用法是第一步。
为了构建能够处理成千上万个并发连接的高性能服务器,你必须深入理解 I/O 多路复用,特别是 epoll 的工作原理和编程模型。
希望这份详细的指南能帮助你顺利入门 Linux C 网络编程!
