2個類輕松構(gòu)建高效Socket通信庫

2個類輕松構(gòu)建高效Socket通信庫

引言

? 在接觸Linux網(wǎng)絡(luò)編程前,一直覺得網(wǎng)絡(luò)編程充滿了神秘與挑戰(zhàn),遙不可及。這種觀念一度讓我對網(wǎng)絡(luò)編程望而卻步。當(dāng)項目需求迫使我直面這一領(lǐng)域,經(jīng)過層層bug考驗,發(fā)現(xiàn)網(wǎng)絡(luò)編程的困難更多源于心理障礙而非技術(shù)本身。

? 在實際調(diào)試中,通過掌握TCP協(xié)議socket接口、I/O復(fù)用TCP抓包等技能,可以有效解決網(wǎng)絡(luò)編程中的問題。許多看似難以解釋的"靈異事件",基本都源于代碼實現(xiàn)不規(guī)范或?qū)υ砝斫獠蛔?。本篇參考了諸多成熟的socket實現(xiàn),設(shè)計了一套優(yōu)雅的接口封裝,旨在簡化和降低socket的使用難度,提高開發(fā)效率與代碼質(zhì)量。

<span style="font-size: 12px;">
<span style="color: blue;">注:文末提供源碼獲取方式。文章不定時更新,歡迎星標(biāo)公眾號以免錯過推送。源碼已開源,若有幫助,幫忙分享、點贊和收藏,提升文章熱度。您的支持有助于內(nèi)容持續(xù)改進(jìn),并讓更多人受益。感謝關(guān)注與支持!</span>
</span>

概述

? 網(wǎng)上已經(jīng)存在很多成熟的網(wǎng)絡(luò)庫,重新造輪子有什么意義?總結(jié)下來主要有如下原因:

  • 適應(yīng)有限資源
    ? 成熟網(wǎng)絡(luò)庫功能全面但體積較大,不適合嵌入式這種資源有限的環(huán)境。
  • 功能需求單一
    ? 實際項目中,可能只需要網(wǎng)絡(luò)庫的一部分功能。定制開發(fā)可以專注于這些特定需求,避免引入不必要的復(fù)雜性和代碼冗余。
  • 基于學(xué)習(xí)目的
    ? 通過實現(xiàn)網(wǎng)絡(luò)庫可以深入理解網(wǎng)絡(luò)編程的原理和技術(shù)細(xì)節(jié),提升個人技術(shù)能力。
  • 便于維護(hù)和優(yōu)化
    ? 自主開發(fā)的庫更容易根據(jù)項目需求進(jìn)行調(diào)整、擴(kuò)展或優(yōu)化,并且能夠更快的定位問題。
  • 減少外部依賴
    ? 自主開發(fā)可以避免對第三方庫的依賴,降低版本兼容性和授權(quán)等方面的風(fēng)險。

? 以上是實現(xiàn)Socket通用庫的原因,主要是希望結(jié)合實際項目總結(jié)一套成熟穩(wěn)定的Socket通用代碼庫,方便項目復(fù)用。

需求分析

? 日常Socket編程,軟件上需求大致羅列如下:

  • 同時支持UDP、TCP、Unix域套接字
    提供UDP、TCPUnix域套接字接口??梢酝ㄟ^靈活的接口創(chuàng)建不同類的socket接口。
  • 采用I/O多路復(fù)用方案
    采用select、pollepoll等多路復(fù)用技術(shù),提高并發(fā)處理能力和響應(yīng)速度。
  • API封裝盡量簡潔,方便使用
    API易于理解和使用,夠快速上手。

詳細(xì)設(shè)計

? 從上述需求分析,可以將Socket通信劃分為兩個類實現(xiàn): EpollEventHandlerIEpollEvent。
? EpollEventHandler用于實現(xiàn)I/O多路復(fù)用邏輯;
? IEpollEvent用于實現(xiàn)具體的I/O事件。IEpollEvent根據(jù)具體的Socket類型分別可以派生子類:PUdpPTcpServer、PTcpClient、PUnixDgram、PUnixStreamServerPUnixStreamClient。類圖關(guān)系如下(省略部分子類):

EpollEventHandler.png

  • EpollEventHandler:
    ① 實現(xiàn)epoll接口封裝,提供添加事件(AddPoll)、刪除事件(DelPoll)等功能。
    ② 向外提供統(tǒng)一的epoll循環(huán)監(jiān)聽接口,負(fù)責(zé)管理多個IEpollEvent實例,并在事件觸發(fā)時調(diào)用相應(yīng)的處理方法。
