Unity C# 异步TcpListener服务端向客户端发数据失败排查
Unity异步TcpListener通信故障排查修复
核心问题点
代码存在4个直接导致通信异常的逻辑错误:
- 服务端发送回调空引用崩溃:服务端调用
BeginWrite时最后一个异步状态参数传了null,但Send_Complete回调里强制从ar.AsyncState取TcpClient实例,拿到null后执行client.GetStream().EndWrite(ar)直接抛出空引用。这类线程池线程上的异常默认不会在Unity控制台高亮提示,会直接中断当前连接的IO流程,导致消息没真正发送完成,后续逻辑全部失效。 - 读循环状态对象与缓冲区不匹配:服务端和客户端的
Client_Received回调里,每次循环读都新建了ClientStateObject实例,但调用BeginRead时用的还是旧state的缓冲区,传给回调的状态参数却是新创建的newState。下一次回调触发时,拿到的新状态对象自带全新的空缓冲区,和实际写入数据的旧缓冲区完全不对应,读出来的数据全是空值,逻辑直接错乱。 - 服务端仅接受一次连接:
BeginAcceptTcpClient只在Start里调用了一次,处理完第一个客户端连接后没有重新注册监听,后续新客户端无法接入。 - 客户端同步连接阻塞主线程:直接在协程(运行在主线程)里调用同步
Connect方法会卡住Unity引擎主线程,极端情况会导致异步IO回调注册失败。
修复后代码
1. 服务端修复
首先修改Socket_Connected方法,处理完连接立刻重新注册监听,同时修复BeginWrite传参错误:
private void Socket_Connected(IAsyncResult ar) { if (ar.IsCompleted) { var listener = ar.AsyncState as TcpListener; // 重新注册监听,接受下一个客户端连接 listener.BeginAcceptTcpClient(Socket_Connected, listener); var client = listener.EndAcceptTcpClient(ar); var state = new ClientStateObject(client); var msg = Encoding.ASCII.GetBytes("Hello, from TcpServer!"); // 修复:传入client作为异步状态,不再传null client.GetStream().BeginWrite(msg, 0, msg.Length, Send_Complete, client); client.GetStream().BeginRead(state.Buffer,0,state.Buffer.Length,Client_Received,state); } }
修改服务端Send_Complete方法,增加异常捕获:
private void Send_Complete(IAsyncResult ar) { if (ar.IsCompleted) { try { var client = ar.AsyncState as TcpClient; client.GetStream().EndWrite(ar); } catch (Exception e) { Debug.LogError($"服务端发送失败: {e.Message}"); } } }
修改服务端Client_Received读逻辑,复用状态对象,增加断开判断和异常捕获:
private void Client_Received(IAsyncResult ar) { if (ar.IsCompleted) { var state = ar.AsyncState as ClientStateObject; try { var bytesIn = state.Stream.EndRead(ar); // 收到0字节表示客户端已断开 if (bytesIn == 0) { state.Client.Close(); return; } var msg = Encoding.ASCII.GetString(state.Buffer,0,bytesIn); print($"From client: {msg}"); // 清空缓冲区避免脏数据,直接复用现有state和缓冲区,不需要新建对象 Array.Clear(state.Buffer, 0, state.Buffer.Length); state.Stream.BeginRead(state.Buffer, 0, state.Buffer.Length, Client_Received, state); } catch (Exception e) { Debug.LogError($"服务端接收异常: {e.Message}"); state.Client?.Close(); } } }
2. 客户端修复
修改客户端Start协程,用异步连接避免阻塞主线程:
IEnumerator Start() { _client = new TcpClient(); bool connectFinished = false; // 异步连接,不卡主线程 _client.BeginConnect(_ipAddress, _port, ar => { try { _client.EndConnect(ar); } catch (Exception e) { Debug.LogError($"连接服务端失败: {e.Message}"); } connectFinished = true; }, null); yield return new WaitUntil(() => connectFinished); if (!_client.Connected) yield break; var msg = Encoding.ASCII.GetBytes("Hello, from TcpClient!"); _client.GetStream().BeginWrite(msg,0,msg.Length,Send_Complete,_client); var state = new ClientStateObject(_client); _client.GetStream().BeginRead(state.Buffer, 0, state.Buffer.Length, Client_Received, state); }
修改客户端Client_Received读逻辑,和服务端逻辑一致(注意打印内容区分服务端消息):
private void Client_Received(IAsyncResult ar) { Debug.Log("receiving"); if (ar.IsCompleted) { var state = ar.AsyncState as ClientStateObject; try { var bytesIn = state.Stream.EndRead(ar); if (bytesIn == 0) { state.Client.Close(); return; } var msg = Encoding.ASCII.GetString(state.Buffer, 0, bytesIn); print($"From Server: {msg}"); Array.Clear(state.Buffer, 0, state.Buffer.Length); state.Stream.BeginRead(state.Buffer, 0, state.Buffer.Length, Client_Received, state); } catch (Exception e) { Debug.LogError($"客户端接收异常: {e.Message}"); state.Client?.Close(); } } }
额外注意事项
- 所有异步回调运行在.NET线程池线程,不是Unity主线程,如果收到消息后要操作GameObject、调用Unity主线程API,必须把逻辑派发到主线程执行,否则会触发线程安全错误。
- 当前实现没有处理粘包拆包问题,实际生产环境使用需要自行设计消息协议,处理消息拆分、合并的情况。
- OnDestroy时除了停止TcpListener,还要遍历关闭所有已连接的TcpClient,释放流资源,避免内存泄漏。
内容的提问来源于stack exchange,提问作者user3762507
相关产品推荐
相关产品推荐

