基于C++的高效TCP文件传输类设计与实现

本文还有配套的精品资源,点击获取

menu-r.4af5f7ec.gif

简介:在IT领域,网络编程是构建分布式系统和客户端-服务器架构的核心技术。TCP作为可靠的传输层协议,广泛应用于文件传输场景。本文介绍一个用C++实现的TCP文件传输类系统,涵盖套接字编程、面向对象设计、文件分块传输、多线程处理及事件驱动机制等关键技术。通过封装TcpServer、TcpSocket和FileSocket等类,结合FastDelegate实现回调机制,并运用设计模式提升代码可维护性,开发者可构建稳定高效的文件传输服务。项目还包含全局配置管理、内存安全控制与性能优化策略,适用于实际网络通信环境。

TCP套接字编程与现代C++网络框架设计

在智能家居设备日益复杂的今天,确保无线连接的稳定性已成为一大设计挑战。想象一下:你家的智能音箱正在播放音乐,突然卡顿、断连,甚至完全失声——这背后可能正是底层通信机制出了问题。而解决这类问题的关键,往往藏在一个看似不起眼的技术名词里: TCP套接字

别被这个术语吓到!它其实是构建所有稳定网络服务的基石。从网页加载到视频通话,从远程控制到实时游戏,只要数据不能丢、顺序不能乱,几乎都离不开TCP。而今天我们不只讲理论,还要手把手带你用现代C++打造一个高性能、高可靠的TCP服务器框架,让它不仅能处理日常通信,还能扛住成千上万客户端的同时接入。

准备好了吗?让我们从最基础的地方开始,一步步揭开它的神秘面纱。


构建你的第一个TCP连接:不只是 socket() 那么简单 🧱

我们先来看一段熟悉的代码:

int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket creation failed"); } 

这段代码创建了一个IPv4的TCP套接字,是整个通信过程的第一步。但你知道吗?光是这一行调用,背后就藏着不少门道。

  • AF_INET 表示我们要使用IPv4地址族;
  • SOCK_STREAM 指定这是一个面向连接的流式套接字,也就是TCP;
  • 第三个参数为0,表示让系统自动选择合适的协议(当然是TCP啦);

但这只是起点。接下来还有绑定、监听、接受连接等一系列操作。更重要的是,默认情况下这些套接字都是 阻塞模式 的——比如你在调用 accept() 的时候,程序就会一直卡在那里,直到有新客户端连上来为止。

这对于单任务的小工具还凑合,可一旦你要同时处理几百个连接,主线程就被“冻住”了。怎么办?

答案就是: 非阻塞IO + 多路复用

你可以通过 fcntl() 把套接字设成非阻塞:

int flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); 

这样一来, recv() accept() 调用不会再傻等,而是立刻返回 -1 并设置 errno EAGAIN EWOULDBLOCK ,告诉你“现在没数据,回头再来”。然后你就可以去做别的事,比如检查其他连接、处理定时任务,或者干脆休息一会儿。

当然,真正的高手不会一个个轮询去问:“你有数据了吗?”、“你准备好接收了吗?”——那太低效了。他们会请一位聪明的管家来帮忙,这位管家的名字叫 epoll(Linux) kqueue(macOS/BSD) ,它能一次性告诉你哪些套接字已经准备好了。

不过别急,后面我们会深入讲这块内容。先来看看怎么把一堆零散的系统调用封装成一个优雅的对象。


面向对象的力量:把原始接口变成漂亮的API ✨

直接裸写系统调用虽然快,但容易出错、难以维护。有没有办法让它更像现代C++一点呢?当然有!

我们来设计一个 TcpServer 类,作为整个服务端架构的核心中枢。它要做的不仅仅是监听端口,更要管理成百上千个活跃连接的生命旅程。

核心职责拆解 🔍

