目录
- 核心概念:Socket 是什么?
- 服务器端实现 (C#)
- 创建 TCP 服务器
- 处理多客户端连接
- 广播消息给所有客户端
- Unity 客户端实现 (C#)
- 连接到服务器
- 发送和接收消息
- 完整项目示例
- 服务器端代码 (
SocketServer.cs) - 客户端代码 (
SocketClient.cs) - Unity 场景设置
- 服务器端代码 (
- 重要注意事项与最佳实践
- 异步编程 (Async/Await)
- 协议设计
- 线程安全
- 性能优化
核心概念:Socket 是什么?
可以把 Socket 想象成一个电话插座,服务器就像一个总机,客户端就像一部部电话。

- 服务器: 监听一个特定的“电话号码”(IP 地址和端口号),等待“电话”(客户端连接)打进来,一旦有电话接入,它就拿起听筒,开始通话。
- 客户端: 知道服务器的“电话号码”,主动拨打电话,建立连接,连接成功后,就可以通过这条线路(Socket)发送和接收数据。
- IP 地址: 服务器的网络地址,本地开发时通常是
0.0.1(代表本机) 或localhost。 - 端口号: 服务器上用来区分不同服务的数字,每个服务器程序都需要一个唯一的端口号(
8080),避免与其他程序冲突。
服务器端实现 (C#)
我们将创建一个简单的 TCP 服务器,它能够:
- 启动并监听客户端连接。
- 当有新客户端连接时,将其加入一个列表。
- 当一个客户端发送消息时,将该消息广播给所有已连接的客户端。
在 Unity 中,你可以创建一个 C# 脚本(SocketServer.cs)来管理服务器逻辑。
// SocketServer.cs
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
public class SocketServer
{
private TcpListener _server;
private bool _isRunning;
private List<TcpClient> _clients = new List<TcpClient>();
private object _lock = new object(); // 用于线程安全
public void Start(int port)
{
_server = new TcpListener(IPAddress.Any, port);
_server.Start();
_isRunning = true;
// 在一个单独的线程中开始接受连接
Thread acceptThread = new Thread(new ThreadStart(AcceptClients));
acceptThread.IsBackground = true;
acceptThread.Start();
UnityEngine.Debug.Log("服务器已启动,等待连接...");
}
private void AcceptClients()
{
while (_isRunning)
{
try
{
TcpClient client = _server.AcceptTcpClient();
lock (_lock)
{
_clients.Add(client);
}
UnityEngine.Debug.Log($"客户端 {client.Client.RemoteEndPoint} 已连接。");
// 为每个客户端创建一个处理线程
Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClient));
clientThread.IsBackground = true;
clientThread.Start(client);
}
catch (SocketException ex)
{
if (_isRunning)
{
UnityEngine.Debug.LogError($"接受连接时出错: {ex.Message}");
}
}
}
}
private void HandleClient(object clientObj)
{
TcpClient client = (TcpClient)clientObj;
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
int bytesRead;
try
{
while (_isRunning)
{
bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
// 客户端断开连接
break;
}
string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
UnityEngine.Debug.Log($"收到来自 {client.Client.RemoteEndPoint} 的消息: {message}");
// 广播消息给所有客户端
Broadcast(message);
}
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"处理客户端 {client.Client.RemoteEndPoint} 时出错: {ex.Message}");
}
finally
{
// 清理资源
lock (_lock)
{
_clients.Remove(client);
}
client.Close();
UnityEngine.Debug.Log($"客户端 {client.Client.RemoteEndPoint} 已断开连接。");
}
}
private void Broadcast(string message)
{
byte[] data = Encoding.UTF8.GetBytes(message);
lock (_lock) // 锁定列表,防止在广播时列表被修改
{
foreach (TcpClient client in _clients)
{
try
{
NetworkStream stream = client.GetStream();
stream.Write(data, 0, data.Length);
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"广播消息给客户端 {client.Client.RemoteEndPoint} 失败: {ex.Message}");
// 如果发送失败,说明客户端可能已断开,可以将其移除
_clients.Remove(client);
}
}
}
}
public void Stop()
{
_isRunning = false;
_server.Stop();
lock (_lock)
{
foreach (TcpClient client in _clients)
{
client.Close();
}
_clients.Clear();
}
UnityEngine.Debug.Log("服务器已停止。");
}
}
代码解释:
TcpListener: 负责监听传入的 TCP 连接请求。AcceptTcpClient(): 阻塞方法,直到有客户端连接,然后返回一个TcpClient对象。Thread: 由于AcceptTcpClient()和stream.Read()都是阻塞操作,我们必须在单独的线程中运行它们,否则 Unity 的主线程会卡住。NetworkStream: 用于通过网络发送和接收数据。lock: 这是一个非常重要的关键字,当多个线程(如一个新客户端连接线程和一个广播线程)同时访问_clients列表时,可能会导致数据竞争。lock确保一次只有一个线程能修改列表。
Unity 客户端实现 (C#)
客户端的逻辑相对简单,主要任务是连接、发送和接收。

// SocketClient.cs
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
public class SocketClient : MonoBehaviour
{
private TcpClient _client;
private NetworkStream _stream;
private Thread _receiveThread;
private bool _isConnected;
public string serverIP = "127.0.0.1";
public int serverPort = 8080;
public void ConnectToServer()
{
try
{
_client = new TcpClient(serverIP, serverPort);
_stream = _client.GetStream();
_isConnected = true;
UnityEngine.Debug.Log("成功连接到服务器!");
// 启动一个线程来持续接收服务器消息
_receiveThread = new Thread(new ThreadStart(ReceiveData));
_receiveThread.IsBackground = true;
_receiveThread.Start();
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"连接服务器失败: {ex.Message}");
}
}
private void ReceiveData()
{
byte[] buffer = new byte[1024];
int bytesRead;
while (_isConnected)
{
try
{
bytesRead = _stream.Read(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
// 服务器断开连接
Disconnect();
break;
}
string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
// 在Unity主线程中更新UI,避免跨线程操作UI
UnityMainThreadDispatcher.Instance().Enqueue(() => {
UnityEngine.Debug.Log($"收到服务器消息: {message}");
});
}
catch (Exception ex)
{
if (_isConnected)
{
UnityEngine.Debug.LogError($"接收数据时出错: {ex.Message}");
}
Disconnect();
}
}
}
public void SendMessage(string message)
{
if (!_isConnected) return;
try
{
byte[] data = Encoding.UTF8.GetBytes(message);
_stream.Write(data, 0, data.Length);
UnityEngine.Debug.Log($"已发送消息: {message}");
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"发送消息失败: {ex.Message}");
Disconnect();
}
}
private void Disconnect()
{
_isConnected = false;
if (_receiveThread != null && _receiveThread.IsAlive)
{
_receiveThread.Abort(); // 注意:Thread.Abort() 已被标记为过时,但为了简单示例使用
}
_stream?.Close();
_client?.Close();
UnityEngine.Debug.Log("与服务器断开连接。");
}
void OnApplicationQuit()
{
Disconnect();
}
}
重要补充:跨线程更新 UI
在 ReceiveData 方法中,我们直接在接收线程里调用了 UnityEngine.Debug.Log,这通常可以工作,但更规范的做法是,如果要在 UI 上显示信息(Text 组件),必须确保在 Unity 的主线程中执行,我们可以使用一个简单的辅助类 UnityMainThreadDispatcher 来实现。
UnityMainThreadDispatcher.cs (创建这个脚本并放在你的项目中)
using System.Collections.Generic;
using UnityEngine;
public class UnityMainThreadDispatcher : MonoBehaviour
{
private static readonly Queue<System.Action> _executionQueue = new Queue<System.Action>();
private static UnityMainThreadDispatcher _instance = null;
public static UnityMainThreadDispatcher Instance()
{
if (_instance == null)
{
_instance = FindObjectOfType<UnityMainThreadDispatcher>();
if (_instance == null)
{
var go = new GameObject("UnityMainThreadDispatcher");
_instance = go.AddComponent<UnityMainThreadDispatcher>();
DontDestroyOnLoad(go);
}
}
return _instance;
}
void Update()
{
lock (_executionQueue)
{
while (_executionQueue.Count > 0)
{
_executionQueue.Dequeue().Invoke();
}
}
}
public void Enqueue(System.Action action)
{
lock (_executionQueue)
{
_executionQueue.Enqueue(action);
}
}
}
然后在你的 SocketClient 中,接收消息后通过它来分发到主线程:

