目录
一.TcpServer模块
TcpServer模块是网络服务器架构中的核心管理单元,它通过对多个底层子模块进行整合与调度,提供了一个高层、易用的接口,使得搭建和管理一个高性能服务器变得简单明了。
该模块的核心职责是协调连接监听、事件循环、连接管理和多线程处理,形成一个完整的事件驱动型服务器。
主要构成组件
- Acceptor 对象:该对象负责创建并监听服务器套接字(监听套接字),等待客户端的连接请求。它是服务器接收新连接的入口点。
- EventLoop 对象 (baseloop):这是主事件循环对象,通常运行在主线程中。它负责监控Acceptor的监听套接字,处理新连接的到达事件,并协调其他模块的工作。
- .std::unordered_map<uint64_t, PtrConnection>_conns:这是一个以连接ID为键的哈希表,用于存储和管理所有活跃客户端连接对应的Connection对象指针。它确保了服务器可以有效地跟踪和访问每一个连接。
- LoopThreadPool 对象:这是一个事件循环线程池。当连接建立后,其后续的读写等事件监控和处理工作会被分发到线程池中某个从属线程的EventLoop中执行,以此实现连接的负载均衡和高并发处理。
核心功能特性
线程池配置:允许用户设置从属事件循环线程池的数量,以匹配服务器的并发需求。
服务器生命周期管理:提供启动服务器的接口。
回调函数设置:允许用户定义并设置一系列关键事件的处理逻辑,包括:
- 连接建立完成回调:当一个新的TCP连接完全建立好后被调用。
- 消息到达回调:当某个连接上有数据可读,并被成功读取后,用于处理这些业务数据。
- 关闭回调:当连接关闭时被调用。
- 任意事件回调:用于处理其他用户自定义的事件。
- 这些回调由用户设置给TcpServer,TcpServer会在创建每个新连接时,将它们设置给对应的Connection对象。
连接生命周期管理:支持为每个连接启用非活跃超时销毁功能。如果一个连接在指定的时间内没有进行任何数据交换,服务器将自动关闭它以释放资源。
定时任务:支持添加定时任务,允许在事件循环中调度执行未来某个时间点或周期性任务。
服务器工作流程详解
整个服务器的工作流程是一个典型的事件驱动循环,具体步骤如下:
- 初始化与监听:在TcpServer内部,会实例化一个Acceptor对象来创建监听套接字,同时创建一个主EventLoop对象(称为baseloop)。
- 事件监控:将Acceptor的"有新连接"可读事件注册到baseloop上进行监控。baseloop开始运行其事件循环,等待事件发生。
- 接受新连接:当有客户端发起连接请求时,Acceptor的监听套接字变为可读(事件就绪)。baseloop检测到该事件,触发并执行对应的读事件回调函数。在该回调中,Acceptor接受(accept)这个连接,得到一个与客户端通信的新套接字。
- 封装与管理连接:对于这个新建的通信套接字,TcpServer会为其创建一个专属的Connection对象进行管理。这个Connection对象封装了该套接字的所有状态、数据缓冲区以及事件处理逻辑。
- 设置回调逻辑:TcpServer会将用户预先设置的各类功能回调(如消息回调、关闭回调等)传递给这个新创建的Connection对象。这样,该连接后续的所有事件都将按照用户定义的逻辑进行处理。
- 启用超时控制:如果用户配置了非活跃连接超时销毁功能,TcpServer会在此刻为该Connection启动一个定时器,以监控其活跃状态。
- 分发至线程池:为了不阻塞主循环并充分利用多核CPU,TcpServer会从LoopThreadPool线程池中选取一个从属线程,并将这个新Connection对象注册到该线程所拥有的EventLoop中进行后续的事件监控(如可读、可写事件)。自此,该连接的所有数据处理工作都在这个从属线程中完成。
- 处理客户端数据:当客户端发送数据到达后,该连接在其从属EventLoop中就绪了可读事件。对应的EventLoop会调用该Connection的读事件回调函数来读取网络数据。数据读取完成后,便会调用之前由TcpServer设置、用户编写的消息回调函数,进行具体的业务逻辑处理。
1.1.分模块讲解
1.1.1.回调函数模块
首先TcpServer需要处理这些回调函数
- 连接建立成功的回调函数
- 消息到达时的回调函数
- 连接关闭时的回调函数
- 任意事件的回调函数
首先我们就定义了下面4个类型的回调函数
cpp
// 回调函数类型定义
using ConnectedCallback = std::function<void(const PtrConnection&)>; // 连接建立回调
using MessageCallback = std::function<void(const PtrConnection&, Buffer *)>; // 消息到达回调
using ClosedCallback = std::function<void(const PtrConnection&)>; // 连接关闭回调
using AnyEventCallback = std::function<void(const PtrConnection&)>; // 任意事件回调(如错误事件)
using Functor = std::function<void()>; // 通用可调用对象类型
// 回调函数成员变量
ConnectedCallback _connected_callback;
MessageCallback _message_callback;
ClosedCallback _closed_callback;
AnyEventCallback _event_callback;
当然这些回调函数都是由用户传递进来的,那么用户怎么传递这些回调函数进来呢?
其实也很简单了,就是通过下面这4个成员函数来进行回调函数的传递
cpp
// 设置各种回调函数
void SetConnectedCallback(const ConnectedCallback&cb) { _connected_callback = cb; }
void SetMessageCallback(const MessageCallback&cb) { _message_callback = cb; }
void SetClosedCallback(const ClosedCallback&cb) { _closed_callback = cb; }
void SetAnyEventCallback(const AnyEventCallback&cb) { _event_callback = cb; }
回调函数传递进来了,是TcpServer模块来调用的吗?显然不是
当然,这些回调函数都只是在TcpServer这里暂存,TcpServer应该将这些回调函数传递给每一个新的连接对应的Connection。
那么我们是在哪里进行传递的呢?
我们在NewConnection这里将回调函数传递给Connection对象
cpp
// 为新连接构造一个Connection进行管理(由Acceptor在接收到新连接时调用)
void NewConnection(int fd) {
......
// 创建Connection对象,分配一个工作线程的事件循环来管理这个连接
PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd));
// 设置Connection的各种回调函数
conn->SetMessageCallback(_message_callback);
conn->SetClosedCallback(_closed_callback);
conn->SetConnectedCallback(_connected_callback);
conn->SetAnyEventCallback(_event_callback);
// 设置服务器关闭连接时的回调,用于从_conns中移除连接
conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, std::placeholders::_1));
......
}
大家仔细观察一下就会发现,这里传递了5个回调函数进去哦!!那么是不是多了应该回调函数啊
下面这个回调函数的设置,设置的可不是我们用户传递进去的哦
cpp
// 设置服务器关闭连接时的回调,用于从_conns中移除连接
conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, std::placeholders::_1));
这个回调函数传递进去的其实是下面这个函数哦!!!
cpp
// 从管理Connection的_conns中移除连接信息(线程安全的封装)
void RemoveConnection(const PtrConnection &conn) {
// 将移除操作放到主事件循环中执行,确保线程安全
_baseloop.RunInLoop(std::bind(&TcpServer::RemoveConnectionInLoop, this, conn));
}
1.1.2.连接建立
首先,我们上面不是说将Acceptor的"有新连接"可读事件注册到baseloop上进行监控。baseloop开始运行其事件循环,等待事件发生。
那么我们是怎么做的
cpp
// 构造函数:初始化服务器,设置监听端口
TcpServer(int port):
_port(port),
_next_id(0),
_enable_inactive_release(false),
_acceptor(&_baseloop, port), // 创建Acceptor,绑定到主事件循环和端口
_pool(&_baseloop) { // 创建线程池,绑定到主事件循环
// 设置Acceptor接收到新连接时的回调函数
_acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));
_acceptor.Listen(); // 开始监听端口,将监听套接字注册到主事件循环
}
- 首先我们先给_acceptor设置新连接到来的回调函数------在这个回调函数里面我们会将Acceptor的"有新连接"可读事件注册到baseloop上进行监控。baseloop开始运行其事件循环,等待事件发生。
- 并且开启读事件监控,表示正式开始监听相关端口,准备建立新连接
我们发现这个_acceptor设置的回调函数是NewConnection函数,而NewConnection函数就是下面这个
cpp
// 为新连接构造一个Connection进行管理(由Acceptor在接收到新连接时调用)
void NewConnection(int fd) {
_next_id++; // 分配新的连接ID
// 创建Connection对象,分配一个工作线程的事件循环来管理这个连接
PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd));
// 设置Connection的各种回调函数
conn->SetMessageCallback(_message_callback);
conn->SetClosedCallback(_closed_callback);
conn->SetConnectedCallback(_connected_callback);
conn->SetAnyEventCallback(_event_callback);
// 设置服务器关闭连接时的回调,用于从_conns中移除连接
conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, std::placeholders::_1));
// 如果启用了非活跃连接释放,则设置超时时间
if (_enable_inactive_release)
{
conn->EnableInactiveRelease(_timeout);
}
// 初始化连接,开始处理事件
conn->Established();// 将实际工作提交到EventLoop线程执行
// 将连接保存到连接映射表中
_conns.insert(std::make_pair(_next_id, conn));
}
这样子就完成了新连接建立时的所有初始化工作
1.1.3.连接超时自动释放模块
我们之前说过我们在新连接建立后的X秒内,如果新连接什么都没有做,那么就可以连接超时自动释放这个连接。
首先这个功能我们默认是关闭的。
如果需要启动这个功能,我们可以调用下面这个函数
cpp
// 启用非活跃连接释放功能,设置超时时间
void EnableInactiveRelease(int timeout) {
_timeout = timeout;
_enable_inactive_release = true;
}
那么这个超时任务是什么时候添加进这个EventLoop里面去的?
我们仔细观察上面的这个连接建立时的回调函数NewConnection函数
我们发现它有下面这一句
cpp
// 如果启用了非活跃连接释放,则设置超时时间
if (_enable_inactive_release)
{
conn->EnableInactiveRelease(_timeout);//这个会将超时释放任务添加进EventLoop里面执行
}
这个就很容易理解。
在新连接建立的时候就必须添加这个超时销毁。
1.1.4.定时任务模块
这个模块其实也很简单,主要是和两个函数相关
这个是对外提供服务的接口
cpp
// 添加一个定时任务(线程安全的接口)
void RunAfter(const Functor &task, int delay) {
// 将定时任务提交到主事件循环中执行
_baseloop.RunInLoop(std::bind(&TcpServer::RunAfterInLoop, this, task, delay));
}
注意:这个RunInLoop处理的可不是定时任务,它处理普通任务
他将一个普通任务添加进来EventLoop等待处理,但是我们仔细观察就会发现,它传递进去的普通任务可是下面这个函数
cpp
// 在主事件循环中添加定时任务
void RunAfterInLoop(const Functor &task, int delay) {
_next_id++; // 使用连接ID自增作为定时器ID(注意:这里定时器和连接ID共用计数器)
_baseloop.TimerAdd(_next_id, delay, task); // 将定时任务添加到主事件循环
}
这个普通任务里面就有 添加一个定时任务进EventLoop里面的操作。
也就是说
添加一个定时任务 这个动作 也算是一个任务。
定时任务 是 另外一个任务。
1.1.5.异常处理
cpp
class NetWork {
public:
NetWork() {
DBG_LOG("SIGPIPE INIT");
signal(SIGPIPE, SIG_IGN); // 将 SIGPIPE 信号的处理方式设置为忽略
}
};
static NetWork nw; // 全局静态对象,在程序启动时自动构造
- 作用
- 忽略 SIGPIPE 信号,防止程序因向已关闭的 socket 写入数据而崩溃
- 问题背景
在网络编程中:
- 当程序向一个已关闭的 socket(套接字)写入数据时
- 操作系统会发送 SIGPIPE 信号给进程
- 默认情况下,SIGPIPE 信号会终止进程,导致服务器崩溃
1.2.代码总览

