基于Boost::asio的多线程异步TCP服务器

http://www.cppblog.com/Solstice/default.html?page=6

https://blog.csdn.net/kongwei1234/article/details/104778613

http://swiftcode.a1feeds.com/United-States/NEW-YORK-NY/JP-MORGAN-CHASE-BANK-NA

基于Boost::asio的多线程异步TCP服务器,实现了io_service线程池,测试了1万左右的并发访问,读写无压力

boost库中的ASIO网络库是目前最流行的几大网络编程框架之一,能支持大规模并发服务器的设计需求,这个库还是很牛逼的。aiso是基于C++进行开发,包含了大量C++11新特性,用它进行服务器开发还是比较方便的,写起来语句也会比较简洁,实现起来比较容易。
博主去年粗略的学习了一下asio,看了网上的许多博客,但是由于对于C++11新特性不熟悉,因此之前一直没有很理解,当时也忙也没有时间系统的学习。最近疫情原因有了很多空闲时间,因此重新学习一下用asio网络库开发服务器。
本篇博客主要实现基于ASIO网络库的异步服务器,所有读写事件都是异步的,并实现了io_service线程池,每个io_service单独运行在一个线程中,通过线程池给每个连接进来的客户端分配io_service。
在用asio进行服务器开发的时候,我们一定要先搞清楚一个概念:
asio中的异步事件,都是通过io_service去管理,在调用run()后,io_service就会去处理当前io_service管理的任务队列中的任务。

代码如下:

#include<iostream>
#include <boost/asio.hpp>
#include <string>
#include <boost/unordered_map.hpp>
using namespace boost::asio::ip;
using namespace std;
#define SERVER_PORT 7000
#define DATA_MAX_LEN 1024;
//数据包头
struct NetPacketHeader
{
    char cFlag;   //数据头的标志——填充‘s’
    long nLenth; //要发送的数据大小 由于需要发送文件,因此nlenth值可能很大,所以用long
    char cDataType;//接收/发送的数据类型 a-文件列表 b-文件 c-数据文本
    char sFileFormat[4];//如果是文件,这里即为文件格式,如果是文本数据,这里用'\0'填充
    char cSendDc; // 校验码 服务器发包填充-"s"  客户端发包填充-"c"
};

class serviceThreadPool
{
    //管理io_service的线程池
    //1.初始化一定数量的io_service,用io_service::work阻塞住,让其不退出
    //2.让io_service运行在不同的线程里,每来一个客户端,分配一个io_service
    std::vector<std::shared_ptr<boost::asio::io_service>> m_ioServiceList;
    std::vector<std::shared_ptr<boost::asio::io_service::work>> m_workList;
    //std::vector<std::thread> m_threadList;
    int m_poolSize;
    int m_io_service_pos; //依次分配io_service
public:
    //在多核的机器上,线程池中的数量最好等于核数
    explicit serviceThreadPool(int pool_size = 4) :m_io_service_pos(0)
    {
        m_poolSize = pool_size > 0 ? pool_size : 1;
        for (int i = 0; i < m_poolSize; i++)
        {
            std::shared_ptr<boost::asio::io_service> io_service(new boost::asio::io_service);
            std::shared_ptr<boost::asio::io_service::work> work(new boost::asio::io_service::work(*io_service));
            m_ioServiceList.push_back(io_service);
            m_workList.push_back(work);
        }
    }
    void startPool()
    {
        for (int i = 0; i < m_poolSize; i++) {

            std::thread runThread([=] {
                m_ioServiceList[i]->run();
            });
            runThread.detach();
        }
    }
    void stop()
    {
        for (int i = 0; i < m_poolSize; i++)
        {
            m_ioServiceList[i]->stop();
        }
    }
    boost::asio::io_service& get_io_service()
    {
        //通过这个函数分配io_service
        boost::asio::io_service& io_service = *m_ioServiceList[m_io_service_pos++];
        if (m_io_service_pos == m_poolSize - 1)m_io_service_pos = 0;
        return io_service;
    }

};

