day17-TcpServer
TcpServer是用户使用 muduo 的唯一入口,负责启动服务器、接收新连接、分配 IO 线程、管理所有客户端连接。所有底层细节全部被 TcpServer 封装隐藏,用户只操作 TcpServer
Callbacks.h
1 |
|
- 给所有网络事件起别名、定义类型
Buffer类和TcpConnection类还未实现。
TcpServer.h
1 |
|
1 | class TcpServer : noncopyable |
noncopyacble服务器只有一个,不能复制ThreadInitCallback线程初始化回调Option端口复用选项,用enum class强枚举类型:类型安全、不会隐式转换
私有成员变量
1 | EventLoop* loop_; //baseLoop 用户设置的loop |
loop_:负责接收监听的主线程循环(mainLoop/baseLoop)name_:服务器名字ipPort_:监听地址(ip + 端口)
1 | ThreadInitCallback threadInitCallback_; // loop线程初始化的回调 |
四个回调函数是网络库与用户业务代码的桥梁—— 网络库只管底层 IO,业务逻辑(你要干嘛)全靠这些回调来实现
- 回调调用时机
1 | 服务器启动 |
1 | std::unique_ptr<Acceptor> acceptor_; |
unique_ptr<Acceptor>:独占所有权,一个 TcpServer 只能有一个接收器,绝不共享。shared_ptr<EventLoopThreadPool>:共享所有权,线程池需要在多处、多线程被安全持有,必须共享。
1 | std::atomic_int started_; |
started_:保证服务器只启动一次,线程安全,用原子变量是为了**防止用户在多线程里乱调用 start ()**(atomic_int是atomic<int>的别名,作用完全一样)nextConnId_:给每一个新连接分配唯一 ID,从 1 开始递增,与name_+nextConnId_拼接成连接唯一名称connecions_:服务器管理所有客户端连接的哈希表
内部函数
1 | private: |
newConnection:处理新客户端连接 → 创建 TcpConnection → 加入管理,Acceptor 监听到新客户端,调用这个函数removeConnection(关闭连接的安全入口)和removeConnectionInLoop(真正删除连接)配合关闭连接
成员函数
1 | TcpServer(EventLoop* loop, |
构造函数:创建一个 TCP 服务器对象,但还不启动监听
1 | void setThreadInitcallback(const ThreadInitCallback &cb) { threadInitCallback_ = cb; } |
设置四类回调的接口
setThreadInitCallback:每个 IO 线程刚启动时调用,初始化线程(绑定 CPU、初始化日志、配置参数),很少用setConnectionCallback:客户端连接和断开时调用- 打印日志
- 统计在线人数
- 初始化连接资源
setMessageCallback:收到客户端数据(socket 有数据可读)时调用,写业务逻辑时常用- 解析协议
- 执行业务逻辑
- 发送回复
setWriteCompleteCallback:数据发送完毕调用,发送缓冲区清空- 大文件分片发送
- 流量控制
- 发送完成日志
1 | //设置底层subloop的个数 |
TcpServer.cc
1 |
1 | static EventLoop* checkLoopNotNull(EventLoop* loop) |
防止用户传入空指针(nullptr)导致程序崩溃
static:只在当前源文件内可见
1 | TcpServer::TcpServer(EventLoop* loop, |
把
TcpServer::newConnection函数绑定给Acceptor,监听到新连接后,调用TcpServer 的 newConnection 函数Acceptor 只负责 accept 连接,拿到 sockfd,Acceptor 不知道怎么处理连接,所以把处理逻辑通过回调交给 TcpServer。
因为 Acceptor 将来调用回调时,会传进来 2 个参数(sockfd 和 peerAddr),现在我们不知道这两个值是什么,所以先用
_1、_2占个位置
1 | TcpServer::~TcpServer() |
析构逻辑:
遍历所有还活着的客户端连接,取出value:
TcpConnectionPtr(也就是shared_ptr<TcpConnection>)把连接从 TcpServer 的管理 map 中移除
把每个连接丢回它自己的 IO 线程安全销毁(
TcpConnection相关函数还没实现)
TcpConnectionPtr conn(item.second);把 map 里的 shared_ptr 引用计数 +1,让连接对象暂时不会被释放item.second.reset();这会让 map 里的shared_ptr断开引用。如果不先复制给局部conn,连接可能在这里直接被释放掉,后面就没法用了销毁链接:
conn->getLoop()每个 TcpConnection 都有 自己的 IO 线程(EventLoop)连接的读写、销毁必须在自己的 IO线程执行
runInLoop()跨线程安全调用
作用:把要执行的函数,丢到连接所属的 IO 线程里去执行,保证线程安全、不发生并发冲突。std::bind(&TcpConnection::connectDestroyed, conn)
绑定要执行的销毁函数
把conn(shared_ptr)绑定进去
→ 引用计数再次 +1
→ 确保函数执行完之前,连接对象不会死
1 | //设置底层subloop的个数 |
线程池才是管理线程的人,通过线程池设置线程数。
1 | //开启服务器监听 |
threadPool_->start(threadInitCallback_);:启动工作线程池,创建并启动所有 subLoop 线程(IO 工作线程)loop_->runInLoop():让主线程安全执行监听,让 Acceptor 在baseLoop线程中调用listen()开始监听端口get():获取被智能指针包装的对象的裸指针
为什么要loop_->runInLoop ()?不能直接调用 acceptor_->listen ()
所有 IO 操作(listen、read、write)必须在创建它的 EventLoop 线程中执行
acceptor_是在loop_(baseLoop 主线程) 中创建的,所以acceptor_->listen()必须在主线程中调用runInLoop()会保证把回调函数丢到loop_ 所属的主线程去执行
==接下来还剩三个内部函数没有实现,因为这三个函数会涉及TcpConnection相关内容,所以等实现完TcpConnection后再实现这三个函数。==
1 | void TcpServer::removeConnection(const TcpConnectionPtr &conn) |
TcpServer::removeConnection:接口函数,把连接销毁函数扔到对应正确线程执行TcpServer::removeConnectionInLoop:- 从 TcpServer 管理的连接列表中删掉这个连接,TcpServer 不再持有这个 TcpConnection 的 shared_ptr
- 找到并取出这个连接所属的 IO 线程
- 把真正的销毁操作,丢到连接所属的 IO 线程去执行
1 | void TcpServer::newConnection(int sockfd, const InetAddress &peerAddr) |
接收客户端新连接 → 分配 IO 线程 → 创建 TcpConnection → 设置所有回调 → 启动连接
用轮询算法从线程池获取下一个 IO 线程
生成唯一连接名
获取本地地址
创建 TcpConnection 并加入管理
设置用户回调
设置关闭回调(连接断开时自动调用)
调用
TcpConnection::connectEstablished,后面这个函数改了一次,所以多了个参数
源码地址
Callbacks.h:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/Callbacks.h
TcpServer.h:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/TcpServer.h
TcpServer.cc:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/TcpServer.cc