cpp
#pragma once
#include "acceptor.hpp"
#include "socket.hpp"
#include "poller.hpp"
#include "channel.hpp"
#include "eventloop.hpp"
#include "connection.hpp"
#include"loopthreadpool.hpp"
#include<signal.h>
class TcpServer {
private:
uint64_t _next_id; // 自动增长的连接ID,用于唯一标识每个连接
int _port; // 服务器监听的端口号
int _timeout; // 非活跃连接的超时时间(单位:秒/毫秒,根据具体实现定)
bool _enable_inactive_release; // 是否启用非活跃连接超时释放功能
EventLoop _baseloop; // 主线程的EventLoop对象,负责监听事件的处理(主Reactor)
Acceptor _acceptor; // 监听套接字的管理对象,负责接受新连接
LoopThreadPool _pool; // 从属EventLoop线程池(工作线程池,SubReactor模式)
std::unordered_map<uint64_t, PtrConnection> _conns; // 保存所有连接的智能指针,key为连接ID
// 回调函数类型定义
using ConnectedCallback = std::function<void(const PtrConnection&)>; // 连接建立回调
using MessageCallback = std::function<void(const PtrConnection&, Buffer *)>; // 消息到达回调
using ClosedCallback = std::function<void(const PtrConnection&)>; // 连接关闭回调
using AnyEventCallback = std::function<void(const PtrConnection&)>; // 任意事件回调(如错误事件)
using Functor = std::function<void()>; // 通用可调用对象类型
// 回调函数成员变量
ConnectedCallback _connected_callback;
MessageCallback _message_callback;
ClosedCallback _closed_callback;
AnyEventCallback _event_callback;
private:
// 在主事件循环中添加定时任务
void RunAfterInLoop(const Functor &task, int delay) {
_next_id++; // 使用连接ID自增作为定时器ID(注意:这里定时器和连接ID共用计数器)
_baseloop.TimerAdd(_next_id, delay, task); // 将定时任务添加到主事件循环
}
// 为新连接构造一个Connection进行管理(由Acceptor在接收到新连接时调用)
void NewConnection(int fd) {
_next_id++; // 分配新的连接ID
// 创建Connection对象,分配一个工作线程的事件循环来管理这个连接
PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd));
// 设置Connection的各种回调函数
conn->SetMessageCallback(_message_callback);
conn->SetClosedCallback(_closed_callback);
conn->SetConnectedCallback(_connected_callback);
conn->SetAnyEventCallback(_event_callback);
// 设置服务器关闭连接时的回调,用于从_conns中移除连接
conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, std::placeholders::_1));
// 如果启用了非活跃连接释放,则设置超时时间
if (_enable_inactive_release)
{
conn->EnableInactiveRelease(_timeout);//这个会将超时释放任务添加进EventLoop里面执行
}
// 初始化连接,开始处理事件
conn->Established();// 将实际工作提交到EventLoop线程执行
// 将连接保存到连接映射表中
_conns.insert(std::make_pair(_next_id, conn));
}
// 在主事件循环中从_conns中移除连接信息
void RemoveConnectionInLoop(const PtrConnection &conn) {
int id = conn->Id();
auto it = _conns.find(id);
if (it != _conns.end()) {
_conns.erase(it); // 从连接映射表中删除
}
}
// 从管理Connection的_conns中移除连接信息(线程安全的封装)
void RemoveConnection(const PtrConnection &conn) {
// 将移除操作放到主事件循环中执行,确保线程安全
_baseloop.RunInLoop(std::bind(&TcpServer::RemoveConnectionInLoop, this, conn));
}
public:
// 构造函数:初始化服务器,设置监听端口
TcpServer(int port):
_port(port),
_next_id(0),
_enable_inactive_release(false),
_acceptor(&_baseloop, port), // 创建Acceptor,绑定到主事件循环和端口
_pool(&_baseloop) { // 创建线程池,绑定到主事件循环
// 设置Acceptor接收到新连接时的回调函数
_acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));
_acceptor.Listen(); // 开始监听端口,开始监听新连接,将监听套接字注册到主事件循环
}
// 设置工作线程数量
void SetThreadCount(int count) { return _pool.SetThreadCount(count); }
// 设置各种回调函数
void SetConnectedCallback(const ConnectedCallback&cb) { _connected_callback = cb; }
void SetMessageCallback(const MessageCallback&cb) { _message_callback = cb; }
void SetClosedCallback(const ClosedCallback&cb) { _closed_callback = cb; }
void SetAnyEventCallback(const AnyEventCallback&cb) { _event_callback = cb; }
// 启用非活跃连接释放功能,设置超时时间
void EnableInactiveRelease(int timeout) {
_timeout = timeout;
_enable_inactive_release = true;
}
// 添加一个定时任务(线程安全的接口)
void RunAfter(const Functor &task, int delay) {
// 将定时任务提交到主事件循环中执行
_baseloop.RunInLoop(std::bind(&TcpServer::RunAfterInLoop, this, task, delay));
}
// 启动服务器:创建工作线程池并启动主事件循环
void Start() {
_pool.Create(); // 创建工作线程池
_baseloop.Start(); // 启动主事件循环(开始处理事件)
}
};
class NetWork {
public:
NetWork() {
DBG_LOG("SIGPIPE INIT");
signal(SIGPIPE, SIG_IGN); // 将 SIGPIPE 信号的处理方式设置为忽略
}
};
static NetWork nw; // 全局静态对象,在程序启动时自动构造
1.3.代码测试
我们直接写一个客户端和一个服务端
tcp_srv.cc
cpp
#include "../../server/tcpserver.hpp"
// 连接建立完成的回调函数
// 当客户端与服务器成功建立TCP连接时,会调用此函数
void OnConnected(const PtrConnection &conn) {
// 打印新连接的信息,conn.get()获取裸指针便于调试
DBG_LOG("NEW CONNECTION:%p", conn.get());
}
// 连接关闭的回调函数
// 当客户端断开连接或服务器主动关闭连接时,会调用此函数
void OnClosed(const PtrConnection &conn) {
// 打印已关闭连接的信息
DBG_LOG("CLOSE CONNECTION:%p", conn.get());
}
// 消息到达的回调函数
// 当客户端发送数据到服务器,数据被成功读取到缓冲区后,会调用此函数
void OnMessage(const PtrConnection &conn, Buffer *buf) {
// 打印接收到的数据内容
DBG_LOG("%s", buf->ReadPosition());
// 将缓冲区中的数据全部消费掉,移动读指针到末尾
buf->MoveReadOffset(buf->ReadAbleSize());
// 准备要发送给客户端的响应数据
std::string str = "Hello World";
// 通过连接对象将响应数据发送给客户端
conn->Send(str.c_str(), str.size());
}
int main()
{
// 创建TcpServer对象,监听8500端口
TcpServer server(8500);
// 设置工作线程数量为2个(从属线程池)
server.SetThreadCount(2);
// 启用非活跃连接超时释放功能,设置超时时间为10秒(此处被注释掉)
server.EnableInactiveRelease(10);
// 设置连接关闭时的回调函数
server.SetClosedCallback(OnClosed);
// 设置连接建立完成时的回调函数
server.SetConnectedCallback(OnConnected);
// 设置消息到达时的回调函数
server.SetMessageCallback(OnMessage);
// 启动服务器,开始监听连接并处理事件
server.Start();
return 0;
}
tcp_cli.hpp
cpp
#include "../server/socket.hpp"
int main()
{
// 创建一个客户端套接字对象,用于连接服务器
Socket cli_sock;
// 创建客户端连接,连接到本地主机(127.0.0.1)的8500端口
// 如果连接失败,程序会返回错误(假设CreateClient内部会处理)
cli_sock.CreateClient(8500, "127.0.0.1");
for(int i=0;i<3;i++)//只发送3次消息
{
// 准备要发送给服务器的消息内容
std::string str = "hello bitejiuyeke!";
// 将消息发送给服务器
// str.c_str() 获取C风格字符串指针,str.size() 获取字符串长度
cli_sock.Send(str.c_str(), str.size());
// 定义缓冲区,用于接收服务器返回的数据,初始化为0
char buf[1024] = {0};
// 从服务器接收响应数据,最多接收1023字节(留一个位置给字符串结束符)
cli_sock.Recv(buf, 1023);
// 打印接收到的服务器响应
// %s 表示以字符串格式输出
DBG_LOG("%s", buf);
sleep(2);
}
//等待
while(1) sleep(1);
// 程序结束,返回0表示成功
return 0;
// 注意:cli_sock对象在main函数结束时会被自动销毁
// Socket类的析构函数会自动调用Close()方法关闭套接字
// 所以这里不需要显式调用cli_sock.Close()
}