class rwhandler
{

#define MSG_HEAD_SIZE 16  //包头大小
    //由于读写操作时,socket要与读写操作的递归函数进行绑定,
    //因此这里单独实现一个读写类,用来处理,每一个连接进来的客户端的读写事件
    tcp::socket m_socket;
    unsigned char *m_getData;
    int m_clientId;
    std::function<void(int)> m_callbackError; //异常回调函数
public:
    explicit rwhandler(boost::asio::io_service& ios) :m_socket(ios)
    {
        m_getData = new unsigned char[1024];
        printf("rwhandler create \n");
    }
    ~rwhandler() {
        delete[]m_getData;
        printf("rwhandler distroy \n");
    }
    tcp::socket& getSock()
    {
        return m_socket;
    }
    void read()
    {
        printf("read \n");
        //可以先解析包头,在异步读操作的完成事件中判断包头是否正确,正确则继续读包体
        //在读包体的异步完成事件中,继续this->read();
        boost::asio::async_read(m_socket, boost::asio::buffer(m_getData, MSG_HEAD_SIZE),
            [=](const boost::system::error_code& ec, size_t size)
        {
            if (ec != nullptr) {
                printf("read error! \n");
                handleError(ec);
                return;
            }
            //这里可以将数据通过回调函数或者信号与槽传递回应用层
            //cout<< string((char*)m_getData,MSG_HEAD_SIZE)<<endl;
            NetPacketHeader *p = (NetPacketHeader *)m_getData;
            printf(" %c %d %c %s %c %d \n", p->cFlag, p->nLenth, p->cDataType, p->sFileFormat, p->cSendDc, size);
            printf("client read thread id is: %d \n", std::this_thread::get_id());
            this->read();
        });
    }
    void setClientID(int id) {
        m_clientId = id;
    }
    int getClientID() {
        return m_clientId;
    }
    //应用层,也就是server中利用SetCallBackError设置执行异常回调的函数
    template<typename F>
    void SetCallBackError(F f) {
        m_callbackError = f;
    }
    void handleError(const boost::system::error_code& ec)
    {
        //这里可以针对不同的ec的错误码进行异常解析
        printf("error! error value is: %d  %s \n", ec.value(), ec.message());
        //异常码为2表示异步任务等待时远端关闭套接字
        if (ec.value() == 2) {
            //m_socket.close();
            boost::system::error_code ec2;
            m_socket.shutdown(tcp::socket::shutdown_send, ec2);
            m_socket.close(ec2);
            printf("clinet close the connected \n");
            if (m_callbackError)
                m_callbackError(m_clientId);
        }
    }
};


