6. Socket
约 2529 字大约 8 分钟
2026-04-14
Socket 简介
Socket 类其实相当于一个 socket fd 的封装器,它把底层的套接字文件描述符和一组常用的 socket 操作封装到一起,让上层代码不用直接到处写 bind、listen、accept、shutdown、setsockopt 这些系统调用。
在 TCP 网络编程中,socket 是最底层的通信句柄。
Socket 类做的事情就是:
- 持有一个
sockfd - 提供绑定地址、监听、接受连接、关闭写端等接口
- 提供常见 socket 选项设置接口
- 在析构时自动关闭 fd,避免资源泄漏
可以把它理解为:
- fd:真正干活的套接字
- Socket:套接字的封装壳子,负责把常用操作组织起来
Socket 类重要的成员变量
1. 重要成员
const int sockfd_:这个 Socket 对象所管理的文件描述符。
它就是底层真正的 socket fd,后面的bind()、listen()、accept()、shutdown()等操作都围绕它展开。
private:
const int sockfd_;提示
为什么要用 const int?
因为一个 Socket 创建之后,它对应的 fd 通常不应该被随意改掉。这能让“一个 Socket 对应一个 fd”的关系更清晰,也更安全。
Socket 类重要的成员方法
1. 构造 / 析构
explicit Socket(int sockfd)
: sockfd_(sockfd)
{
}
~Socket();Socket::~Socket()
{
::close(sockfd_);
}构造函数非常简单,只是把传进来的 sockfd 保存到成员变量里。
explicit 可以防止编译器把一个 int 隐式转换成 Socket 对象,避免误用。
析构函数的作用
析构时调用 close(sockfd_),把 socket fd 关闭掉。
这是一种典型的 RAII 思想:对象活着,资源就存在;对象销毁,资源自动释放。
1. fd()
int fd() const { return sockfd_; }这个函数用于返回 Socket 管理的 fd。
上层如果想把这个 socket 交给 Channel、Poller 或其他模块,就可以通过 fd() 拿到真正的文件描述符。
2. bindAddress(const InetAddress &localaddr)
void bindAddress(const InetAddress &localaddr);void Socket::bindAddress(const InetAddress &localaddr)
{
if (0 != ::bind(sockfd_, (sockaddr *)localaddr.getSockAddr(), sizeof(sockaddr_in)))
{
LOG_FATAL("bind sockfd:%d fail\n", sockfd_);
}
}这个函数的作用是把 socket 绑定到指定的本地地址和端口上。
bind() 告诉内核:这个 socket 要使用哪个 IP 和端口来提供服务。
提示
服务器启动时,把监听 socket 绑定到本地地址,例如 0.0.0.0:8888,失败时怎么办?
如果 bind() 失败,直接打印 fatal 日志。这通常意味着地址被占用、权限不足或者传入地址有问题。
3. listen()
void listen();void Socket::listen()
{
if (0 != ::listen(sockfd_, 1024))
{
LOG_FATAL("listen sockfd:%d fail\n", sockfd_);
}
}这个函数把 socket 变成监听 socket。
调用 listen() 之后,这个 socket 就不再只是普通 socket,而是开始等待客户端连接请求。
提示
监听队列长度 1024
这里的 1024 是 backlog,表示内核为这个监听 socket 维护的连接等待队列长度上限。
4. accept(InetAddress *peeraddr)
int accept(InetAddress *peeraddr);int Socket::accept(InetAddress *peeraddr)
{
/**
* 1. accept函数的参数不合法
* 2. 对返回的connfd没有设置非阻塞
* Reactor模型 one loop per thread
* poller + non-blocking IO
**/
sockaddr_in addr;
socklen_t len = sizeof(addr);
::memset(&addr, 0, sizeof(addr));
// fixed : int connfd = ::accept(sockfd_, (sockaddr *)&addr, &len);
int connfd = ::accept4(sockfd_, (sockaddr *)&addr, &len, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (connfd >= 0)
{
peeraddr->setSockAddr(addr);
}
return connfd;
}这个函数用于从监听 socket 上接收一个新连接。
它做了什么
- 调用
accept4()接收客户端连接; - 得到新的连接 fd connfd;
- 把对端地址写到 peeraddr;
- 返回这个新连接 fd。
提示
为什么用 accept4()?
因为它可以在接收连接的同时直接设置:
SOCK_NONBLOCK:非阻塞 SOCK_CLOEXEC:exec 时自动关闭
这样能减少后续再调用 fcntl() 设置属性的步骤。
提示
为什么返回 int connfd?
因为新连接和监听 socket 是两个不同的 fd。 监听 socket 继续负责接新连接,而 connfd 交给后续 Channel、TcpConnection 继续管理。
6. shutdownWrite()
void shutdownWrite();void Socket::shutdownWrite()
{
if (::shutdown(sockfd_, SHUT_WR) < 0)
{
LOG_ERROR("shutdownWrite error");
}
}这个函数用于关闭 socket 的写方向。
shutdown(sockfd_, SHUT_WR) 表示:
我这边不再发送数据了,但还可以继续接收对方发来的数据。
常见场景:TCP 连接半关闭,服务端发送完最后一批数据后,优雅关闭写端。
7. setTcpNoDelay(bool on)
void setTcpNoDelay(bool on);void Socket::setTcpNoDelay(bool on)
{
// TCP_NODELAY 用于禁用 Nagle 算法。
// Nagle 算法用于减少网络上传输的小数据包数量。
// 将 TCP_NODELAY 设置为 1 可以禁用该算法,允许小数据包立即发送。
int optval = on ? 1 : 0;
::setsockopt(sockfd_, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval));
}这个函数用于设置是否禁用 Nagle 算法。
- on = true:禁用 Nagle 算法,小包尽快发送
- on = false:启用 Nagle 算法,可能会合并小包发送
提示
为什么常常要禁用?
在低延迟场景下,禁用 Nagle 可以减少延迟。 例如实时通信、RPC、交互式应用等。
8. setReuseAddr(bool on)
void setReuseAddr(bool on);void Socket::setReuseAddr(bool on)
{
// SO_REUSEADDR 允许一个套接字强制绑定到一个已被其他套接字使用的端口。
// 这对于需要重启并绑定到相同端口的服务器应用程序非常有用。
int optval = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
}这个函数用于设置端口复用。
允许 socket 绑定到一个已经被占用或处于 TIME_WAIT 状态的地址端口,常用于服务重启场景。
常见用途:服务器重启后快速重新绑定同一端口,避免 TIME_WAIT 导致无法立即启动
9. setReusePort(bool on)
void setReusePort(bool on);void Socket::setReusePort(bool on)
{
// SO_REUSEPORT 允许同一主机上的多个套接字绑定到相同的端口号。
// 这对于在多个线程或进程之间负载均衡传入连接非常有用。
int optval = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
}这个函数用于设置端口共享。
允许多个 socket 绑定同一个地址和端口,内核可以帮助它们分担连接负载。
常见用途:多线程 / 多进程网络服务器,负载均衡监听连接
10. setKeepAlive(bool on)
void setKeepAlive(bool on);void Socket::setKeepAlive(bool on)
{
// SO_KEEPALIVE 启用在已连接的套接字上定期传输消息。
// 如果另一端没有响应,则认为连接已断开并关闭。
// 这对于检测网络中失效的对等方非常有用。
int optval = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));
}这个函数用于开启 TCP keepalive。
当连接长期没有数据时,内核会定期发送探测包,用于检测对端是否还活着。
常见用途:长连接,需要检测死连接的场景
Socket 和上层模块的关系
可以这样理解:
- Socket 负责底层 socket 系统调用;
- Channel 负责监听 socket 上是否有事件;
- EventLoop / Poller 负责把事件挂到内核并分发;
- Acceptor、TcpConnection 等上层模块则在此基础上完成服务器逻辑。
Socket 的整体工作流程
1. 创建 socket
Socket sockfd(fd);
把底层 fd 封装起来。
2. 配置 socket
通常会设置:
setReuseAddr(true)setReusePort(true)setTcpNoDelay(true)setKeepAlive(true)
3. 绑定和监听
服务器启动时:
sock.bindAddress(addr);sock.listen();
这样就能开始接收客户端连接。
4. 接收连接
当监听 socket 可读时:int connfd = sock.accept(&peerAddr);
得到一个新的连接 fd,后续交给 Channel 和 TcpConnection 处理。
5. 关闭写端或关闭连接
需要半关闭写方向时调用:sock.shutdownWrite();
对象销毁时自动关闭 fd。
总结
Socket 的核心职责
- 封装一个 socket fd
- 提供常用系统调用接口
- 负责资源生命周期管理
- 作为上层网络模块的底层基础
Socket 是对底层 socket fd 的轻量封装,负责把 bind、listen、accept、shutdown 和常用 socket 选项统一管理起来。
Socket 源码
#pragma once
#include "noncopyable.h"
class InetAddress;
// 封装socket fd
class Socket : noncopyable
{
public:
explicit Socket(int sockfd)
: sockfd_(sockfd)
{
}
~Socket();
int fd() const { return sockfd_; }
void bindAddress(const InetAddress &localaddr);
void listen();
int accept(InetAddress *peeraddr);
void shutdownWrite();
void setTcpNoDelay(bool on);
void setReuseAddr(bool on);
void setReusePort(bool on);
void setKeepAlive(bool on);
private:
const int sockfd_;
};#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include "Socket.h"
#include "Logger.h"
#include "InetAddress.h"
Socket::~Socket()
{
::close(sockfd_);
}
void Socket::bindAddress(const InetAddress &localaddr)
{
if (0 != ::bind(sockfd_, (sockaddr *)localaddr.getSockAddr(), sizeof(sockaddr_in)))
{
LOG_FATAL("bind sockfd:%d fail\n", sockfd_);
}
}
void Socket::listen()
{
if (0 != ::listen(sockfd_, 1024))
{
LOG_FATAL("listen sockfd:%d fail\n", sockfd_);
}
}
int Socket::accept(InetAddress *peeraddr)
{
/**
* 1. accept函数的参数不合法
* 2. 对返回的connfd没有设置非阻塞
* Reactor模型 one loop per thread
* poller + non-blocking IO
**/
sockaddr_in addr;
socklen_t len = sizeof(addr);
::memset(&addr, 0, sizeof(addr));
// fixed : int connfd = ::accept(sockfd_, (sockaddr *)&addr, &len);
int connfd = ::accept4(sockfd_, (sockaddr *)&addr, &len, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (connfd >= 0)
{
peeraddr->setSockAddr(addr);
}
return connfd;
}
void Socket::shutdownWrite()
{
if (::shutdown(sockfd_, SHUT_WR) < 0)
{
LOG_ERROR("shutdownWrite error");
}
}
void Socket::setTcpNoDelay(bool on)
{
// TCP_NODELAY 用于禁用 Nagle 算法。
// Nagle 算法用于减少网络上传输的小数据包数量。
// 将 TCP_NODELAY 设置为 1 可以禁用该算法,允许小数据包立即发送。
int optval = on ? 1 : 0;
::setsockopt(sockfd_, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval));
}
void Socket::setReuseAddr(bool on)
{
// SO_REUSEADDR 允许一个套接字强制绑定到一个已被其他套接字使用的端口。
// 这对于需要重启并绑定到相同端口的服务器应用程序非常有用。
int optval = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
}
void Socket::setReusePort(bool on)
{
// SO_REUSEPORT 允许同一主机上的多个套接字绑定到相同的端口号。
// 这对于在多个线程或进程之间负载均衡传入连接非常有用。
int optval = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
}
void Socket::setKeepAlive(bool on)
{
// SO_KEEPALIVE 启用在已连接的套接字上定期传输消息。
// 如果另一端没有响应,则认为连接已断开并关闭。
// 这对于检测网络中失效的对等方非常有用。
int optval = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));
}