Socket 到底是什么
Socket 是操作系统提供给应用程序的网络通信接口。应用层不直接操作网卡和协议栈,而是通过创建套接字、绑定地址、收发数据来完成通信。可以把 Socket 理解为“文件描述符的网络版本”:一旦建立,读写方式与文件操作高度相似。
一个典型流程是:服务端 socket → bind → listen → accept;客户端 socket → connect。连接建立后,双方通过 send/recv(或 write/read)交换数据,最后 close 释放资源。
TCP 与 UDP 的选择
TCP:可靠但需要成本
TCP 提供面向连接、可靠、有序的字节流。它通过握手建立连接,用确认与重传保证数据到达。适合对完整性要求高的场景,如 HTTP、数据库连接、文件传输。代价是额外的头部开销、连接维护和拥塞控制带来的延迟波动。
使用 TCP 时要注意“粘包”问题:TCP 是字节流,不保留消息边界。应用层需要自定义长度字段或分隔符来切分消息,不能假设一次 recv 就对应一条完整业务消息。
UDP:轻量但需自担责任
UDP 无连接、不保证到达和顺序,但开销小、延迟低。适合实时音视频、游戏状态同步、DNS 查询等能容忍少量丢包的场景。使用 UDP 需要应用层自行处理重传、去重和乱序,复杂度从协议栈转移到了业务代码。
阻塞、非阻塞与事件驱动
最直观的写法是阻塞式:accept 和 recv 会一直等待,代码简单,但一个连接一个线程的模式在连接数上升后线程开销明显。
非阻塞配合 IO 多路复用(select、poll、epoll、kqueue)是更常见的做法:由内核告知哪些套接字就绪,程序再处理。epoll 在大量连接下效率较好,是 Linux 服务端的常用选择。理解“就绪通知”与“实际读写”的区别,是掌握事件驱动编程的关键。
常见陷阱与调试思路
第一,忽略返回值。send 可能只发送部分数据,recv 返回 0 表示对端关闭,这些都必须处理。第二,地址复用与端口占用,服务端重启时可通过 SO_REUSEADDR 减少等待。第三,忘记设置超时,导致连接长期挂起。第四,字节序问题,网络传输使用大端序,跨平台时需转换。
调试时可借助抓包工具观察握手与数据流,并结合日志确认连接状态。先用最小可运行示例验证连通性,再逐步加入业务逻辑,往往比一次性写复杂代码更高效。
小结
Socket 编程的核心不在 API 数量,而在对连接生命周期、数据边界和 IO 模型的理解。选对 TCP 或 UDP,处理好粘包与超时,再根据并发规模选择合适的 IO 方式,大多数网络问题都能在源头避免。