一个好的类应该职责清晰。我们的 TcpServer 主要干四件事:

  1. 生命周期管理 :创建监听套接字、绑定IP和端口、开启监听。
  2. 连接接入 :不断接收新的客户端连接请求。
  3. 连接池维护 :记录当前所有在线用户,方便后续广播或点对点通信。
  4. 事件通知 :当有新连接、收到消息或断开时,告诉上层业务逻辑该做啥。

至于具体的数据收发、心跳检测、协议解析?交给另一个专门的 TcpSocket 类去管吧。关注点分离才是王道!

内部结构一览 👀

我们可以这样定义 TcpServer 的成员变量:

class TcpServer { private: int m_listenFd; // 监听套接字 struct sockaddr_in m_addr; // 绑定地址 bool m_running; std::map<int, std::shared_ptr<TcpSocket>> m_connections; // 所有活跃连接 std::function<void(std::shared_ptr<TcpSocket>)> onConnection; std::function<void(std::shared_ptr<TcpSocket>, const char*, int)> onData; std::function<void(std::shared_ptr<TcpSocket>)> onClose; }; 

是不是有点眼熟?没错,这就是之前那个 Mermaid 图里的内容。不过我们现在把它变得更真实了。

看看这张图,你能感受到那种层次分明的设计美感吗?

classDiagram class TcpServer { +int m_listenFd +struct sockaddr_in m_addr +bool m_running +std::map<int, std::shared_ptr<TcpSocket>> m_connections +std::function<void(std::shared_ptr<TcpSocket>)> onConnection +std::function<void(std::shared_ptr<TcpSocket>, const char*, int)> onData +std::function<void(std::shared_ptr<TcpSocket>)> onClose +TcpServer(const std::string& ip, uint16_t port) +~TcpServer() +void start() +void stop() +void broadcast(const std::string& msg) } class TcpSocket { +int m_fd +struct sockaddr_in m_peerAddr +bool m_connected +send(const void* data, size_t len) +recv(void* buf, size_t len) } TcpServer --> "manages" TcpSocket : contains shared_ptr 

TcpServer 管着一群 TcpSocket 实例,每个代表一个客户端连接。它们之间通过智能指针关联,资源释放全自动,再也不用手动 delete 了,简直是强迫症患者的福音 😌。

而且你看那些 std::function 回调函数,多灵活!你想在连接建立后打印日志?注册个lambda就行:

server.onConnection = [](auto sock) { std::cout << "New client from " << getPeerIp(sock) << "\n"; }; 

想收到消息就转发给所有人?也是一行搞定:

server.onData = [&](auto sock, const char* data, int len) { server.broadcast(std::string(data, len)); }; 

这才是现代C++的味道啊~ 🍜


RAII加持下的异常安全初始化 💪

初始化服务器可不是简单地一路 socket() bind() listen() 就完事了。万一中间哪一步失败了怎么办?比如端口被占用了,或者内存不够了……

传统的做法是在每一步后检查返回值,错了就跳转到错误处理段落(还记得C语言里的 goto cleanup; 吗?),听起来就很麻烦。

但我们是C++程序员,我们有更好的武器: RAII (Resource Acquisition Is Initialization)。

简单说就是: 资源即对象,对象析构即释放

举个例子,在构造函数中创建套接字:

TcpServer::TcpServer(const std::string& ip, uint16_t port) : m_listenFd(-1), m_running(false), m_port(port) { if (!initSocket(ip, port)) { throw std::runtime_error("Failed to initialize server socket"); } } 

而在 initSocket 中,如果某步失败,我们就关闭已打开的文件描述符,并返回 false

bool TcpServer::initSocket(const std::string& ip, uint16_t port) { m_listenFd = ::socket(AF_INET, SOCK_STREAM, 0); if (m_listenFd < 0) { perror("socket creation failed"); return false; } // 重用地址,避免重启时报 Address already in use int opt = 1; if (::setsockopt(m_listenFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { perror("setsockopt SO_REUSEADDR failed"); ::close(m_listenFd); m_listenFd = -1; return false; } struct sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(ip.c_str()); if (::bind(m_listenFd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind failed"); ::close(m_listenFd); m_listenFd = -1; return false; } if (::listen(m_listenFd, SOMAXCONN) < 0) { perror("listen failed"); ::close(m_listenFd); m_listenFd = -1; return false; } m_addr = addr; return true; } 

看到没?每一个失败路径都会清理资源。即使抛出异常,外面捕获后也不会留下半拉子工程。这就是所谓的 基本异常安全保证 :要么成功,要么干净退出。


地址复用的艺术:告别“Address already in use” ❌

你一定遇到过这种情况:刚改完代码,一运行,报错:

bind failed: Address already in use 

怎么回事?明明没人用这个端口啊!

其实是因为上次程序退出时,主动关闭的一方进入了 TIME_WAIT 状态,持续约2分钟。期间操作系统不允许重复绑定同一个五元组(协议+本地IP+本地端口+远端IP+远端端口)。

解决方案很简单:加上 SO_REUSEADDR

int opt = 1; setsockopt(m_listenFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); 

这样即使前一个连接还在 TIME_WAIT ,也能立即重用地址。开发调试神器,必备!

不过要注意:这个选项只允许你在没有冲突的情况下快速重启。如果有另一个进程真正在监听同一个端口,你还是会被拒绝,防止端口劫持。

场景 是否允许重用
上次连接处于 TIME_WAIT ✅ 允许
另一进程正监听同一端口 ❌ 不允许
绑定 INADDR_ANY 和特定 IP ⚠️ 视系统而定

所以放心大胆地加上吧,它是生产级服务的基本配置。


如何优雅地接受千万级连接?👂

你以为 listen() 只是挂起等待那么简单?Too young too simple!

实际上,内核有两个队列在默默工作:

  • 半连接队列(SYN Queue) :存放已完成第一次握手(SYN)但还没完成三次握手的连接;
  • 全连接队列(Accept Queue) :存放已完成三次握手、等待被 accept() 取走的连接。

listen(sockfd, backlog) 中的 backlog 参数,控制的就是全连接队列的最大长度。

默认一般是128,但在高并发场景下远远不够。如果你的服务器来不及调用 accept() ,新来的连接就会被丢弃。

怎么办?两个办法:

  1. 调大系统限制
    bash echo 4096 > /proc/sys/net/core/somaxconn
  2. 在代码中传更大的值
    cpp listen(m_listenFd, 4096);

这样就能缓冲更多连接,提升抗压能力。


惊群效应:多个worker抢一个连接?🐑🐑🐑

当你用多进程或多线程模型时,可能会让多个工作线程同时等待 accept() 。这时候问题来了:每当有新连接到来,操作系统会唤醒所有等待中的进程/线程,结果只有一个能成功获取连接,其余又陷入睡眠。

这种“狼多肉少”的现象叫做 惊群效应(Thundering Herd) ,会造成大量无效的上下文切换,严重影响性能。

如何破解?

方法一:锁保护 + 单 accept 线程

让所有 worker 共享一把互斥锁,每次只有一个线程可以进入 accept() 。简单有效,但串行化处理连接请求,吞吐受限。

方法二:eventfd + epoll 分发

主进程监听连接,一旦有新客户端接入,就通过 eventfd 通知某个空闲 worker 去处理。复杂但高效。

方法三(推荐):SO_REUSEPORT 💥

Linux 3.9+ 引入的新特性,允许多个进程/线程绑定同一个 IP:Port,内核自动进行负载均衡!

int opt = 1; setsockopt(m_listenFd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); 

启用后,每个进程都可以独立调用 accept() ,内核帮你公平分发连接。不仅彻底消除惊群,还能充分利用多核CPU。

实战建议:结合 CPU 亲和性设置,每个 worker 绑定不同核心,极致优化!


安全加固:IP白名单 + 连接频率限制 🔐

公网暴露的服务最容易遭受扫描和DDoS攻击。我们得加点防护措施。

IP白名单过滤

很简单, accept() 成功后立刻检查客户端IP:

std::unordered_set<std::string> m_whiteList = {"192.168.1.100", "10.0.0.5"}; void handleNewConnection(int fd, const sockaddr_in& addr) { std::string ip = inet_ntoa(addr.sin_addr); if (m_whiteList.find(ip) == m_whiteList.end()) { std::cout << "Blocked connection from " << ip << std::endl; ::close(fd); return; } // 正常处理 } 

当然,生产环境可以用更高级的方式,比如集成防火墙规则或基于地理位置的访问控制。

连接频率限制(防暴力攻击)

再进一步,我们可以实现一个简单的滑动窗口限流器:

std::map<std::string, int> m_connCount; std::chrono::steady_clock::time_point m_windowStart; const int MAX_CONN_PER_SEC = 10; bool allowConnection(const std::string& ip) { auto now = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::seconds>( now - m_windowStart).count(); if (duration > 1) { m_connCount.clear(); m_windowStart = now; } if (m_connCount[ip] >= MAX_CONN_PER_SEC) { return false; } m_connCount[ip]++; return true; } 

每秒最多允许10次连接尝试,超过则拒绝。虽简单,但足够应对大多数扫描行为。

未来还可以升级为令牌桶算法,支持突发流量。


客户端也要讲究:状态机驱动的可靠连接 🔄

如果说 TcpServer 是舞台导演,那 TcpSocket 就是台上的演员。每个客户端连接都需要精确的状态管理,否则很容易出现“明明断开了却还在发数据”这种尴尬场面。

为此,我们引入 有限状态机(FSM)

定义连接状态

enum class SocketState { Closed, // 初始/断开状态 Connecting, // 正在连接中(非阻塞 connect) Connected, // 已建立连接 Closing, // 正在优雅关闭 Error // 发生不可恢复错误 }; 

状态转换图长这样:

stateDiagram-v2 [*] --> Closed Closed --> Connecting : Connect() Connecting --> Connected : connect success Connecting --> Error : connect failed Connected --> Closing : Close() Connected --> Error : network error / peer reset Closing --> Closed : shutdown complete Error --> Closed : Cleanup 

有了这个图,谁都能一眼看懂连接是怎么一步步走下来的。而且测试人员还能根据它写出完整的路径覆盖用例,大大增强健壮性。


异步连接怎么搞?⚡

同步 connect() 会阻塞线程,显然不行。我们要用非阻塞方式:

bool TcpSocket::Connect(const std::string& ip, uint16_t port) { if (m_state != SocketState::Closed) return false; sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(port); inet_pton(AF_INET, ip.c_str(), &addr.sin_addr); int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); if (sockfd < 0) return false; m_socketFd = sockfd; int result = connect(m_socketFd, (struct sockaddr*)&addr, sizeof(addr)); if (result == 0) { m_state = SocketState::Connected; OnConnected(); return true; } else if (errno == EINPROGRESS) { m_state = SocketState::Connecting; GetEventLoop()->RegisterWritable(this); // 等待可写事件 return true; } else { Close(); return false; } } 

重点来了:当 connect() 返回 -1 errno == EINPROGRESS 时,说明连接正在后台建立。此时我们应该注册“可写事件”,一旦socket变为可写,就意味着连接完成了!

然后调用 FinishConnect() 检查是否真正成功:

void TcpSocket::FinishConnect() { int err; socklen_t len = sizeof(err); getsockopt(m_socketFd, SOL_SOCKET, SO_ERROR, &err, &len); if (err == 0) { m_state = SocketState::Connected; OnConnected(); } else { m_state = SocketState::Error; HandleError(err); } } 

这套机制完美适配 Reactor 模式,也是 libevent、Netty 等主流框架的做法。


心跳保活:让连接“活着” ❤️

长时间空闲的连接容易被路由器或防火墙掐断。为了维持活跃,我们需要定期发送心跳包。

有两种方式:

方式一:应用层心跳(推荐)

自己定义一个轻量消息,比如:

字段 类型 说明
magic uint32_t 0xHEARTBEAT
timestamp uint64_t 当前时间戳

每隔30秒发一次,对方回复ACK。若连续3次无响应,则判定连接失效。

优点:可控性强,适合移动弱网环境。

方式二:TCP KeepAlive

启用内核自带机制:

int keepAlive = 1; int keepIdle = 60; // 空闲60秒后探测 int keepInterval = 5; // 每隔5秒发一次 int keepCount = 3; // 最多发3次 setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(keepAlive)); setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(keepIdle)); setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(keepInterval)); setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(keepCount)); 

但它有个致命缺点:首次探测延迟太长(通常几分钟),不适合要求快速感知断连的场景。

所以最佳实践是: 两者共存 ,互补长短。


数据收发:别再被“粘包”折磨了 📦

TCP是字节流协议,不保留消息边界。这意味着你发了两次“hello”和“world”,对方可能一次性收到“helloworld”,也可能分三次收到“hel”、“lo”、“world”。

这就是臭名昭著的“ 粘包/拆包 ”问题。

怎么破?三种主流方案:

方法 说明 适用场景
固定长度 每条消息固定N字节 协议简单,浪费带宽
分隔符法 \n \r\n 分隔 文本协议如HTTP
长度前缀法 先发4字节长度,再发数据 推荐,通用性强 ✅

我们选第三种。

长度前缀协议格式

字段 类型 长度(字节)
payload_length uint32_t 4
payload_data byte[] N

发送时先写入大端整数表示的数据长度,再写实际内容。

接收端怎么做?

void TcpSocket::OnDataReceived(const void* data, size_t len) { m_recvBuffer.Append(data, len); while (m_recvBuffer.ReadableBytes() >= 4) { uint32_t payloadLen = m_recvBuffer.PeekUInt32(); // 查看头部但不移动指针 if (m_recvBuffer.ReadableBytes() >= 4 + payloadLen) { m_recvBuffer.Retrieve(4); // 跳过长度字段 std::string message = m_recvBuffer.RetrieveAsString(payloadLen); OnMessage(message); // 交给上层处理 } else { break; // 数据不完整,等待下次 } } } 

关键在于: 只处理完整的包,剩下的留在缓冲区里

这就引出了下一个话题: 缓冲区设计


高效缓冲区:Buffer类实现 🧠

我们需要一个既能动态扩容又能减少内存拷贝的缓冲区。

class Buffer { private: std::vector<char> m_buffer; size_t m_readIndex{0}; size_t m_writeIndex{0}; public: void EnsureWritable(size_t len) { if (WritableBytes() < len) { ExpandBuffer(len); } } void Append(const char* data, size_t len) { EnsureWritable(len); std::copy(data, data + len, BeginWrite()); m_writeIndex += len; } char* BeginWrite() { return &m_buffer[m_writeIndex]; } size_t WritableBytes() const { return m_buffer.size() - m_writeIndex; } size_t ReadableBytes() const { return m_writeIndex - m_readIndex; } void Retrieve(size_t len) { m_readIndex += len; } std::string RetrieveAsString(size_t len) { std::string str(BeginRead(), len); Retrieve(len); return str; } private: void ExpandBuffer(size_t needed) { size_t readable = ReadableBytes(); if (readable + WritableBytes() >= needed) { std::copy(BeginRead(), BeginRead() + readable, &m_buffer[0]); m_writeIndex = readable; m_readIndex = 0; } else { m_buffer.resize(m_writeIndex + needed); } } char* BeginRead() { return &m_buffer[m_readIndex]; } }; 

这个设计妙在哪?

  • 使用 m_readIndex m_writeIndex 实现读写分离;
  • 当空间不足时,优先移动已有数据到开头,腾出空间;
  • 只有实在不够才扩容,减少内存分配次数;
  • 支持零拷贝提取字符串,性能杠杠的!

这就是工业级库(如 Mudo、Netty)的标准做法。


出错了怎么办?精准识别与自动恢复 🛠️

网络世界充满不确定性。我们必须学会分辨各种异常情况,并做出恰当反应。

异常类型 检测方法
对端正常关闭 recv() 返回 0
对端强制关闭 recv() 返回 -1, errno= ECONNRESET
连接超时 recv() 返回 -1, errno=ETIMEDOUT
本地资源耗尽 socket() 返回 -1

于是我们可以写出这样的错误处理器:

void TcpSocket::HandleError(int errCode) { switch (errCode) { case ECONNRESET: case EPIPE: OnPeerReset(); // 对端崩溃 break; case ETIMEDOUT: OnTimeout(); break; case ENETDOWN: case ENETUNREACH: OnNetworkDown(); break; default: LogError("Unknown socket error: %d", errCode); break; } Close(); } 

但注意,Windows 和 Linux 的错误码不一样!怎么办?

封装一层跨平台接口即可:

int GetLastSocketError() { #ifdef _WIN32 return WSAGetLastError(); #else return errno; #endif } std::string ErrnoToString(int err) { static std::unordered_map<int, std::string> errMap = { {ECONNREFUSED, "Connection refused"}, {ECONNRESET, "Connection reset by peer"}, {ETIMEDOUT, "Connection timed out"}, {ENOTCONN, "Socket not connected"} }; auto it = errMap.find(err); return it != errMap.end() ? it->second : "Unknown error"; } 

从此一套代码跑遍天下 🌍。


自动重连:别轻易放弃!🔁

连接断了就完了?不,我们要努力重生。

但不能盲目重试,否则会造成“雪崩效应”。推荐使用 指数退避算法

void TcpSocket::Reconnect() { if (m_reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { OnPermanentFailure(); return; } int delayMs = 1000 * (1 << m_reconnectAttempts); // 1s, 2s, 4s, ... delayMs = std::min(delayMs, 60000); // 最大60秒 GetEventLoop()->RunAfter(delayMs, [this]() { if (Connect(m_remoteIp, m_remotePort)) { m_reconnectAttempts = 0; } else { m_reconnectAttempts++; Reconnect(); } }); } 
尝试次数 延迟时间(秒)
1 2
2 4
3 8
4 16
5 32
6 60

既给了系统恢复的时间,又不会永久放弃。稳得很!


实战项目:做一个即时消息模块 💬

学了这么多,来练练手吧!

我们要做一个简易IM客户端,支持:

  • 登录认证
  • 发送文本消息
  • 接收并显示消息
  • 断线自动重连

协议格式采用 TLV(Type-Length-Value):

Field Size (bytes) Type
msg_type 1 uint8_t
payload_len 4 uint32_t BE
payload N UTF-8 string

发送函数:

bool SendMessage(uint8_t type, const std::string& content) { Buffer buf; buf.Append(&type, 1); uint32_t len = htonl(content.size()); buf.Append(&len, 4); buf.Append(content.data(), content.size()); return Send(buf.Data(), buf.Size()); } 

服务器侧只需维护所有在线连接列表,收到消息后遍历发送即可:

void BroadcastMessage(const std::string& msg) { for (auto& sock : m_clients) { sock->Send(msg); } } 

当然,真实场景还需要考虑:
- 消息序列号防丢失
- ACK确认机制
- 群聊与私聊区分
- 用户身份认证

但核心思想不变: 小步迭代,逐步完善


文件传输扩展:不只是聊天,还能传东西 📁

除了文字,我们还想传文件。怎么做?

设计文件头协议

#pragma pack(push, 1) struct FileHeader { uint32_t magic; uint8_t cmd; char filename[256]; uint64_t total_size; uint64_t offset; char md5[33]; uint8_t encrypt_flag; }; #pragma pack(pop) 

总共315字节。客户端先发头部,服务端解析后再决定是否接收、从哪续传。

分块传输 + 缓冲优化

大文件不能一口气读进内存,要用固定缓冲区循环读取:

const size_t BUFFER_SIZE = 64 * 1024; std::unique_ptr<char[]> buffer(new char[BUFFER_SIZE]); while ((bytes_read = fread(buffer.get(), 1, BUFFER_SIZE, fp))) { send(sock_fd, buffer.get(), bytes_read, 0); update_progress(...); } 

对于超大文件(>1GB),可以用 mmap 提升I/O效率:

void* mapped = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0); send(sock_fd, mapped, file_size, 0); munmap(mapped, file_size); 

减少拷贝,利用页缓存,速度飞起!

完整性校验:MD5比对 🔍

传输完成后计算MD5:

std::string compute_md5(FILE* fp) { MD5_CTX ctx; MD5_Init(&ctx); char buf[8192]; while (size_t len = fread(buf, 1, sizeof(buf), fp)) { MD5_Update(&ctx, buf, len); } unsigned char digest[16]; MD5_Final(digest, &ctx); return bytes_to_hex(digest, 16); } 

两端对比一致才算成功,否则标记失败并提示重传。


更上一层楼:工厂模式 + 线程池 + 跨平台兼容 🚀

最后,我们把整个系统打造成可扩展的架构。

工厂模式统一创建Socket

enum SocketType { TCP_SOCKET, FILE_SOCKET, ENCRYPTED_FILE_SOCKET }; class SocketFactory { public: static std::unique_ptr<TcpSocket> create(SocketType type, int fd) { switch (type) { case TCP_SOCKET: return std::make_unique<TcpSocket>(fd); case FILE_SOCKET: return std::make_unique<FileSocket>(fd); case ENCRYPTED_FILE_SOCKET: return std::make_unique<EncryptedFileSocket>(fd); default: throw std::invalid_argument("Unknown socket type"); } } }; 

以后新增加密Socket、压缩Socket都不用改原有逻辑。

线程池处理并发任务

用线程池管理多个文件传输任务:

线程数 平均吞吐量(MB/s) CPU利用率(%) 内存峰值(MB)
1 112 35 87
2 210 62 165
4 380 89 310
8 410 95 590
16 405 97 980

结论:4~8线程性价比最高,再多反而调度开销抵消收益。

跨平台兼容:一份代码,到处运行 🌐

写个 GlobalDef.h 来屏蔽差异:

#ifndef GLOBAL_DEF_H #define GLOBAL_DEF_H #include <stdint.h> #if defined(_WIN32) || defined(_WIN64) #define PLATFORM_WINDOWS #include <winsock2.h> #define close closesocket #define socklen_t int #else #define PLATFORM_LINUX #include <sys/socket.h> #include <unistd.h> #endif #define MAX_FILENAME_LEN 256 #define DEFAULT_CHUNK_SIZE (64 * 1024) #define ENABLE_ENCRYPTION 1 #endif 

条件编译搞定一切,从此不怕换系统!


结语:从理论到工程,你离高手只差一步 🏁

回顾这一路,我们从最基本的 socket() 调用出发,逐步构建了一个功能完整、结构清晰、性能优异的TCP网络框架。过程中涉及的知识点包括:

  • 套接字编程基础
  • 面向对象封装
  • RAII与异常安全
  • 状态机设计
  • 粘包处理与缓冲区管理
  • 心跳保活与自动重连
  • 文件传输协议
  • 多线程与线程池
  • 跨平台兼容

这些不仅是面试常考题,更是实际工作中天天打交道的内容。

而最重要的是: 理解原理,动手实践 。不要停留在“我知道”,而是要做到“我能造出来”。

毕竟,真正的能力,永远藏在你亲手敲出的每一行代码里 💻❤️。

继续加油吧,未来的系统工程师!🌟

本文还有配套的精品资源,点击获取

menu-r.4af5f7ec.gif

简介:在IT领域,网络编程是构建分布式系统和客户端-服务器架构的核心技术。TCP作为可靠的传输层协议,广泛应用于文件传输场景。本文介绍一个用C++实现的TCP文件传输类系统,涵盖套接字编程、面向对象设计、文件分块传输、多线程处理及事件驱动机制等关键技术。通过封装TcpServer、TcpSocket和FileSocket等类,结合FastDelegate实现回调机制,并运用设计模式提升代码可维护性,开发者可构建稳定高效的文件传输服务。项目还包含全局配置管理、内存安全控制与性能优化策略,适用于实际网络通信环境。


本文还有配套的精品资源,点击获取

menu-r.4af5f7ec.gif


Read more

Python-Chess实战指南:从零构建智能象棋应用

Python-Chess实战指南:从零构建智能象棋应用 【免费下载链接】python-chessA chess library for Python, with move generation and validation, PGN parsing and writing, Polyglot opening book reading, Gaviota tablebase probing, Syzygy tablebase probing, and UCI/XBoard engine communication 项目地址: https://gitcode.com/gh_mirrors/py/python-chess 还在为象棋程序开发而烦恼吗?Python-Chess让你轻松搞定象棋编程的各个环节。这个纯Python实现的国际象棋库,为开发者提供了从基础棋局管理到高级AI集成的完整解决方案。 🎯 实战应用场景:解决真实开发问题 场景一:象棋AI对战系统开发 Python-Chess与AI引擎集成示意图 import

By Ne0inhk
Python保姆级下载安装教程-->Windows版本

Python保姆级下载安装教程-->Windows版本

Windows版本保姆级下载安装 一、下载Python  1、点击下载官网地址 Python官方网站地址https://www.python.org/downloads/ 2、官网页面如下: 3、点击下载界面: 上面最新的版本是3.14.2版本,一般来说新版较之老版优化了一些内容且版本向下兼容,但是不建议下载最新版本,因为python在很多地方使用时没有更新到最新版本,向下兼容性并不好,但也不要太低版本的,很多不适用。 点击Downloads,选择适合自己电脑系统的版本,我的电脑是Windows系统,就选择了Windows,点击后会跳转到另一个页面 【Stable Releases】:稳定发布版本,是官方完成全面测试、修复已知 Bug 的成熟版本,运行稳定、风险低,无论入门学习还是机器视觉项目开发,都优先选这个版本; 【Pre-releases】:预发布版本,属于测试阶段的 “体验版”,可能包含新功能但存在未修复的 Bug,稳定性差,小白或做实际项目(如机器视觉开发)千万别选,易出现代码报错、

By Ne0inhk
python-简单AI应用

python-简单AI应用

1 基础 * AI:人工智能(Artificial Intelligence),是一个学科领域的统称,目标就是使机器能够像人类一样思考、学习、推理和解决问题。 * AI大模型:也称为大语言模型(Large Language Models, LLM),是AI技术的一个分支。其实就是一个用代码模拟人脑神经网络的程序(参数量极其庞大,通常达到数十亿至数千亿级别),通过大量的数据训练后,使其具备理解人类语言、思考、推理并输出人类语言的能力。 * AI应用:是指将AI大模型技术落地到具体的业务场景中,用来解决实际问题的产品或者服务。 1.1 大模型部署 * 本地部署 * 优点:数据安全、自主可控、长期成本低 * 缺点:初始成本高、需长期维护、性能受限 * 官方开放API * 优点:前期成本低、无需部署和维护、随时访问 * 缺点:隐私不能保障、长期成本高、可控性差 * 云服务平台 * 优点:

By Ne0inhk
千面之法: 释放 C++ 多态的灵活威力

千面之法: 释放 C++ 多态的灵活威力

目录 1:多态的概念 1.1:概念 2.多态的定义与实现 2.1:多态的构成条件 2.2:虚函数 2.3:虚函数的重写 2.3.1:虚函数重写的两个例外 2.3.1.1:协变(基类与派生类函数的返回值不同,基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时) 2.3.1.2:析构函数的重写 2.4:C++11 override和final 2.4.1:final关键字 2.4.2:override关键字 2.5:重载、

By Ne0inhk