一,基本函数
在 C++ 的网络编程中,socket 是实现网络通信的基础。以下是针对 TCP 和 UDP 协议中服务端与客户端常用的 socket 相关函数的介绍。TCP 是面向连接的协议,保证数据传输的可靠性;而 UDP 是无连接的协议,简单快速,但不保证可靠传输。
| 函数 | TCP 服务端 | TCP 客户端 | UDP 服务端 | UDP 客户端 |
|---|---|---|---|---|
socket() |
创建一个新的 socket,返回一个文件描述符 | 创建一个新的 socket,返回一个文件描述符 | 创建一个新的 socket,用于发送和接收数据 | 创建一个新的 socket,用于发送和接收数据 |
bind() |
将本地地址绑定到 socket 上,指定本地端口 | 通常可选,用于绑定到特定的本地端口(不常用) | 将本地地址绑定到 socket 上,用于监听特定端口 | 通常可选,用于绑定到特定的本地端口(不常用) |
listen() |
监听绑定的网络端口,准备接受客户端连接 | – | — | — |
accept() |
阻塞等待,接受来自客户端的连接,返回新的 socket 文件描述符 | – | — | — |
connect() |
— | 连接到远程服务器的地址和端口 | — | — |
sendto() |
— | — | 用于在无连接模式下发送数据到指定的远程地址和端口 | 发送数据到指定的服务器地址和端口 |
recvfrom() |
— | — | 用于接收远程发送来的数据,同时获取发送者的地址和端口 | 接收来自服务器的数据,同时获得服务器的地址和端口 |
send() |
发送数据到已连接的 socket | 发送数据到已连接的 socket | (需要bind,不常用) | (需要bind,不常用) |
recv() |
从已连接的 socket 接收数据 | 从已连接的 socket 接收数据 | (需要bind,不常用) | (需要bind,不常用) |
说明:
socket() TCP & UDP: 使用时需要指定协议类型(socket(AF_INET, SOCK_STREAM, 0) 为 TCP, socket(AF_INET, SOCK_DGRAM, 0) 为 UDP)。
bind() TCP & UDP: 此步骤是将端口和地址分配给 socket,使其确定数据交换的网络入口点。
listen() TCP: 初始化服务器以接受连接请求。
accept() TCP: 为每个连接请求,此函数都会创建一个新的已连接 socket,专用于与请求的客户端进行通信。
connect() TCP: 用于客户端建立与服务器的连接。
sendto() recvfrom() UDP: 由于 UDP 是无连接的,每次发送和接收操作都需要指明对方的地址信息。
send() recv() TCP: 这两个函数分别用于发送和接收数据,操作基于一个已经建立的连接。
UDP: 不常用,因为UDP面向无连接的,可以用sendto,向局域网随便发送而不用建立连接,当然如果固定了IP和端口,也可以用
这个表格提供了 TCP 和 UDP 在两种常见的网络通信环境(客户端与服务端)下各自使用的主要 Socket 函数。根据你的具体需求,选择合适的函数和操作方法是实现有效网络通讯的关键。
二,属性操作
1.setsockopt
1.1 SO_SNDBUF/SO_RCVBUF 设置内核IP层发送缓存大小
对于 TCP 和 UDP 来说,增大 SO_SNDBUF 大小会增加可用于队列输出数据的缓冲区,理论上可以提高传输性能,特别是在高延迟或高吞吐量的应用场景中。然而,对于 UDP 来说,由于缺乏重传机制,发件人需要特别注意保证数据不会因为网络或接收方的状况而丢失。对于 TCP,较大的缓冲区可能帮助改善网络的整体利用率,尤其是在窗口规模较大时(如在长距离延迟大的连接中)。在实际应用中,选择适当的 SO_SNDBUF 大小需要考虑网络条件、应用场景和系统资源。注意MTU是工作的在第二层,比SO_SNDBUF低,设置SO_SNDBUF大于MTU会被分包,TCP倒是没问题,但UDP可能出问题,①防火墙不支持udp分包②UDP是不可靠链接,接受的顺序可能不对了
SO_RCVBUF 比较容易理解:它是指内核为特定套接字分配的缓冲区大小,用于存储从网络到达但尚未被拥有该套接字的程序读取的数据。对于 TCP,如果数据到达而你没有读取,缓冲区会逐渐填满,发送方会被通知减慢发送速度(通过 TCP 窗口调整机制)。对于 UDP,一旦缓冲区满了,新的数据包将被直接丢弃。
SO_SNDBUF 仅对 TCP 有意义(在 UDP 中,你发送的数据会直接发送到网络)。对于 TCP,如果远程端没有读取数据(导致远程缓冲区填满,TCP 会将此情况通知你的内核,你的内核会停止发送数据,而是将数据积累在本地缓冲区中,直到本地缓冲区也填满)。或者,如果网络出现问题,内核没有收到发送数据的确认,也可能导致缓冲区填满。此时,内核会减慢发送数据的速度,直到最终发送缓冲区填满。如果发生这种情况,应用程序对该套接字的后续 write() 调用将被阻塞(或者如果设置了 O_NONBLOCK 选项,会返回 EAGAIN 错误)。
int snd_size = (8192*2 + 10);
socklen_t optlen = sizeof(snd_size);
if(setsockopt(fd,SOL_SOCKET,SO_SNDBUF, (void *)&snd_size, optlen) == -1){
debug("Warning: setsockopt SO_SNDBUF failed.\n");
return -1;
}
1.2 SO_SNDTIMEO/SO_RCVTIMEO 设置发送接受延迟
在非阻塞模式下,设置 SO_RCVTIMEO 或 SO_SNDTIMEO 后,如果没有数据可读或发送缓冲区满,操作会立即返回 EAGAIN/EWOULDBLOCK,不会等待超时时间。如果超时时间设置为零(默认值),则操作永远不会超时。超时仅对执行套接字 I/O 的系统调用有效(例如 accept(2)、connect(2)、read(2)、recvmsg(2)、send(2)、sendmsg(2));超时对 select(2)、poll(2)、epoll_wait(2) 等调用无效。
if (SOCKET_ERR == setsockopt(ptFtpClass->fd_ctl_socket, SOL_SOCKET, SO_RCVTIMEO,(char*) (&time_out), sizeof(struct timeval)))
{
debug("set ftp socket timeout setsockopt(SO_RCVTIMEO) error!\n" );
ftp_close_ctl_socket(ptFtpClass);
return SOCKET_ERR;
}
1.3 SO_KEEPALIVE/TCP_KEEPIDLE
仅仅对TCP有效,TCP_KEEPIDLE 是 SO_KEEPALIVE 的补充,只有在 SO_KEEPALIVE (默认关闭)启用时,TCP_KEEPIDLE 才有效。如果未设置 TCP_KEEPIDLE,仅仅用SO_KEEPALIVE则使用系统级参数 tcp_keepalive_time(Linux 默认通常为 7200 秒,即 2 小时)。
// 启用 SO_KEEPALIVE
int keepalive = 1;
if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive)) < 0) {
perror("setsockopt SO_KEEPALIVE");
return 1;
}
// 设置 TCP_KEEPIDLE(60秒空闲后开始探测)
int idle = 60;
if (setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle)) < 0) {
perror("setsockopt TCP_KEEPIDLE");
return 1;
}
1.4 SO_REUSEADDR
用于控制绑定地址的行为,特别是在处理 TCP/UDP 套接字绑定时的地址重用问题。它解决了在某些场景下地址绑定失败的问题,例如服务器重启或多个套接字绑定到相同地址的情况。以下是 SO_REUSEADDR 的详细解释,包括它的作用、适用场景和注意事项。特别是服务器快速重启:服务器进程崩溃或重启后,TCP 连接可能仍处于 TIME_WAIT 状态,导致新进程无法绑定到原端口。
int reuse = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
perror("setsockopt SO_REUSEADDR");
return 1;
}
2.ioctl
2.1FIONBIO
unsigned long ul = 1;
if (ioctl(socket, FIONBIO, &ul) < 0) {
return -1;
}
return 0;
3.socket函数
以下是一些常见的 socket 参数组合及其用途:
| domain | type | protocol | 用途示例 |
|---|---|---|---|
| AF_INET | SOCK_STREAM | 0 or IPPROTO_TCP | TCP 服务器/客户端(如 HTTP、FTP)。 |
| AF_INET | SOCK_DGRAM | 0 or IPPROTO_UDP | UDP 通信(如 DNS、实时音视频)。 |
| AF_INET | SOCK_RAW | IPPROTO_ICMP | ICMP 通信(如 ping 程序)。 |
| AF_INET6 | SOCK_STREAM | 0 or IPPROTO_TCP | IPv6 TCP 连接。 |
| AF_UNIX | SOCK_STREAM | 0 | 本地进程间通信(如数据库连接)。 |
| AF_PACKET | SOCK_RAW | 0 | 网络数据包捕获(如 Wireshark)。 |
| AF_BLUETOOTH | SOCK_STREAM | 0 | 蓝牙 RFCOMM 通信。 |
AF_UNIX + SOCK_DGRAM 例子
#include
#include <sys/socket.h>
#include <sys/un.h>
#include
#include
#include
#define SOCKET_PATH "/tmp/unix_dgram_socket"
#define CLIENT_PATH "/tmp/unix_dgram_client"
int main() {
// 创建 Unix 域数据报套接字
int sock = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
// 设置客户端地址并绑定(可选,方便服务器回复)
unlink(CLIENT_PATH);
struct sockaddr_un client_addr = {0};
client_addr.sun_family = AF_UNIX;
strncpy(client_addr.sun_path, CLIENT_PATH, sizeof(client_addr.sun_path) - 1);
if (bind(sock, (struct sockaddr *)&client_addr, sizeof(client_addr)) < 0) {
perror("bind");
close(sock);
return 1;
}
// 设置服务器地址
struct sockaddr_un server_addr = {0};
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
// 发送消息
const char *message = "Hello, Server!";
if (sendto(sock, message, strlen(message), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("sendto");
close(sock);
return 1;
}
printf("Sent to server: %s\n", message);
// 接收服务器回复
char buffer[1024];
ssize_t len = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, NULL, NULL);
if (len < 0) {
perror("recvfrom");
} else {
buffer[len] = '\0';
printf("Received from server: %s\n", buffer);
}
// 清理
close(sock);
unlink(CLIENT_PATH);
return 0;
}
4.poll
表格:POLLERR、POLLHUP、POLLNVAL 发生原因
| 事件 | 描述 | 发生原因 | 典型场景 |
|---|---|---|---|
| POLLERR | 表示文件描述符上发生了错误,通常是 socket 级别的异常。 | 1. 连接被对端重置(如客户端发送 RST 分组)2. 网络错误(如不可达的网络)3. socket 配置错误(如设置了无效选项)4. 对端发送非法数据(如协议错误)5. SO_RCVBUF/SO_SNDBUF 溢出(缓冲区满)。 | – 客户端突然断开,服务器 socket 触发 POLLERR。- 网络中断导致 TCP 连接失败。- 尝试在已关闭的 socket 上执行操作(如 recv)。 |
| POLLHUP | 表示文件描述符的连接被挂起,通常是对端关闭了连接或 socket 已断开。 | 1. 对端正常关闭连接(如客户端调用 close 或 shutdown)2. 网络中断(如拔掉网线)。3. 对端进程崩溃(未正常关闭 socket)4. TCP 连接超时(如保活探测失败)。 | – 客户端退出,服务器 socket 触发 POLLHUP。- 客户端网络断开,服务器检测到连接中断。- TCP 保活机制检测到对端不可达。 |
| POLLNVAL | 表示文件描述符无效,通常是描述符未打开、已关闭或不合法。 | 1. 文件描述符已关闭(如调用 close 后仍监控)2. 描述符未初始化(如传递了 -1)3. 描述符不是 socket(如普通文件误用)4. pollfd 数组越界(访问无效索引)。 | – 程序 bug:监控已关闭的 socket。错误地将非 socket 描述符(如普通文件)传入 poll。数组越界导致 poll 访问无效描述符。 |
#include
#include
#include
#include
#include <sys/socket.h>
#include <netinet/in.h>
#include
#include
#define PORT 8080
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
// 创建监听 socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
exit(1);
}
// 设置 SO_REUSEADDR,允许快速重用端口
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("setsockopt");
exit(1);
}
// 配置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// 绑定和监听
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
exit(1);
}
if (listen(server_fd, 5) < 0) {
perror("listen");
exit(1);
}
printf("Server listening on port %d\n", PORT);
// 初始化 pollfd 数组
struct pollfd fds[MAX_CLIENTS + 1]; // +1 用于监听 socket
int nfds = 1; // 当前监控的文件描述符数量
fds[0].fd = server_fd;
fds[0].events = POLLIN; // 监控监听 socket 是否有新连接
// 初始化客户端 socket 数组
for (int i = 1; i <= MAX_CLIENTS; ++i) {
fds[i].fd = -1; // -1 表示未使用
fds[i].events = POLLIN; // 监控客户端 socket 是否有数据
}
while (1) {
// 调用 poll 监控所有 socket
int ret = poll(fds, nfds, -1); // -1 表示无限超时
if (ret < 0) {
perror("poll");
exit(1);
}
// 检查每个文件描述符的状态
for (int i = 0; i < nfds; ++i) {
if (fds[i].fd < 0) continue; // 跳过未使用的描述符
// 检查是否有事件
if (fds[i].revents == 0) continue;
// 1. 监听 socket 有新连接 (POLLIN)
if (i == 0 && (fds[i].revents & POLLIN)) {
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
continue;
}
// 查找空闲位置存储新客户端
int j;
for (j = 1; j <= MAX_CLIENTS; ++j) { if (fds[j].fd == -1) { fds[j].fd = client_fd; fds[j].events = POLLIN; // 监控数据可读 if (j >= nfds) nfds = j + 1; // 更新监控数量
printf("New client connected: fd=%d, slot=%d\n", client_fd, j);
break;
}
}
if (j > MAX_CLIENTS) {
printf("Too many clients, rejecting connection\n");
close(client_fd);
}
continue;
}
// 2. 客户端 socket 有数据可读 (POLLIN)
if (fds[i].revents & POLLIN) {
ssize_t len = recv(fds[i].fd, buffer, BUFFER_SIZE - 1, 0);
if (len > 0) {
buffer[len] = '\0';
printf("Received from fd=%d: %s\n", fds[i].fd, buffer);
// 回复客户端
const char *response = "Server received your message\n";
send(fds[i].fd, response, strlen(response), 0);
} else if (len == 0) {
// 客户端关闭连接
printf("Client disconnected: fd=%d\n", fds[i].fd);
close(fds[i].fd);
fds[i].fd = -1; // 标记为未使用
} else {
perror("recv");
}
continue;
}
// 3. 客户端 socket 发生异常 (POLLERR, POLLHUP, POLLNVAL)
if (fds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) {
if (fds[i].revents & POLLERR) {
printf("Error on fd=%d (POLLERR)\n", fds[i].fd);
}
if (fds[i].revents & POLLHUP) {
printf("Hang up on fd=%d (POLLHUP)\n", fds[i].fd);
}
if (fds[i].revents & POLLNVAL) {
printf("Invalid fd=%d (POLLNVAL)\n", fds[i].fd);
}
// 关闭异常 socket
close(fds[i].fd);
fds[i].fd = -1; // 标记为未使用
continue;
}
}
}
// 清理(实际不会到达)
close(server_fd);
return 0;
}
Mr.Zhang