完全没有问题
shared_ptr 的 get() 成员函数
shared_ptr 的 get() 成员函数用于获取其内部保存的原始指针(raw pointer)。这个原始指针就是 shared_ptr 所管理的对象指针。
cpp
#include <memory>
#include <iostream>
int main() {
// 创建一个 shared_ptr,管理一个 int
std::shared_ptr<int> sp = std::make_shared<int>(42);
// 使用 get() 获取原始指针
int* raw_ptr = sp.get();
std::cout << *raw_ptr << std::endl; // 输出: 42
return 0;
}
二.回显服务器搭建
2.1.服务器搭建
这个其实很简单

echo.hpp
cpp
#pragma once
#include "../../server/tcpserver.hpp"
// EchoServer类:一个简单的回声服务器实现
// 功能:接收客户端发送的数据,原样返回给客户端,然后关闭连接
class EchoServer {
private:
TcpServer _server; // 内部使用的TCP服务器对象
// 连接建立完成的回调函数(私有成员函数)
// 当客户端与服务器成功建立TCP连接时被调用
void OnConnected(const PtrConnection &conn) {
// 打印新连接的信息,conn.get()获取原始指针便于调试
DBG_LOG("NEW CONNECTION:%p", conn.get());
//shared_ptr 的 get() 成员函数用于获取其内部保存的原始指针(raw pointer)。
//这个原始指针就是 shared_ptr 所管理的对象指针。
}
// 连接关闭的回调函数(私有成员函数)
// 当客户端断开连接或服务器主动关闭连接时被调用
void OnClosed(const PtrConnection &conn) {
// 打印已关闭连接的信息
DBG_LOG("CLOSE CONNECTION:%p", conn.get());
}
// 消息到达的回调函数(私有成员函数)
// 当客户端发送数据到服务器,数据被成功读取到缓冲区后,会调用此函数
// 回声服务器核心逻辑:将接收到的数据原样返回给客户端
void OnMessage(const PtrConnection &conn, Buffer *buf) {
// 将接收到的数据原样发送回客户端
// ReadPosition():获取缓冲区中可读数据的起始位置
// ReadAbleSize():获取缓冲区中可读数据的大小
conn->Send(buf->ReadPosition(), buf->ReadAbleSize());
// 将缓冲区中的数据全部消费掉,移动读指针到末尾
// 表示这些数据已经被处理
buf->MoveReadOffset(buf->ReadAbleSize());
}
public:
// 构造函数:初始化回声服务器
// port:服务器监听的端口号
EchoServer(int port):_server(port) {
// 设置工作线程数量为2个(从属线程池)
_server.SetThreadCount(2);
// 启用非活跃连接超时释放功能,设置超时时间为10秒
// 如果一个连接在10秒内没有数据交换,服务器会自动关闭它
_server.EnableInactiveRelease(10);
// 设置连接关闭时的回调函数
// 使用std::bind将成员函数绑定到this指针
// std::placeholders::_1表示回调函数的第一个参数
_server.SetClosedCallback(std::bind(&EchoServer::OnClosed, this, std::placeholders::_1));
// 设置连接建立完成时的回调函数
_server.SetConnectedCallback(std::bind(&EchoServer::OnConnected, this, std::placeholders::_1));
// 设置消息到达时的回调函数
// std::placeholders::_1和_2分别表示回调函数的第一个和第二个参数
_server.SetMessageCallback(std::bind(&EchoServer::OnMessage, this,
std::placeholders::_1, std::placeholders::_2));
}
// 启动服务器
// 调用内部TcpServer对象的Start()方法开始监听连接并处理事件
void Start() {
_server.Start();
}
};
main.cpp
cpp
#include "echo.hpp"
int main()
{
EchoServer server(8500);
server.Start();
return 0;
}
这个就是我们的服务器模块,那么现在我们还需要一个客户端

