在上一篇 Unity Socket 学习笔记(一)中,我们已经掌握了使用 Socket 进行简单通信的基础。但实际项目中的网络需求远不止于此。今天我们来深入探讨 Unity Socket 的进阶用法,解决更复杂的问题。
问题场景:高并发服务器性能瓶颈
设想一个多人在线游戏,需要同时处理数千甚至上万玩家的连接请求。如果仍然采用简单的单线程 Socket 监听模式,服务器很快就会不堪重负。这涉及到高并发场景下,TCP连接的建立、维护以及数据的快速收发。这时候,我们必须考虑多线程、异步 Socket、以及连接池等技术。
底层原理:异步 Socket 与线程池
传统的同步 Socket 在 Accept() 方法处会阻塞线程,直到有新的连接到来。在高并发场景下,这会严重影响服务器的响应速度。而异步 Socket 则允许我们非阻塞地监听连接请求,并在连接建立后,将数据收发任务交给线程池中的线程处理。这可以有效提高服务器的并发能力。
线程池,顾名思义,是一组预先创建好的线程。当有新的任务需要执行时,线程池会从线程池中取出一个空闲线程来处理任务,而不是每次都创建新的线程。这可以减少线程创建和销毁的开销,提高服务器的性能。类似 Nginx 通过 worker 进程池处理客户端请求,避免频繁的进程创建和销毁,提升并发连接数。
代码解决方案:异步 Socket + 线程池实现高并发服务器
以下是一个简单的异步 Socket + 线程池服务器的示例代码:
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using UnityEngine;
public class AsyncSocketServer : MonoBehaviour
{
private Socket listener;
private ThreadPool threadPool;
private int maxThreads = 100; // 最大线程数
private int port = 8888;
void Start()
{
// 初始化线程池
threadPool = new ThreadPool(maxThreads);
// 创建 Socket
listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 绑定 IP 地址和端口
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, port);
listener.Bind(localEndPoint);
// 开始监听
listener.Listen(100); // 最大连接数
Debug.Log("服务器已启动,监听端口:" + port);
// 开始异步接受连接
StartAccept(null);
}
private void StartAccept(SocketAsyncEventArgs acceptEventArg)
{
if (acceptEventArg == null)
{
acceptEventArg = new SocketAsyncEventArgs();
acceptEventArg.Completed += AcceptEventArg_Completed;
}
else
{
acceptEventArg.AcceptSocket = null;
}
if (!listener.AcceptAsync(acceptEventArg))
{
ProcessAccept(acceptEventArg);
}
}
private void AcceptEventArg_Completed(object sender, SocketAsyncEventArgs e)
{
ProcessAccept(e);
}
private void ProcessAccept(SocketAsyncEventArgs e)
{
Socket clientSocket = e.AcceptSocket;
if (clientSocket != null)
{
Debug.Log("客户端连接:" + clientSocket.RemoteEndPoint);
// 将客户端连接交给线程池处理
threadPool.QueueUserWorkItem(HandleClient, clientSocket);
// 继续接受新的连接
StartAccept(e);
}
}
private void HandleClient(object obj)
{
Socket clientSocket = (Socket)obj;
byte[] buffer = new byte[1024];
int bytesReceived;
try
{
while ((bytesReceived = clientSocket.Receive(buffer)) > 0)
{
string data = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesReceived);
Debug.Log("收到客户端数据:" + data);
// 发送响应数据
string response = "服务器已收到:" + data;
byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
clientSocket.Send(responseBytes);
}
}
catch (Exception ex)
{
Debug.LogError("客户端连接异常:" + ex.Message);
}
finally
{
// 关闭连接
clientSocket.Shutdown(SocketShutdown.Both);
clientSocket.Close();
Debug.Log("客户端连接已关闭:" + clientSocket.RemoteEndPoint);
}
}
void OnApplicationQuit()
{
// 关闭 Socket
if (listener != null)
{
listener.Close();
}
}
}
这个示例代码使用了 SocketAsyncEventArgs 类来实现异步 Socket,并使用 ThreadPool.QueueUserWorkItem() 方法将客户端连接交给线程池处理。当然,实际项目中需要考虑更完善的线程池实现,例如使用 Semaphore 来控制并发数量,防止线程池耗尽系统资源。
实战避坑:Nagle 算法与延迟
在使用 Socket 进行网络编程时,一个常见的坑是 Nagle 算法导致的延迟。Nagle 算法会延迟小数据包的发送,以减少网络拥塞。但在某些实时性要求高的应用中,例如 FPS 游戏,这种延迟是无法接受的。可以通过设置 socket.NoDelay = true 来禁用 Nagle 算法。
另外,对于高并发服务器,需要关注操作系统的连接数限制(例如 Linux 下的 ulimit -n),合理调整 TCP 参数,如 tcp_tw_recycle 和 tcp_tw_reuse,并在必要时使用负载均衡技术,如 Nginx 反向代理,将请求分发到多台服务器上,进一步提高服务器的承载能力。
在后续的 Unity Socket 学习笔记中,我们将继续探讨更高级的网络编程技术,例如 UDP 协议、KCP 协议、以及更复杂的网络架构设计。
冠军资讯
键盘上的咸鱼