Socket 是对 Linux 系统 socket 文件描述符(fd)的 C++ 封装,专门用来管理 “监听 fd” 和 “连接 fd”,提供一套方便、安全、面向对象的接口。

Socket.h

1
2
3
4
5
6
7
#pragma once

#include "noncopyable.h"

class InetAddress;

class Socket : noncopyable
1
2
private:
const int sockfd_;
  • socket文件描述符
  • const保证一个 Socket 对象,只对应一个 fd,改不了
1
2
3
4
5
explicit Socket(int sockfd) : sockfd_(sockfd) {}
~Socket();

int fd() const { return sockfd_; }
void shutdownWrite();
  • const 成员变量只能在初始化列表赋值,不能在函数体内赋值。

  • 不加explicit可能会发生的事:本想传 Socket,结果误传了个 int,还编译通过

    • void func(Socket sock) { // 操作 sock }
    • func(100); // 编译通过不加 explicit,可以直接传 int 进去
    • func(Socket(100));编译器偷偷做了这件事
  • void shutdownWrite();优雅关闭写端,发送 FIN 包,不直接 close

1
2
3
void bindAddress(const InetAddress& localAddr);
void listen();
int accept(InetAddress* peerAddr);

封装bind()、listen()accept()`相关函数。

为什么bindAddress(const InetAddress& localAddr)const &

  • bind 只需要读取 IP 和端口,不会修改地址。

  • 安全上:引用不能为空

    • const T& 一定合法,不可能是空
    • 指针可能是 nullptr,还要判断,麻烦又不安全
  • C++ 优先用 const & 传递只读对象

为什么accept(InetAddress* peerAddr)传指针?

  • 要把客户端地址写出去,属于输出型参数。

  • accept 获取客户端地址 是可选的!

    • 有时候我只想拿 connfd,不关心客户端 IP
    • 指针可以传 nullptr 表示 “我不要地址”
    • 引用不能为 null,必须传一个合法对象
1
2
3
4
void setTcpNoDelay(bool on);    // 关闭Nagle算法 → 低延迟
void setReuseAddr(bool on); // 地址复用 → 解决重启报错
void setReusePort(bool on); // 端口复用 → 多核负载均衡
void setKeepAlive(bool on); // TCP 保活 → 防止死连接

Socket 属性配置

Socket.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Socket.h"
#include "InetAddress.h"
#include "Logger.h"

#include <errno.h>
#include <sys/socket.h>
#include <netinet/tcp.h> //TCP套接字选项(例:TCP_NODELAY...)
#include <unistd.h>
#include <strings.h>

Socket::~Socket()
{
::close(sockfd_);
}

析构中关闭文件描述符

1
2
3
4
5
6
7
void Socket::bindAddress(const InetAddress& localAddr)
{
if(0 != ::bind(sockfd_, (sockaddr*)localAddr.getSockAddr(), sizeof(sockaddr_in)))
{
LOG_FATAL("bind socket : {} failed, errno : {}", sockfd_, errno);
}
}
  • getSockAddr() 返回的是 sockaddr_in*强转bind所需的sockaddr*类型。

  • muduo设计的日志库打印FATAL级别日志会自动捕捉errno,但我们实现的日志库没有,所以我们自行打印一下errno

  • 为什么是 sizeof(sockaddr_in)
    因为我们用的是 IPv4,真实结构体大小就是 sockaddr_in,不能用别的,写死最安全。

1
2
3
4
5
6
7
void Socket::listen()
{
if(0 != ::listen(sockfd_, 1024))
{
LOG_FATAL("listen socket : {} failed, errno : {}", sockfd_, errno);
}
}

listen()第二个参数的讲解参考socket编程中的讲解。

1
2
3
4
5
6
7
8
9
10
11
12
int Socket::accept(InetAddress* peerAddr)
{
sockaddr_in addr = { 0 };
socklen_t len = sizeof addr;
bzero(&addr, sizeof addr);
int connfd = accept4(sockfd_, (sockaddr*)&addr, &len, SOCK_NONBLOCK | SOCK_CLOEXEC);
if(connfd > 0)
{
peerAddr->setSockAddr(addr);
}
return connfd;
}
  • addr的初始化muduo用的是bzero()(是 BSD 废弃函数);memset 是 C 标准函数。

  • 现代 C++推荐:

    1
    2
    sockaddr_in addr{};
    sockaddr_in addr = { 0 };
  • 使用 accept4 而不是 accept,可以原子设置非阻塞 + CLOEXEC

    • SOCK_NONBLOCK:Reactor 模型必须非阻塞 IO。

    • SOCK_CLOEXEC:防止 fork/exec 时 fd 泄漏。

  • accept 里为什么不判断 connfd < 0?

    • 非阻塞模式下,没有连接时返回 -1 是正常现象
    • 不能 LOG_FATAL,否则服务器会直接崩溃。
    • 错误处理交给上层 Acceptor,Socket 只负责返回 -1。
1
2
3
4
5
6
7
void Socket::shutdownWrite()
{
if(::shutdown(sockfd_, SHUT_WR) < 0)
{
LOG_ERROR("shutdownWrite failed");
}
}
  • ::shutdown(sockfd_, SHUT_WR)不能再发送数据但还能继续接收对方数据

    • ::shutdown:系统调用,关闭 TCP 连接的某个方向
    • SHUT_WRWrite Only → 只关闭写端
  • 为什么用 shutdown,不用 close?

    对比项 close(fd) shutdown(fd, SHUT_WR)
    关闭范围 直接关闭整个 socket,读写全关 只关闭写方向,读方向保持打开
    对 TCP 的影响 可能直接发 RST,强制断开 发送 FIN,告知对方不再发送数据
    数据安全性 缓冲区未发完数据可能丢失 保证发送缓冲区数据发完,不丢包
    连接状态 完全释放 fd,连接彻底断开 半关闭状态,还能继续接收对方数据
    使用场景 彻底不要这个连接时使用 优雅结束发送,等待对方回复时使用
  • 为什么失败只 LOG_ERROR,不LOG_FATAL?

    因为:可能连接已经断开,可能 fd 已经关闭,这些都不是致命错误程序不需要崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Socket::setTcpNoDelay(bool on)
{
int optval = on ? 1 : 0;
::setsockopt(sockfd_, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof optval);
}

void Socket::setReuseAddr(bool on)
{
int optval = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval);
}

void Socket::setReusePort(bool on)
{
int optval = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof optval);
}

void Socket::setKeepAlive(bool on)
{
int optval = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof optval);
}
  • int optval = on ? 1 : 0;把 C++ 的 bool(true/false) 转成系统调用需要的 int(1/0)setsockopt 只识别 1 开启、0 关闭。

  • ::setsockopt(
        sockfd_,        // 哪个 socket
        层级,           // IP层 或 Socket层
        选项名字,       // 要设置的开关
        &optval,        // 开/关
        sizeof(optval)  // 选项大小
    );
    
  • setTcpNoDelay关闭 Nagle 攒包算法

    • 默认 TCP 会等一会儿,攒一批再发
    • 打开 NoDelay = 有数据立刻发,不等待
    • 开启场景:游戏、聊天、RPC等低延时场景
  • setReuseAddr:允许端口快速复用,解决服务器重启报错

    • TCP 有个状态叫 TIME_WAIT,端口会被占住 30 秒~2 分钟。
    • 加了 setReuseAddr (true) 后:允许立刻绑定正在 TIME_WAIT 的端口
  • setReusePort:多个进程 / 线程可以 bind 同一个端口(正常1 个端口 = 只能 1 个程序监听),内核自动负载均衡提高并发性能。

  • setKeepAlive开启 TCP 保活

    • 自动检测死连接(客户端断电、断网)
    • 防止占着 fd 不释放

源码地址

Socket.h:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/Socket.h

Socket.cc:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/Socket.cc