在接触Linux网络编程
前,一直觉得网络编程
充满了神秘与挑战,遥不可及。这种观念一度让我对网络编程
望而却步。当项目需求迫使我直面这一领域,经过层层bug
考验,发现网络编程
的困难更多源于心理障碍而非技术本身。
在实际调试中,通过掌握TCP协议
、socket
接口、I/O复用
和TCP抓包
等技能,可以有效解决网络编程中的问题。许多看似难以解释的"灵异事件",基本都源于代码实现不规范或对原理理解不足。本篇参考了诸多成熟的socket
实现,设计了一套优雅的接口封装,旨在简化和降低socket
的使用难度,提高开发效率与代码质量。
注:文末提供源码获取方式。文章不定时更新,欢迎星标公众号以免错过推送。源码已开源,若有帮助,帮忙分享、点赞和收藏,提升文章热度。您的支持有助于内容持续改进,并让更多人受益。感谢关注与支持!
网上已经存在很多成熟的网络库,重新造轮子有什么意义?总结下来主要有如下原因:
以上是实现Socket
通用库的原因,主要是希望结合实际项目总结一套成熟稳定的Socket
通用代码库,方便项目复用。
日常Socket
编程,软件上需求大致罗列如下:
UDP
、TCP
、Unix
域套接字
提供UDP
、TCP
、Unix
域套接字接口。可以通过灵活的接口创建不同类的socket
接口。select
、poll
或epoll
等多路复用技术,提高并发处理能力和响应速度。API
封装尽量简洁,方便使用
API
易于理解和使用,够快速上手。 从上述需求分析,可以将Socket
通信划分为两个类实现:EpollEventHandler
和IEpollEvent
。
EpollEventHandler
用于实现I/O多路复用逻辑;
IEpollEvent
用于实现具体的I/O
事件。IEpollEvent
根据具体的Socket
类型分别可以派生子类:PUdp
、PTcpServer
、PTcpClient
、PUnixDgram
、PUnixStreamServer
和PUnixStreamClient
。类图关系如下(省略部分子类):
EpollEventHandler
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");
}
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服务器对象
PTcpClient
实例来处理该连接,并将其加入到clients
列表中。② 启动TCP服务器
调用AsTcpServer
接口,传入监听端口和队列长度,开始TCP
服务器的Socket
业务。这一步会将自身事件添加到EpollEventHandler
中,准备接受新的连接请求。
③ 进入epoll
事件监听循环
调用epollHandler->EpollLoop()
进入主事件循环,监听并处理所有注册的事件(包括来自PTcpServer
的新连接事件和来自各个PTcpClient
的数据读写事件)。
④ 处理客户端通信
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
接口显著提升了编码便捷性,开发时无需关注监听事件和资源回收等底层细节,从而专注于业务逻辑的实现。IP
和端口,而非预先建立连接。这一设计满足了多数应用场景的需求,而针对特殊场景,可在实际遇到时进行相应调整。Socket API
进行网络编程时,通常无需深入了解TCP
协议的细节,因为这些已被封装处理。但在排查网络通信故障时,理解TCP
协议会极大地帮助问题的诊断和解决。用心感悟,认真记录,写好每一篇文章,分享每一框干货。