Socket基础概念

socket基础概念

在网络编程中,Socket(套接字) 是进程间通信的抽象接口,用于实现不同主机或同一主机内不同进程之间的网络数据传输。它是 TCP/IP 协议栈的编程接口,现代 C++ 网络开发中依然是核心基础(尽管常被封装为更高层库如 Boost.Asio)。

一、Socket 核心概念

  1. 本质:Socket 是一个 “文件描述符”(Linux/Unix)或 “句柄”(Windows),代表一个网络通信端点,通过它可进行数据的发送 / 接收。

  2. 分类

    • 流式套接字(SOCK_STREAM):基于 TCP 协议,提供可靠、有序、双向的字节流传输(无数据丢失 / 重复,适合文件传输、HTTP 等)。
    • 数据报套接字(SOCK_DGRAM):基于 UDP 协议,提供无连接、不可靠的数据包传输(速度快,适合视频通话、DNS 等)。
    • 原始套接字(SOCK_RAW):直接操作底层协议(如 IP),用于网络诊断或自定义协议(需管理员权限)。
  3. 通信模型

    • TCP 采用 “客户端 - 服务器” 模型,需先建立连接(三次握手),再传输数据。
    • UDP 无连接,直接发送数据包,无需提前建立连接。

二、核心函数解析(基于 TCP,现代 C++ 视角)

以下函数是 TCP 通信的核心,主要用于服务器端和客户端的连接建立过程。

1. socket():创建套接字

作用:初始化一个套接字,返回其描述符(后续操作的句柄)。

函数原型

1
2
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数解析

  • domain:地址族(协议族),指定网络层协议。常见值:

    • AF_INET:IPv4 协议(最常用)。
    • AF_INET6:IPv6 协议。
    • AF_UNIX:本地进程间通信(Unix 域套接字)。
  • type:套接字类型,指定传输层协议特性:

    • SOCK_STREAM:TCP 流式套接字。
    • SOCK_DGRAM:UDP 数据报套接字。
  • protocol:具体协议(通常为 0,由系统根据前两个参数自动选择)。

返回值:成功返回非负套接字描述符(sockfd),失败返回 - 1(需检查errno)。

现代 C++ 实践:用智能指针(如std::unique_ptr)配合自定义删除器管理sockfd,避免资源泄漏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 自定义删除器:关闭套接字
auto socket_deleter = [](int fd) {
if (fd != -1) ::close(fd);
};
using UniqueSocket = std::unique_ptr<int, decltype(socket_deleter)>;

// 创建套接字
UniqueSocket create_socket() {
int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
throw std::runtime_error("socket creation failed: " + std::string(strerror(errno)));
}
return UniqueSocket(new int(sockfd), socket_deleter);
}

2. bind():绑定地址与端口

作用:将套接字与特定的 IP 地址和端口号绑定(主要用于服务器端,客户端通常不绑定,由系统自动分配端口)。

函数原型

1
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

参数解析

  • sockfdsocket()返回的套接字描述符。

  • addr:指向地址结构的指针(需根据domain选择对应类型,如 IPv4 用sockaddr_in)。

  • addrlen:地址结构的大小(字节数)。

地址结构处理(以 IPv4 为例):sockaddr是通用地址结构,实际使用时需转换为具体协议的结构(如sockaddr_in):

1
2
3
4
5
6
struct sockaddr_in {
sa_family_t sin_family; // 地址族(必须为AF_INET)
uint16_t sin_port; // 端口号(需转换为网络字节序)
struct in_addr sin_addr; // IP地址(需转换为网络字节序)
char sin_zero[8]; // 填充字段(通常为0)
};

字节序转换:网络协议使用大端字节序(网络字节序),而主机可能是小端 / 大端,需用函数转换:

  • htons():主机字节序→网络字节序(16 位,用于端口)。

  • htonl():主机字节序→网络字节序(32 位,用于 IPv4 地址)。

  • inet_pton():字符串 IP(如 “127.0.0.1”)→网络字节序的整数(现代 C++ 推荐,替代旧的inet_addr())。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 绑定到IPv4地址127.0.0.1:8080
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 端口转换为网络字节序

// IP地址转换("0.0.0.0"表示监听所有网络接口)
if (inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr) <= 0) {
throw std::runtime_error("invalid IP address: " + std::string(strerror(errno)));
}

// 绑定
if (::bind(sockfd, reinterpret_cast<struct sockaddr*>(&server_addr), sizeof(server_addr)) == -1) {
throw std::runtime_error("bind failed: " + std::string(strerror(errno)));
}

3. listen():监听连接(服务器端)

作用:将套接字转为 “监听状态”,允许接收客户端的连接请求(仅用于 TCP 服务器端的sockfd)。