client.cpp
cpp
#include "../../server/socket.hpp"
int main()
{
// 创建一个客户端套接字对象,用于连接服务器
Socket cli_sock;
// 创建客户端连接,连接到本地主机(127.0.0.1)的8500端口
// 如果连接失败,程序会返回错误(假设CreateClient内部会处理)
cli_sock.CreateClient(8500, "127.0.0.1");
for(int i=0;i<3;i++)//只发送3次消息
{
// 准备要发送给服务器的消息内容
std::string str = "hello bitejiuyeke!";
// 将消息发送给服务器
// str.c_str() 获取C风格字符串指针,str.size() 获取字符串长度
cli_sock.Send(str.c_str(), str.size());
// 定义缓冲区,用于接收服务器返回的数据,初始化为0
char buf[1024] = {0};
// 从服务器接收响应数据,最多接收1023字节(留一个位置给字符串结束符)
cli_sock.Recv(buf, 1023);
// 打印接收到的服务器响应
// %s 表示以字符串格式输出
DBG_LOG("%s", buf);
sleep(2);
}
//等待
while(1) sleep(1);
// 程序结束,返回0表示成功
return 0;
// 注意:cli_sock对象在main函数结束时会被自动销毁
// Socket类的析构函数会自动调用Close()方法关闭套接字
// 所以这里不需要显式调用cli_sock.Close()
}



