云计算百科
云计算领域专业知识百科平台

C++ 仿muduo高并发服务器:Socket模块

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

C++ 仿muduo高并发服务器:Socket模块


1. 为什么需要封装 Socket?

Linux 原生 socket API 是一组 C 函数,直接使用存在以下问题:

  • 参数复杂,容易出错(如 sockaddr 类型转换、字节序转换)。
  • 资源管理麻烦(fd 需要手动 close)。
  • 错误处理重复(每个调用都要判断返回值、打印日志)。
  • 阻塞/非阻塞、地址重用等选项设置分散。

封装成 Socket 类后:

  • 提供面向对象的接口,语义清晰。
  • 利用 RAII 自动关闭 fd(需完善析构函数)。
  • 统一错误日志输出。
  • 方便在 Reactor 模型中作为基础组件使用。

2. 类定义概览

#define MAX_LISTEN 1024

class Socket {
private:
int _sockfd;
public:
Socket() {}
Socket(int fd) : _sockfd(fd) {}
~Socket() {}

int Fd() { return _sockfd; }

bool Create();
bool Bind(const std::string &ip, uint16_t port);
bool Listen(int backlog = MAX_LISTEN);
bool Connect(const std::string &ip, uint16_t port);
int Accept();

ssize_t Recv(void *buf, size_t len, int flag = 0);
ssize_t NonBlockRecv(void *buf, size_t len);
ssize_t Send(const void *buf, size_t len, int flag = 0);
ssize_t NonBlockSend(void *buf, size_t len);

void Close();

bool CreaterServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false);
bool CreateClient(uint16_t port, const std::string &ip);

void ReuseAddress();
void NonBlock();
};


3. 成员变量与构造/析构

int _sockfd;

  • _sockfd:套接字文件描述符,-1 表示无效。

Socket() :_sockfd(–1){}
Socket(int fd) : _sockfd(fd) {}
~Socket() {}

  • 析构函数:应调用 Close() 自动关闭 fd,当前为空,存在资源泄漏风险。

改进建议:后期我们再加上

~Socket() { Close(); }


4. 核心函数详解

4.1 Create():创建套接字

bool Create() {
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0) {
ERR_LOG("CREATE SOCKET FAILED!!");
return false;
}
return true;
}

  • AF_INET:IPv4 协议族。
  • SOCK_STREAM:TCP 流式套接字。
  • IPPROTO_TCP:TCP 协议。
  • 成功返回非负 fd,失败返回 -1。

4.2 Bind():绑定地址

bool Bind(const std::string &ip, uint16_t port) {
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port); // 端口转网络字节序
addr.sin_addr.s_addr = inet_addr(ip.c_str()); // IP 转网络字节序
socklen_t len = sizeof(struct sockaddr_in);

int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0) {
ERR_LOG("BIND ADDRESS FAILED!");
return false;
}
return true;
}

  • htons:主机字节序 → 网络字节序(16 位)。
  • inet_addr:点分十进制 IP → 网络字节序 32 位整数。
  • 服务器必须 bind,客户端通常不需要。

4.3 Listen():开始监听

bool Listen(int backlog = MAX_LISTEN) {
int ret = listen(_sockfd, backlog);
if (ret < 0) {
ERR_LOG("SOCKET LISTEN FAILED!");
return false;
}
return true;
}

  • backlog:已完成连接队列的最大长度,默认 MAX_LISTEN(1024)。
  • 将主动套接字变为被动监听套接字,内核开始接受连接。

4.4 Connect():客户端发起连接

bool Connect(const std::string &ip, uint16_t port) {
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);

int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0) {
ERR_LOG("CONNECT SERVER FAILED!");
return false;
}
return true;
}

  • 向服务器发起 TCP 连接,触发三次握手。
  • 阻塞模式下会等待连接完成。

4.5 Accept():接受新连接

int Accept() {
int newfd = accept(_sockfd, NULL, NULL);
if (newfd < 0) {
ERR_LOG("SOCKET ACCEPT FAILED!");
return –1;
}
return newfd;
}

  • 从已完成连接队列取出一个连接,返回新的通信 fd。
  • 监听 fd 继续监听,新 fd 用于和客户端通信。
  • 非阻塞模式下,无连接时返回 -1,errno 为 EAGAIN。

4.6 Recv() 与 NonBlockRecv()

ssize_t Recv(void *buf, size_t len, int flag = 0) {
ssize_t ret = recv(_sockfd, buf, len, flag);
if (ret <= 0) {
if (errno == EAGAIN || errno == EINTR) {
return 0; // 暂时无数据
}
ERR_LOG("SCOKET RECV FAILED!"); // 拼写错误:SCOKET -> SOCKET
return –1;
}
return ret;
}