函数原型

1
int listen(int sockfd, int backlog);

参数解析

  • sockfd:已绑定的套接字描述符(bind()之后)。

  • backlog:未完成连接队列(三次握手未完成)的最大长度(超过则客户端连接失败)。实际值受系统限制(如/proc/sys/net/core/somaxconn),现代系统通常建议设为 128 或更高。

返回值:成功返回 0,失败返回 - 1。

注意listen()不阻塞,仅设置套接字状态。后续需通过accept()获取实际连接。

4. accept():接受连接(服务器端)

作用:从listen()创建的连接队列中取出一个已完成的连接,返回一个新的套接字描述符(用于与该客户端通信)。

函数原型

1
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);

参数解析

  • sockfd:处于监听状态的套接字(监听套接字)。

  • addr:输出参数,用于存储客户端的地址信息(可设为nullptr忽略)。

  • addrlen:输入输出参数,传入addr的大小,输出实际存储的地址长度(可设为nullptr)。

返回值:成功返回新的套接字描述符(client_fd,用于与客户端通信),失败返回 - 1。

特性accept()默认是阻塞函数,直到有客户端连接到达才返回。现代 C++ 中可结合非阻塞 I/O 或多线程处理(如用std::thread为每个新连接创建线程)。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);

// 阻塞等待客户端连接
int client_fd = ::accept(listen_fd, reinterpret_cast<struct sockaddr*>(&client_addr), &client_addr_len);
if (client_fd == -1) {
throw std::runtime_error("accept failed: " + std::string(strerror(errno)));
}

// 打印客户端信息(将网络字节序转为字符串)
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
uint16_t client_port = ntohs(client_addr.sin_port); // 网络字节序→主机字节序
std::cout << "New connection from " << client_ip << ":" << client_port << std::endl;

5. connect():发起连接(客户端)

作用:客户端通过该函数向服务器发起 TCP 连接(三次握手的触发点)。

函数原型

1
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

参数解析

  • sockfd:客户端的套接字描述符(socket()创建,通常不bind())。

  • addr:服务器的地址结构(包含服务器 IP 和端口)。

  • addrlen:服务器地址结构的大小。

返回值:成功返回 0(连接建立),失败返回 - 1(如服务器未监听、网络不可达等)。

特性:默认是阻塞函数,直到连接建立或失败才返回。非阻塞模式下,可能返回EINPROGRESS(连接正在建立),需通过select()/poll()等检查状态。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 客户端连接服务器(127.0.0.1:8080)
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);

// 转换服务器IP
if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
throw std::runtime_error("invalid server IP");
}

// 发起连接
if (::connect(client_fd, reinterpret_cast<struct sockaddr*>(&server_addr), sizeof(server_addr)) == -1) {
throw std::runtime_error("connect failed: " + std::string(strerror(errno)));
}

三、现代 C++ 最佳实践

  1. 资源管理:用 RAII(如智能指针、自定义封装类)管理sockfd,避免手动close()导致的泄漏。

  2. 错误处理:用异常(std::runtime_error)替代传统的if-else检查,提高代码可读性。

  3. 类型安全:用reinterpret_cast显式转换地址结构,避免隐式转换错误。

  4. 异步操作:结合 Boost.Asio 或 C++20 的std::net(提案阶段)实现非阻塞 I/O,避免线程阻塞。

  5. 跨平台:封装系统差异(如 Windows 的WSASend与 Linux 的send),用条件编译或跨平台库。

四、总结

socket()/bind()/listen()/accept()/connect()是 TCP 通信的 “骨架”:服务器通过socket()创建套接字→bind()绑定地址→listen()监听→accept()接受连接;客户端通过socket()创建套接字→connect()连接服务器。理解这些函数的参数和交互流程,是掌握网络编程的基础。现代 C++ 通过封装和抽象(如智能指针、异常),可让这些底层操作更安全、易维护。

附: socket 选项

