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