class EpollEventHandler
{
public:
    virtual ~EpollEventHandler();
    static EpollEventHandler* GetInstance(int size = 0, int blockTimeOut = -1);

    void AddPoll(IEpollEvent* p);
    void DelPoll(IEpollEvent* p);
    void EpollLoop();
    void ExitLoop();
    virtual void HandleEpollEvent(IEpollEvent& pEvent);

protected:
    explicit EpollEventHandler(int size = 0, int blockTimeOut = -1);

private:
    bool    mRun;
    int     mHandle;
    int     mTimeOut;
    std::map<int, IEpollEvent*> mEpollMap;   // fd, type, IEpollEvent
};

void EpollEventHandler::EpollLoop()
{
    struct epoll_event ep[32];
    mRun = true;
    while(mRun) {
        if (!mRun) {
            break;
        }

        // 無事件時, epoll_wait阻塞, 等待
        int count = epoll_wait(mHandle, ep, sizeof(ep)/sizeof(ep[0]), mTimeOut);
        if (count <= 0) {
            continue;
        }

        for (int i = 0; i < count; i++) {
            IEpollEvent* p = (IEpollEvent*)ep[i].data.ptr;
            if (p == nullptr) {
                continue;
            }

            HandleEpollEvent(*p);
        }
    }

    SPR_LOGD("EpollLoop exit\n");
}
  • IEpollEvent:
    ① 定義監(jiān)聽事件的抽象基類,規(guī)范通用接口如Read()Write(),確保所有派生類遵循一致的行為模式。
    ② 提供必要的虛函數(shù)或純虛函數(shù),使得具體子類可以根據(jù)自身特性實現(xiàn)差異化的讀寫操作。
class IEpollEvent
{
public:
    IEpollEvent(int fd, EpollType eType = EPOLL_TYPE_BEGIN, void* arg = nullptr)
        : mReady(true), mEvtFd(fd), mEpollType(eType), mArgs(arg) {};

    virtual ~IEpollEvent();
    virtual ssize_t Write(int fd, const char* data, size_t size);
    virtual ssize_t Write(int fd, const std::string& bytes);
    virtual ssize_t Write(const char* data, size_t size);
    virtual ssize_t Write(const std::string& bytes);

    virtual ssize_t Read(int fd, char* data, size_t size);
    virtual ssize_t Read(int fd, std::string& bytes);
    virtual ssize_t Read(char* data, size_t size);
    virtual ssize_t Read(std::string& bytes);

    virtual bool    IsReady();
    virtual void    Close();
    virtual void    AddToPoll();
    virtual void    DelFromPoll();
    virtual void*   EpollEvent(int fd, EpollType eType, void* arg) = 0;

    int         GetEvtFd()      { return mEvtFd; }
    EpollType   GetEpollType()  { return mEpollType; }
    void*       GetArgs()       { return mArgs; }

protected:
    void        SetReady(bool ready) { mReady = ready; }

protected:
    bool        mReady;
    int         mEvtFd;
    EpollType   mEpollType;
    void*       mArgs;
};
  • 具體事件類 (如PUdp, PTcpServer, PTcpClient, PUnixDgram, PUnixStreamServer, PUnixStreamClient):
    ① 繼承自IEpollEvent,根據(jù)各自特點實現(xiàn)特定的Read()Write()方法。
    ② 每個子類專注于一種類型的Socket通信(UDP, TCP Server/Client, Unix域套接字),并可能包含額外的方法以支持該類型特有的功能。
class PUdp : public IEpollEvent
{
public:
    PUdp(const std::function<void(int fd, void*)>& cb, void* arg = nullptr)
        : IEpollEvent(-1, EPOLL_TYPE_SOCKET, arg), mCb1(cb), mCb2(nullptr) {}
    PUdp(const std::function<void(ssize_t, std::string, std::string addr, uint16_t port, void*)>& cb, void* arg = nullptr)
        : IEpollEvent(-1, EPOLL_TYPE_SOCKET, arg), mCb1(nullptr), mCb2(cb) {}
    virtual ~PUdp();

    int32_t AsUdp(uint16_t port = 0, int32_t rcvLen = DEFAULT_BUFFER_LIMIT, int32_t sndLen = DEFAULT_BUFFER_LIMIT);

    int32_t Write(const std::string& bytes, const std::string& addr, uint16_t port);
    int32_t Write(const void* data, size_t size, const std::string& addr, uint16_t port);
    int32_t Read(std::string& bytes, std::string& addr, uint16_t& port);
    int32_t Read(void* data, size_t size, std::string& addr, uint16_t& port);

