尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Linux Socket 编程实战:从 TCP/UDP 到 epoll 高并发与避坑指南

发布时间:2026/9/29 2:12:11

资讯中心
01
ARTICLE

Linux Socket 编程实战:从 TCP/UDP 到 epoll 高并发与避坑指南

Linux Socket 编程实战:从 TCP/UDP 到 epoll 高并发与避坑指南
简介这是一份面向Linux网络编程初学者与进阶开发者的Socket编程学习资料围绕跨主机进程通信这一核心问题展开系统梳理从Socket概念到TCP连接建立与释放的完整知识链路。内容涵盖网络中进程如何唯一标识、Socket作为特殊文件的本质、socket()、bind()、listen()、connect()、accept()、read()/write()、close()等基本操作函数并详解TCP三次握手建立连接与四次挥手释放连接的过程最后配有一个可动手实践的服务端与客户端示例帮助读者把理论落到代码。资源包为1个docx文档约77KB结构紧凑适合作为随查随用的笔记式教程。目前已有241人学习适合希望夯实网络编程基础、理解TCP/IP协议栈与Socket接口对应关系、并需要一份可快速回顾关键函数与连接流程的开发者参考。1. 从一次“连接被拒绝”说起Linux Socket 编程到底在写什么很多人第一次写 Linux Socket 程序跑起来第一件事就是看到Connection refused然后开始怀疑内核、怀疑防火墙、怀疑人生。其实 Socket 编程没那么玄学它本质就是一套让两个进程跨越网络或本机交换字节的接口约定。你写的每一行socket()、bind()、listen()、accept()都是在跟内核的 TCP/IP 协议栈打交道。标题里的“Linux Socket 编程”不是让你去实现协议栈而是学会用系统调用把数据从 A 点搬到 B 点并且搬得稳、搬得快、搬得不出错。这篇文章面向的是已经会写 C 或 Python、但没系统做过网络编程的 Linux 开发者也适合运维同学理解服务端到底在干什么。我会从 TCP 和 UDP 两条线讲清楚最小可运行代码再拆开阻塞、非阻塞、多路复用这些绕不开的选型问题最后落到几个我实际踩过的坑。看完你至少能做到手写一个能扛住几十个并发连接的 echo 服务知道SO_REUSEADDR什么时候必须加以及为什么你的send()返回了但对方没收到。2. TCP 三次握手在代码里长什么样从 socket() 到 accept() 的完整链路2.1 服务端和客户端的最小闭环先看服务端。下面这段代码是我每次开新项目都会先跑一遍的骨架去掉业务逻辑后它就是一个标准的 TCP echo server。#include stdio.h #include string.h #include unistd.h #include arpa/inet.h #include sys/socket.h #define PORT 9000 #define BUF_SIZE 1024 int main(void) { int listen_fd socket(AF_INET, SOCK_STREAM, 0); // SOCK_STREAM 对应 TCP if (listen_fd 0) { perror(socket); return 1; } int opt 1; // 关键让服务器重启后能立刻复用端口否则会卡在 TIME_WAIT setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_addr.s_addr htonl(INADDR_ANY); // 监听所有网卡 addr.sin_port htons(PORT); // 主机字节序转网络字节序 if (bind(listen_fd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(bind); return 1; } if (listen(listen_fd, 128) 0) { // backlog 设为 128 perror(listen); return 1; } printf(listening on %d\n, PORT); while (1) { struct sockaddr_in cli; socklen_t cli_len sizeof(cli); int conn_fd accept(listen_fd, (struct sockaddr *)cli, cli_len); if (conn_fd 0) { perror(accept); continue; } char buf[BUF_SIZE]; ssize_t n read(conn_fd, buf, sizeof(buf) - 1); if (n 0) { buf[n] \0; printf(recv: %s\n, buf); write(conn_fd, buf, n); // echo 回去 } close(conn_fd); } return 0; }逻辑说明socket()返回一个文件描述符bind()把它绑到具体 IP 和端口listen()把主动套接字变成被动套接字accept()从已完成连接队列里取一个连接。参数上SOCK_STREAM是 TCPSOCK_DGRAM是 UDPINADDR_ANY表示不限定网卡htons()和htonl()负责字节序转换忘了写就会绑到奇怪端口或者连不上。backlog参数在 Linux 上实际影响的是全连接队列长度设太小会在高并发时丢连接。客户端更简单int fd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv; serv.sin_family AF_INET; serv.sin_port htons(9000); inet_pton(AF_INET, 127.0.0.1, serv.sin_addr); connect(fd, (struct sockaddr *)serv, sizeof(serv)); write(fd, hello, 5); read(fd, buf, sizeof(buf));connect()触发三次握手返回成功时连接已经建立。注意inet_pton比老旧的inet_addr更安全能正确处理错误。2.2 UDP 不是“不可靠的 TCP”它是另一种模型UDP 的代码短很多因为没有连接状态int fd socket(AF_INET, SOCK_DGRAM, 0); bind(fd, ...); // 服务端需要 bind客户端通常不需要 recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)cli, len); sendto(fd, buf, n, 0, (struct sockaddr *)cli, len);选型理由很直接如果你要的是低延迟、能容忍丢包、或者天然就是一问一答的小包DNS、日志上报UDP 更合适。但别以为 UDP 就不用处理“连接”问题——你得自己维护客户端地址、自己做超时重传、自己处理乱序。我见过太多项目一开始图省事用 UDP后来硬生生在上面实现了一套 TCP那还不如一开始就用 TCP。2.3 阻塞模式下的并发困境上面那个 echo server 是阻塞的accept()没连接就卡住read()没数据也卡住。这意味着它一次只能服务一个客户端。要支持多客户端最直接的办法是每个连接开一个线程pthread_t tid; int *fd_ptr malloc(sizeof(int)); *fd_ptr conn_fd; pthread_create(tid, NULL, handle_client, fd_ptr); pthread_detach(tid);但线程不是免费的。每个线程默认栈 8MB几百个连接就是几个 G 虚拟内存上下文切换也会吃掉 CPU。所以真实服务端要么用线程池要么走 IO 多路复用。这就是下一章要展开的东西。3. 从 select 到 epoll高并发场景下怎么选 IO 模型3.1 阻塞、非阻塞和 IO 多路复用的区别阻塞 IO 的问题是“一个线程等一个连接”。非阻塞 IO 把 fd 设成O_NONBLOCK后read()没数据会立刻返回EAGAIN但你需要不停轮询CPU 空转。IO 多路复用是折中用一个线程同时监视多个 fd哪个就绪就处理哪个。select()是最老的接口它用位图表示 fd 集合每次调用都要把集合从用户态拷贝到内核态而且 fd 数量有限制通常 1024。poll()改进了数量限制但拷贝问题还在。epoll是 Linux 特有的用红黑树管理 fd就绪事件通过回调放进就绪链表epoll_wait()直接取不需要每次遍历全部 fd。3.2 epoll 的 LT 和 ET 模式怎么选先看 epoll 的服务端骨架int epfd epoll_create1(0); struct epoll_event ev, events[1024]; ev.events EPOLLIN; // 水平触发 ev.data.fd listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, ev); while (1) { int n epoll_wait(epfd, events, 1024, -1); for (int i 0; i n; i) { if (events[i].data.fd listen_fd) { int conn accept(listen_fd, ...); ev.events EPOLLIN | EPOLLET; // 边缘触发 ev.data.fd conn; epoll_ctl(epfd, EPOLL_CTL_ADD, conn, ev); } else { // 处理可读事件 } } }LT水平触发模式下只要缓冲区还有数据epoll_wait就会一直通知你写起来简单不容易丢事件。ET边缘触发只在状态变化时通知一次你必须一次把数据读干净直到read()返回EAGAIN否则剩下的数据可能永远不触发新事件。ET 效率更高但代码复杂度也更高。我一般建议除非你明确知道自己在做什么否则先用 LT性能不够再换 ET。3.3 参数调优backlog、SO_REUSEADDR 和 TCP_NODELAYlisten()的 backlog 参数在 Linux 上对应net.core.somaxconn默认 128。高并发服务要把它调大sysctl -w net.core.somaxconn65535SO_REUSEADDR前面提过服务器重启时避免Address already in use。还有一个SO_REUSEPORT允许多个进程绑同一端口内核做负载均衡适合多进程模型。TCP_NODELAY关掉 Nagle 算法。Nagle 会把小包攒起来一起发降低网络开销但对延迟敏感的应用比如游戏、实时通信是灾难。设置方式int flag 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, flag, sizeof(flag));代价是可能增加小包数量所以要在延迟和带宽之间权衡。4. 避坑与排查那些让我熬夜的 Socket 问题4.1 Connection refused服务端根本没在听现象客户端connect()立刻返回ECONNREFUSED。 原因目标端口没有进程在listen()或者绑到了127.0.0.1而客户端连的是外网 IP。 解决ss -tlnp看端口监听状态确认bind()的地址不是INADDR_LOOPBACK。如果服务端在容器里检查端口映射。4.2 send() 返回成功但对方没收到现象send()返回了正数但抓包看不到数据或者对方read()一直阻塞。 原因send()只是把数据写进了内核发送缓冲区不代表已经发到网络上。如果对方接收缓冲区满了TCP 会做流控你的数据还在本地排队。 解决需要应用层确认机制ACK不能依赖send()返回值判断对方收到。抓包用tcpdump -i any port 9000 -w dump.pcap看实际流量。4.3 大量 TIME_WAIT 导致端口耗尽现象服务器作为主动关闭方时ss -tan看到大量TIME_WAIT新连接绑不上端口。 原因TCP 主动关闭方要等 2MSL通常 60 秒才能释放端口。 解决加SO_REUSEADDR如果是客户端考虑用连接池调低net.ipv4.tcp_fin_timeout和开启tcp_tw_reuse注意内核版本差异。4.4 忘记处理 EINTR 导致 accept 失败现象accept()返回 -1errno是EINTR。 原因信号中断了系统调用。 解决用while循环判断errno EINTR就重试或者用sigaction时设SA_RESTART。4.5 字节序搞反导致连不上现象bind()成功但客户端连不上或者连到了奇怪端口。 原因sin_port忘了htons()在小端机器上端口号会变成另一个值。 解决所有网络字节序字段都用htons()/htonl()接收时用ntohs()/ntohl()。写代码时养成习惯看到sin_port就检查。5. 进阶技巧用 strace 和 tcpdump 把黑匣子打开当你遇到一个 Socket 问题最有效的办法不是猜而是看系统调用和网络包。strace能告诉你进程在read、write、accept上到底卡在哪strace -f -e tracenetwork -p pid-f跟踪子进程-e tracenetwork只看网络相关调用。如果看到accept一直没返回说明没有新连接如果read返回EAGAIN说明是非阻塞模式且数据没到。tcpdump看包tcpdump -i any -nn port 9000 -A-nn不解析主机名和端口名-A以 ASCII 显示 payload。三次握手有没有完成、有没有 RST、窗口大小多少一目了然。还有一个我常用的技巧用ss -ti看 TCP 内部状态包括拥塞窗口、RTT、重传次数。如果retrans字段在涨说明网络在丢包这时候调应用层参数没用得先解决网络问题。最后说一个习惯每次写完 Socket 代码先在本机用127.0.0.1跑通再用0.0.0.0测局域网最后才上外网。这样能把“代码问题”和“网络问题”分开。我早期经常把防火墙问题当成代码 bug 查半天后来学会先telnet一下端口能省很多时间。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。