muduo库 --socket的封装

作者:爱装代码的小瓶子日期:2026/6/12

本系列主要旨在帮助初学者学习和巩固 Linux 高性能网络编程,同时记录笔者在学习与手写 muduo 网络库项目过程中的心得体会。

本系列会围绕 muduo 网络库的核心思想展开,包括 Reactor 模式、事件循环、Channel、Poller、TcpServer、TcpConnection、Buffer、线程池等内容。

个人主页: 爱装代码的小瓶子
文章系列: Linux
2. C++
3. muduo 网络库项目


文章目录

  • 1. 知识准备:
    • 1.1什么是socket?
    • 1.2 大致的流程:
  • 2. 开始实现:
    • 2.1 创建套接字
    • 2.2 开始绑定:
    • 2.3 监听函数:
    • 2.4 监听之后就可以开始连接和accept了:
    • 2.5 发送消息和接收消息
      • 接下来是发送消息了:
    • 2.6 设置非阻塞和套接字复用
    • 2.7 提供便捷的创建函数:
  • 总结:

1. 知识准备:

1.1什么是socket?

socket 是操作系统提供的一个通信端点抽象。本质上它是一个文件描述符(fd),背后对应一个内核缓冲区对。你对它做 read/write,内核负责把数据发到另一端——可以是同机器另一个进程,也可以是网络上另一台机器。
为什么需要它?进程的虚拟地址空间是完全隔离的,A 进程直接读 B 进程的内存会触发 segfault。socket 是内核开凿的一条受控通道,让两个进程能安全地交换数据,而且这套接口(socket/bind/listen/accept/connect/send/recv)是 POSIX 标准,跨语言、跨机器都通用。

1.2 大致的流程:

所以,我们需要提供一些封装好的函数来完成调用,这样便于后面的操作:

  1. 创建套接字
  2. 开始绑定
  3. 开始监听
  4. 进行连接
  5. 接收套接字,放回服务员
  6. 接收不同的消息
  7. 发送消息
  8. 设置非阻塞和套接字复用
  9. 快捷创建客户端和服务器

2. 开始实现:

2.1 创建套接字

1    bool create()
2    {
3        _sockfd = ::socket(AF_INET,SOCK_STREAM,0);
4        if(_sockfd < 0)
5        {
6            ERR_LOG("socket error");
7            return false;
8        }
9        INF_LOG("socket success");
10        return true;
11    }
12

第一个比较简单,还是比较容易,利用socket来创建套接字,AF_INET 表示是网络通讯,其中SOCK_STEAM表示自己是字节流,我们可以在稍微介绍一下这三个参数:

参数含义这里的作用
AF_INET地址族使用 IPv4
SOCK_STREAM套接字类型使用 面向连接、可靠、字节流 的通信方式
0协议类型让系统自动选择默认协议
其实还有一种常用的套接字的类型就是: SOCK_DGRAM
类型含义对应协议
SOCK_STREAM字节流套接字TCP
SOCK_DGRAM数据报套接字UDP
其实本质就是TCP和UDP的区别。

2.2 开始绑定:

1    bool Bind(std::string ip, uint16_t port)
2    {
3        struct sockaddr_in addr;
4        bzero(&addr, sizeof(addr));
5        addr.sin_family = AF_INET;
6        addr.sin_port = htons(port);
7        addr.sin_addr.s_addr = inet_addr(ip.c_str());
8        socklen_t len = sizeof(addr);
9        int n = ::bind(_sockfd, (sockaddr *)&addr, len);
10        if (n == -1)
11        {
12            ERR_LOG("bind error");
13            return false;
14        }
15        INF_LOG("bind success");
16        return true;
17    }
18

先创建结构体 sockaddr_in,随后再使用进行填充,随后就进行绑定即可,
struct sockaddr_in 是 IPv4 专用地址结构体,大概可以理解成:

1struct sockaddr_in
2{
3    sa_family_t sin_family; // 地址族    
4    in_port_t sin_port;     // 端口号    
5    struct in_addr sin_addr; // IP 地址
6};
7

所以它主要存三样东西:IPv4 / IP / 端口。随后利用bind即可完成:以后有客户端访问这个 IP + port,就交给我的 _sockfd 处理。

2.3 监听函数:

1    bool Listen(int backlog = MAX_BACKLOG)
2    {
3        int ret = ::listen(_sockfd, backlog);
4        if (ret == -1)
5        {
6            ERR_LOG("listen error");
7            return false;
8        }
9        ERR_LOG("listen success");
10        return true;
11    }
12