    void*   EpollEvent(int fd, EpollType eType, void* arg) override;

private:
    std::function<void(int fd, void*)> mCb1;
    std::function<void(ssize_t, std::string, std::string addr, uint16_t port, void*)> mCb2;
};

class PTcpServer : public IEpollEvent
{
public:
    PTcpServer(const std::function<void(int, void*)>& cb, void* arg = nullptr)
        : IEpollEvent(-1, EPOLL_TYPE_SOCKET, arg), mCb(cb) {}
    virtual ~PTcpServer();

    int32_t AsTcpServer(uint16_t port, int32_t backlog, const std::string& addr = "");
    void*   EpollEvent(int fd, EpollType eType, void* arg) override;

private:
    std::function<void(int, void*)> mCb;
};

實例使用

? 實現(xiàn)一套TCP服務(wù)端與客戶端通信,服務(wù)端能夠自動管理多個客戶端資源。

服務(wù)端代碼

int main(int argc, char *argv[])
{
    if (argc != 2) {
        SPR_LOGE("Usage: %s <port>\n", argv[0]);
        return -1;
    }

    uint16_t port = atoi(argv[1]);
    std::list<std::shared_ptr<PTcpClient>> clients;
    auto epollHandler = EpollEventHandler::GetInstance();

    auto pTcpSrv = make_shared<PTcpServer>([&](int clifd, void* arg) {
        auto pTcpSrv = reinterpret_cast<PTcpServer*>(arg);
        if (!pTcpSrv) {
            SPR_LOGE("pTcpSrv is nullptr\n");
            return;
        }

       auto pTcpClient = make_shared<PTcpClient>(clifd, [&](int fd, void* arg) {
            auto pTcpCli = reinterpret_cast<PTcpClient*>(arg);
            if (!pTcpCli) {
                SPR_LOGE("pTcpCli is nullptr\n");
                return;
            }

            std::string bytes;
            int32_t rc = pTcpCli->Read(fd, bytes);
            if (rc > 0) {
                SPR_LOGD("# RECV [%d]> %s\n", fd, bytes.c_str());
                std::string sBuf = "Hello, tcp client";
                rc = pTcpCli->Write(fd, sBuf);
                if (rc > 0) {
                    SPR_LOGD("# SEND [%d]> %s\n", fd, sBuf.c_str());
                }
            }

            if (rc <= 0) {
                clients.remove_if([fd, pTcpCli](shared_ptr<PTcpClient>& v) {
                    return (v->GetEvtFd() == fd);
                });
                SPR_LOGD("Del client %d, total = %ld\n", fd, clients.size());
            }
        });

        int rc = pTcpClient->AsTcpClient();
        if (rc != -1) {
            clients.push_back(pTcpClient);
            SPR_LOGD("Add client %d, total = %ld\n", pTcpClient->GetEvtFd(), clients.size());
        }
    });

    int ret = pTcpSrv->AsTcpServer(port, 5);
    if (ret != -1) {
        SPR_LOGD("As TCP server success!\n");
    }

    epollHandler->EpollLoop();
    return 0;
}

① 初始化TCP服務(wù)器對象

  • 創(chuàng)建一個PTcpServer對象,并為其設(shè)置新客戶端連接到來時的回調(diào)函數(shù)。
  • 在回調(diào)中,每當(dāng)有新的客戶端連接時,會創(chuàng)建一個新的PTcpClient實例來處理該連接,并將其加入到clients列表中。

② 啟動TCP服務(wù)器
? 調(diào)用AsTcpServer接口,傳入監(jiān)聽端口和隊列長度,開始TCP服務(wù)器的Socket業(yè)務(wù)。這一步會將自身事件添加到EpollEventHandler中,準(zhǔn)備接受新的連接請求。

③ 進(jìn)入epoll事件監(jiān)聽循環(huán)
? 調(diào)用epollHandler->EpollLoop()進(jìn)入主事件循環(huán),監(jiān)聽并處理所有注冊的事件(包括來自PTcpServer的新連接事件和來自各個PTcpClient的數(shù)據(jù)讀寫事件)。

④ 處理客戶端通信

  • 當(dāng)接收到數(shù)據(jù)時,服務(wù)端會回顯一條消息給客戶端,并記錄日志。
  • 如果讀取操作返回值小于等于0,則認(rèn)為客戶端已斷開連接,此時從clients列表中移除對應(yīng)的PTcpClient實例,并完成客戶端資源的回收。

客戶端代碼