// UnityMainThreadDispatcher.Instance().Enqueue(() => {
// // 在这里执行所有需要主线程操作的代码
// Debug.Log("主线程收到消息");
// // uiText.text = message;
// });
完整项目示例
创建 Unity 项目
- 创建一个新的 3D 或 2D Unity 项目。
创建服务器
- 在 Unity 中创建一个空 GameObject,命名为
ServerManager。 - 创建一个 C# 脚本
SocketServer.cs,将代码复制进去。 - 将
SocketServer.cs挂载到ServerManager上。 - 在
ServerManager的 Inspector 中,你可以添加一个按钮来启动和停止服务器。
ServerManager.cs (用于控制服务器启停)
using UnityEngine;
using UnityEngine.UI; // 如果需要按钮
public class ServerManager : MonoBehaviour
{
public SocketServer server;
public Button startButton;
public Button stopButton;
void Start()
{
if (startButton != null) startButton.onClick.AddListener(StartServer);
if (stopButton != null) stopButton.onClick.AddListener(StopServer);
}
public void StartServer()
{
if (server == null)
{
server = new SocketServer();
}
server.Start(8080);
}
public void StopServer()
{
if (server != null)
{
server.Stop();
}
}
void OnApplicationQuit()
{
StopServer();
}
}
创建客户端
- 创建另一个空 GameObject,命名为
ClientManager。 - 创建一个 C# 脚本
SocketClient.cs,将代码复制进去。 - 将
SocketClient.cs挂载到ClientManager上。 - 在 Inspector 中设置好
Server IP(默认0.0.1) 和Server Port(默认8080)。 - 添加一个
InputField用于输入消息,一个Button用于发送消息。
ClientUI.cs (用于处理客户端输入和发送)
using UnityEngine;
using UnityEngine.UI;
public class ClientUI : MonoBehaviour
{
public SocketClient client;
public InputField messageInputField;
public Button sendButton;
public Text outputText; // 用于显示聊天记录的UI文本
void Start()
{
if (sendButton != null) sendButton.onClick.AddListener(SendMessage);
if (messageInputField != null) messageInputField.onEndEdit.AddListener((value) => SendMessage());
}
public void SendMessage()
{
if (client != null && !string.IsNullOrEmpty(messageInputField.text))
{
client.SendMessage(messageInputField.text);
messageInputField.text = ""; // 清空输入框
}
}
}
创建 UnityMainThreadDispatcher
- 按照前面的说明创建
UnityMainThreadDispatcher.cs脚本,并确保场景中有一个 GameObject 挂载了它(或者它会自动创建)。
场景布局
- 一个
ServerManagerGameObject,挂载ServerManager.cs和SocketServer.cs。 - 一个
ClientManagerGameObject,挂载SocketClient.cs。 - 一个
UIGameObject,包含:- 一个
Text用于显示服务器状态和聊天记录(挂载UnityMainThreadDispatcher)。 - 一个
InputField用于输入消息。 - 一个
Button用于发送消息。 - 将
InputField和Button拖到ClientUI脚本的相应字段中。
- 一个
重要注意事项与最佳实践
异步编程 (Async/Await)
上面的示例使用了 Thread,这是一种比较传统的多线程方式,现代 C# 推荐使用 async/await 模式,它能让代码更简洁,并且更好地与 Unity 的主线程集成。
服务器端的 AcceptClients 和 HandleClient 可以改写成 async 方法,使用 AcceptTcpClientAsync() 和 ReadAsync()/WriteAsync(),这样可以避免手动管理线程池,代码也更易于维护。
协议设计
直接发送 string (UTF8 字符串) 在简单场景下可行,但在复杂应用中会遇到问题:
- 消息粘包: TCP 是流式协议,连续发送的两条消息可能会被合并成一条接收。
- 消息边界: 接收方如何知道一条消息在哪里结束,下一条在哪里开始?
解决方案:自定义协议
最简单的方式是在消息前加上消息的长度(头部)。
要发送消息 "Hello":
- 计算
"Hello"的字节长度,假设为 5。 - 将长度
5转换成 4 字节的头 (00000005)。 - 将头部
00000005和消息体"Hello"一起发送。 - 接收方先读取 4 个字节,得到消息长度 5,然后再读取 5 个字节,得到完整的消息体。
线程安全
我们已经通过 lock 保证了列表操作的线程安全,在任何共享资源(如列表、变量)被多个线程访问时,都要考虑使用 lock 或其他并发控制机制(如 Mutex, Semaphore)。
性能优化
- 对象池: 对于频繁创建和销毁的
TcpClient或NetworkStream,可以考虑使用对象池来减少 GC 压力。 - 缓冲区管理: 对于大文件传输,需要设计更高效的缓冲区管理策略。
- 使用更高级的库: 对于商业级项目,可以考虑使用成熟的网络库,如 LiteNetLib (轻量级,高性能) 或 Mirror (基于 UNET 的现代网络框架),它们已经处理了底层的复杂性和性能优化。
希望这份详细的指南能帮助你成功地在 Unity 中实现 Socket 通信!
