系列文章目录
《ZLToolKit源码学习笔记》(1)VS2019源码编译
《ZLToolKit源码学习笔记》(2)工具模块之日志功能分析
《ZLToolKit源码学习笔记》(3)工具模块之终端命令解析
《ZLToolKit源码学习笔记》(4)工具模块之消息广播器
《ZLToolKit源码学习笔记》(6)线程模块之整体框架概述
《ZLToolKit源码学习笔记》(7)线程模块之线程池组件:任务队列与线程组
《ZLToolKit源码学习笔记》(8)线程模块之线程负载计算器
《ZLToolKit源码学习笔记》(9)线程模块之任务执行器
《ZLToolKit源码学习笔记》(11)线程模块之工作线程池WorkThreadPool
《ZLToolKit源码学习笔记》(12)事件轮询模块之整体框架概述
《ZLToolKit源码学习笔记》(13)事件轮询模块之管道的简单封装
《ZLToolKit源码学习笔记》(14)事件轮询模块之定时器
《ZLToolKit源码学习笔记》(15)事件轮询模块之事件轮询器EventPoller
《ZLToolKit源码学习笔记》(16)网络模块之整体框架概述
《ZLToolKit源码学习笔记》(17)网络模块之基础接口封装类SockUtil
《ZLToolKit源码学习笔记》(18)网络模块之Buffer缓存
《ZLToolKit源码学习笔记》(19)网络模块之套接字封装
《ZLToolKit源码学习笔记》(20)网络模块之TcpServer(本文)
《ZLToolKit源码学习笔记》(21)网络模块之TcpClient与Session
《ZLToolKit源码学习笔记》(22)网络模块之UdpServer
前言
本节学习ZLToolKit的TCP服务器封装。
目录
一、概述
1.1、类图
Server类作为基类,将服务器和EventPoller进行了关联。
1.2、网络模型
服务器模型:多线程+epoll (select),一个listen fd + 多个epoll实例
每一个线程都创建了一个epoll实例,并以ET边沿触发模式监听同一个listen fd的读事件,使用EPOLLEXCLUSIVE
标志位防止惊群效应,线程阻塞在epoll_wait上等待客户端连接,默认情况下,当前线程accept的client fd也会添加到当前线程epoll中管理,但TcpServer改变了这一情况,它将client fd添加到了负载较轻的epoll线程中管理。client fd同样使用ET边沿触发模式。
1.3、服务器处理流程
创建TcpServer对象时,会根据CPU核心数创建多个EventPoller线程对象,比如,本人PC是8核心,那么将会创建8个事件轮询器对象,TcpServer对象会与其中一个EventPoller进行关联。
在调用start接口时,会克隆7个TcpServer子对象,分别与其余7个EventPoller对象关联。
这8个TcpServer的listen fd完全一致,并且分别在8个EventPoller线程中进行读事件(accept)监听,即8个线程监听了同一个fd的读事件。
当收到客户端请求时,由操作系统选择其中一个线程来处理,实现了客户端会话处理的负载均衡。(需考虑不同平台下系统是否都能做到负载均衡,以及是否有惊群问题)
每一个client请求,都被封装成一个TcpSession,交由对应的TcpServer对象管理。并在关联的EventPoller线程中,监听了client fd的读、写、异常事件。
服务器与客户端大致的交互流程如下:
二、功能分析
2.1、setOnCreateSocket
void TcpServer::setOnCreateSocket(Socket::onCreateSocket cb) {
if (cb) {
_on_create_socket = std::move(cb);
} else {
_on_create_socket = [](const EventPoller::Ptr &poller) {
return Socket::createSocket(poller, false);
};
}
for (auto &pr : _cloned_server) {
pr.second->setOnCreateSocket(cb);
}
}
自定义socket对象的构造方式,因为该接口只能在TcpServer对象实例化之后调用,而监听socket在构造函数中就已经创建,所以通过该接口只能控制之后的client socket的创建方式。
2.2、getPort
获取服务器监听端口号, 如果没有指定端口号,将选择监听随机端口,通过该接口可以获取。
2.3、构造函数
TcpServer::TcpServer(const EventPoller::Ptr &poller) : Server(poller) {
setOnCreateSocket(nullptr);
_socket = createSocket(_poller);
_socket->setOnBeforeAccept([this](const EventPoller::Ptr &poller) {
return onBeforeAcceptConnection(poller);
});
_socket->setOnAccept([this](Socket::Ptr &sock, shared_ptr<void> &complete) {
auto ptr = sock->getPoller().get();
auto server = getServer(ptr);
ptr->async([server, sock, complete]() {
//该tcp客户端派发给对应线程的TcpServer服务器
server->onAcceptConnection(sock);
});
});
}
创建监听socket,并设置accept时,socket构造事件回调(即client socket构造)以及tcp监听接收到连接回调。
2.4、start
主要看下以下流程,该接口内部会创建多个子TcpServer对象,这些子TcpServer对象通过Socket对象克隆的方式在多个poller线程中监听同一个listen fd,这样这个TCP服务器将会通过抢占式accept的方式把客户端均匀的分布到不同的poller线程,通过该方式能实现客户端负载均衡以及提高连接接收速度。
EventPollerPool::Instance().for_each([&](const TaskExecutor::Ptr &executor) {
EventPoller::Ptr poller = dynamic_pointer_cast<EventPoller>(executor);
if (poller == _poller || !poller) {
return;
}
auto &serverRef = _cloned_server[poller.get()];
if (!serverRef) {
serverRef = onCreatServer(poller);
}
if (serverRef) {
serverRef->cloneFrom(*this);
}
});
void TcpServer::cloneFrom(const TcpServer &that) {
if (!that._socket) {
throw std::invalid_argument("TcpServer::cloneFrom other with null socket!");
}
_on_create_socket = that._on_create_socket;
_session_alloc = that._session_alloc;
_socket->cloneFromListenSocket(*(that._socket));
weak_ptr<TcpServer> weak_self = std::dynamic_pointer_cast<TcpServer>(shared_from_this());
_timer = std::make_shared<Timer>(2.0f, [weak_self]() -> bool {
auto strong_self = weak_self.lock();
if (!strong_self) {
return false;
}
strong_self->onManagerSession();
return true;
}, _poller);
this->mINI::operator=(that);
_parent = &that;
}
在此处再次对listen fd进行了监听。
_socket->cloneFromListenSocket(*(that._socket));
三、使用
TCP服务器的测试程序见ZLToolKit\tests\test_tcpEchoServer.cpp。
四、知识点整理(后续补充)
4.1、如何处理客户端的connect
ET+循环accept
4.2、 如何接收客户端发送过来的数据
4.3、服务器向客户端发送数据的过程
使用缓存,监听了socket的写事件,默认直接写数据,当写满时,等待触发可写事件,然后继续发送缓存中剩余数据;