vlambda博客
学习文章列表

Unity【Multiplayer 多人在线】- Socket 通用服务端框架(一)、定义套接字和多路复用

介绍

一、通用服务端框架

(一)、定义套接字和多路复用

https://blog.csdn.net/qq_42139931/article/details/124051945?spm=1001.2014.3001.5501

(二)、客户端信息类和通用缓冲区结构

https://blog.csdn.net/qq_42139931/article/details/124053571?spm=1001.2014.3001.5502

(三)、Protobuf 通信协议

https://blog.csdn.net/qq_42139931/article/details/124054972?spm=1001.2014.3001.5501

(四)、数据处理和关闭连接

https://blog.csdn.net/qq_42139931/article/details/124055227?spm=1001.2014.3001.5501

(五)、Messenger 事件发布、订阅系统

https://blog.csdn.net/qq_42139931/article/details/124055392?spm=1001.2014.3001.5501

(六)、单点发送和广播数据

https://blog.csdn.net/qq_42139931/article/details/124055482?spm=1001.2014.3001.5501

(七)、时间戳和心跳机制

https://blog.csdn.net/qq_42139931/article/details/124055856?spm=1001.2014.3001.5501

二、通用客户端网络模块

(一)、Connect 连接服务端

https://blog.csdn.net/qq_42139931/article/details/124091349?spm=1001.2014.3001.5502

(二)、Receive 接收并处理数据 

https://blog.csdn.net/qq_42139931/article/details/124092588?spm=1001.2014.3001.5502

(三)、Send 发送数据

https://blog.csdn.net/qq_42139931/article/details/124094323?spm=1001.2014.3001.5502

(四)、Close 关闭连接

https://blog.csdn.net/qq_42139931/article/details/124094895?spm=1001.2014.3001.5502


本篇内容:

Socket套接字的定义:

    首先编写服务器初始化的方法Init,接受一个参数port,即监听的端口,在Main函数中调用Init传入端口以启动服务器。

