本系列主要旨在帮助初学者学习和巩固 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 大致的流程:

所以,我们需要提供一些封装好的函数来完成调用,这样便于后面的操作:
- 创建套接字
- 开始绑定
- 开始监听
- 进行连接
- 接收套接字,放回服务员
- 接收不同的消息
- 发送消息
- 设置非阻塞和套接字复用
- 快捷创建客户端和服务器
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的返回值我们一般做出
- > 0 ,成功了
- 等于 0 ,说明对端进行挂断,我们的返回值设为-1,交给上层处理
- 如果是被信号打断,或者是阻塞住了,那么返回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 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的封装》 是转载文章,点击查看原文。