Socket通信全解析:TCP/UDP基础与多路复用进阶
Socket常规用法详解:从TCP/UDP基础到多路复用进阶
一、TCP通信流程
服务端实现步骤
创建Socket:
int socket(int domain, int type, int protocol)
// Java示例 ServerSocket serverSocket = new ServerSocket();
绑定地址:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
serverSocket.bind(new InetSocketAddress("0.0.0.0", 8080));
监听连接:
int listen(int sockfd, int backlog)
serverSocket.listen(50); // 设置等待队列长度为50
接受连接:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
Socket clientSocket = serverSocket.accept(); // 阻塞等待客户端连接
数据读写:
// 读取数据 InputStream in = clientSocket.getInputStream(); // 写入数据 OutputStream out = clientSocket.getOutputStream();
关闭连接:
clientSocket.close(); serverSocket.close();
客户端实现步骤
创建Socket:
Socket socket = new Socket();
连接服务器:
socket.connect(new InetSocketAddress("127.0.0.1", 8080));
- 数据读写:(同服务端)
关闭连接:
socket.close();
TCP通信流程图
实践建议:
- 服务端建议使用线程池处理多个客户端连接
- 设置合理的SO_RCVBUF/SO_SNDBUF缓冲区大小(通常8K-64K)
- 使用try-with-resources确保Socket正确关闭
二、UDP通信流程
无连接模式特点
- 无需建立连接,直接发送数据报
- 每个数据包独立路由,可能乱序到达
- 适合实时性要求高、允许丢包的场景
核心API使用
// 服务端
DatagramSocket serverSocket = new DatagramSocket(8080);
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
serverSocket.receive(packet); // 阻塞接收
// 客户端
DatagramSocket clientSocket = new DatagramSocket();
byte[] data = "Hello".getBytes();
DatagramPacket sendPacket = new DatagramPacket(
data, data.length, InetAddress.getByName("127.0.0.1"), 8080);
clientSocket.send(sendPacket);
地址复用关键设置
// 允许地址复用(多个进程绑定相同端口)
socket.setReuseAddress(true);
// 对于多播需要设置TTL
socket.setTimeToLive(1);
实践建议:
- UDP单次传输建议控制在1472字节以内(MTU 1500 - IP头20 - UDP头8)
- 应用层需要实现重传和排序机制
- 使用connect()方法可关联默认目标地址
三、多路复用技术
select/poll模型
// select示例
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
select(sockfd+1, &readfds, NULL, NULL, NULL);
特点:
- 线性扫描文件描述符集合
- 最大支持1024个连接(select)
- 跨平台兼容性好
epoll/kqueue模型
// Java NIO示例
Selector selector = Selector.open();
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
// 处理新连接
}
}
}
优势对比:
特性 | select/poll | epoll/kqueue |
---|---|---|
时间复杂度 | O(n) | O(1) |
最大连接数 | 有限制(1024) | 系统内存决定 |
触发方式 | 水平触发 | 支持边缘触发 |
内存拷贝 | 每次调用需拷贝 | 内核事件表 |
实践建议:
- Linux平台优先选择epoll
- 高并发场景使用ET模式+非阻塞IO
- 配合内存池减少GC压力
四、非阻塞IO实现要点
设置非阻塞模式:
channel.configureBlocking(false);
处理不完整读写:
while (buffer.hasRemaining()) { channel.write(buffer); // 可能只写入部分数据 }
- 结合状态机设计:
最佳实践:
- 每个连接维护独立的上下文
- 使用ByteBuffer池避免频繁分配
- 合理设置超时时间(SO_TIMEOUT)
通过掌握这些核心用法,开发者可以构建从简单到复杂的不同网络应用。实际项目中建议结合Netty等成熟框架,避免重复造轮子。