using System.Net;using System.Net.Sockets;
namespace SK.Framework.Sockets{ /// <summary> /// 服务器 /// </summary> public class Server { //定义套接字 private static Socket socket; private static void Main(string[] args) { Init(8801); } //服务器初始化 //port: 端口 private static void Init(int port) { Console.WriteLine("服务器启动..."); //TODO } }}
using System.Net;using System.Net.Sockets;
namespace SK.Framework.Sockets{ /// <summary> /// 服务器 /// </summary> public class Server { //定义套接字 private static Socket socket; private static void Main(string[] args) { Init(8801); } //服务器初始化 //port: 端口 private static void Init(int port) { Console.WriteLine("服务器启动..."); //Socket Tcp协议 socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //服务器IP地址 IPAddress ipAddress = IPAddress.Parse("0.0.0.0"); IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port); //Bind socket.Bind(ipEndPoint); //Listen 开启监听 socket.Listen(100); //TODO } }}
Select多路复用:
// 摘要:// Determines the status of one or more sockets.//// 参数:// checkRead:// An System.Collections.IList of System.Net.Sockets.Socket instances to check for// readability.//// checkWrite:// An System.Collections.IList of System.Net.Sockets.Socket instances to check for// writability.//// checkError:// An System.Collections.IList of System.Net.Sockets.Socket instances to check for// errors.//// microSeconds:// The time-out value, in microseconds. A -1 value indicates an infinite time-out.//// 异常:// T:System.ArgumentNullException:// The checkRead parameter is null or empty. -and- The checkWrite parameter is null// or empty -and- The checkError parameter is null or empty.//// T:System.Net.Sockets.SocketException:// An error occurred when attempting to access the socket.//// T:System.ObjectDisposedException:// .NET 5.0 and later: One or more sockets are disposed.public static void Select(IList? checkRead, IList? checkWrite, IList? checkError, int microSeconds)
关于Select方法的官方文档链接地址:

https://docs.microsoft.com/zh-cn/dotnet/api/system.net.sockets.socket.select?view=net-6.0

该方法可以帮助我们实现non-block非阻塞方式,第一个参数checkRead代表需要检测可读性的Socket列表,第四个参数microSeconds代表阻塞等待的时长,单位为毫秒,例如传入1000则代表设置1秒的阻塞等待时长,当1秒内没有可读消息时,它会停止阻塞,返回空的checkRead列表,程序继续运行。

代码实现如下,其中的Client类定义了代表客户端信息的相关内容,在后续章节中进行介绍。

using ProtoBuf;using System.Net;using System.Net.Sockets;
namespace SK.Framework.Sockets{ /// <summary> /// 服务器 /// </summary> public class Server { //定义套接字 private static Socket socket; //用于检测可读性的Socket列表 private readonly static List<Socket> checkReadableList = new List<Socket>(); //客户端Socket及客户端信息字典 private readonly static Dictionary<Socket, Client> clients = new Dictionary<Socket, Client>();
private static void Main(string[] args) { Init(8801); } //服务器初始化 //port: 端口 private static void Init(int port) { Console.WriteLine("服务器启动..."); //Socket Tcp协议 socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //服务器IP地址 IPAddress ipAddress = IPAddress.Parse("0.0.0.0"); IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port); //Bind socket.Bind(ipEndPoint); //Listen 开启监听 socket.Listen(0);
//循环 while (true) { //首先重置用于检测可读性的Socket列表 OnCheckReadableListReset(); //使用Select检测可读 实现non-block非阻塞方式 //arg4: 超时值 单位毫秒 此处设置1000表示 1秒内没有可读消息时停止阻塞 返回空的列表 Socket.Select(checkReadableList, null, null, 1000); //遍历检查可读对象 for (int i = 0; i < checkReadableList.Count; i++) { Socket s = checkReadableList[i]; if (s == socket) OnListenEvent(s); else OnClientEvent(s); } } } private static void OnCheckReadableListReset() { checkReadableList.Clear(); //进行Select的列表包含监听套接字socket以及每个已经连接的客户端套接字 checkReadableList.Add(socket); foreach (Client client in clients.Values) { checkReadableList.Add(client.socket); } } //监听事件 private static void OnListenEvent(Socket s) {} //客户端消息事件 private static void OnClientEvent(Socket s) {} }}

其中OnListenEvent方法用于处理客户端连接的消息,代码如下:

//监听事件private static void OnListenEvent(Socket s){ try { //接受客户端连接 Socket socket = s.Accept(); Console.WriteLine($"客户端接入: {socket.RemoteEndPoint}"); Client client = new Client(socket); //加入字典 clients.Add(socket, client); } catch (SocketException error) { Console.WriteLine($"客户端接入失败: {error}"); }}
OnClientEvent方法用于处理客户端发送来的消息,代码如下:
//客户端消息事件private static void OnClientEvent(Socket s){ //从字典中获取该客户端信息类 Client client = clients[s]; //该客户端的读缓冲区 ByteArray readBuff = client.readBuff; //如果缓冲区剩余空间不足 清除 if (readBuff.remain <= 0) { OnReceiveData(client); readBuff.MoveBytes(); } //如果依然不足 接收数据失败 关闭客户端连接 返回 //缓冲区默认大小为1024 根据最大单条数据长度进行调整 if (readBuff.remain <= 0) { Console.WriteLine($"接收数据失败,超出缓冲区长度。{s.RemoteEndPoint}"); //关闭客户端连接 Close(client); return; } //接收数据长度 int length = 0; try { length = s.Receive(readBuff.bytes, readBuff.writeIdx, readBuff.remain, 0); } catch (SocketException error) { Console.WriteLine($"接收数据失败: {error}. {s.RemoteEndPoint}"); Close(client); return; } //客户端关闭 if (length <= 0) { Close(client); return; } //处理数据 readBuff.writeIdx += length; OnReceiveData(client); //移动缓冲区 readBuff.CheckAndMoveBytes();}//数据处理private static void OnReceiveData(Client client) {}



参考资料:《Unity3D网络游戏实战》(第2版)罗培羽 著