没有一点问题。
2.2.性能简单测试
这里需要借助一个工具------webbench
Webbench是一个在linux下使用的非常简单的网站压测工具。它使用fork()模拟多个客户端同时访问我们设定的URL,测试网站在压力下工作的性能,最多可以模拟3万个并发连接去测试网站的负载能力。

首先我们需要安装依赖
cpp
sudo apt-get update && sudo apt-get install universal-ctags
我们执行下面这个命令来进行编译
cpp
sudo make

没有这个头文件,我们就直接进去把这个头文件引用给删除掉
我们进入webbench.c里面将下面这个给删除掉
cpp
#include <rpc/types.h>

编译完之后就有一个可执行程序了!!

命令行参数
| 短参 | 长参数 | 作用 |
|---|---|---|
| -f | --force | 不需要等待服务器响应 |
| -r | --reload | 发送重新加载请求 |
| -t | --time | 运行多长时间,单位:秒 |
| -p | --proxy server:port | 使用代理服务器来发送请求 |
| -c | --clients | 创建多少个客户端,默认1个 |
| -9 | --http09 | 使用 HTTP/0.9 |
| -1 | --http10 | 使用 HTTP/1.0 协议 |
| -2 | --http11 | 使用 HTTP/1.1 协议 |
| --get | 使用 GET请求方法 | |
| --head | 使用 HEAD请求方法 | |
| --options | 使用 OPTIONS请求方法 | |
| --trace | 使用 TRACE请求方法 | |
| -? / -h | --help | 打印帮助信息 |
| -V | --version | 显示版本号 |
我们现在就运行这个来进行压测
首先我们先启动我们的服务器

