文章目录
- 引言
- [1. Server 端:TCP 服务器核心实现](#1. Server 端:TCP 服务器核心实现)
-
- [1.1 TcpServer 类的结构](#1.1 TcpServer 类的结构)
- [1.2 TcpServer 的接口实现](#1.2 TcpServer 的接口实现)
-
- [1.2.1 构造函数和析构函数](#1.2.1 构造函数和析构函数)
- [1.2.2 Init 函数:初始化核心流程](#1.2.2 Init 函数:初始化核心流程)
-
- [步骤 1:创建 TCP 套接字](#步骤 1:创建 TCP 套接字)
- [步骤 2:绑定地址(bind)](#步骤 2:绑定地址(bind))
- [步骤 3:监听连接(listen)](#步骤 3:监听连接(listen))
- [Init 函数完整实现:](#Init 函数完整实现:)
- [1.2.3 Start 函数:启动服务器并处理通信](#1.2.3 Start 函数:启动服务器并处理通信)
-
- [步骤 1:接收连接(accept)](#步骤 1:接收连接(accept))
- [步骤 2:与客户端收发数据(recv/send)](#步骤 2:与客户端收发数据(recv/send))
- [Start 函数完整实现(基础版:单客户端处理):](#Start 函数完整实现(基础版:单客户端处理):)
- [1.2.4 自定义数据处理示例](#1.2.4 自定义数据处理示例)
- [1.3 TcpServer.cc:服务器入口](#1.3 TcpServer.cc:服务器入口)
- [2. Client 端:TCP 客户端实现](#2. Client 端:TCP 客户端实现)
-
- [2.1 客户端核心流程](#2.1 客户端核心流程)
- [2.2 TcpClient.cc:客户端完整代码](#2.2 TcpClient.cc:客户端完整代码)
- [3. 编译与运行示例](#3. 编译与运行示例)
-
- [3.1 Makefile](#3.1 Makefile)
- [3.2 运行步骤](#3.2 运行步骤)
- [4. 可扩展的地方](#4. 可扩展的地方)
-
- [4.1 多客户端并发处理](#4.1 多客户端并发处理)
- [4.2 解决 TCP 粘包问题](#4.2 解决 TCP 粘包问题)
- [4.3 心跳机制](#4.3 心跳机制)
- [4.4 线程池优化](#4.4 线程池优化)
- [5. 总结](#5. 总结)
引言
在网络编程中,TCP(Transmission Control Protocol,传输控制协议)是一种面向连接、可靠的字节流协议。与 UDP 的 "无连接、轻量但不可靠" 不同,TCP 通过三次握手建立连接、四次挥手关闭连接,提供了数据的有序传输、重传机制、流量控制和拥塞控制 ------ 这些特性让 TCP 成为对可靠性要求较高的场景(如文件传输、HTTP/HTTPS 通信、即时通讯)的核心协议。
本教程将以 C++ 为载体,从零实现一个基础 TCP 服务器与客户端,完整覆盖 TCP 通信的核心流程(创建套接字、绑定地址、监听连接、建立通信、收发数据),并深入解析关键接口的用法与原理。无论你是网络编程新手,还是希望巩固 TCP 基础的开发者,都能通过本教程掌握实战技能,并了解后续扩展方向(如多客户端并发、粘包解决)。
1. Server 端:TCP 服务器核心实现
TCP 服务器的核心流程比 UDP 更复杂,需经历 "创建监听套接字→绑定地址→监听连接→接收连接→与客户端通信" 五个关键步骤。我们将通过TcpServer类封装这些逻辑,保证代码的模块化与可扩展性。
1.1 TcpServer 类的结构
首先定义TcpServer类的私有成员变量,明确核心资源与状态:
cpp
class TcpServer {
public:
TcpServer(uint16_t port, func_t handler);
~TcpServer();
bool Init(); // 初始化:创建套接字、绑定、监听
void Start(); // 启动:循环接收连接,并进行数据的收发和处理
void Stop(); // 停止服务器
private:
int _listen_fd; // 监听套接字
uint16_t _listen_port; // 服务器监听端口
bool _is_running; // 服务器运行状态标志
func_t _data_handler; // 数据处理回调函数
};
私有成员变量解析:
_listen_fd:TCP 特有的 "监听套接字",作用是监听指定端口的连接请求(因为 TCP 是面向连接的,所以需要通过监听套接字专门去监听连接请求)。而具体数据的收发则是通过accept返回的服务套接字来完成。_listen_port:服务器监听的端口,客户端需要通过该端口发起连接。_is_running:服务器运行状态的标识_data_handler:因为我是通过头源分离的方式来写的服务器,所以通过std::functional去包装数据处理回调函数,到时候在使用的时候可以直接在main函数中传入这么个回调函数,提高代码的可维护性。
1.2 TcpServer 的接口实现
服务器的接口其实就是初始化、启动和停止。
1.2.1 构造函数和析构函数
构造函数初始化端口和数据处理函数,析构函数确保资源释放:
cpp
TcpServer(uint16_t port, func_t handler)
: _listen_fd(-1), _listen_port(port), _is_running(false), _data_handler(handler) {}
~TcpServer() {
if (_listen_fd != -1) {
close(_listen_fd);
std::cout << "监听套接字已关闭" << std::endl;
}
}
1.2.2 Init 函数:初始化核心流程
Init 是服务器的 "启动准备",需要完成创建套接字 、绑定地址 、监听连接三个关键操作。
步骤 1:创建 TCP 套接字
TCP 使用的是 SOCK_STREAM (流式套接字),与 UDP 的 SOCK_DGRAM (数据报套接字)区分:
cpp
// 创建套接字
int socket(int domain, int type, int protocol);
- 参数 :
domain:AF_INET(IPv4 协议);type:SOCK_STREAM(TCP 流式套接字);protocol:0(默认协议,TCP)。
- 返回值 :成功返回非负套接字描述符,失败返回
-1并设置errno。
步骤 2:绑定地址(bind)
与 UDP 类似,需要将套接字与 "IP + 端口" 绑定,但是 TCP 绑定的是 "监听地址":
cpp
// 绑定套接字到指定IP和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 关键注意 :IP 通常设为
INADDR_ANY(监听所有网卡的指定端口,如服务器有多个网卡时无需指定具体 IP)。
步骤 3:监听连接(listen)
TCP 特有操作,将套接字转为 "监听状态",等待客户端连接:
cpp
// 开始监听套接字,等待客户端连接
int listen(int sockfd, int backlog);
- 参数
backlog:未完成连接队列(SYN 队列)的最大长度,通常设为 5~10(表示最多同时处理 5 个未完成连接)。
Init 函数完整实现:
cpp
bool Init() {
// 1. 创建套接字
_listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_fd == -1) {
perror("socket 创建失败!");
return false;
}
std::cout << "套接字创建成功,listen_fd: " << _listen_fd << std::endl;
// 2. 填充服务器地址结构
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); // 清空内存避免随机值
server_addr.sin_family = AF_INET; // IPV4 协议
server_addr.sin_port = htons(_listen_port); // 本地字节序 -> 网络字节序
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
// 3. 绑定套接字与地址
int bind_ret = bind(_listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (bind_ret == -1) {
perror("绑定失败");
close(_listen_fd);
_listen_fd = -1;
return false;
}
std::cout << "绑定成功,成功监听端口:" << _listen_port << std::endl;
// 4. 开始监听连接
int listen_ret = listen(_listen_fd, 5); // backlog=5
if (listen_ret == -1) {
perror("listen 失败");
close(_listen_fd);
_listen_fd = -1;
return false;
}
std::cout << "监听中,等待客户端连接..." << std::endl;
_is_running = true;
return true;
}
1.2.3 Start 函数:启动服务器并处理通信
Start 是服务器的主循环,核心是接收客户端连接(accept)→与客户端收发数据(recv/send)。
步骤 1:接收连接(accept)
TCP 特有操作,阻塞等待客户端连接,成功后返回 "通信套接字":
cpp
// 接收客户端连接,返回与该客户端通信的套接字
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 参数 :
sockfd:监听套接字(_listen_fd);addr:输出参数,存储客户端的地址信息(IP + 端口);addrlen:输入输出参数,地址结构的长度。
- 返回值 :成功返回 "通信套接字"(与该客户端专用),失败返回
-1。
步骤 2:与客户端收发数据(recv/send)
TCP 是面向连接的,一旦建立连接,后续收发无需指定客户端地址(与 UDP 的recvfrom/sendto不同):
cpp
// 从通信套接字接收数据
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);
- 参数
flags:通常设为0(阻塞模式); - 返回值 :
recv:成功返回接收的字节数,0表示客户端关闭连接,-1表示错误;send:成功返回发送的字节数,-1表示错误。
Start 函数完整实现(基础版:单客户端处理):
cpp
void Start() {
if (!_is_running || _listen_fd == -1) {
perror("服务器未初始化,无法启动");
return;
}
// 主循环:持续接收客户端连接
while (_is_running) {
struct sockaddr_in client_addr; // 存储客户端地址
socklen_t client_addr_len = sizeof(client_addr);
// 1. 接收客户端连接(阻塞)
int client_fd = accept(_listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == -1) {
perror("accept 失败!");
continue; // 接收连接失败肯定不可能让整个服务器都退出啊,跳过该次循环就可以了
}
// 解析客户端地址(网络字节序 -> 本地字节序)
std::string client_ip = inet_ntoa(client_addr.sin_addr); // IP:网络字节序 -> 点分十进制
uint16_t client_port = ntohs(client_addr.sin_port); // 端口:网络字节序 -> 本地字节序
std::cout << "\n客户端连接成功:[" << client_ip << ":" << client_port << "],client_fd: " << client_fd << std::endl;
// 2. 与客户端通信(循环收发数据)
char recv_buf[1024] = {0}; // 接收缓冲区
while (true) {
// 接收客户端数据
ssize_t recv_len = recv(client_fd, recv_buf, sizeof(recv_buf)-1, 0);
if (recv_len == -1) {
perror("recv 失败");
break; // 接收错误,直接断开与该客户端的连接
} else if (recv_len == 0) {
std::cout << "客户端[" << client_ip << ":" << client_port << "] 主动断开连接" << std::endl;
break; // 客户端断开,退出通信循环
}
// 处理接收的数据(调用自定义回调函数)
recv_buf[recv_len] = '\0'; // 添加字符串结束符
std::cout << "收到[" << client_ip << ":" << client_port << "] 的数据:" << recv_buf << std::endl;
std::string response = _data_handler(recv_buf); // 自定义处理
// 向客户端发送响应
ssize_t send_len = send(client_fd, response.c_str(), response.size(), 0);
if (send_len == -1) {
perror("send 失败");
break;
}
std::cout << "向客户端发送响应:" << response << std::endl;
memset(recv_buf, 0, sizeof(recv_buf)); // 清空缓冲区
}
// 3. 关闭与该客户端的通信套接字
close(client_fd);
std::cout << "与客户端[" << client_ip << ":" << client_port << "]的连接已关闭" << std::endl;
}
}
1.2.4 自定义数据处理示例
用户可通过 func_t 自定义数据处理逻辑,例如给客户端消息加前缀:
cpp
// 示例:给客户端消息加"TCP Server Response: "前缀
std::string DefaultDataHandler(const std::string& client_data) {
return "TCP Server Response: " + client_data;
}
1.3 TcpServer.cc:服务器入口
通过命令行参数传入监听端口,创建 TcpServer 对象并启动:
cpp
#include <memory>
#include "TcpServer.hpp"
void Usage(std::string proc) {
std::cerr << "Usage: " << proc << "port" << std::endl;
}
// 示例:给客户端消息加"TCP Server Response: "前缀
std::string DefaultDataHandler(const std::string& client_data) {
return "TCP Server Response: " + client_data;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
Usage(argv[2]);
return 1;
}
// 解析端口号(字符串→整数)
uint16_t listen_port = std::stoi(argv[1]);
if (listen_port < 1024 || listen_port > 65535) {
std::cerr << "端口号无效(需在1024~65535之间)" << std::endl;
return 2;
}
// 创建TCP服务器对象(使用智能指针自动释放资源)
std::unique_ptr<TcpServer> tcp_server =
std::make_unique<TcpServer>(listen_port, DefaultDataHandler);
// 初始化并启动服务器
if (!tcp_server->Init()) {
std::cerr << "服务器初始化失败" << std::endl;
return 3;
}
tcp_server->Start();
return 0;
}
2. Client 端:TCP 客户端实现
TCP 客户端流程比服务器简单,核心是 "创建套接字→连接服务器(connect)→收发数据",无需绑定固定端口(系统自动分配临时端口)。
2.1 客户端核心流程
- 创建 TCP 套接字 :与服务器一致,使用
SOCK_STREAM; - 连接服务器(connect):TCP 特有操作,向服务器发起连接请求(三次握手);
- 收发数据 :使用
send/recv与服务器通信; - 关闭连接:通信结束后关闭套接字。
2.2 TcpClient.cc:客户端完整代码
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstdlib>
void Usage(std::string proc) {
std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}
// 主函数:./tcpclient 服务器IP 服务器端口
int main(int argc, char* argv[]) {
if (argc != 3) {
Usage(argv[0]);
return 1;
}
// 解析服务器地址和端口
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建 TCP 套接字
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd == -1) {
perror("socket 创建失败");
return 2;
}
std::cout << "客户端创建套接字成功,client_fd: " << client_fd << std::endl;
// 2. 填充服务器地址结构
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(server_port); // 端口:主机→网络字节序
// IP:字符串→网络字节序(inet_addr支持点分十进制IP)
server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());
// 3. 连接服务器(三次握手)
int connect_ret = connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (connect_ret == -1) {
perror("connect 失败(请检查服务器IP和端口是否正确)");
close(client_fd);
return 3;
}
std::cout << "已成功连接到服务器[" << server_ip << ":" << server_port << "]" << std::endl;
// 4. 循环收发数据
std::string input_data;
char recv_buf[1024] = {0};
while (true) {
std::cout << "\n请输入发送给服务器的数据(输入\"exit\"退出)";
std::getline(std::cin, input_data);
// 退出条件
if (input_data == "exit") {
std::cout << "客户端退出" << std::endl;
break;
}
// 发送数据给服务器
ssize_t send_len = send(client_fd, input_data.c_str(), input_data.size(), 0);
if (send_len == -1) {
perror("send 失败");
break;
}
std::cout << "已发送数据" << input_data << "(字节数:" << send_len << ")" << std::endl;
// 接收服务器响应
memset(recv_buf, 0, sizeof(recv_buf));
ssize_t recv_len = recv(client_fd, recv_buf, sizeof(recv_buf)-1, 0);
if (recv_len == -1) {
perror("recv 失败");
break;
} else if (recv_len == 0) {
std::cout << "服务器已关闭连接" << std::endl;
break;
}
// 打印服务器响应
std::cout << "收到服务器响应:" << recv_buf << std::endl;
}
// 5. 关闭客户端套接字
close(client_fd);
std::cout << "客户端套接字已关闭,退出程序" << std::endl;
return 0;
}
3. 编译与运行示例
3.1 Makefile
需要注意的一点是,我在管理服务器资源的时候,使用的是 make_unique 创建服务器实例,这是 C++14 的语法。
bash
.PHONY:all
all:tcpserver tcpclient
tcpserver:TcpServer.cc
g++ -o $@ $^ -std=c++14
tcpclient:TcpClient.cc
g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
rm -f tcpserver tcpclient
3.2 运行步骤
-
启动服务器 (监听 8888 端口):
bash./tcpserver 8080服务器输出:
bash套接字创建成功,listen_fd: 3 绑定成功,成功监听端口:8888 监听中,等待客户端连接... -
启动客户端 (连接服务器,127.0.0.1 为本地换回地址):
bash./tcpclient 127.0.0.1 8888客户端输出:
bash客户端创建套接字成功,client_fd: 3 已成功连接到服务器[127.0.0.1:8888] 请输入发送给服务器的数据(输入"exit"退出) -
测试通信
客户端输入:Hello Zkp!
客户端输出:bash已发送数据Hello Zkp!(字节数:10) 收到服务器响应:TCP Server Response: Hello Zkp! 请输入发送给服务器的数据(输入"exit"退出)服务端输出:
bash客户端连接成功:[127.0.0.1:36164],client_fd: 4 收到[127.0.0.1:36164] 的数据:Hello Zkp! 向客户端发送响应:TCP Server Response: Hello Zkp!
4. 可扩展的地方
基础版 TCP 服务器仅支持单客户端(处理完一个客户端才能接收下一个),实际应用中需针对高并发、可靠性等需求扩展:
4.1 多客户端并发处理
- 方案 1 :多线程 / 多进程 :每接收一个客户端连接(
accept),创建一个线程 / 进程处理该客户端的通信,避免主循环阻塞。- 缺点:频繁创建线程 / 进程开销大,适合客户端数量较少的场景。
- 方案 2 :IO 多路复用 :使用
select/poll/epoll(Linux)管理多个套接字(监听套接字 + 所有客户端通信套接字),单线程处理多客户端请求,适合高并发场景(如 thousands of clients)。
4.2 解决 TCP 粘包问题
TCP 是字节流协议,数据会被缓冲,可能出现 "粘包"(多个发送操作的数据被合并接收)。解决方式:
- 定长数据包:约定每次发送固定长度的数据(如 1024 字节),不足补 0,接收端每次按固定长度读取。
- 分隔符 :在数据末尾添加分隔符(如
\n),接收端按分隔符拆分数据。 - 头部带长度:数据包分为 "头部(4 字节,存储数据长度)+ 数据",接收端先读头部,再按长度读数据。
4.3 心跳机制
TCP 连接可能因网络异常(如断网)变成 "死连接",需通过心跳包 检测:
服务器定期向客户端发送心跳包(如ping);
客户端收到心跳包后回复pong;
若服务器多次未收到pong,则关闭该客户端连接。
4.4 线程池优化
针对多线程方案,使用线程池预先创建固定数量的线程,避免频繁创建 / 销毁线程的开销:
- 主线程:
accept客户端连接,将通信套接字加入任务队列;
线程池中的工作线程:从任务队列获取套接字,处理通信逻辑。
5. 总结
本教程从零实现了 C++ TCP 服务器与客户端,核心知识点回顾:
- TCP 核心流程:服务器(创建→绑定→监听→accept→收发),客户端(创建→connect→收发);
- 关键接口 :
socket(创建套接字)、bind(绑定)、listen(监听)、accept(接收连接)、connect(发起连接)、recv/send(收发数据); - TCP 与 UDP 的核心区别:TCP 面向连接、可靠、字节流;UDP 无连接、不可靠、数据报;
- 扩展方向:多并发、粘包解决、心跳机制、线程池,这些是工业级 TCP 应用的必备特性。
通过本教程的代码实践,你可以掌握 TCP 通信的基础原理,并基于此扩展更复杂的网络应用(如文件传输服务器、简易聊天系统)。