仿写 muduo 网络库
网络库实现的核心目标
- 线程安全,支持多核多线程,实现高并发的连接请求和低延时的客户端请求。
- 不考虑可移植性,不跨平台,只支持 Linux,不支持 Windows。
- 主要支持 x86-64,兼顾 IA32。
- 不支持 UDP,只支持 TCP。
- 不支持 IPv6,只支持 IPv4。
- 不考虑广域网应用,只考虑局域网。
- 不考虑公网,只考虑内网。不为安全性做特别的增强。
- 只支持一种使用模式:非阻塞 IO+one event loop per thread,不支持阻塞 IO。
- API 简单易用,只暴露具体类和标准库里的类。API 不使用 non-trivial templates,也不使用虚函数。
- 只满足常用需求的 90%,不面面俱到,必要的时候以 app 来适应 lib。
- 只做 libray (库),不做成 framework (构架)
muduo 网络库成功的原因将解决的问题控制在一个范围内,不追求大而全。
1. 第一版本的需求
在客户端获取日志信息,将日志信息发送到服务端,服务端接受日志信息,再接着将日志信息写入文件。代码最终的要求是:
服务器端程序:
- 指定服务器 IP 地址和 Port 端口号;
- 定义服务器对象,执行
bind
绑定和listen
监听操作; - 接受客户端连接(
accept
); - 与客户端进行数据读写;
- 关闭客户端的连接。
客户端程序:
- 指定连接的服务器 IP 地址和 port 端口号;
- 定义客户端对象;
- 发起与服务器的连接;
- 与服务器进行数据读写;
- 关闭连接。
2. 需求分析
使用 C/S 模型实现日志信息在网络中的发送。
2.1 TCP 客户端和 TCP 服务器的工作流程
客户端工作流程
设置地址信息 ==> 创建 socket ==> 发起连接(connect) ==> 读写数据(read/write) ==> 关闭连接(close)
服务器工作流程
设置地址信息 ==> 创建 socket ==> 绑定(bind)和监听(listen) ==> 监听连接(accept) ==> 读写数据(read/write) ==> 关闭链接(close)
套接字作用说明
- 客户端:创建套接字用于连接服务端;
- 服务器:首先创建套接字用于绑定、监听和获取客户端连接,
accept
成功后将获取一个新的连接套接字,专门用于 I/O 事件处理。(一个监听套接字对应多个连接套接字,连接套接字负责具体 I/O 事件)
2.3 识别问题空间中的对象
- InetAddress 对象;
- Socket 对象;
- Acceptor 接受连接对象;
- TcpConnection 全相关连接对象;
- TcpServer 对象;
- Connect 主动发起连接器对象;
- TcpClient 对象;
2.4 确定对象功能
1. InetAddress 对象
- 功能:作为
sockaddr_in
的包装器(wrapper),为其它对象提供sockaddr_in
地址信息。sockaddr_in
是 socket 的专用地址结构体,保存的信息包括地址族、端口号、地址结构体。 - 方法:构造地址,返回地址族信息,设置地址,获取主机 IP 等。
2. Socket 对象
- 功能:为服务器端对象提供监听套接字的封装,完成
bind
、listen
和接受客户端连接请求(accept
)。Socket 对象是 socket 文件描述符(sockfd)的轻量级封装,提供操作底层 sockfd 的常用方法。采用 RAII 方式管理 sockfd,Socket 对象本身不创建或打开 sockfd,仅负责关闭,析构时调用close
关闭套接字。 - 方法:绑定 IP 地址(
bind
);监听套接字(listen
);接收连接请求(accept
);关闭连接写方向(shutdown
);获取 TCP 协议栈信息(tcp_info
)等。
3. Acceptor 接受连接器对象
- 功能:包含 Socket 对象,用于服务器监听新的客户端连接请求(即 server socket)。当有新的连接请求完成时,构建
TcpConnection
连接管理对象。
4. TcpConnection 连接管理对象
- 功能:在执行
accept()
函数之后创建,由 Socket 对象持有连接套接字(connfd),包含本地地址(localAddr_
)、对端地址(peerAddr_
)、读写缓冲区等。作为整个网络库的核心,封装一次 TCP 连接(注意不能发起连接),主要实现连接套接字上的读 / 写、关闭连接等操作。 - 状态:包含四种状态 —— 已连接、未连接、正在连接、正在断开。
- 特殊说明:
TcpConnection
类是网络库最核心的类,唯一默认用shared_ptr
管理的类,唯一继承自enable_shared_from_this
的类。这是因为其生命周期模糊:可能在连接断开时,还有其他地方持有它的引用,贸然delete
会造成空悬指针。只有确保其他地方没有持有该对象的引用时,才能安全销毁对象。
5. TcpServer Tcp 服务器对象
2.3 识别问题空间中的对象
2.4 确定对象功能
1. InetAddress 对象
2. Socket 对象
3. Acceptor 接受连接器对象
4. TcpConnection 连接管理对象
5. TcpServer Tcp 服务器对象
- 功能:创建监听套接字对象,用于接收新的客户端连接,并将新连接存储到 map 容器中,管理数据的接受和发送。
- 组成:由
Acceptor
连接对象和TcpConnection
连接管理对象组成。Acceptor
对象负责接受新的 TCP 连接,TcpConnection
对象负责对连接进行管理。 - 关联方式:定义回调函数与连接的使用者取得关联。
- 提供接口:(原文未展开,保持原样)
- 对象图:
客户端工作流程
设置地址信息 ==> 创建 socket ==> 发起连接(connect) ==> 读写数据(read/write) ==> 关闭连接(close)
服务器工作流程
设置地址信息 ==> 创建 socket ==> 绑定(bind)和监听(listen) ==> 监听连接(accept) ==> 读写数据(read/write) ==> 关闭链接(close)
套接字作用说明
- 客户端:创建套接字用于连接服务端;
- 服务器:首先创建套接字用于绑定、监听和获取客户端连接,
accept
成功后将获取一个新的连接套接字,专门用于 I/O 事件处理。(一个监听套接字对应多个连接套接字,连接套接字负责具体 I/O 事件) - InetAddress 对象;
- Socket 对象;
- Acceptor 接受连接对象;
- TcpConnection 全相关连接对象;
- TcpServer 对象;
- Connect 主动发起连接器对象;
- TcpClient 对象;
- 功能:作为
sockaddr_in
的包装器(wrapper),为其它对象提供sockaddr_in
地址信息。sockaddr_in
是 socket 的专用地址结构体,保存的信息包括地址族、端口号、地址结构体。 - 方法:构造地址,返回地址族信息,设置地址,获取主机 IP 等。
- 功能:为服务器端对象提供监听套接字的封装,完成
bind
、listen
和接受客户端连接请求(accept
)。Socket 对象是 socket 文件描述符(sockfd)的轻量级封装,提供操作底层 sockfd 的常用方法。采用 RAII 方式管理 sockfd,Socket 对象本身不创建或打开 sockfd,仅负责关闭,析构时调用close
关闭套接字。 - 方法:绑定 IP 地址(
bind
);监听套接字(listen
);接收连接请求(accept
);关闭连接写方向(shutdown
);获取 TCP 协议栈信息(tcp_info
)等。 - 功能:包含 Socket 对象,用于服务器监听新的客户端连接请求(即 server socket)。当有新的连接请求完成时,构建
TcpConnection
连接管理对象。 - 功能:在执行
accept()
函数之后创建,由 Socket 对象持有连接套接字(connfd),包含本地地址(localAddr_
)、对端地址(peerAddr_
)、读写缓冲区等。作为整个网络库的核心,封装一次 TCP 连接(注意不能发起连接),主要实现连接套接字上的读 / 写、关闭连接等操作。 - 状态:包含四种状态 —— 已连接、未连接、正在连接、正在断开。
- 特殊说明:
TcpConnection
类是网络库最核心的类,唯一默认用shared_ptr
管理的类,唯一继承自enable_shared_from_this
的类。这是因为其生命周期模糊:可能在连接断开时,还有其他地方持有它的引用,贸然delete
会造成空悬指针。只有确保其他地方没有持有该对象的引用时,才能安全销毁对象。 - 功能:创建监听套接字对象,用于接收新的客户端连接,并将新连接存储到 map 容器中,管理数据的接受和发送。
- 组成:由
Acceptor
连接对象和TcpConnection
连接管理对象组成。Acceptor
对象负责接受新的 TCP 连接,TcpConnection
对象负责对连接进行管理。 - 关联方式:定义回调函数与连接的使用者取得关联。
- 提供接口:(原文未展开,保持原样)
- 对象图:
客户端工作流程
设置地址信息 ==> 创建 socket ==> 发起连接(connect) ==> 读写数据(read/write) ==> 关闭连接(close)
服务器工作流程
设置地址信息 ==> 创建 socket ==> 绑定(bind)和监听(listen) ==> 监听连接(accept) ==> 读写数据(read/write) ==> 关闭链接(close)
套接字作用说明
- 客户端:创建套接字用于连接服务端;
- 服务器:首先创建套接字用于绑定、监听和获取客户端连接,
accept
成功后将获取一个新的连接套接字,专门用于 I/O 事件处理。(一个监听套接字对应多个连接套接字,连接套接字负责具体 I/O 事件)
2.3 识别问题空间中的对象
- InetAddress 对象;
- Socket 对象;
- Acceptor 接受连接对象;
- TcpConnection 全相关连接对象;
- TcpServer 对象;
- Connect 主动发起连接器对象;
- TcpClient 对象;
2.4 确定对象功能
1. InetAddress 对象
- 功能:作为
sockaddr_in
的包装器(wrapper),为其它对象提供sockaddr_in
地址信息。sockaddr_in
是 socket 的专用地址结构体,保存的信息包括地址族、端口号、地址结构体。 - 方法:构造地址,返回地址族信息,设置地址,获取主机 IP 等。
2. Socket 对象
- 功能:为服务器端对象提供监听套接字的封装,完成
bind
、listen
和接受客户端连接请求(accept
)。Socket 对象是 socket 文件描述符(sockfd)的轻量级封装,提供操作底层 sockfd 的常用方法。采用 RAII 方式管理 sockfd,Socket 对象本身不创建或打开 sockfd,仅负责关闭,析构时调用close
关闭套接字。 - 方法:绑定 IP 地址(
bind
);监听套接字(listen
);接收连接请求(accept
);关闭连接写方向(shutdown
);获取 TCP 协议栈信息(tcp_info
)等。
3. Acceptor 接受连接器对象
- 功能:包含 Socket 对象,用于服务器监听新的客户端连接请求(即 server socket)。当有新的连接请求完成时,构建
TcpConnection
连接管理对象。
4. TcpConnection 连接管理对象
- 功能:在执行
accept()
函数之后创建,由 Socket 对象持有连接套接字(connfd),包含本地地址(localAddr_
)、对端地址(peerAddr_
)、读写缓冲区等。作为整个网络库的核心,封装一次 TCP 连接(注意不能发起连接),主要实现连接套接字上的读 / 写、关闭连接等操作。 - 状态:包含四种状态 —— 已连接、未连接、正在连接、正在断开。
- 特殊说明:
TcpConnection
类是网络库最核心的类,唯一默认用shared_ptr
管理的类,唯一继承自enable_shared_from_this
的类。这是因为其生命周期模糊:可能在连接断开时,还有其他地方持有它的引用,贸然delete
会造成空悬指针。只有确保其他地方没有持有该对象的引用时,才能安全销毁对象。
5. TcpServer Tcp 服务器对象
2.3 识别问题空间中的对象
2.4 确定对象功能
1. InetAddress 对象
2. Socket 对象
3. Acceptor 接受连接器对象
4. TcpConnection 连接管理对象
5. TcpServer Tcp 服务器对象
- 功能:创建监听套接字对象,用于接收新的客户端连接,并将新连接存储到 map 容器中,管理数据的接受和发送。
- 组成:由
Acceptor
连接对象和TcpConnection
连接管理对象组成。Acceptor
对象负责接受新的 TCP 连接,TcpConnection
对象负责对连接进行管理。 - 关联方式:定义回调函数与连接的使用者取得关联。
- 提供接口:(原文未展开,保持原样)
- 对象图:
客户端工作流程
设置地址信息 ==> 创建 socket ==> 发起连接(connect) ==> 读写数据(read/write) ==> 关闭连接(close)
服务器工作流程
设置地址信息 ==> 创建 socket ==> 绑定(bind)和监听(listen) ==> 监听连接(accept) ==> 读写数据(read/write) ==> 关闭链接(close)
套接字作用说明
- 客户端:创建套接字用于连接服务端;
- 服务器:首先创建套接字用于绑定、监听和获取客户端连接,
accept
成功后将获取一个新的连接套接字,专门用于 I/O 事件处理。(一个监听套接字对应多个连接套接字,连接套接字负责具体 I/O 事件) - InetAddress 对象;
- Socket 对象;
- Acceptor 接受连接对象;
- TcpConnection 全相关连接对象;
- TcpServer 对象;
- Connect 主动发起连接器对象;
- TcpClient 对象;
- 功能:作为
sockaddr_in
的包装器(wrapper),为其它对象提供sockaddr_in
地址信息。sockaddr_in
是 socket 的专用地址结构体,保存的信息包括地址族、端口号、地址结构体。 - 方法:构造地址,返回地址族信息,设置地址,获取主机 IP 等。
- 功能:为服务器端对象提供监听套接字的封装,完成
bind
、listen
和接受客户端连接请求(accept
)。Socket 对象是 socket 文件描述符(sockfd)的轻量级封装,提供操作底层 sockfd 的常用方法。采用 RAII 方式管理 sockfd,Socket 对象本身不创建或打开 sockfd,仅负责关闭,析构时调用close
关闭套接字。 - 方法:绑定 IP 地址(
bind
);监听套接字(listen
);接收连接请求(accept
);关闭连接写方向(shutdown
);获取 TCP 协议栈信息(tcp_info
)等。 - 功能:包含 Socket 对象,用于服务器监听新的客户端连接请求(即 server socket)。当有新的连接请求完成时,构建
TcpConnection
连接管理对象。 - 功能:在执行
accept()
函数之后创建,由 Socket 对象持有连接套接字(connfd),包含本地地址(localAddr_
)、对端地址(peerAddr_
)、读写缓冲区等。作为整个网络库的核心,封装一次 TCP 连接(注意不能发起连接),主要实现连接套接字上的读 / 写、关闭连接等操作。 - 状态:包含四种状态 —— 已连接、未连接、正在连接、正在断开。
- 特殊说明:
TcpConnection
类是网络库最核心的类,唯一默认用shared_ptr
管理的类,唯一继承自enable_shared_from_this
的类。这是因为其生命周期模糊:可能在连接断开时,还有其他地方持有它的引用,贸然delete
会造成空悬指针。只有确保其他地方没有持有该对象的引用时,才能安全销毁对象。 - 功能:创建监听套接字对象,用于接收新的客户端连接,并将新连接存储到 map 容器中,管理数据的接受和发送。
- 组成:由
Acceptor
连接对象和TcpConnection
连接管理对象组成。Acceptor
对象负责接受新的 TCP 连接,TcpConnection
对象负责对连接进行管理。 - 关联方式:定义回调函数与连接的使用者取得关联。
- 提供接口:(原文未展开,保持原样)
- 对象图:
- 时序图:
6. Connector 主动发起连接器对象
核心功能
Connector
负责主动发起连接,不负责创建sockfd
,仅负责连接的建立。外部通过调用Connector::start()
即可发起连接,具备重连功能和停止连接功能,连接成功建立后将结果返回给TcpClient
。
与 Acceptor 的区别
与Acceptor
相比,Connector
少了acceptSocket_
成员,因为Connector
是创建新的sockfd
并执行connect
操作,其创建流程如下:
Connector::start()
→ Connector::startInLoop()
→ void Connector::connect()
关联关系:一个
TcpClient
对应一个TcpConnection
和一个Connector
;而一个TcpServer
对应一个TcpConnection
列表和一个Acceptor
。
非阻塞连接的难点及解决方案
在非阻塞网络中,主动发起连接比被动接收连接更复杂,需考虑错误处理和重试机制,主要难点包括:
-
socket 一次性特性:
socket 是一次性资源,一旦出错无法恢复,仅能关闭后重新创建。使用新的文件描述符(fd)后,需搭配新的channel
。 -
连接状态二次确认:
非阻塞模式下,socket
可写并不意味着连接已成功建立,需通过getsockopt(sockfd, SOL_SOCKET, SO_ERROR, …)
再次确认连接状态。
其他确认方法:- 调用
getpeername
,若失败返回ENOTCONN
,表示连接失败; - 调用
read
且长度参数为 0,若失败表示connect
失败; - 再次调用
connect
,若返回错误EISCONN
,表示套接字已成功建立连接。
- 调用
-
指数退避重试机制:
重试间隔时间应逐渐延长(直至back-off
),重试通过EventLoop::runAfter
实现。为防止Connector
在定时器到期前析构,需在Connector
的析构函数中注销定时器。 -
防止自连接:
需通过逻辑避免自连接情况的发生。
连接建立后的处理
在handleWrite()
中需要调用removeAndResetChannel()
,因为此时连接已建立,无需再关注channel
的可写事件,最终执行channel_.reset()
析构channel
。此外,该函数需返回sockfd
,交由TcpConnection
接管。
7. TcpClient
核心功能
TcpClient
负责发起连接,内部包含一个Connector
连接器,通过Connector
发起连接。连接建立成功后,使用socket
创建TcpConnection
来管理连接。每个TcpClient
类仅管理一个TcpConnection
,连接建立成功后会设置相应的回调函数。
组件关系
TcpClient
用于管理客户端连接,实际连接操作由Connector
执行。Connector
类不单独使用,而是封装在TcpClient
中,一个Connector
对应一个TcpClient
。Connector
负责建立连接,建立成功后将控制权交给TcpConnection
,因此TcpClient
中也封装了一个TcpConnection
对象。