这个监听是比较简单的,其中的backlog 是设置监听的最大数,能同时监听的数量。设置服务器“等待被 accept 的连接队列”最大长度,即只要accept够快,其实能连接的主机远远多于backlog。

2.4 监听之后就可以开始连接和accept了:

我们先说客户端应该做什么:向服务器发送连接:

1    bool Connect(std::string ip, uint16_t port)
2    {
3        struct sockaddr_in peer;
4        bzero(&peer, sizeof(peer));
5        peer.sin_family = AF_INET;
6        peer.sin_port = htons(port);
7        peer.sin_addr.s_addr = inet_addr(ip.c_str());
8        socklen_t len = sizeof(peer);
9        int n = ::connect(_sockfd, (sockaddr *)&peer, len);
10        if (n == -1)
11        {
12            printf("%d", errno);
13            ERR_LOG("connect error");
14            return false;
15        }
16        INF_LOG("connect success");
17        return true;
18    }
19

在这里客户端主动访问客户端,同样我们也要知道peer(远端服务器)的地址和port才能进行连接。

随后我们再来看:accept服务器应该怎么办:

1    int Accept()
2    {
3        int sockfd = ::accept(_sockfd, nullptr, nullptr);
4        if (sockfd == -1)
5        {
6            ERR_LOG("accept error");
7            return -1;
8        }
9        INF_LOG("accept success");
10        return sockfd;
11    }
12

我们监听之后,会得到一个新的sockfd,随后这个sockfd就是专门处理这个连接的。

2.5 发送消息和接收消息

1    ssize_t Recv(void* buf,size_t len,int flag = 0)
2    {
3        ssize_t ret = ::recv(_sockfd, buf,len, flag);
4        if(ret > 0)
5        {
6            // 成功了,直接返回n
7            return ret;
8        }
9        if(ret == 0)
10        {
11            // 说明上端挂断了:
12            INF_LOG("PEER CLOSED CONNECTION");
13            return -1; // 让上层走 shutdown / Release
14        }
15        if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
16        {
17            //errno == EAGAIN || errno == EWOULDBLOCK 没有消息了,稍后再读
18            // errno == EAGAIN,可以再次尝试读取
19            return 0; // 接下来再次继续读取
20        }
21        //找不到上面的任何一种情况,就直接退出
22        ERR_LOG("SOCKET RECV FAILED!!");
23        return -1;
24    }
25};
26

当我们收到消息,我们需要buf去接收,这个也是输出型参数,用来接收发送过来的消息。对recv的返回值我们一般做出

  1. > 0 ,成功了
  2. 等于 0 ,说明对端进行挂断,我们的返回值设为-1,交给上层处理
  3. 如果是被信号打断,或者是阻塞住了,那么返回0,表示再次尝试

同时再提供一个函数。非阻塞发送:

1    ssize_t NoBlockRecv(void *buf, size_t len)
2    {
3        return Recv(buf, len, MSG_DONTWAIT);
4    }
5

接下来是发送消息了:

1    ssize_t Send(const void *buf, size_t len, int flag = 0)
2    {
3        ssize_t n = ::send(_sockfd, buf, len, flag);
4        if (n <= 0)
5        {
6            if (errno == EINTR || errno == EAGAIN)
7                return 0;
8            ERR_LOG("SOCKET SEND FAILED!!");
9            return n;
10        }
11        INF_LOG("SOCKET SEND SUCCESS!");
12        return n;
13    }
14    ssize_t NoBlockSend(const void *buf, size_t len)
15    {
16        return Send(buf, len, MSG_DONTWAIT);
17    }
18

和上面的接收消息,我们需要提供需要发送的数据和字节大小,还有什么模式:
如果n < 0;那么开始查,如果是还是阻塞的,就放回0,等下epoll模型提醒再次seed.

2.6 设置非阻塞和套接字复用

为什么需要这样设置:
结论:在 muduo 这种 Reactor 网络库里:

  • 套接字复用:主要是为了服务器重启时不容易出现 Address already in use
  • 非阻塞 socket:为了保证 EventLoop 不会被某个连接卡死
1    void NoBlock()
2    {
3        int flag = fcntl(_sockfd, F_GETFL, 0);
4        fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
5    }
6
7    void ReuseAdress()
8    {
9        int val = 1;
10        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void *)&val, sizeof(int));
11        val = 1;
12        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void *)&val, sizeof(int));
13    }
14