ssize_t NonBlockRecv(void *buf, size_t len) {
return Recv(buf, len, MSG_DONTWAIT);
}

  • recv 返回实际接收字节数。
  • ret == 0:对端关闭连接。
  • ret < 0:
    • EAGAIN / EWOULDBLOCK:非阻塞下无数据。
    • EINTR:被信号中断。
    • 其他:真正错误。
  • MSG_DONTWAIT:本次接收非阻塞,不改变套接字整体属性。

4.7 Send() 与 NonBlockSend()

ssize_t Send(const void *buf, size_t len, int flag = 0) {
ssize_t ret = send(_sockfd, buf, len, flag);
if (ret < 0) {
ERR_LOG("SOCKET SEND FAILED!");
return –1;
}
return ret;
}

ssize_t NonBlockSend(void *buf, size_t len) {
return Send(buf, len, MSG_DONTWAIT);
}

  • 返回实际发送字节数。
  • 非阻塞下可能只发送部分数据,需要应用层缓冲剩余数据。
  • SIGPIPE 信号需忽略,否则向已关闭连接发送会崩溃。

4.8 Close():关闭套接字

void Close() {
if (_sockfd != –1)
close(_sockfd);
_sockfd = –1;
}

  • 关闭 fd,置为 -1,防止重复关闭。

4.9 CreaterServer():一键创建服务端

bool CreaterServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false) {
if (Create() == false) return false;
if (block_flag) NonBlock(); // 注意:block_flag 为 true 时反而设非阻塞,命名相反
if (Bind(ip, port) == false) return false;
if (Listen() == false) return false;
ReuseAddress(); // 建议移到 Bind 之前
return true;
}

  • 默认 ip = "0.0.0.0" 监听所有网卡。
  • block_flag 命名有歧义,实际 true 表示非阻塞。
  • ReuseAddress() 应在 Bind() 前调用。

4.10 CreateClient():一键创建客户端

bool CreateClient(uint16_t port, const std::string &ip) {
if (Create() == false) return false;
if (Connect(ip, port) == false) return false;
return true;
}

4.11 ReuseAddress():地址端口重用

void ReuseAddress() {
int val = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void*)&val, sizeof(int));
val = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void*)&val, sizeof(int));
}

  • SO_REUSEADDR:允许绑定处于 TIME_WAIT 的端口,服务器可立即重启。
  • SO_REUSEPORT:允许多个套接字绑定相同 IP:端口,实现负载均衡。
  • val = 1 表示开启选项,0 表示关闭。
  • SOL_SOCKET:通用套接字层选项。

4.12 NonBlock():设置非阻塞

void NonBlock() {
int flag = fcntl(_sockfd, F_GETFL, 0);
fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
}

  • 获取当前文件状态标志,加上 O_NONBLOCK,再设置回去。
  • 非阻塞是 Reactor 模型的基础,防止 accept/recv/send 阻塞 EventLoop 线程。

5. 关键知识点

知识点说明
字节序转换 htons / htonl 主机转网络,ntohs / ntohl 网络转主机
地址转换 inet_addr 字符串转网络字节序,推荐 inet_pton
错误处理 errno,EAGAIN / EWOULDBLOCK 表示非阻塞无数据,EINTR 表示被信号中断
阻塞 vs 非阻塞 非阻塞配合 epoll,避免线程卡死
地址重用 SO_REUSEADDR 解决 TIME_WAIT,SO_REUSEPORT 支持多进程监听
RAII 析构自动关闭 fd,避免资源泄漏
SIGPIPE 需忽略,防止向已关闭连接发送数据导致进程退出

6. 使用示例

6.1 服务端

#include "server.hpp"

int main() {
Socket lst_sock;
lst_sock.CreaterServer(8500); // 默认阻塞
while (1) {
int newfd = lst_sock.Accept();
if (newfd < 0) continue;

Socket cli_sock(newfd);
char buf[1024] = {0};
int ret = cli_sock.Recv(buf, 1023);
if (ret <= 0) {
cli_sock.Close();
continue;
}
cli_sock.Send(buf, ret);
cli_sock.Close();
}
lst_sock.Close();
return 0;
}

6.2 客户端

#include "server.hpp"

int main() {
Socket cli_sock;
cli_sock.CreateClient(8500, "127.0.0.1");
std::string str = "hello";
cli_sock.Send(str.c_str(), str.size());
char buf[1024] = {0};
cli_sock.Recv(buf, 1023);
DBG_LOG("%s", buf);
return 0;
}


7. 总结

Socket 类是对 Linux 原生 socket API 的轻量级封装,核心接口:

  • 创建:Create
  • 绑定:Bind
  • 监听:Listen
  • 连接:Connect
  • 接受:Accept
  • 收发:Recv / Send,支持非阻塞版本
  • 关闭:Close
  • 辅助:ReuseAddress、NonBlock
赞(0)
未经允许不得转载:网硕互联帮助中心 » C++ 仿muduo高并发服务器:Socket模块
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!