Server.h
TcpServer.h
TcpSession.h
TcpClient.h
UdpServer.h
Server
has a poller
EventPoller::Ptr _poller;
构造时默认绑定一个poller
Server::Server(EventPoller::Ptr poller) {
_poller = poller ? std::move(poller) : EventPollerPool::Instance().getPoller();
}
TcpServer
结构
bool _is_on_manager = false;
const TcpServer *_parent = nullptr;
Socket::Ptr _socket;
std::shared_ptr<Timer> _timer;
Socket::onCreateSocket _on_create_socket; //Socket的默认生成方式
std::unordered_map<SessionHelper *, SessionHelper::Ptr> _session_map;
std::function<SessionHelper::Ptr(const TcpServer::Ptr &server, const Socket::Ptr &)> _session_alloc;
std::unordered_map<const EventPoller *, Ptr> _cloned_server;
//对象个数统计
ObjectStatistic<TcpServer> _statistic;
构造
创建Socket对象,绑定accept前,accept的动作:该tcp客户端派发给对应线程的TcpServer服务器
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::createSocket的方式创建socket
_on_create_socket = [](const EventPoller::Ptr &poller) {
return Socket::createSocket(poller, false);
};
start/listen
根据传入的SessionType不同,有不同的session分配函数
template<typename SessionType>
void start(uint16_t port, const std::string &host = "::", uint32_t backlog = 1024) {
//TcpSession创建器,通过它创建不同类型的服务器
_session_alloc = [](const TcpServer::Ptr &server, const Socket::Ptr &sock) {
auto session = std::make_shared<SessionType>(sock);
session->setOnCreateSocket(server->_on_create_socket);
return std::make_shared<SessionHelper>(server, session);
};
start_l(port, host, backlog);
}
start_l
给Socket对象绑定listen_fd,对应的poller添加可读事件监听,对应的回调Socket::onAccept
添加一个定时器,定时调用session()->onManager();
给所有的poller添加对于listen_fd的监听,给**_cloned_server**添加东西,调用cloneFrom
cloneFrom/cloneFromListenSocket
从另外一个Socket克隆 目的是一个socket可以被多个poller对象监听,提高性能。不同的poller监听相同的listen_fd。实现所谓的抢占式accept
onAccept
TcpServer::onAcceptConnection
创建一个对应的SessionHelper(server, session),传递服务器配置给TcpSession,加入_session_map
注册会话接受数据事件setOnRead,调用session()->onRecv
注册会话接收到错误事件setOnErr,移除SessionHelper From _session_map
void TcpServer::onAcceptConnection(const Socket::Ptr &sock) {
assert(_poller->isCurrentThread());
weak_ptr<TcpServer> weak_self = std::dynamic_pointer_cast<TcpServer>(shared_from_this());
//创建一个TcpSession;这里实现创建不同的服务会话实例
auto helper = _session_alloc(std::dynamic_pointer_cast<TcpServer>(shared_from_this()), sock);
auto session = helper->session();
//把本服务器的配置传递给TcpSession
session->attachServer(*this);
//_session_map::emplace肯定能成功
auto success = _session_map.emplace(helper.get(), helper).second;
assert(success == true);
weak_ptr<Session> weak_session = session;
//会话接收数据事件
sock->setOnRead([weak_session](const Buffer::Ptr &buf, struct sockaddr *, int) {
//获取会话强应用
auto strong_session = weak_session.lock();
if (!strong_session) {
return;
}
try {
strong_session->onRecv(buf);
.......
});
SessionHelper *ptr = helper.get();
//会话接收到错误事件
sock->setOnErr([weak_self, weak_session, ptr](const SockException &err) {
//在本函数作用域结束时移除会话对象
//目的是确保移除会话前执行其onError函数
//同时避免其onError函数抛异常时没有移除会话对象
onceToken token(nullptr, [&]() {
//移除掉会话
auto strong_self = weak_self.lock();
if (!strong_self) {
return;
}
assert(strong_self->_poller->isCurrentThread());
if (!strong_self->_is_on_manager) {
//该事件不是onManager时触发的,直接操作map
strong_self->_session_map.erase(ptr);
} else {
//遍历map时不能直接删除元素
strong_self->_poller->async([weak_self, ptr]() {
auto strong_self = weak_self.lock();
if (strong_self) {
strong_self->_session_map.erase(ptr);
}
}, false);
}
});
//获取会话强应用
auto strong_session = weak_session.lock();
if (strong_session) {
//触发onError事件回调
strong_session->onError(err);
}
});
}
getServer
根据传入的poller判断派发到子服务器还是父服务器,
Ptr getServer(const EventPoller *) const;
TcpServer::Ptr TcpServer::getServer(const EventPoller *poller) const {
auto &ref = _parent ? _parent->_cloned_server : _cloned_server;
auto it = ref.find(poller);
if (it != ref.end()) {
//派发到cloned server
return it->second;
}
//派发到parent server
return static_pointer_cast<TcpServer>(_parent ? const_cast<TcpServer *>(_parent)->shared_from_this() :
const_cast<TcpServer *>(this)->shared_from_this());
}
TcpSessionWithSSL
通过该模板可以让TCP服务器快速支持TLS
UdpServer
SO_REUSEPORT:支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,允许多个套接字 bind()/listen() 同一个TCP/UDP端口。内核层面使用负载均衡。
SO_REUSEPORT多个socket可以绑定相同的ip和端口
SO_REUSEADDR允许将TIME_WAIT状态的IP/PORT重用;允许在绑定地址全局IP地址(0.0.0.0)的基础上再绑定非全零的IP地址
start/start_l
和tcp不同的是,udp主server才创建session map,其他cloned server共享之
bindUdpSock: 绑定端口+监听可读可写事件,其他从服务器,bind相同的端口,但是是不同的fd(SO_RESUEPORT),监听相同的事件
在SO_REUSEPORT没有出现之前,多线程编程一般有两种获取到来的请求。
指派一条线程专门进行accept,获取socket后分派给worker线程。这种方法使得进行accept的线程成为了单点,容易成为性能的瓶颈。
多个线程同时进行accept。这种方法的问题是每一个线程accept成功的概率不均匀,导致负载不均衡。
SO_REUSEPORT的负载均衡算法
使用(remote_ip, remote_port, local_ip, local_port)来进行哈希,因此可以保证同一个client的包可以路由到同一个进程。但是,当一个listen的进程加进来或者terminate的时候,由于没有实现一致性哈希,结果可能导致有些请求由于路由到另外一个进程上,client-server的三次握手过程可能会被重置。核心的实现主要有三
- 扩展 socket option,增加 SO_REUSEPORT 选项,用来设置 reuseport
- 修改 bind 系统调用实现,以便支持可以绑定到相同的 IP 和端口
- 修改处理新建连接的实现,查找 listener 的时候,能够支持在监听相同 IP 和端口的多个 sock 之间均衡选择。
有了SO_RESUEPORT后,每个进程可以自己创建socket、bind、listen、accept相同的地址和端口,各自是独立平等的
让多进程监听同一个端口,各个进程中accept socket fd不一样,有新连接建立时,内核只会唤醒一个进程来accept,并且保证唤醒的均衡性。安全性考虑
第一个进程必须enable了这个选项之后,后续的进程才可以通过enable这个选项将socket绑定到同一个端口上。
绑定到同一个端口的进程的effective user id必须一致。
上述规定是为了避免hijacking:恶意用户通过监听相同的端口来获取用户信息。
onRead_l
!getOrCreateSession
最大的不同是,普通的Udp服务器没有listen_fd,无连接的,也就没有会话的概念。
这里在收到udpclient后,创建session,分配新的socket,绑定自己端口和对端ip(服务器也可以调用connect),也放入poller中监听,使得udp也有并行处理的能力
socket->bindUdpSock(_socket->get_local_port(), _socket->get_local_ip());
socket->bindPeerAddr((struct sockaddr *) addr_str.data(), addr_str.size());
//在connect peer后再取消绑定关系, 避免在 server 的 socket 或其他cloned server中收到后续数据包.
SockUtil::dissolveUdpSock(_socket->rawFD());
如果系统已经有了10000个 reuseport bind到同一个端口,connect到不同客户端的socket ,当这些客户端发来数据的时候,内核是如何根据四元组找到对应的socket的呢?
从最老的代码直到现在,内核都只是根据bind的源IP/源端口对组织hash表,bind到同一个地址和端口的socket会链接在同一个链表中,当报文到来的时候,内核协议栈会根据目标地址和目标端口做hash,在对应的hash链表中遍历查找:
在链表中遍历,冒泡排序,找到得分最高的。
如果一个socket connect了remote,那么将会匹配四元组,匹配成功得高分,如果没有connect的socket,将只会匹配二元组,匹配成功得低分,如果连二元组都不匹配,不得分,一遍冒泡之后,就会得到一个最佳匹配结果。
dissolveUdpSock为什么可以取消udpsock?猜测新的udp session创立后,为了降低listen_fd对应的socket的匹配得分
onManager
udp主服务器,定时的去调用onManager做超时管理,删除_session_map中的元素
// 新建一个定时器定时管理这些 udp 会话,这些对象只由主server做超时管理,cloned server不管理
std::weak_ptr<UdpServer> weak_self = std::dynamic_pointer_cast<UdpServer>(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);
SessionHelper
一个将服务器和会话绑定的类
class SessionHelper {
public:
using Ptr = std::shared_ptr<SessionHelper>;
SessionHelper(const std::weak_ptr<Server> &server, Session::Ptr session);
~SessionHelper();
const Session::Ptr &session() const;
private:
std::string _identifier;
Session::Ptr _session;
SessionMap::Ptr _session_map;
std::weak_ptr<Server> _server;
};
TcpClient
client也是基于poller对socket监听的,需要实现对应的接口
virtual void onConnect(const SockException &ex) = 0;
virtual void onRecv(const Buffer::Ptr &buf) = 0;
virtual void onErr(const SockException &ex) = 0;
总结
-
主从复制怎么实现的
-
udp怎么实现高并发,实现负载均衡,SO_REUSEPORT选项的存在,udp照样可以维护多个socket的session,新起一个socket绑定服务器和对端,多线程的继续监听读写事件,socket数量先上去。这样udp最初的fd,也就成了listen_fd,accept就是维护connect后的fd,还是listen/accept模型
-
SO_REUSEADDR && SO_REUSEPORT
-
udp的超时管理怎么实现的,定时器管理session_map