两个函数一个是设置为非阻塞状态,一个是要求地址复用,我们来详细的看看:
第一个是修改标志位,即使用了F_GETFL,随后在通过fcntl进行控制修改为非阻塞状态,注意使用的是 |.
第二个函数则是设置地址和port的复用,即val ==1 ,将addr和prot复用。

选项主要作用
SO_REUSEADDR解决端口处于残留状态时 bind 失败的问题
SO_REUSEPORT允许多个进程/线程同时监听同一个端口

2.7 提供便捷的创建函数:

我们需要提供快捷创建客户端和服务器的函数,即结合上面封装的函数来完成:

  1. 创建服务器端:
1	bool CreateServer(uint16_t port, const std::string ip = "0.0.0.0", bool BLockFlag = false)  
2{  
3    if (Create() == false)  
4        return false;  
5    ReuseAdress(); // 复用  
6    if (BLockFlag)  
7        NoBlock();  
8    if (Bind(ip, port) == false)  
9        return false;  
10    if (Listen() == false)  
11        return false;  
12    return true;  
13}  

即先创建套接字,随后进行绑定,和监听。 2. 创建客户端:

1	bool CreateClient(std::string ip, uint16_t port)  
2{  
3    if (Create() == false)  
4        return false;  
5    if (Connect(ip, port) == false)  
6        return false;  
7    return true;  
8}  

这个就比较简单了,先创建套接字,随后进行连接就可以了。

最后关闭套接字,也放在这里了:

1    void Close()
2    {
3        ::close(_sockfd);
4        _sockfd = -1;
5    }
6

总结:

本篇把原生socket接口封装成一个可复用的Socket类,核心是把create/bind/listen/connect/accept/send/recv这套POSIX流程包成易用的成员函数。

  • socket本质是一个fd,背后对应内核缓冲区,是内核开凿的受控通道,让隔离的进程间安全交换数据。
  • create:用AF_INET+SOCK_STREAM建TCP套接字,SOCK_STREAM对应TCP、SOCK_DGRAM对应UDP。
  • Bind:填sockaddr_in(IP+端口)后绑定到fd。
  • Listen:开始监听,backlog控制待accept队列长度。
  • Connect/Accept:客户端主动连远端,服务端accept返回专门处理该连接的新fd。
  • Recv:>0成功;0对端挂断返回-1交上层shutdown/Release;EAGAIN/EWOULDBLOCK/EINTR返回0待重试。
  • Send:失败且阻塞返回0,等epoll再触发重发。
  • NoBlock+ReuseAdress:非阻塞防EventLoop被单连接卡死;SO_REUSEADDR治重启端口残留,SO_REUSEPORT允许多进程/线程监听同端口。
  • CreateServer/CreateClient:组合上述函数一键建端,Close负责收尾关闭。


muduo库 --socket的封装》 是转载文章,点击查看原文


相关推荐


【无标题】
Jiliang.Li2026/6/5

最近找到一个免费云服务器 近期在刷一套题,就想着找一个免费的云服务器,部署一个在线题库,有空的时候刷刷题,正好找到了这个 阿贝云https://www.abeiyun.com/,1G网站空间月5G流量50M数据库空间,而且重要的是 免费! 免费! 免费合适的很啊。随手写了一个刷题网站布上,看看效果: 希望阿贝云https://www.abeiyun.com/坚持住免费云服务啊,祝我冲100分


【C++】深入浅出,理解 C++ 奇异递归模板模式(CRTP)
PAK向日葵2026/5/30

在现代 C++ 中,除了基于虚函数的运行时多态,还有一种被称为"奇异递归模板模式"(Curiously Recurring Template Pattern,CRTP)的静态多态(调用目标在编译期确定),被广泛应用于 Chromium、V8、LLVM、UE 等大型项目当中。 1. 一个具体的例子 下面先给出一个我们最熟悉不过的基于虚函数的运行时多态的例子: #include <iostream> class Animal { public: virtual void Speak() =


手写 Mini React:从 JSX 到虚拟 DOM 再到 render,搞懂 React 底层原理
不会敲代码12026/5/8

手写 Mini React:从 JSX 到虚拟 DOM 再到 render,搞懂 React 底层原理 引言:为什么要手写 React? 我日常写 React 组件很熟练: function App() { return ( <div style={{ background: 'salmon' }}> <h1>Hello React</h1> <h2>Hello Didact</h2> </div> ); } 但写完之后脑子里总有几个问题挥之不去


