一,基本函数
在 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] = '#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; }'; 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] = '#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; }'; 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; }