目录
- 核心概念: 了解 TCP 服务器通信的基本流程。
- 完整代码: 一个最简单的单线程回显服务器。
- 代码分步详解: 逐行解释代码的每一部分。
- 编译与运行: 如何编译和测试这个服务器。
- 进阶与改进: 如何让服务器更健壮、更高效。
- 处理客户端断开连接
- 使用
select实现简单的 I/O 多路复用 - 使用多线程处理多个客户端
核心概念
一个 TCP 服务器的工作流程可以概括为以下几个步骤,就像一个电话总机:

- 创建套接字: 就像总机安装了一部电话机,使用
socket()函数创建一个通信端点。 - 绑定地址和端口: 就像给总机分配了一个电话号码和地址,使用
bind()函数将套接字与一个特定的 IP 地址和端口号关联起来,这样客户端才能找到它。 - 监听连接: 就像总机开始等待来电,使用
listen()函数让套接字进入被动监听状态,准备接收客户端的连接请求。 - 接受连接: 就像总机接线员接起电话,使用
accept()函数从等待队列中取出一个客户端连接请求,并创建一个新的套接字与这个客户端进行一对一通信,原来的监听套接字继续等待其他客户端的连接。 - 收发数据: 就像接线员和通话方对话,使用
read()(或recv()) 和write()(或send()) 函数通过新创建的套接字与客户端交换数据。 - 关闭连接: 就像挂断电话,当通信结束时,使用
close()函数关闭套接字,释放资源。
完整代码 (单线程回显服务器)
这是一个最基础的服务器,它只能同时处理一个客户端,当它与一个客户端通信时,其他客户端必须等待。
// 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>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
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);
}
// 2. 绑定地址和端口
address.sin_family = AF_INET; // IPv4
address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
address.sin_port = htons(PORT); // 将端口号从主机字节序转换为网络字节序
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 3. 开始监听连接
if (listen(server_fd, 3) < 0) { // 3 是最大等待连接数
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 接受新的连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 打印连接的客户端信息
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &address.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("Client connected: IP = %s, Port = %d\n", client_ip, ntohs(address.sin_port));
// 5. 循环收发数据 (回显服务器)
int valread;
while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
printf("Received from client: %s", buffer);
send(new_socket, buffer, valread, 0); // 将收到的数据回写给客户端
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
}
if (valread == 0) {
printf("Client disconnected.\n");
} else if (valread < 0) {
perror("read error");
}
// 6. 关闭套接字
close(new_socket);
close(server_fd);
return 0;
}
代码分步详解
包含的头文件
#include <sys/socket.h> // socket(), bind(), listen(), accept(), connect() #include <netinet/in.h> // struct sockaddr_in, IPPROTO_TCP, htons(), ntohl() #include <arpa/inet.h> // inet_addr(), inet_ntop() #include <unistd.h> // read(), write(), close() #include <stdio.h> // perror(), printf() #include <stdlib.h> // exit() #include <string.h> // memset()
创建套接字 (socket)
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
// ... 错误处理
}
AF_INET: 指定使用 IPv4 地址族。SOCK_STREAM: 指定使用面向连接的、可靠的 TCP 协议。0: 通常设为 0,表示让系统自动选择与AF_INET和SOCK_STREAM对应的协议,也就是 TCP。server_fd: 是一个文件描述符,后续所有操作都通过它来进行。
绑定地址和端口 (bind)
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
address.sin_port = htons(PORT); // 端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
// ... 错误处理
}
struct sockaddr_in: 是struct sockaddr的 IPv4 版本,包含了 IP 地址和端口号。sin_family: 地址族,设为AF_INET。sin_addr.s_addr: IP 地址。INADDR_ANY是一个特殊的宏,表示服务器将监听所有网络接口(如0.0.1和168.x.x)上的连接请求。sin_port: 端口号。重要:计算机内存中存储多字节数据的顺序(字节序)可能和网络传输的标准顺序(网络字节序,大端序)不同。htons()(host to network short) 函数将主机字节序的端口号转换为网络字节序。
监听连接 (listen)
if (listen(server_fd, 3) < 0) {
// ... 错误处理
}
server_fd: 要监听的套接字。3:backlog参数,表示内核中等待队列的最大长度,即,当服务器繁忙时,最多可以有多少个客户端连接请求在队列中等待。3是一个常用的值。
接受连接 (accept)
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
// ... 错误处理
}
server_fd: 监听套接字。new_socket:accept()会返回一个新的套接字文件描述符,服务器将通过这个new_socket与客户端进行后续的数据收发,而server_fd继续负责监听新的连接。address: 一个输出参数,用于存放连接客户端的地址信息(IP 和端口)。addrlen: 输入输出参数,传入sizeof(address),返回实际地址的长度。
收发数据 (read / write 或 recv / send)
int valread;
while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
send(new_socket, buffer, valread, 0);
memset(buffer, 0, BUFFER_SIZE);
}