int main(int argc, char *argv[])
{
    if (argc != 3) {
        SPR_LOGE("Usage: %s <ip> <port>\n", argv[0]);
        return -1;
    }

    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    auto pTcpClient = make_shared<PTcpClient>([&](size_t ret, string bytes, void* arg) {
        auto pTcpCli = reinterpret_cast<PTcpClient*>(arg);
        if (!pTcpCli) {
            SPR_LOGE("pTcpCli is nullptr\n");
            return;
        }

        if (ret <= 0) {
            SPR_LOGD("read fail! ret = %ld (%s)\n", ret, strerror(errno));
            pTcpCli->Close();
            return;
        }

        sleep(2);   // 避免調(diào)試刷屏
        SPR_LOGD("# RECV [%d]> %s\n", pTcpCli->GetEvtFd(), bytes.c_str());
        std::string sBuf = "Hello, tcp server";
        int32_t rc = pTcpCli->Write(sBuf);
        if (rc > 0) {
            SPR_LOGD("# SEND [%d]> %s\n", pTcpCli->GetEvtFd(), sBuf.c_str());
        }
    });

    auto epollHandler = EpollEventHandler::GetInstance();
    pTcpClient->AsTcpClient(true, ip, port);
    pTcpClient->Write("Hello, tcp server");
    epollHandler->EpollLoop();
    return 0;
}

客戶端的實現(xiàn)與服務(wù)端類似:
① 創(chuàng)建客戶端實例;
② 傳入數(shù)據(jù)處理回調(diào);
③ 數(shù)據(jù)應(yīng)答處理;
epoll監(jiān)聽循環(huán)。

客戶端、服務(wù)端效果

  • 服務(wù)端
$ ./main_tcp_srv 8080
MainTcpSrv D:   84 As TCP server success!
MainTcpSrv D:   78 Add client 5, total = 1
MainTcpSrv D:   59 # RECV [5]> Hello, tcp server
MainTcpSrv D:   63 # SEND [5]> Hello, tcp client
MainTcpSrv D:   59 # RECV [5]> Hello, tcp server
MainTcpSrv D:   63 # SEND [5]> Hello, tcp client
MainTcpSrv D:   59 # RECV [5]> Hello, tcp server
MainTcpSrv D:   63 # SEND [5]> Hello, tcp client
MainTcpSrv D:   59 # RECV [5]> Hello, tcp server
MainTcpSrv D:   63 # SEND [5]> Hello, tcp client
  94 IEpEvt E: read fail! (Connection reset by peer)
 154 IEpEvt D: Close fd: 5
  93 EpEvtHandler D: Delete epoll fd 5
MainTcpSrv D:   71 Del client 5, total = 0

通過上述觀察,可以發(fā)現(xiàn)服務(wù)端能夠準(zhǔn)確的監(jiān)聽到客戶端數(shù)據(jù)。同時,客戶端主動斷開后,服務(wù)端能夠及時監(jiān)聽并關(guān)閉socket,完成客戶端資源回收。

  • 客戶端
$ ./main_tcp_client 127.0.0.1 8080
MainTcpClient D:   57 # RECV [3]> Hello, tcp client
MainTcpClient D:   61 # SEND [3]> Hello, tcp server
MainTcpClient D:   57 # RECV [3]> Hello, tcp client
MainTcpClient D:   61 # SEND [3]> Hello, tcp server
MainTcpClient D:   57 # RECV [3]> Hello, tcp client
MainTcpClient D:   61 # SEND [3]> Hello, tcp server
  • 客戶端能夠準(zhǔn)確發(fā)送數(shù)據(jù)。
  • 示例代碼中沒有做斷開重連機(jī)制,實際也可以在回調(diào)中加上斷開重連的處理:即監(jiān)聽到對端斷開時,釋放當(dāng)前對象,并增加定時器,周期創(chuàng)建客戶端嘗試連接服務(wù)器,連接失敗回收客戶端對象。

總結(jié)

  • 封裝 Socket 接口顯著提升了編碼便捷性,開發(fā)時無需關(guān)注監(jiān)聽事件和資源回收等底層細(xì)節(jié),從而專注于業(yè)務(wù)邏輯的實現(xiàn)。
  • 在實現(xiàn) UDP 發(fā)送與接收功能時,鑒于其無連接特性,設(shè)計上選擇了在每次操作時指定目標(biāo) IP 和端口,而非預(yù)先建立連接。這一設(shè)計滿足了多數(shù)應(yīng)用場景的需求,而針對特殊場景,可在實際遇到時進(jìn)行相應(yīng)調(diào)整。
  • 使用 Socket API 進(jìn)行網(wǎng)絡(luò)編程時,通常無需深入了解 TCP 協(xié)議的細(xì)節(jié),因為這些已被封裝處理。但在排查網(wǎng)絡(luò)通信故障時,理解 TCP 協(xié)議會極大地幫助問題的診斷和解決。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容