选项名 数据类型 功能描述 可操作(get/set) 备注
SOL_SOCKET 级别(通用选项)
SO_DEBUG int (0 / 非 0) 启用调试模式,内核记录套接字调试信息 get/set 仅超级用户可设置
SO_REUSEADDR int (0 / 非 0) 允许重用本地地址和端口(如 TIME_WAIT 状态端口复用) get/set 需在 bind 前设置,解决 “地址已被使用” 错误
SO_REUSEPORT int (0 / 非 0) 允许多个套接字绑定到同一端口(需同 UID,内核分发数据) get/set 部分系统支持(如 Linux 3.9+),常用于多进程监听同一端口
SO_KEEPALIVE int (0 / 非 0) 启用 TCP 保活机制,定期检测对端是否存活 get/set 超时时间由系统配置(通常 2 小时无活动后触发)
SO_DONTROUTE int (0 / 非 0) 禁止路由,数据仅在本地子网传输 get/set 用于本地通信或诊断
SO_BROADCAST int (0 / 非 0) 允许发送广播报文 get/set 仅适用于 UDP,需在 sendto 前设置
SO_SNDBUF int 设置 / 获取发送缓冲区大小(字节) get/set 内核可能调整实际大小(不超过系统上限)
SO_RCVBUF int 设置 / 获取接收缓冲区大小(字节) get/set 影响接收性能和拥塞控制,建议在 connect/bind 前设置
SO_SNDTIMEO struct timeval 设置发送操作(send/sendto)的超时时间 get/set 超时后返回 EAGAIN/EWOULDBLOCK
SO_RCVTIMEO struct timeval 设置接收操作(recv/recvfrom)的超时时间 get/set 同 SO_SNDTIMEO
SO_TYPE int 获取套接字类型(如 SOCK_STREAM、SOCK_DGRAM) get(不可 set) 用于判断套接字是 TCP 还是 UDP
SO_ERROR int 获取套接字上的待处理错误(如连接失败原因) get(不可 set) 常用于非阻塞 connect 后的错误检查
SO_LINGER struct linger 控制 close 时的延迟关闭行为(等待未发送数据) get/set struct linger {int l_onoff; int l_linger;},l_onoff=1 启用,l_linger 为等待秒数
SO_PASSCRED int (0 / 非 0) 允许接收辅助数据中的凭证信息(如 UID/GID) get/set 用于 Unix 域套接字的权限验证
SO_PEERCRED struct ucred 获取对端 Unix 域套接字的用户凭证(UID/GID/PID) get(不可 set) 仅适用于已连接的 Unix 域套接字
IPPROTO_IP 级别(IPv4 选项)
IP_HDRINCL int (0 / 非 0) 应用程序自行构造 IP 头部,不依赖内核 get/set 仅超级用户可设置,用于自定义 IP 报文
IP_OPTIONS void * 设置 / 获取 IP 选项(如源路由、记录路由) get/set 选项数据需符合 IP 协议规范,长度有限制
IP_TOS int 设置 / 获取 IP 服务类型(TOS),控制路由优先级 get/set 值为 0~255,具体含义见 RFC 规范
IP_TTL int 设置 / 获取 IP 生存时间(TTL),限制报文转发跳数 get/set 通常设为 64、128 等,超过 TTL 则报文被丢弃
IP_MULTICAST_TTL u_char 设置多播报文的 TTL 值 get/set 控制多播范围(0 = 本地,1 = 同一子网,…,255 = 全局)
IP_MULTICAST_LOOP u_char (0/1) 启用多播报文回环(本地发送的多播自己能否接收) get/set 默认 1(启用),多播应用需根据需求关闭
IP_ADD_MEMBERSHIP struct ip_mreq 加入多播组 set(不可 get) struct ip_mreq 包含多播组 IP 和本地接口 IP
IP_DROP_MEMBERSHIP struct ip_mreq 退出多播组 set(不可 get) 同 IP_ADD_MEMBERSHIP
IPPROTO_TCP 级别(TCP 选项)
TCP_NODELAY int (0 / 非 0) 禁用 Nagle 算法(立即发送小数据包,不延迟合并) get/set 适用于低延迟场景(如实时通信),可能增加网络拥塞
TCP_MAXSEG int 设置 / 获取 TCP 最大段大小(MSS) get/set 通常由内核协商,手动设置需谨慎
TCP_CORK int (0 / 非 0) 启用 “塞子” 模式,累积数据后一次性发送(类似 Nagle 但更激进) get/set 适用于批量数据发送,需在完成后关闭(避免延迟)
TCP_KEEPIDLE int 设置 TCP 保活探测的空闲时间(秒) get/set 需 SO_KEEPALIVE 启用,默认 7200 秒(2 小时)
TCP_KEEPINTVL int 设置 TCP 保活探测的间隔时间(秒) get/set 探测未响应时,间隔多久再次发送,默认 75 秒
TCP_KEEPCNT int 设置 TCP 保活探测的最大重试次数 get/set 超过次数则判定对端不可达,默认 9 次

说明

  1. 选项级别说明:SOL_SOCKET为通用选项,IPPROTO_IP为 IPv4 协议选项,IPPROTO_TCP为 TCP 协议选项,还有IPPROTO_UDP(UDP 选项,较少用)等。

  2. 部分选项为系统特定(如SO_REUSEPORT在不同 OS 上行为可能不同),使用前需参考目标系统的man手册。

  3. 多播相关选项(如IP_ADD_MEMBERSHIP)需结合多播地址(224.0.0.0~239.255.255.255)使用。