一文搞懂网络库的分层设计
关注「开源Linux」,选择“设为星标” 回复「学习」,有我为您特别筛选的学习资料~
“对于计算机科学领域中的任何问题,都可以通过增加一个间接的中间层来解决”这句话几乎概括了计算机软件体系结构的设计要点。
计算机软件体系结构从上到下都是按照严格的层次结构设计的,不仅整个体系如此,体系里面的每个组件如OS本身、很多应用程序、软件系统甚至很多硬件结构也如此。
常见的网络通信库根据功能也可以分成很多层。
根据离业务的远近从上到下依次是Session层、Connection层、Channel层、Socket层。
其中Session层属于业务层,Connection层、Channel层、Socket层属于技术层,示意图如下。
下面依次介绍各层的作用。
▊ Session层
Session 层处于顶层,在设计上不属于网络框架本身,用于记录各种业务状态数据和处理各种业务逻辑。在业务逻辑处理完毕后,如果需要进行网络通信,则依赖Connection层进行数据收发。
例如,一个IM服务的Session类可能有如下接口和成员数据:
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef const TcpConnectionPtr& CTcpConnectionPtrR;
class ChatSession
{
public:
ChatSession(CTcpConnectionPtrR conn, int sessionid);
virtual ~ChatSession();
int32_t GetSessionId()
{
return m_id;
}
int32_t GetUserId()
{
return m_userinfo.userid;
}
std::string GetUsername()
{
return m_userinfo.username;
}
int32_t GetClientType()
{
return m_userinfo.clienttype;
}
int32_t GetUserStatus()
{
return m_userinfo.status;
}
int32_t GetUserClientType()
{
return m_userinfo.clienttype;
}
void SendUserStatusChangeMsg(int32_t userid, int type, int status = 0);
private:
//各个业务逻辑的处理方法
bool Process(CTcpConnectionPtrR conn, const char* inbuf, size_t buflength);
void OnHeartbeatResponse(CTcpConnectionPtrR conn);
void OnRegisterResponse(const std::string& data, CTcpConnectionPtrR conn);
void OnLoginResponse(const std::string& data, CTcpConnectionPtrR conn);
void OnGetFriendListResponse(CTcpConnectionPtrR conn);
void OnFindUserResponse(const std::string& data, CTcpConnectionPtrR conn);
void OnChangeUserStatusResponse(const std::string& data, CTcpConnectionPtrR conn);
TcpConnectionPtr GetConnectionPtr()
{
if (m_tmpConn.expired())
return NULL;
return m_tmpConn.lock();
}
//调用下层Connection层发送数据的方法
void Send(int32_t cmd, int32_t seq, const std::string& data);
void Send(int32_t cmd, int32_t seq, const char* data, int32_t dataLength);
void Send(const std::string& p);
void Send(const char* p, int32_t length);
private:
int32_t m_id; //session id
OnlineUserInfo m_userinfo; //该Session对应的用户信息
int32_t m_seq; //当前Session数据包的序列号
bool m_isLogin; //当前Session对应的用户是否已登录
//引用下层Connection层的成员变量
//但不管理TcpConnection对象的生命周期
std::weak_ptr<TcpConnection> m_tmpConn;
};
但是,Session对象并不拥有Connection对象,也就是说Session对象不控制Connection对象的生命周期。这是因为虽然Session对象的主动销毁(如收到非法的客户端数据并关闭Session对象)会引起Connection对象的销毁,但Connection对象本身也可能因为网络出错等原因被销毁,进而引起Session对象被销毁。
因此,在上述类接口描述中,ChatSession类使用了一个std::weak_ptr来引用TCPConnection对象。这是需要注意的地方。
▊ Connection层
Connection 层是技术层的顶层,每一路客户端连接都对应一个 Connection 对象,该层一般用于记录连接的各种状态信息。
常见的状态信息有连接状态、数据收发缓冲区信息、数据流量信息、本端和对端的地址和端口号信息等,同时提供对各种网络事件的处理接口,这些接口或被本层自己使用,或被Session层使用。
Connection持有一个Channel对象,而且掌管Channel对象的生命周期。
一个Connection对象可以提供的接口和记录的数据状态如下:
class TcpConnection
{
public:
TcpConnection(EventLoop* loop,
const string& name,
int sockfd,
const InetAddress& localAddr,
const InetAddress& peerAddr);
~TcpConnection();
const InetAddress& localAddress() const { return m_localAddr;}
const InetAddress& peerAddress() const { return m_peerAddr; }
bool connected() const { return m_state == kConnected; }
void send(const void* message, int len);
void send(const string& message);
void send(Buffer* message);
void shutdown();
void forceClose();
void setConnectionCallback(const ConnectionCallback& cb);
void setMessageCallback(const MessageCallback& cb);
void setCloseCallback(const CloseCallback& cb);
void setErrorCallback(const ErrorCallback& cb);
Buffer* getInputBuffer();
Buffer* getOutputBuffer();
private:
enum StateE { kDisconnected, kConnecting, kConnected, kDisconnecting };
void handleRead(Timestamp receiveTime);
void handleWrite();
void handleClose();
void handleError();
void sendInLoop(const string& message);
void sendInLoop(const void* message, size_t len);
void shutdownInLoop();
void forceCloseInLoop();
void setState(StateE s) { m_state = s; }
private:
//连接状态信息
StateE m_state;
//引用Channel对象
std::shared_ptr<Channel> m_spChannel;
//本端的地址信息
const InetAddress m_localAddr;
//对端的地址信息
const InetAddress m_peerAddr;
ConnectionCallback m_connectionCallback;
MessageCallback m_messageCallback;
CloseCallback m_closeCallback;
ErrorCallback m_errorCallback;
//接收缓冲区
Buffer m_inputBuffer;
//发送缓冲区
Buffer m_outputBuffer;
//流量统计类
CFlowStatistics m_flowStatistics;
};
▊ Channel层
Channel层一般持有一个socket句柄,是实际进行数据收发的地方,因而一个Channel对象会记录当前需要监听的各种网络事件(读写和出错事件)的状态,同时提供对这些事件状态的查询和增删改接口。
在部分网络库的实现中,Channel对象管理着socket对象的生命周期,因此Channel对象需要提供创建和关闭socket对象的接口;而在另外一些网络库的实现中由Connection对象直接管理socket对象的生命周期,也就是说没有Channel层。
所以,Channel层不是必需的。
由于TCP收发数据是全双工的(收发走独立的通道,互不影响),所以收发逻辑一般不会有依赖关系,但收发操作一般会被放在同一个线程中进行,这样做的目的是防止在收发过程中改变socket状态时,对另一个操作产生影响。假设收发操作分别使用一个线程,在一个线程中收数据时因出错而关闭了连接,但另一个线程可能正在发送数据,这样就会出问题。
一个Channel对象提供的函数接口和状态数据如下:
class Channel
{
public:
Channel(EventLoop* loop, int fd);
~Channel();
void handleEvent(Timestamp receiveTime);
int fd() const;
int events() const;
void setRevents(int revt);
void addRevents(int revt);
void removeEvents();
bool isNoneEvent() const;
bool enableReading();
bool disableReading();
bool enableWriting();
bool disableWriting();
bool disableAll();
bool isWriting() const;
private:
const int m_fd; //当前需要检测的事件
int m_events; //处理后的事件
int m_revents;
▊ Socket层
严格来说,并不存在Socket层,这一层通常只是对常用的socket函数进行封装,例如屏蔽不同操作系统操作socket函数的差异性来实现跨平台,方便上层使用。
如果存在 Channel 层,则 Socket 层的上层就是 Channel 层;如果不存在Channel层,则Socket层的上层就是Connection层。
Socket层也不是必需的,因此很多网络库都没有Socket层。
下面是某Socket层对常用socket函数的功能进行一层简单封装的接口示例:
namespace sockets
{
typedef int SOCKET;
SOCKET createOrDie();
SOCKET createNonblockingOrDie();
void setNonBlockAndCloseOnExec(SOCKET sockfd);
void setReuseAddr(SOCKET sockfd, bool on);
void setReusePort(SOCKET sockfd, bool on);
int connect(SOCKET sockfd, const struct sockaddr_in& addr);
void bindOrDie(SOCKET sockfd, const struct sockaddr_in& addr);
void listenOrDie(SOCKET sockfd);
int accept(SOCKET sockfd, struct sockaddr_in* addr);
int32_t read(SOCKET sockfd, void *buf, int32_t count);
ssize_t readv(SOCKET sockfd, const struct iovec *iov, int iovcnt);
int32_t write(SOCKET sockfd, const void *buf, int32_t count);
void close(SOCKET sockfd);
void shutdownWrite(SOCKET sockfd);
void toIpPort(char* buf, size_t size, const struct sockaddr_in& addr);
void toIp(char* buf, size_t size, const struct sockaddr_in& addr);
void fromIpPort(const char* ip, uint16_t port, struct sockaddr_in* addr);
int getSocketError(SOCKET sockfd);
struct sockaddr_in getLocalAddr(SOCKET sockfd);
struct sockaddr_in getPeerAddr(SOCKET sockfd);
}
在实际开发中,有的服务在设计网络通信模块时会将Connection层与Channel层合并成一层,当然,这取决于业务的复杂程度。所以在某些服务代码中只看到 Connection 对象或者Channel对象时,请不要觉得奇怪。
另外,对于服务端程序,抛开业务本身,从技术层面上来说,我们需要一个 Server对象(如TcpServer)来集中管理多个Connection对象,这也是网络库自身需要处理好的部分。一个TcpServer对象可能需要提供如下函数接口和状态数据:
class TcpServer
{
public:
typedef std::function<void(EventLoop*)> ThreadInitCallback;
enum Option
{
kNoReusePort,
kReusePort,
};
TcpServer(EventLoop* loop,
const InetAddress& listenAddr,
const std::string& nameArg,
Option option = kReusePort);
~TcpServer();
void addConnection(int sockfd, const InetAddress& peerAddr);
void removeConnection(const TcpConnection& conn);
typedef std::map<string, TcpConnectionPtr> ConnectionMap;
private:
int m_nextConnId;
ConnectionMap m_connections;
};
不同的服务,其业务可能千差万别,在实际开发中,我们可以根据业务场景将Session层进一步拆分成多个层,使每一层都专注于自己的业务逻辑。
例如,假设现在有一个需要支持聊天消息压缩的即时通信服务,我们可以将Session划分为三个层,从上到下依次是ChatSession、CompressionSession和TcpSession。ChatSession负责处理聊天业务本身,CompressSession 负责数据的解压缩,TcpSession负责将数据加工成网络层需要的格式或者将网络层发送的数据还原成业务需要的格式(如数据装包和解包),示意图如下。
结合前面介绍的one thread one loop思想,每一路连接信息都只能属于一个loop,也就是说只属于某个线程;但是反过来,一个 loop 或者一个线程可以同时拥有多个连接信息,这就保证了我们只会在同一个线程里面处理特定的socket收发事件。
往期推荐
关注「开源Linux」加星标,提升IT技能
点个在看少个 bug 👇