一文搞懂網(wǎng)絡(luò)庫的分層設(shè)計
關(guān)注「開源Linux」,選擇“設(shè)為星標(biāo)” 回復(fù)「學(xué)習(xí)」,有我為您特別篩選的學(xué)習(xí)資料~
“對于計算機(jī)科學(xué)領(lǐng)域中的任何問題,都可以通過增加一個間接的中間層來解決”這句話幾乎概括了計算機(jī)軟件體系結(jié)構(gòu)的設(shè)計要點(diǎn)。
計算機(jī)軟件體系結(jié)構(gòu)從上到下都是按照嚴(yán)格的層次結(jié)構(gòu)設(shè)計的,不僅整個體系如此,體系里面的每個組件如OS本身、很多應(yīng)用程序、軟件系統(tǒng)甚至很多硬件結(jié)構(gòu)也如此。
常見的網(wǎng)絡(luò)通信庫根據(jù)功能也可以分成很多層。
根據(jù)離業(yè)務(wù)的遠(yuǎn)近從上到下依次是Session層、Connection層、Channel層、Socket層。
其中Session層屬于業(yè)務(wù)層,Connection層、Channel層、Socket層屬于技術(shù)層,示意圖如下。
下面依次介紹各層的作用。
▊ Session層
Session 層處于頂層,在設(shè)計上不屬于網(wǎng)絡(luò)框架本身,用于記錄各種業(yè)務(wù)狀態(tài)數(shù)據(jù)和處理各種業(yè)務(wù)邏輯。在業(yè)務(wù)邏輯處理完畢后,如果需要進(jìn)行網(wǎng)絡(luò)通信,則依賴Connection層進(jìn)行數(shù)據(jù)收發(fā)。
例如,一個IM服務(wù)的Session類可能有如下接口和成員數(shù)據(jù):
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://各個業(yè)務(wù)邏輯的處理方法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();}//調(diào)用下層Connection層發(fā)送數(shù)據(jù)的方法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 idOnlineUserInfo m_userinfo; //該Session對應(yīng)的用戶信息int32_t m_seq; //當(dāng)前Session數(shù)據(jù)包的序列號bool m_isLogin; //當(dāng)前Session對應(yīng)的用戶是否已登錄//引用下層Connection層的成員變量//但不管理TcpConnection對象的生命周期std::weak_ptr<TcpConnection> m_tmpConn;};
但是,Session對象并不擁有Connection對象,也就是說Session對象不控制Connection對象的生命周期。這是因為雖然Session對象的主動銷毀(如收到非法的客戶端數(shù)據(jù)并關(guān)閉Session對象)會引起Connection對象的銷毀,但Connection對象本身也可能因為網(wǎng)絡(luò)出錯等原因被銷毀,進(jìn)而引起Session對象被銷毀。
因此,在上述類接口描述中,ChatSession類使用了一個std::weak_ptr來引用TCPConnection對象。這是需要注意的地方。
▊ Connection層
Connection 層是技術(shù)層的頂層,每一路客戶端連接都對應(yīng)一個 Connection 對象,該層一般用于記錄連接的各種狀態(tài)信息。
常見的狀態(tài)信息有連接狀態(tài)、數(shù)據(jù)收發(fā)緩沖區(qū)信息、數(shù)據(jù)流量信息、本端和對端的地址和端口號信息等,同時提供對各種網(wǎng)絡(luò)事件的處理接口,這些接口或被本層自己使用,或被Session層使用。
Connection持有一個Channel對象,而且掌管Channel對象的生命周期。
一個Connection對象可以提供的接口和記錄的數(shù)據(jù)狀態(tài)如下:
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://連接狀態(tài)信息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;//接收緩沖區(qū)Buffer m_inputBuffer;//發(fā)送緩沖區(qū)Buffer m_outputBuffer;//流量統(tǒng)計類CFlowStatistics m_flowStatistics;};
▊ Channel層
Channel層一般持有一個socket句柄,是實際進(jìn)行數(shù)據(jù)收發(fā)的地方,因而一個Channel對象會記錄當(dāng)前需要監(jiān)聽的各種網(wǎng)絡(luò)事件(讀寫和出錯事件)的狀態(tài),同時提供對這些事件狀態(tài)的查詢和增刪改接口。
在部分網(wǎng)絡(luò)庫的實現(xiàn)中,Channel對象管理著socket對象的生命周期,因此Channel對象需要提供創(chuàng)建和關(guān)閉socket對象的接口;而在另外一些網(wǎng)絡(luò)庫的實現(xiàn)中由Connection對象直接管理socket對象的生命周期,也就是說沒有Channel層。
所以,Channel層不是必需的。
由于TCP收發(fā)數(shù)據(jù)是全雙工的(收發(fā)走獨(dú)立的通道,互不影響),所以收發(fā)邏輯一般不會有依賴關(guān)系,但收發(fā)操作一般會被放在同一個線程中進(jìn)行,這樣做的目的是防止在收發(fā)過程中改變socket狀態(tài)時,對另一個操作產(chǎn)生影響。假設(shè)收發(fā)操作分別使用一個線程,在一個線程中收數(shù)據(jù)時因出錯而關(guān)閉了連接,但另一個線程可能正在發(fā)送數(shù)據(jù),這樣就會出問題。
一個Channel對象提供的函數(shù)接口和狀態(tài)數(shù)據(jù)如下:
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; //當(dāng)前需要檢測的事件int m_events; //處理后的事件int m_revents;
▊ Socket層
嚴(yán)格來說,并不存在Socket層,這一層通常只是對常用的socket函數(shù)進(jìn)行封裝,例如屏蔽不同操作系統(tǒng)操作socket函數(shù)的差異性來實現(xiàn)跨平臺,方便上層使用。
如果存在 Channel 層,則 Socket 層的上層就是 Channel 層;如果不存在Channel層,則Socket層的上層就是Connection層。
Socket層也不是必需的,因此很多網(wǎng)絡(luò)庫都沒有Socket層。
下面是某Socket層對常用socket函數(shù)的功能進(jìn)行一層簡單封裝的接口示例:
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);}
在實際開發(fā)中,有的服務(wù)在設(shè)計網(wǎng)絡(luò)通信模塊時會將Connection層與Channel層合并成一層,當(dāng)然,這取決于業(yè)務(wù)的復(fù)雜程度。所以在某些服務(wù)代碼中只看到 Connection 對象或者Channel對象時,請不要覺得奇怪。
另外,對于服務(wù)端程序,拋開業(yè)務(wù)本身,從技術(shù)層面上來說,我們需要一個 Server對象(如TcpServer)來集中管理多個Connection對象,這也是網(wǎng)絡(luò)庫自身需要處理好的部分。一個TcpServer對象可能需要提供如下函數(shù)接口和狀態(tài)數(shù)據(jù):
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;};
不同的服務(wù),其業(yè)務(wù)可能千差萬別,在實際開發(fā)中,我們可以根據(jù)業(yè)務(wù)場景將Session層進(jìn)一步拆分成多個層,使每一層都專注于自己的業(yè)務(wù)邏輯。
例如,假設(shè)現(xiàn)在有一個需要支持聊天消息壓縮的即時通信服務(wù),我們可以將Session劃分為三個層,從上到下依次是ChatSession、CompressionSession和TcpSession。ChatSession負(fù)責(zé)處理聊天業(yè)務(wù)本身,CompressSession 負(fù)責(zé)數(shù)據(jù)的解壓縮,TcpSession負(fù)責(zé)將數(shù)據(jù)加工成網(wǎng)絡(luò)層需要的格式或者將網(wǎng)絡(luò)層發(fā)送的數(shù)據(jù)還原成業(yè)務(wù)需要的格式(如數(shù)據(jù)裝包和解包),示意圖如下。

結(jié)合前面介紹的one thread one loop思想,每一路連接信息都只能屬于一個loop,也就是說只屬于某個線程;但是反過來,一個 loop 或者一個線程可以同時擁有多個連接信息,這就保證了我們只會在同一個線程里面處理特定的socket收發(fā)事件。
往期推薦
關(guān)注「開源Linux」加星標(biāo),提升IT技能
點(diǎn)個在看少個 bug ??



