当前位置: 首页 > news >正文

从零开始:C++ TCP 服务器实战教程

文章目录

  • 引言
  • 1. Server 端:TCP 服务器核心实现
    • 1.1 TcpServer 类的结构
    • 1.2 TcpServer 的接口实现
      • 1.2.1 构造函数和析构函数
      • 1.2.2 Init 函数:初始化核心流程
        • 步骤 1:创建 TCP 套接字
        • 步骤 2:绑定地址(bind)
        • 步骤 3:监听连接(listen)
        • Init 函数完整实现:
      • 1.2.3 Start 函数:启动服务器并处理通信
        • 步骤 1:接收连接(accept)
        • 步骤 2:与客户端收发数据(recv/send)
        • Start 函数完整实现(基础版:单客户端处理):
      • 1.2.4 自定义数据处理示例
    • 1.3 TcpServer.cc:服务器入口
  • 2. Client 端:TCP 客户端实现
    • 2.1 客户端核心流程
    • 2.2 TcpClient.cc:客户端完整代码
  • 3. 编译与运行示例
    • 3.1 Makefile
    • 3.2 运行步骤
  • 4. 可扩展的地方
    • 4.1 多客户端并发处理
    • 4.2 解决 TCP 粘包问题
    • 4.3 心跳机制
    • 4.4 线程池优化
  • 5. 总结

引言

在网络编程中,TCP(Transmission Control Protocol,传输控制协议)是一种面向连接、可靠的字节流协议。与 UDP 的 “无连接、轻量但不可靠” 不同,TCP 通过三次握手建立连接、四次挥手关闭连接,提供了数据的有序传输、重传机制、流量控制和拥塞控制 —— 这些特性让 TCP 成为对可靠性要求较高的场景(如文件传输、HTTP/HTTPS 通信、即时通讯)的核心协议。

本教程将以 C++ 为载体,从零实现一个基础 TCP 服务器与客户端,完整覆盖 TCP 通信的核心流程(创建套接字、绑定地址、监听连接、建立通信、收发数据),并深入解析关键接口的用法与原理。无论你是网络编程新手,还是希望巩固 TCP 基础的开发者,都能通过本教程掌握实战技能,并了解后续扩展方向(如多客户端并发、粘包解决)。

1. Server 端:TCP 服务器核心实现

TCP 服务器的核心流程比 UDP 更复杂,需经历 “创建监听套接字→绑定地址→监听连接→接收连接→与客户端通信” 五个关键步骤。我们将通过TcpServer类封装这些逻辑,保证代码的模块化与可扩展性。

1.1 TcpServer 类的结构

首先定义TcpServer类的私有成员变量,明确核心资源与状态:

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 构造函数和析构函数

构造函数初始化端口和数据处理函数,析构函数确保资源释放:

    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 (数据报套接字)区分:

// 创建套接字
int socket(int domain, int type, int protocol);
  • 参数
    • domainAF_INET(IPv4 协议);
    • typeSOCK_STREAM(TCP 流式套接字);
    • protocol0(默认协议,TCP)。
  • 返回值:成功返回非负套接字描述符,失败返回 -1 并设置 errno
步骤 2:绑定地址(bind)

与 UDP 类似,需要将套接字与 “IP + 端口” 绑定,但是 TCP 绑定的是 “监听地址”:

// 绑定套接字到指定IP和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 关键注意:IP 通常设为 INADDR_ANY(监听所有网卡的指定端口,如服务器有多个网卡时无需指定具体 IP)。
步骤 3:监听连接(listen)

TCP 特有操作,将套接字转为 “监听状态”,等待客户端连接:

// 开始监听套接字,等待客户端连接
int listen(int sockfd, int backlog);
  • 参数 backlog:未完成连接队列(SYN 队列)的最大长度,通常设为 5~10(表示最多同时处理 5 个未完成连接)。
