本章导读
TCP 提供可靠、有序、面向连接的字节流传输。与 HTTP 不同,TCP 没有"请求-响应"的语义——它只是一根双向管道,你发什么对方收什么,消息边界需要你自己定义(长度前缀、分隔符、固定长度等)。xlang 的 TCP API 封装在 tcp 命名空间和 TcpServer、TcpStream 两个类型中。
- 用
tcp.listen创建 TCP 服务器并接受连接 - 用
tcp.connect建立客户端连接 - 用
send/recv收发文本,sendBytes/recvBytes收发二进制 - 理解
drain的作用和必要性 - 用异步 API(acceptAsync/connectAsync/sendAsync/recvAsync)实现非阻塞通信
- 配置 NoDelay、KeepAlive、ReuseAddr 等套接字选项
- 使用
net.resolve进行域名解析 - 实现一个完整的 Echo 服务器
45.1 监听与接受连接
TCP 服务器的第一步是监听端口:
// 创建 TCP 服务器——端口 0 = 系统自动分配
TcpServer server = TcpServer(tcp.listen(0))
int port = int(server.port())
println("监听端口: " + port.toString())
// 接受连接(异步)
future<point> accepted = server.acceptAsync(8000) // 超时 8 秒
point peerHandle = await accepted
TcpStream peer = TcpStream(peerHandle)
println("客户端已连接")
tcp.listen(port) 返回一个原始句柄(point),用 TcpServer() 包装后获得面向对象接口。acceptAsync(timeout) 异步等待一个客户端连接,返回 future<point>。
传 0 让操作系统自动分配一个可用端口。这在测试和临时服务中非常有用——不需要担心端口冲突。通过 server.port() 获取实际分配的端口号。
45.2 建立连接
客户端用 tcp.connect 连接服务器:
// 同步连接——阻塞直到连接建立或超时
TcpStream client = TcpStream(tcp.connect("127.0.0.1", port, 8000))
println("已连接到服务器")
// 异步连接——不阻塞当前线程
future<point> connectFuture = tcp.connectAsync("127.0.0.1", port, 8000)
TcpStream asyncClient = TcpStream(await connectFuture)
tcp.connect(host, port, timeout) 的三个参数:目标地址、目标端口、超时毫秒。连接失败(超时、拒绝)时返回 null 句柄。
45.3 文本收发:send/recv
最简单的数据交换——字符串形式:
// 客户端发送
bool sent = client.send("ping")
bool drained = client.drain(8000) // 等待数据实际发出
// 服务端接收
string message = peer.recv(8000) // 阻塞等待数据,超时 8 秒
println("收到: " + message) // "ping"
// 服务端回复
peer.send("pong")
peer.drain(3000)
// 客户端接收回复
string reply = client.recv(8000)
println("回复: " + reply) // "pong"
关键要点:
send(s)将字符串写入发送缓冲区,返回是否成功recv(timeout)阻塞等待对端数据,返回收到的字符串- TCP 是字节流——一次 send 的数据可能被对端分多次 recv 收到,也可能多次 send 被合并为一次 recv 收到(粘包/拆包)
45.4 二进制收发:sendBytes/recvBytes
传输非文本数据(图片、协议帧、序列化结构)时使用二进制接口:
// 构建二进制数据
Buffer data = Buffer()
data.appendByte(0) // 消息类型
data.appendByte(1) // 版本
data.appendByte(128) // 载荷长度高位
data.appendByte(255) // 载荷长度低位
// 发送二进制
bool sent = client.sendBytes(data.data(), c_size(data.len()))
client.drain(8000)
// 接收二进制
Buffer received = peer.recvBytes(8000)
println("收到 " + received.len().toString() + " 字节")
println("第一字节: " + received[0].toString()) // 0
println("最后字节: " + received[3].toString()) // 255
sendBytes(ptr, size) 接受原始指针和长度;recvBytes(timeout) 返回 Buffer 对象,可以按索引访问每个字节。
45.5 drain:确保数据发完
send 只是将数据写入操作系统的发送缓冲区,不保证数据已经到达对端。drain 等待缓冲区清空:
client.send("important data")
bool ok = client.drain(8000) // 等待数据实际发送到网络
if not ok then
println("发送超时或连接断开")
end
什么时候需要 drain?
- 发送后紧跟
close()——不 drain 可能丢数据 - 需要确认数据已发出(如协议握手的 ACK)
- 发送大量数据后检查是否阻塞
如果你 send 后不立即关闭连接,且对时序没有严格要求,可以省略 drain——TCP 栈会自动发送缓冲区中的数据。但在协议实现中,drain 是保证时序正确的重要工具。
45.6 异步 TCP 操作
同步 API 会阻塞线程。高并发场景使用异步版本:
// 异步接受连接
TcpServer server = TcpServer(tcp.listen(0))
int port = int(server.port())
future<point> acceptFuture = server.acceptAsync(8000)
// 异步建立连接
future<point> connectFuture = tcp.connectAsync("127.0.0.1", port, 8000)
// 等待双方就绪
TcpStream client = TcpStream(await connectFuture)
TcpStream peer = TcpStream(await acceptFuture)
// 异步接收(先发起)
future<string> recvFuture = peer.recvAsync(8000)
// 异步发送
bool sent = await client.sendAsync("async hello")
bool drained = await client.drainAsync(8000)
// 等待接收完成
string message = await recvFuture
println("异步收到: " + message) // "async hello"
client.close()
peer.close()
异步 API 对照表:
| 同步 | 异步 | 返回类型 |
|---|---|---|
server.acceptAsync(t) | (本身就是异步) | future<point> |
tcp.connect(h,p,t) | tcp.connectAsync(h,p,t) | future<point> |
stream.send(s) | stream.sendAsync(s) | future<bool> |
stream.recv(t) | stream.recvAsync(t) | future<string> |
stream.drain(t) | stream.drainAsync(t) | future<bool> |
45.7 连接与监听配置
高级场景需要调整套接字选项:
// 连接配置
TcpConnectConfig connectCfg = tcp.connectConfig("127.0.0.1", 65535)
connectCfg.flags(tcp.connectNoDelay + tcp.connectKeepAlive)
// NoDelay:禁用 Nagle 算法,减少小包延迟
// KeepAlive:启用 TCP 保活探测
// 监听配置
TcpListenConfig listenCfg = tcp.listenConfig(0, "127.0.0.1")
listenCfg.flags(tcp.listenReuseAddr + tcp.listenReusePort + tcp.listenNoDelay + tcp.listenKeepAlive)
// ReuseAddr:允许绑定 TIME_WAIT 状态的地址
// ReusePort:允许多个进程/线程绑定同一端口(负载均衡)
| 标志 | 作用 | 典型场景 |
|---|---|---|
tcp.connectNoDelay | 禁用 Nagle,立即发送小包 | 游戏、实时通信 |
tcp.connectKeepAlive | TCP 保活探测 | 长连接、检测死连接 |
tcp.listenReuseAddr | 允许重用 TIME_WAIT 地址 | 服务器快速重启 |
tcp.listenReusePort | 多进程共享端口 | 多核负载均衡 |
tcp.listenNoDelay | 接受的连接自动设 NoDelay | 低延迟服务 |
tcp.listenKeepAlive | 接受的连接自动设 KeepAlive | 长连接服务 |
45.8 域名解析与网络工具
net 命名空间提供网络辅助功能:
// 域名解析
NetAddress addr = net.resolve("localhost", 80)
println("解析结果: " + addr.text()) // "127.0.0.1"
println("端口: " + addr.port.toString()) // 80
// 本机网络信息
string ip = net.localIP() // 本机 IP
string mac = net.localMAC() // 本机 MAC 地址
string name = net.localName() // 主机名
// 代理配置
NetProxy noProxy = net.proxy(net.proxyNone, "127.0.0.1", 0)
NetProxy socks = net.proxy(net.proxySocks5, "127.0.0.1", 1080)
NetProxy httpProxy = net.proxy(net.proxyHttpConnect, "127.0.0.1", 8080)
// 关闭标志
// net.closeAbort = 1 立即关闭(发 RST)
// net.closeGraceful = 2 优雅关闭(发 FIN)
// net.closeWaitPeer = 4 等待对端关闭
45.9 网络引擎 NetEngine
NetEngine 是底层网络事件循环,通常由标准库自动管理。高级用户可以显式控制:
// 创建网络引擎:1 个 I/O 线程,0 = 自动工作线程数
NetEngine engine = net.engine(1, net.engineAutoWorkers)
// 启动
assert engine.start()
// ... 执行网络操作 ...
// 停止
engine.stop()
大多数应用不需要手动管理 NetEngine——tcp.connect、tcp.listen 等 API 内部会自动使用全局引擎。只有在需要隔离网络资源或精确控制线程数时才显式创建。
45.10 实战:Echo 服务器
把前面的知识组合成一个完整的 Echo 服务器——收到什么就回什么:
// === Echo 服务器 ===
TcpServer server = TcpServer(tcp.listen(0))
int port = int(server.port())
println("Echo 服务器运行在端口 " + port.toString())
// 接受一个连接
future<point> acceptF = server.acceptAsync(30000)
// 客户端连接
TcpStream client = TcpStream(tcp.connect("127.0.0.1", port, 5000))
TcpStream peer = TcpStream(await acceptF)
// 客户端发送多条消息
client.send("Hello")
client.drain(3000)
string echo1 = client.recv(5000)
println("Echo: " + echo1) // "Hello"
client.send("xlang TCP")
client.drain(3000)
string echo2 = client.recv(5000)
println("Echo: " + echo2) // "xlang TCP"
// 二进制 echo
Buffer binData = Buffer()
binData.appendByte(0xDE)
binData.appendByte(0xAD)
client.sendBytes(binData.data(), c_size(binData.len()))
client.drain(3000)
Buffer binEcho = client.recvBytes(5000)
println("二进制 Echo 长度: " + binEcho.len().toString())
// 关闭
client.close()
peer.close()
上面的示例只处理一次连接。真实的 Echo 服务器会在 while 循环中反复 recv → send,直到客户端关闭连接(recv 返回空字符串)。多客户端则需要为每个连接启动一个线程或使用异步模型。
深入细节
TCP 是字节流协议,没有消息边界。你 send("Hello") 再 send("World"),对端可能一次 recv 收到 "HelloWorld",也可能分三次收到 "Hel"、"loW"、"orld"。解决方案:① 长度前缀(先发 4 字节长度,再发载荷);② 分隔符(如 \n);③ 固定长度帧。xlang 的 recv 返回"当前缓冲区中可用的数据",不保证对应一次 send。
net.closeAbort(发 RST)立即断开,对端可能丢失未读数据;net.closeGraceful(发 FIN)正常四次挥手;net.closeWaitPeer 发 FIN 后等待对端也关闭。TcpStream.close() 默认使用优雅关闭。如果你需要立即断开(如检测到恶意客户端),可以用 abort 模式。
server.acceptAsync(8000) 的超时是"等待新连接的最大时间"。如果 8 秒内没有客户端连接,await 会返回 null 句柄。这不是错误——只是没有客户端。服务器通常用循环反复 accept:while true { peer = await server.acceptAsync(30000); if peer != null then handle(peer) }。
⚠️ 常见错误
client.send("bye"); client.close() 可能丢失 "bye"——因为 send 只写入缓冲区,close 可能在数据发出前就关闭了连接。正确做法:send 后调用 drain 确认数据发出,再 close。
对端 send("Hello World") 不代表你 recv 一次就能收到完整的 "Hello World"。TCP 可能拆包。必须用长度前缀或分隔符协议来界定消息边界。
recv 超时返回空字符串,对端关闭连接也返回空字符串。如果不检查就使用返回值,可能导致逻辑错误。空返回应视为"连接结束"或"超时无数据"。
TcpStream 持有操作系统文件描述符。不 close 会泄漏资源,最终耗尽系统 fd 上限。每个连接用完必须 close,建议用 try/finally 模式保证关闭。
本章小结
| 主题 | 核心要点 |
|---|---|
| 监听 | TcpServer(tcp.listen(port));port() 获取实际端口;acceptAsync 等待连接 |
| 连接 | TcpStream(tcp.connect(host, port, timeout));connectAsync 异步版本 |
| 文本 | send(s) 发送字符串;recv(timeout) 接收字符串 |
| 二进制 | sendBytes(ptr, size);recvBytes(timeout) 返回 Buffer |
| drain | 等待发送缓冲区清空;send 后 close 前必须 drain |
| 异步 | sendAsync/recvAsync/drainAsync 返回 future;await 获取结果 |
| 配置 | connectConfig/listenConfig + flags(NoDelay/KeepAlive/ReuseAddr/ReusePort) |
| 网络工具 | net.resolve 域名解析;net.localIP/localMAC/localName 本机信息 |
| 引擎 | net.engine(ioThreads, workers) 显式控制事件循环(通常不需要) |
| 关闭 | stream.close() 优雅关闭;net.closeAbort 强制断开 |
复习题
- tcp.listen(0) 的 0 是什么含义?如何获取实际分配的端口?
- send 和 drain 的区别是什么?什么情况下必须调用 drain?
- TCP 的"粘包"是什么现象?xlang 中如何应对?
- recv 返回空字符串有哪两种可能?如何区分?
- tcp.connectAsync 和 tcp.connect 的使用场景有什么不同?
- tcp.connectNoDelay 禁用了什么算法?什么场景下需要它?
- tcp.listenReusePort 解决什么问题?
- net.resolve("localhost", 80) 返回什么?如果域名无法解析会怎样?
- sendBytes 的参数为什么是 (ptr, size) 而不是直接传 Buffer?
- 为什么 acceptAsync 超时返回 null 不算错误?服务器应如何处理?
编程练习
实现一个简单的长度前缀协议:发送方先发 4 字节大端整数表示消息长度,再发消息体;接收方先读 4 字节获取长度,再精确读取对应字节数。用 sendBytes/recvBytes 实现,验证多条不同长度的消息能正确收发。
参考思路:用 Buffer 构建 4 字节长度头 + 消息体;接收端循环 recvBytes 直到凑够指定长度。
实现一个 Echo 服务器,能同时处理 3 个客户端。用 acceptAsync 接受连接,为每个连接启动一个线程(thread),在线程中循环 recv → send 直到客户端断开。客户端发送 "quit" 时服务器关闭该连接。
参考思路:主线程循环 acceptAsync;每个连接 spawn 一个 thread 处理;线程内 while recv != "" 循环。
在练习 2 的基础上,实现广播功能:任一客户端发送的消息,服务器转发给所有其他已连接的客户端。维护一个在线连接列表(用 list 存储 TcpStream),收到消息后遍历列表逐一 send。
参考思路:用 mutex 保护连接列表;新连接加入列表,断开时移除;广播时跳过发送者自身。