2个类轻松构建高效Socket通信库

2个类轻松构建高效Socket通信库

引言

  在接触Linux网络编程前,一直觉得网络编程充满了神秘与挑战,遥不可及。这种观念一度让我对网络编程望而却步。当项目需求迫使我直面这一领域,经过层层bug考验,发现网络编程的困难更多源于心理障碍而非技术本身。

  在实际调试中,通过掌握TCP协议socket接口、I/O复用TCP抓包等技能,可以有效解决网络编程中的问题。许多看似难以解释的"灵异事件",基本都源于代码实现不规范或对原理理解不足。本篇参考了诸多成熟的socket实现,设计了一套优雅的接口封装,旨在简化和降低socket的使用难度,提高开发效率与代码质量。

<span style="font-size: 12px;">
<span style="color: blue;">注:文末提供源码获取方式。文章不定时更新,欢迎星标公众号以免错过推送。源码已开源,若有帮助,帮忙分享、点赞和收藏,提升文章热度。您的支持有助于内容持续改进,并让更多人受益。感谢关注与支持!</span>
</span>

概述

  网上已经存在很多成熟的网络库,重新造轮子有什么意义?总结下来主要有如下原因:

  • 适应有限资源
      成熟网络库功能全面但体积较大,不适合嵌入式这种资源有限的环境。
  • 功能需求单一
      实际项目中,可能只需要网络库的一部分功能。定制开发可以专注于这些特定需求,避免引入不必要的复杂性和代码冗余。
  • 基于学习目的
      通过实现网络库可以深入理解网络编程的原理和技术细节,提升个人技术能力。
  • 便于维护和优化
      自主开发的库更容易根据项目需求进行调整、扩展或优化,并且能够更快的定位问题。
  • 减少外部依赖
      自主开发可以避免对第三方库的依赖,降低版本兼容性和授权等方面的风险。

  以上是实现Socket通用库的原因,主要是希望结合实际项目总结一套成熟稳定的Socket通用代码库,方便项目复用。

需求分析

  日常Socket编程,软件上需求大致罗列如下:

  • 同时支持UDPTCPUnix域套接字
    提供UDPTCPUnix域套接字接口。可以通过灵活的接口创建不同类的socket接口。
  • 采用I/O多路复用方案
    采用selectpollepoll等多路复用技术,提高并发处理能力和响应速度。
  • API封装尽量简洁,方便使用
    API易于理解和使用,够快速上手。

详细设计

  从上述需求分析,可以将Socket通信划分为两个类实现: EpollEventHandlerIEpollEvent
EpollEventHandler用于实现I/O多路复用逻辑;
IEpollEvent用于实现具体的I/O事件。IEpollEvent根据具体的Socket类型分别可以派生子类:PUdpPTcpServerPTcpClientPUnixDgramPUnixStreamServerPUnixStreamClient。类图关系如下(省略部分子类):

EpollEventHandler.png

  • EpollEventHandler:
    ① 实现epoll接口封装,提供添加事件(AddPoll)、删除事件(DelPoll)等功能。
    ② 向外提供统一的epoll循环监听接口,负责管理多个IEpollEvent实例,并在事件触发时调用相应的处理方法。
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:
    ① 定义监听事件的抽象基类,规范通用接口如Read()Write(),确保所有派生类遵循一致的行为模式。
    ② 提供必要的虚函数或纯虚函数,使得具体子类可以根据自身特性实现差异化的读写操作。
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,根据各自特点实现特定的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;
};

实例使用

  实现一套TCP服务端与客户端通信,服务端能够自动管理多个客户端资源。

服务端代码

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服务器对象

  • 创建一个PTcpServer对象,并为其设置新客户端连接到来时的回调函数。
  • 在回调中,每当有新的客户端连接时,会创建一个新的PTcpClient实例来处理该连接,并将其加入到clients列表中。

② 启动TCP服务器
  调用AsTcpServer接口,传入监听端口和队列长度,开始TCP服务器的Socket业务。这一步会将自身事件添加到EpollEventHandler中,准备接受新的连接请求。

③ 进入epoll事件监听循环
  调用epollHandler->EpollLoop()进入主事件循环,监听并处理所有注册的事件(包括来自PTcpServer的新连接事件和来自各个PTcpClient的数据读写事件)。

④ 处理客户端通信

  • 当接收到数据时,服务端会回显一条消息给客户端,并记录日志。
  • 如果读取操作返回值小于等于0,则认为客户端已断开连接,此时从clients列表中移除对应的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);   // 避免调试刷屏
        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;
}

客户端的实现与服务端类似:
① 创建客户端实例;
② 传入数据处理回调;
③ 数据应答处理;
epoll监听循环。

客户端、服务端效果

  • 服务端
$ ./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

通过上述观察,可以发现服务端能够准确的监听到客户端数据。同时,客户端主动断开后,服务端能够及时监听并关闭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
  • 客户端能够准确发送数据。
  • 示例代码中没有做断开重连机制,实际也可以在回调中加上断开重连的处理:即监听到对端断开时,释放当前对象,并增加定时器,周期创建客户端尝试连接服务器,连接失败回收客户端对象。

总结

  • 封装 Socket 接口显著提升了编码便捷性,开发时无需关注监听事件和资源回收等底层细节,从而专注于业务逻辑的实现。
  • 在实现 UDP 发送与接收功能时,鉴于其无连接特性,设计上选择了在每次操作时指定目标 IP 和端口,而非预先建立连接。这一设计满足了多数应用场景的需求,而针对特殊场景,可在实际遇到时进行相应调整。
  • 使用 Socket API 进行网络编程时,通常无需深入了解 TCP 协议的细节,因为这些已被封装处理。但在排查网络通信故障时,理解 TCP 协议会极大地帮助问题的诊断和解决。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容