
Socket 网络编程全解Socket套接字是操作系统提供的网络通信编程接口它把复杂的 TCP/IP 协议族封装成一组函数调用让应用层开发者无需关心底层协议细节就能实现跨主机的进程通信。Linux 下 socket 本质是一个特殊的文件描述符我们可以像操作普通文件一样通过read/write接口在两个 socket 之间收发数据。常见套接字类型流式套接字SOCK_STREAM对应 TCP 协议面向连接、可靠、有序、字节流传输数据报套接字SOCK_DGRAM对应 UDP 协议无连接、不可靠、报文边界明确原始套接字SOCK_RAW跳过传输层直接操作 IP 层报文用于自定义协议开发一、TCP Socket 编程面向连接TCP 是面向连接的可靠传输协议通信分为「服务端被动监听」和「客户端主动连接」两个角色流程固定是网络编程的核心。1. 完整通信流程服务端流程创建套接字调用socket()生成监听用的文件描述符绑定地址调用bind()给套接字绑定固定的 IP 地址 端口号开启监听调用listen()将套接字转为被动监听状态设置连接等待队列长度接受连接调用accept()阻塞等待客户端连接成功后返回全新的通信套接字专门用于和该客户端收发数据数据交互通过read()/write()或recv()/send()收发数据关闭连接调用close()关闭通信套接字和监听套接字客户端流程创建套接字调用socket()发起连接调用connect()向服务端 IP 端口发起 TCP 连接对应三次握手数据交互连接建立后通过read()/write()收发数据关闭连接调用close()断开连接对应四次挥手2. 核心 API 详解1socket () - 创建套接字c运行#include sys/socket.h int socket(int domain, int type, int protocol);domain地址族常用AF_INETIPv4、AF_INET6IPv6type套接字类型TCP 用SOCK_STREAMUDP 用SOCK_DGRAMprotocol协议一般填 0自动匹配对应类型的默认协议返回值成功返回文件描述符≥0失败返回 -12bind () - 绑定 IP 与端口c运行int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockfdsocket 返回的文件描述符addr地址结构体指针实际使用sockaddr_inIPv4调用时强转为sockaddr*addrlen地址结构体的长度IPv4 地址结构体c运行struct sockaddr_in { sa_family_t sin_family; // 地址族填 AF_INET in_port_t sin_port; // 端口号必须使用网络字节序 struct in_addr sin_addr; // IP 地址结构体 };3listen () - 开启监听c运行int listen(int sockfd, int backlog);backlog已完成三次握手的连接等待队列最大长度Linux 默认上限通常为 128作用将主动套接字转为被动监听套接字让操作系统开始接收发往该端口的连接请求4accept () - 接受客户端连接c运行int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);阻塞等待客户端连接连接成功返回新的通信套接字原sockfd是监听套接字继续负责接收新连接后续和客户端通信只用返回的新 fdaddr会被填充为客户端的 IP 和端口信息5connect () - 客户端发起连接c运行int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);向指定的服务端地址发起 TCP 连接对应三次握手过程连接成功返回 0失败返回 -16数据收发c运行// 通用读写和文件操作一致 ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); // 网络专用支持更多控制标志 ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t send(int sockfd, const void *buf, size_t len, int flags);3. 关键概念网络字节序TCP/IP 协议规定所有网络数据必须使用大端字节序高字节存低地址而主流 PC 主机大多是小端字节序。因此端口号、IP 地址在填入 socket 结构体前必须做字节序转换。常用转换函数htons(port)主机字节序 → 网络字节序short 类型用于端口ntohs(port)网络字节序 → 主机字节序inet_pton(AF_INET, 127.0.0.1, addr)点分十进制 IP → 网络字节序整数inet_ntoa(addr)网络字节序整数 → 点分十进制字符串4. 完整代码示例回声服务器服务端 server.cc运行#include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h #define PORT 8888 #define BUF_SIZE 1024 int main() { // 1. 创建监听套接字 int listen_fd socket(AF_INET, SOCK_STREAM, 0); if (listen_fd -1) { perror(socket); exit(1); } // 设置端口复用避免重启时地址占用 int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); // 2. 配置服务端地址 struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(PORT); server_addr.sin_addr.s_addr htonl(INADDR_ANY); // 绑定本机所有IP // 3. 绑定地址 if (bind(listen_fd, (struct sockaddr*)server_addr, sizeof(server_addr)) -1) { perror(bind); exit(1); } // 4. 开启监听 if (listen(listen_fd, 128) -1) { perror(listen); exit(1); } printf(服务端已启动监听端口%d...\n, PORT); // 5. 接受客户端连接 struct sockaddr_in client_addr; socklen_t client_len sizeof(client_addr); int conn_fd accept(listen_fd, (struct sockaddr*)client_addr, client_len); if (conn_fd -1) { perror(accept); exit(1); } printf(客户端连接成功IP%s\n, inet_ntoa(client_addr.sin_addr)); // 6. 循环收发数据回声 char buf[BUF_SIZE]; while (1) { memset(buf, 0, BUF_SIZE); int len recv(conn_fd, buf, BUF_SIZE, 0); if (len 0) { printf(客户端断开连接\n); break; } printf(收到%s, buf); send(conn_fd, buf, len, 0); // 原封不动发回 } // 7. 关闭套接字 close(conn_fd); close(listen_fd); return 0; }客户端 client.cc运行#include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h #define PORT 8888 #define BUF_SIZE 1024 int main() { // 1. 创建套接字 int sock_fd socket(AF_INET, SOCK_STREAM, 0); if (sock_fd -1) { perror(socket); exit(1); } // 2. 配置服务端地址 struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(PORT); inet_pton(AF_INET, 127.0.0.1, server_addr.sin_addr); // 3. 连接服务端 if (connect(sock_fd, (struct sockaddr*)server_addr, sizeof(server_addr)) -1) { perror(connect); exit(1); } printf(连接服务端成功\n); // 4. 循环发送数据 char buf[BUF_SIZE]; while (1) { fgets(buf, BUF_SIZE, stdin); send(sock_fd, buf, strlen(buf), 0); memset(buf, 0, BUF_SIZE); recv(sock_fd, buf, BUF_SIZE, 0); printf(服务端回声%s, buf); } close(sock_fd); return 0; }编译运行bashgcc server.c -o server gcc client.c -o client ./server # 先启动服务端 ./client # 再启动客户端二、UDP Socket 编程无连接UDP 是无连接协议不需要建立连接直接通过 IP 端口发送报文没有可靠保证但延迟低、速度快适合直播、实时游戏等场景。1. 整体流程服务端socket()→bind()→ 循环recvfrom()/sendto()收发客户端socket()→ 直接sendto()/recvfrom()收发不需要 connect、listen、accept2. 核心收发函数c运行ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);src_addr调用后会填充发送方的地址信息dest_addr每次发送都需要指定接收方的完整地址三、进阶核心知识点1. 阻塞 vs 非阻塞阻塞模式默认accept、recv等函数没有数据时会一直挂起等待线程卡住非阻塞模式调用后立即返回没数据时返回 -1 并设置errno为EAGAIN需要轮询检查设置方式通过fcntl()函数设置O_NONBLOCK标志2. IO 多路复用高性能网络核心当需要同时处理上千个连接时多线程模式开销过大此时使用 IO 多路复用用一个线程监控大量文件描述符哪个有事件就处理哪个。select跨平台通用连接数上限 1024性能随连接数线性下降pollLinux 下无连接数上限但仍需遍历所有描述符检测epollLinux 专属事件驱动性能极高是 Nginx、Redis 等高性能服务的核心技术3. 常见坑点与解决方案端口占用 / TIME_WAIT服务端重启时提示地址已被占用是因为 TCP 四次挥手后主动关闭方会进入 TIME_WAIT 状态端口保留 2MSL 时间。 解决设置端口复用选项c运行int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));TCP 粘包问题TCP 是字节流协议没有报文边界多次发送的数据可能被合并接收也可能被拆分。 解决固定包长、包尾加特殊分隔符、包头 包体格式先发包长度再发包体4. 协议流程与 socket 调用对应客户端调用connect()→ 发起 SYN完成三次握手后 connect 返回服务端listen()后三次握手完成的连接进入 backlog 等待队列服务端调用accept()→ 从队列中取出一个已完成连接任意一端调用close()→ 发起 FIN进入四次挥手流程