MyBatis-Plus:让数据库操作飞起来的神器
小码哥_常2026/4/28

MyBatis-Plus:让数据库操作飞起来的神器 一、MyBatis-Plus 是什么? 在 Java 开发的世界里,数据库操作是不可或缺的一部分。MyBatis 作为一款优秀的持久层框架,深受开发者喜爱,而 MyBatis-Plus 则是在 MyBatis 基础上诞生的强大增强工具。它就像是给 MyBatis 这位 “武林高手” 配备了一套超级装备,让开发变得更加高效和便捷。 MyBatis-Plus 秉持着 “只做增强不做改变” 的设计理念 ,这意味着当你引入它到项目中时,就像给原有的 M


开源 Wiki 神器 Docmost:团队协作知识库的终极解决方案
修己xj2026/4/20

在团队协作中,文档管理始终是一个让人头疼的问题。传统的文档工具要么功能单一,要么价格昂贵,要么数据不在自己手里。今天,我要向大家推荐一款开源的协作式 Wiki 软件 —— Docmost。 什么是Docmost? Docmost 是一款开源的协作式 Wiki 和文档管理软件,专为团队知识管理而设计。它提供了实时协作、权限管理、空间隔离等企业级功能,同时保持了开源软件的透明性和可控性。 github 地址: github.com/docmost/doc… 文档地址: docmost.com/do


Laravel vs ThinkPHP3.x:现代框架对决
普通网友2026/4/11

好的,我们来比较一下 Laravel 和 ThinkPHP 3.x 这两个 PHP 框架的主要特点和差异。请注意,ThinkPHP 3.x 是一个相对较老的版本(ThinkPHP 已发展至 5.x/6.x/8.x),而 Laravel 则代表了更现代的 PHP 开发框架。 核心架构与设计理念 Laravel 设计哲学: 遵循 SOLID 原则和 DRY(Don't Repeat Yourself)原则。强调优雅、简洁、表达力强的代码。架构: 采用了 MVC(Model-View-Co


如何使用edge浏览器打开控制台?
宇宙计算机2026/4/3

如何使用edge浏览器打开控制台? 我们在edge浏览器中,点击F12,打开检查页面。(或者在页面上,鼠标右键 -> 工具) 但是我们点开检查页面以后,有的时候我们的屏幕过小,上面没有中文,我们不知道哪一个才是控制台。 那看下图,下图这个图标就是控制台。 然后我们就能在控制台的右下角键入JavaScript代码了


Linux 基础指令与权限管理完全指南
凤年徐2026/3/26

Linux 基础指令与权限管理完全指南 前言 在开始Linux系统编程之前,掌握常用的Linux命令和权限管理是必不可少的一步。本文将从零开始,带你熟悉Linux环境下的基本操作,包括文件管理、目录操作、文本查看、权限设置等核心内容。无论你是刚接触Linux的新手,还是希望系统梳理知识的老手,这篇文章都能为你提供清晰的指引。 思考:为什么生产环境中很少使用Linux的图形界面?因为图形界面会占用大量系统资源,且远程管理时效率低下,命令行才是Linux的精髓。 一、环境准备与远程登录 1. 查


mcp学习笔记(三)-Mcp传输协议代码示例
Shawn_Shawn2026/3/18

一、三种传输方式详解 1. Stdio 传输 1.1 原理说明 Stdio(标准输入输出)传输是最简单的传输方式。MCP Client 将Server 作为 子进程 启动,通过进程的 stdin 和 stdout 进行双向通信。 关键规则: 每条JSON-RPC 消息以 换行符 \n 分隔 stdout 专用于协议消息,日志/调试信息必须输出到 stderr Client 与 Server 一对一绑定,生命周期同步 1.2 原理图(draw.io) 1.3 交互流程图(draw.io 时


奔图M6200-6500系列打印机拆解教程?
喝酒没肉不香2026/3/9

1.后盖 取下 1 处两个螺丝,撬开 2 的卡爪,取下后盖。 2.左盖 拆下后盖后,撬开 1 处两个卡爪,取下左盖。 3.右盖 拆下后盖后,撬开 1、2 处 6 个卡爪,取下右盖。 4.扫描台 4.1拆下左盖后,取下 1 处的螺丝,拔除 2 处的三根连接线。 4.2取下 1 处的卡环,拔除支撑杆固定销,取下扫描台。 5.引擎板 拆除右盖后,拔除 1 处,取下 2 处螺丝,取下引擎板。 7.高压电源板 7.1取下引擎板后,取下

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读