网络编程学习记录
- 使用的语言为C/C++
- 源码支持的平台为:Windows / Linux
笔记一:建立基础TCP服务端/客户端 点我跳转
笔记二:网络数据报文的收发 点我跳转
笔记三:升级为select网络模型 点我跳转
笔记四:跨平台支持Windows、Linux系统 点我跳转
笔记五:源码的封装 点我跳转
笔记六:缓冲区溢出与粘包分包 点我跳转
笔记七:服务端多线程分离业务处理高负载 点我跳转
笔记八:对socket select网络模型的优化 点我跳转
笔记九:消息接收与发送分离 点我跳转
一、思路与准备
在之前的 C++网络编程学习:服务端多线程分离业务处理高负载 笔记中,我将业务处理相关内容从主线程中分离,大大提高了服务端的效率。随着服务端的进一步完善,我会把各个模块进行分离,从而使代码的结构更加便于修改与完善,同时也可以提高代码的运行效率。
本篇笔记中,我会记录自己如何把消息发送的相关内容从业务处理线程中分离出来。首先下面是我的思路图。
- 大致思路如下:处理事件线程 >> 发送线程缓冲区 >> 发送线程
我会在新建业务子线程时创建一条发送子线程。其中由主线程创建子线程对象,子线程对象中包含两条子线程,一条为业务子线程,一条为发送子线程。这样两条线程可以在一个对象中进行相关操作,便于实现。同时,主线程只需声明一个对象即可,封装性良好且低耦合。
在业务线程需要发送消息时,首先创建一个消息发送对象,其中包含发送的目标和内容。随后把该对象加入缓冲区。在发送线程中,首先把缓冲区内的对象提取到正式发送队列中,随后把正式发送队列中的待发送事件挨个进行处理即可。
二、代码实现
1.发送线程类文件相关
首先,我们得新建两个类,一个是发送任务基类,一个是发送线程类。
发送任务基类中,含有一个虚方法DoTask
用来执行发送任务。该方法在服务端源码中被重载。
发送线程类中,含有一条发送线程,同时有一个Start
方法用来启动该线程。含有两条链表,一条为缓冲区链表,一条为正式消息链表,同时有一个addTask
方法用来把消息任务加入缓冲区。线程执行OnRun
方法把缓冲区中的任务加入正式队列并执行该任务的DoTask
方法。
- 发送线程类文件
celltask.hpp
大致代码如下:
#ifndef _CELL_Task_hpp_
#define _CELL_Task_hpp_
#include<thread>
#include<mutex>
#include<list>
//任务基类
class CellTask
{
public:
//执行任务
virtual void DoTask() = 0;
};
//发送线程类
class CellTaskServer
{
public:
CellTaskServer()
{
}
virtual ~CellTaskServer()
{
}
//添加任务
void addTask(CellTask* ptask)
{
std::lock_guard<std::mutex>lock(_mutex);
_tasksBuf.push_back(ptask);
}
//启动服务
void Start()
{
//线程
std::thread t(std::mem_fn(&CellTaskServer::OnRun),this);
t.detach();
}
protected:
//工作函数
void OnRun()
{
while(1)
{
//将缓冲区内数据加入
if(!_tasksBuf.empty())//不为空
{
std::lock_guard<std::mutex>lock(_mutex);
for(auto pTask : _tasksBuf)
{
_tasks.push_back(pTask);
}
_tasksBuf.clear();
}
//如果无任务
if(_tasks.empty())
{
//休息一毫秒
std::chrono::milliseconds t(1);
std::this_thread::sleep_for(t);
continue;
}
//处理任务
for(auto pTask:_tasks)
{
pTask->DoTask();
delete pTask;
}
//清空任务
_tasks.clear();
}
}
private:
//任务数据
std::list<CellTask*>_tasks;
//任务数据缓冲区
std::list<CellTask*>_tasksBuf;
//锁 锁数据缓冲区
std::mutex _mutex;
};
#endif
- 其中的缓冲区加入操作涉及到临界操作,所以加个自解锁。
- 因为缓冲区以及正式队列涉及到频繁进出,所以用的是链表
list
。 - 当没有发送任务时,会进行一毫秒的休息,防止消耗太多的内存。
2.主文件相关
在导入上述celltask.hpp
头文件后,我们需要重载DoTask
方法,从而实现把主文件内的相关类型数据进行发送。
我们可以创建一个新类,使他继承CellTask
任务基类。其中导入主文件内的相关类型。例如下面的例子中就导入了ClientSocket
客户端类与DataHeader
报文结构体。同时重写了DoTask
方法,使其调用ClientSocket
客户端类的SendData
方法发送报文。
//网络消息发送任务
class CellSendMsgTask : public CellTask
{
public:
CellSendMsgTask(ClientSocket* pClient,DataHeader* pHead)
{
_pClient = pClient;
_pHeader = pHead;
}
//执行任务
virtual void DoTask()
{
_pClient->SendData(_pHeader);
delete _pHeader;
}
private:
ClientSocket* _pClient;
DataHeader* _pHeader;
};
而当我们想要发送报文时,只需要新建上述CellSendMsgTask
对象,并调用CellTaskServer
任务线程类的addTask
方法添加至发送队列即可。
//向 pClient 客户端发送 pHead 报文
void AddSendTask(ClientSocket* pClient,DataHeader* pHead)
{
CellSendMsgTask* ptask = new CellSendMsgTask(pClient,pHead);
_taskServer.addTask(ptask);
}