Linux 系统下使用 C 语言实现的简单 TCP 服务器
1. 整体思路
一个 TCP 服务器的工作流程通常如下:
- 创建套接字 (
socket()):创建一个网络通信的端点。 - 绑定地址 (
bind()):将套接字与一个具体的 IP 地址和端口号关联起来。 - 监听端口 (
listen()):让套接字处于被动监听状态,等待客户端的连接请求。 - 接受连接 (
accept()):当有客户端连接时,接受连接并创建一个新的套接字用于与该客户端通信。 - 数据交互 (
send()/recv()):通过新创建的套接字与客户端进行数据收发。 - 关闭套接字 (
close()):通信结束后,关闭套接字释放资源。
2. 分步实现
步骤 1:创建套接字 (socket())
#include
#include
#include
#include
#include
#include
int main() {
// 创建套接字
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
printf("套接字创建成功,文件描述符:%d
", server_fd);
AF_INET:使用 IPv4 地址族。SOCK_STREAM:使用 TCP 协议(面向连接、可靠传输)。0:表示使用默认的协议(对于 TCP 就是IPPROTO_TCP)。- 返回值:成功则返回一个非负整数(套接字文件描述符),失败则返回
-1。
步骤 2:绑定地址 (bind())
// 设置服务器地址结构体
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡的 IP 地址
server_addr.sin_port = htons(8888); // 端口号(转换为网络字节序)
// 绑定套接字到指定地址和端口
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("绑定成功,监听端口:%d
", ntohs(server_addr.sin_port));
struct sockaddr_in:专门用于存储 IPv4 地址信息的结构体。INADDR_ANY:表示监听本机所有可用的 IP 地址(例如127.0.0.1、局域网 IP 等)。htons(port):将主机字节序(小端)的端口号转换为网络字节序(大端)。bind()的第二个参数需要强制转换为struct sockaddr *类型(通用地址结构体)。
步骤 3:监听端口 (listen())
// 开始监听端口
int backlog = 5; // 等待连接队列的最大长度
if (listen(server_fd, backlog) == -1) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("服务器正在监听端口 %d,等待客户端连接...
", 8888);
backlog:指定了内核为该套接字排队的最大连接数。当队列满时,新的连接请求会被拒绝。- 此时套接字从 主动套接字 变为 被动套接字,只用于接受连接,不用于数据传输。
步骤 4:接受连接 (accept())
// 客户端地址结构体(用于存储连接客户端的信息)
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 接受客户端连接(阻塞等待)
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_fd == -1) {
perror("accept failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 打印客户端信息
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("客户端连接成功:IP=%s,端口=%d,客户端文件描述符:%d
",
client_ip, ntohs(client_addr.sin_port), client_fd);
accept()会 阻塞 程序执行,直到有客户端连接到达。- 成功后返回一个新的套接字文件描述符 (
client_fd),用于与该客户端进行通信。 - 原有的
server_fd继续保持监听状态,可接受其他客户端的连接。 inet_ntop():将网络字节序的 IP 地址转换为字符串格式(例如192.168.1.100)。
步骤 5:数据交互 (send()/recv())
// 与客户端进行数据交互
char buffer[1024] = {0};
ssize_t recv_len = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (recv_len == -1) {
perror("recv failed");
close(client_fd);
close(server_fd);
exit(EXIT_FAILURE);
} else if (recv_len == 0) {
printf("客户端 %s:%d 主动断开连接
", client_ip, ntohs(client_addr.sin_port));
close(client_fd);
close(server_fd);
return 0;
}
buffer[recv_len] = '0'; // 确保字符串以 null 结尾
printf("收到客户端 %s:%d 的消息:%s
", client_ip, ntohs(client_addr.sin_port), buffer);
// 向客户端发送响应
const char *response = "服务器已收到你的消息:";
send(client_fd, response, strlen(response), 0);
send(client_fd, buffer, recv_len, 0);
printf("响应已发送给客户端 %s:%d
", client_ip, ntohs(client_addr.sin_port));
recv():从客户端套接字client_fd读取数据,存入buffer。send():向客户端套接字client_fd发送数据。ssize_t:用于表示字节数或错误码(-1表示失败)。
步骤 6:关闭套接字 (close())
// 关闭套接字,释放资源
close(client_fd);
close(server_fd);
printf("服务器已关闭所有套接字,程序退出
");
return 0;
}
close():关闭套接字文件描述符,释放与之关联的资源(如网络连接、缓冲区等)。- 必须关闭所有不再使用的套接字,否则会造成资源泄漏。
3. 完整代码
#include
#include
#include
#include
#include
#include
#include
int main() {
// 步骤 1:创建套接字
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
printf("套接字创建成功,文件描述符:%d
", server_fd);
// 步骤 2:绑定地址和端口
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有IP
server_addr.sin_port = htons(8888); // 端口8888(网络字节序)
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("绑定成功,监听端口:%d
", ntohs(server_addr.sin_port));
// 步骤 3:监听端口
int backlog = 5;
if (listen(server_fd, backlog) == -1) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("服务器正在监听端口 %d,等待客户端连接...
", 8888);
// 步骤 4:接受客户端连接
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_fd == -1) {
perror("accept failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 打印客户端信息
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("客户端连接成功:IP=%s,端口=%d,客户端文件描述符:%d
",
client_ip, ntohs(client_addr.sin_port), client_fd);
// 步骤 5:数据交互
char buffer[1024] = {0};
ssize_t recv_len = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (recv_len == -1) {
perror("recv failed");
close(client_fd);
close(server_fd);
exit(EXIT_FAILURE);
} else if (recv_len == 0) {
printf("客户端 %s:%d 主动断开连接
", client_ip, ntohs(client_addr.sin_port));
close(client_fd);
close(server_fd);
return 0;
}
buffer[recv_len] = '0';
printf("收到客户端 %s:%d 的消息:%s
", client_ip, ntohs(client_addr.sin_port), buffer);
// 发送响应
const char *response = "服务器已收到你的消息:";
send(client_fd, response, strlen(response), 0);
send(client_fd, buffer, recv_len, 0);
printf("响应已发送给客户端 %s:%d
", client_ip, ntohs(client_addr.sin_port));
// 步骤 6:关闭套接字
close(client_fd);
close(server_fd);
printf("服务器已关闭所有套接字,程序退出
");
return 0;
}
4. 编译与运行
- 保存代码:将上述代码保存为
server.c。 - 编译:使用
gcc编译代码:gcc server.c -o server - 运行服务器:
./server - 测试连接:在另一个终端使用
telnet或nc连接服务器:
或者:telnet 127.0.0.1 8888
然后输入任意消息,服务器会回复你发送的内容。nc 127.0.0.1 8888
5. 关键注意事项
- 字节序转换:端口号和 IP 地址在网络传输中必须使用 网络字节序(大端),而主机字节序可能是小端,因此需要使用
htons()(端口)、htonl()(IP 地址)、ntohs()、ntohl()进行转换。 - 错误处理:网络编程中每个函数都可能失败(如端口被占用、连接被中断等),必须检查返回值并进行错误处理,否则程序可能崩溃或出现不可预期的行为。
- 阻塞与非阻塞:默认情况下,
accept()、recv()等函数是 阻塞 的,即程序会暂停执行直到有事件发生。如果需要处理多个客户端,可以使用非阻塞套接字 +select()/poll()/epoll()等 I/O 多路复用技术。 - 资源释放:程序退出前必须关闭所有打开的套接字,否则会导致文件描述符泄漏。
- 端口占用:如果运行时提示
bind failed: Address already in use,说明端口 8888 已被其他程序占用,可以更换一个端口(如 9999)。
6. 扩展:处理多个客户端
上述代码只能处理一个客户端连接,处理完后程序就会退出。如果需要服务器同时处理多个客户端,可以使用以下方法:
- 多进程:每接受一个客户端连接,创建一个子进程处理该客户端,父进程继续监听新连接。
- 多线程:每接受一个客户端连接,创建一个线程处理该客户端。
- I/O 多路复用:使用
select()、poll()或epoll()监听多个套接字(监听套接字 + 所有客户端套接字),实现单进程处理多个客户端。
其中,epoll() 是 Linux 下最高效的 I/O 多路复用技术,适合处理高并发场景。