Init 函数完整实现:
    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=5if (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 特有操作,阻塞等待客户端连接,成功后返回 “通信套接字”:

// 接收客户端连接,返回与该客户端通信的套接字
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 参数
    • sockfd:监听套接字(_listen_fd);
    • addr:输出参数,存储客户端的地址信息(IP + 端口);
    • addrlen:输入输出参数,地址结构的长度。
  • 返回值:成功返回 “通信套接字”(与该客户端专用),失败返回 -1
步骤 2:与客户端收发数据(recv/send)

TCP 是面向连接的,一旦建立连接,后续收发无需指定客户端地址(与 UDP 的recvfrom/sendto不同):

// 从通信套接字接收数据
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 函数完整实现(基础版:单客户端处理):
    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 自定义数据处理逻辑,例如给客户端消息加前缀:

// 示例:给客户端消息加"TCP Server Response: "前缀
std::string DefaultDataHandler(const std::string& client_data) {return "TCP Server Response: " + client_data;
}

1.3 TcpServer.cc:服务器入口

通过命令行参数传入监听端口,创建 TcpServer 对象并启动:

#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 客户端核心流程

  1. 创建 TCP 套接字:与服务器一致,使用 SOCK_STREAM
  2. 连接服务器(connect):TCP 特有操作,向服务器发起连接请求(三次握手);
  3. 收发数据:使用 send/recv 与服务器通信;
  4. 关闭连接:通信结束后关闭套接字。

2.2 TcpClient.cc:客户端完整代码

#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 的语法。

.PHONY:all
all:tcpserver tcpclienttcpserver:TcpServer.ccg++ -o $@ $^ -std=c++14
tcpclient:TcpClient.ccg++ -o $@ $^ -std=c++14.PHONY:clean
clean:rm -f tcpserver tcpclient

3.2 运行步骤

  1. 启动服务器(监听 8888 端口):
    ./tcpserver 8080
    
    服务器输出:
    套接字创建成功,listen_fd: 3
    绑定成功,成功监听端口:8888
    监听中,等待客户端连接...
    
  2. 启动客户端(连接服务器,127.0.0.1 为本地换回地址):
    ./tcpclient 127.0.0.1 8888
    
    客户端输出:
    客户端创建套接字成功,client_fd: 3
    已成功连接到服务器[127.0.0.1:8888]请输入发送给服务器的数据(输入"exit"退出)
    
  3. 测试通信
    客户端输入:Hello Zkp!
    客户端输出:
    已发送数据Hello Zkp!(字节数:10)
    收到服务器响应:TCP Server Response: Hello Zkp!请输入发送给服务器的数据(输入"exit"退出)
    
    服务端输出:
    客户端连接成功:[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),创建一个线程 / 进程处理该客户端的通信,避免主循环阻塞。
    • 缺点:频繁创建线程 / 进程开销大,适合客户端数量较少的场景。
  • 方案 2IO 多路复用:使用 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 服务器与客户端,核心知识点回顾:

  1. TCP 核心流程:服务器(创建→绑定→监听→accept→收发),客户端(创建→connect→收发);
  2. 关键接口socket(创建套接字)、bind(绑定)、listen(监听)、accept(接收连接)、connect(发起连接)、recv/send(收发数据);
  3. TCP 与 UDP 的核心区别:TCP 面向连接、可靠、字节流;UDP 无连接、不可靠、数据报;
  4. 扩展方向:多并发、粘包解决、心跳机制、线程池,这些是工业级 TCP 应用的必备特性。

通过本教程的代码实践,你可以掌握 TCP 通信的基础原理,并基于此扩展更复杂的网络应用(如文件传输服务器、简易聊天系统)。

http://www.dtcms.com/a/528401.html

相关文章:

  • csv文件用Excel打开后出现乱码的问题及其解决方法
  • 【Swift】LeetCode 56. 合并区间
  • 上海免费建站模板iis添加网站 别名
  • Linux: 网络: SIPp导致的网络风暴
  • 从0开始学java--day6.5
  • 厦门网站制作公司域名注册需要多少钱
  • AN-25101701 UG56网关与WS101传感器连接TKE132 LoRaWAN服务器指导说明书
  • 如何做网站制作杭州高端网站建设到蓝韵网络
  • Z.EntityFramework.Extensions.EFCore 批量更新(BulkUpdate)指定字段
  • MLLM-LLaVA-FL: Multimodal Large Language Model Assisted FederatedLearning
  • 欧美教育网站模板中国建设银行信用卡网站首页
  • 【同步 / 异步 日志系统】--- 全局接口 性能测试
  • GitHub等平台形成的开源文化正在重也有人
  • 03_Pushgateway使用Prometheus的服务发现机制
  • Speckit和Claude 的初体验
  • 当夸克遇上大模型:中国 AI 产品的“第二阶段”来临了
  • AI大模型弹性伸缩实战:自动扩缩容+模型轻量化+Serverless三大技术方案详解
  • 网站怎么做的qq邮件订阅页面设计存在的问题
  • CMP(类ClouderaCDP7.3(404次编译) )完全支持华为鲲鹏Aarch64(ARM),粉丝数超过200就开源下载
  • HeidiSQL的下载安装和使用
  • 线性代数直觉(五):行列式——让空间坍缩
  • word文档模板通过poi-tl导出问题注意点
  • Java在大数据分布式存储中的创新实践
  • ThinkPHP5 RCE+Linux find提权渗透实战:原理+复现(CVE-2018-20062)
  • 昆明网站排名优化电商网站的功能
  • 代码随想录Day59|dijkstra(堆优化版)精讲、Bellman_ford 算法精讲
  • 四川住建厅官方网站的网址教务管理系统学生登录入口
  • [MySQL]数据类型
  • 3w字一文讲透Java IO
  • 多模态学习大纲笔记(未完成)