/* 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
网硕互联帮助中心





评论前必须登录!
注册