接着我们执行下面这个来进行压测
bash
./webbench -c 500 -t 60 http://127.0.0.1:8500/hello

服务端这边

我们也不需要去关注,我们直接等待webbench返回

性能指标:
-
Speed=3000 pages/min: 每分钟处理3000个页面(请求)
-
这表示服务器每分钟可以处理3000个请求
-
换算成每秒:3000 ÷ 60 = 50 请求/秒 (QPS)
-
-
3850 bytes/sec: 每秒传输3850字节
-
这是数据传输速率
-
表示服务器每秒向客户端发送3850字节的数据
-
注意:
当前的压测方案存在一个值得注意的问题:**我们的服务端和客户端均部署在同一台服务器上。**这意味着,随着模拟客户端数量的增加,服务器本身需要同时承担服务端处理请求和客户端模拟请求的双重负载,导致资源消耗显著上升。
由于服务端和客户端都会占用大量CPU、内存及网络资源,在同一主机上运行时会形成资源竞争,相互干扰,从而影响压测结果的准确性与可靠性。这种架构难以真实反映服务端在独立环境下的实际性能表现,可能导致对服务容量和性能评估的偏差。
这个压测的效果是不准的。我们后续写完整个项目的时候,会在最后进行详细的压测分析。
2.3.模块间关系理解
我们可以直接复制下面这个html代码,我们自己去查看即可
bash
<iframe frameborder='0' style='width:100%;height:100%;' src='https://diagram-viewer.giteeusercontent.com?repo=qigezi/tcp-server&ref=master&file=image/EchoServer%E6%A8%A1%E5%9D%97%E5%85%B3%E7%B3%BB%E5%9B%BE.drawio' />

结语:其实到这里,我们整个服务器模块就算是搭建完了,但是我们这个服务器只是处理好了连接相关的所有操作,但是数据以什么样式进行传输,如何解决粘包问题,我们这里是解决不了的,要是想要去解决这些问题,就必须去借助应用层协议,只不过应用层协议千千万万,我们这里只会去讲一种------HTTP协议。