class server
{
#define MAX_CLINET_NUM 7000//最大连接数量
    //这里利用m_rwIdList去管理客户端的无序表的ID,每次有新客户端连接进来,就从m_rwIdList的头部取一个ID,并将该ID从m_rwIdList中删除
    //如果有客户端异常或者主动断开,则根据客户端ID去移除无序表中的rwhandle,然后将该ID放回m_rwIdList的尾部
public:
    explicit server(boost::asio::io_service& ios, short port, int serPoolSize = 4) :m_ios(ios)
        , m_acceptor(ios, tcp::endpoint(tcp::v4(), port)),
        m_rwIdList(MAX_CLINET_NUM), m_servicePool(serPoolSize)
    {
        int current = 0;
        //初始化m_rwIdList,产生MAX_CLINET_NUM数量的ID
        std::generate_n(m_rwIdList.begin(), MAX_CLINET_NUM, [&current] {return ++current; });
        m_servicePool.startPool(); //开启线程池
    }
    ~server()
    {
        printf("server distroy! \n");
    }
    void accept()
    {
        //这里不用智能指针,使用rwhandler rw;就会报“尝试引用已删除对象”的错误
        //智能指针在离开作用于时应该也是会析构,为什么不会报错呢?
        //是因为在async_accept的lambda中将rw当作形参传入,这时候rw的引用计数就加1了,因此不会析构
        //将智能指针放入容器后,智能指引用计数就会加1
        std::shared_ptr<rwhandler> rw = std::make_shared<rwhandler>(m_servicePool.get_io_service());
        rw->setClientID(m_rwIdList.front());  //设置client ID
        m_rwIdList.pop_front();
        //设置异常回调函数
        rw->SetCallBackError([this](int id) {
            printf("start callback!  id:%d \n", id);
            printf("client callback thread id is:%d \n", std::this_thread::get_id());
            reSetRwID(id);
        });
        printf(" id:%d \n", rw->getClientID());
        printf("start accept  thread id : %d  \n", std::this_thread::get_id());
        //这里捕获列表,必须捕获rw,否则智能指针就会自动析构
        m_acceptor.async_accept(rw->getSock(), [this, rw](const boost::system::error_code& error) {
            if (error)
            {
                printf("------------------------acceptor error--------------------------------------");
                printf("error! error value is: %d  %s \n", error.value(), error.message());
                return;
            }
            
            printf("a client coming in! \n");
            printf("client adress is: %s  id: %d \n", rw->getSock().remote_endpoint().address(), rw->getClientID());
            //m_rwList.push_back(rw); //果然,这里在注释掉push_back操作后,智能指针随后就析构了
            {
                std::unique_lock<std::mutex> lock(this->m_lock);
                m_rwMap.insert(std::make_pair(rw->getClientID(), rw));
                printf("map size : %d \n", m_rwMap.size());
            }
            m_rwMap.insert(std::make_pair(rw->getClientID(), rw));
            printf("map size : %d \n", m_rwMap.size());
            printf("client have acceptor thread id is: %d \n", std::this_thread::get_id());
            rw->read();
            accept();
        });
    }

    void reSetRwID(int id)
    {
        std::unique_lock<std::mutex> lock(this->m_lock);
        auto it = m_rwMap.find(id);
        if (it != m_rwMap.end())
            m_rwMap.erase(it);
        m_rwIdList.push_back(id);
        printf("--------map size is: %d \n--------", m_rwMap.size());
    }
    size_t getClientNum() {
        return  m_rwMap.size();
    }

private:
    //std::vector<std::shared_ptr<rwhandler>> m_rwList;
    //无序表,用于管理连接来的客户端的读写
    boost::unordered_map<int, std::shared_ptr<rwhandler>> m_rwMap;
    std::list<int> m_rwIdList; //管理连接进来的客户端的ID
    boost::asio::io_service& m_ios; //这个io_service只用于acceptor
    tcp::acceptor m_acceptor;
    serviceThreadPool m_servicePool;
    std::mutex m_lock;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
使用方法:

int main()
{    
    boost::asio::io_service ios;
    server ser(ios, SERVER_PORT);
    ser.accept();
    ios.run();

    system("pause");
    return 0;
}
1
2
3
4
5
6
7
8
9
10
其中大家一起要注意,所有对m_rwMap进行操作的地方一定要加锁,因为异常回调函数时运行在读写事件的线程中的,不同的io_service运行在不同的线程中,因此不同io_service下的读写事件也是运行在不同的线程中,所以m_rwMap可能会被不同的线程进行访问。博主刚开始就忽略了这一点,因此测试的时候总是异常,调试了一下午才发现问题,太难受了!

博主在写这个服务器时,是想用它代替FTP服务器进行文件传输,因此,这里实现了自定义协议头,这里的协议头只为了测试,因此比较粗糙。这里再解析时,在读取到协议头大小的数据后,只对数据进行了打印,正常我们项目中,这里需要先解析协议头,再根据协议头中的包体大小对包体进行读取,读取到的数据可以利用回调函数回调到应用层(数据回调的方法这里没有实现,但是实现了异常回调,方法都一样,大家可以参考)。
这个代码基本的框架都写好了,但只是框架,其中细节部分还需要大家根据自己的需求进行修改添加即可。其中接收到协议头后,可以根据协议头读取包体,然后用异步写的方法回复数据包。


————————————————
版权声明:本文为CSDN博主「E404」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/kongwei1234/article/details/104778613

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值