zlMediaKit 8 server模块--TCP&&UDP同样可以高并发

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通讯中的connect()和bind()函数

基于UDP服务的负载均衡方法

UDP实现高并发其实非常简单

UDP实现高并发其实非常简单(续集)

最大的不同是,普通的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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值