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

举报
CoderZ1010 发表于 2022/09/25 06:05:45 2022/09/25
【摘要】 介绍         在阅读了罗培羽著作的Unity3D网络游戏实战一书后,博主综合自己的开发经验与考虑进行部分修改和调整,将通用的客户端网络模块和通用的服务端框架进行提取,形成专栏,介绍Socket网络编程,希望对其他人有所帮助。目录如下: &nbsp...

介绍

        在阅读了罗培羽著作的Unity3D网络游戏实战一书后,博主综合自己的开发经验与考虑进行部分修改和调整,将通用的客户端网络模块和通用的服务端框架进行提取,形成专栏,介绍Socket网络编程,希望对其他人有所帮助。目录如下:

    一、通用服务端框架

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

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

        (三)、Protobuf 通信协议

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

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

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

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

 二、通用客户端网络模块

        (一)、Connect 连接服务端

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

        (三)、Send 发送数据

        (四)、Close 关闭连接

本篇内容:

Socket套接字的定义:

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


  
  1. using System.Net;
  2. using System.Net.Sockets;
  3. namespace SK.Framework.Sockets
  4. {
  5. /// <summary>
  6. /// 服务器
  7. /// </summary>
  8. public class Server
  9. {
  10. //定义套接字
  11. private static Socket socket;
  12. private static void Main(string[] args)
  13. {
  14. Init(8801);
  15. }
  16. //服务器初始化
  17. //port: 端口
  18. private static void Init(int port)
  19. {
  20. Console.WriteLine("服务器启动...");
  21. //TODO
  22. }
  23. }
  24. }

Socket在调用Listen监听方法之前,必须先调用Bind方法,需要声明服务器的IP地址及监听的端口,如果不关心使用哪个本地端口,可以使用0作为端口号,系统将会自动分配1024到5000之间的可用端口号。Listen方法中参数backlog代表可排队等待接受的传入连接的数量,即挂起的连接队列的最大长度。


  
  1. using System.Net;
  2. using System.Net.Sockets;
  3. namespace SK.Framework.Sockets
  4. {
  5. /// <summary>
  6. /// 服务器
  7. /// </summary>
  8. public class Server
  9. {
  10. //定义套接字
  11. private static Socket socket;
  12. private static void Main(string[] args)
  13. {
  14. Init(8801);
  15. }
  16. //服务器初始化
  17. //port: 端口
  18. private static void Init(int port)
  19. {
  20. Console.WriteLine("服务器启动...");
  21. //Socket Tcp协议
  22. socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  23. //服务器IP地址
  24. IPAddress ipAddress = IPAddress.Parse("0.0.0.0");
  25. IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port);
  26. //Bind
  27. socket.Bind(ipEndPoint);
  28. //Listen 开启监听
  29. socket.Listen(100);
  30. //TODO
  31. }
  32. }
  33. }

Select多路复用:


  
  1. // 摘要:
  2. // Determines the status of one or more sockets.
  3. //
  4. // 参数:
  5. // checkRead:
  6. // An System.Collections.IList of System.Net.Sockets.Socket instances to check for
  7. // readability.
  8. //
  9. // checkWrite:
  10. // An System.Collections.IList of System.Net.Sockets.Socket instances to check for
  11. // writability.
  12. //
  13. // checkError:
  14. // An System.Collections.IList of System.Net.Sockets.Socket instances to check for
  15. // errors.
  16. //
  17. // microSeconds:
  18. // The time-out value, in microseconds. A -1 value indicates an infinite time-out.
  19. //
  20. // 异常:
  21. // T:System.ArgumentNullException:
  22. // The checkRead parameter is null or empty. -and- The checkWrite parameter is null
  23. // or empty -and- The checkError parameter is null or empty.
  24. //
  25. // T:System.Net.Sockets.SocketException:
  26. // An error occurred when attempting to access the socket.
  27. //
  28. // T:System.ObjectDisposedException:
  29. // .NET 5.0 and later: One or more sockets are disposed.
  30. 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类定义了代表客户端信息的相关内容,在后续章节中进行介绍。


  
  1. using ProtoBuf;
  2. using System.Net;
  3. using System.Net.Sockets;
  4. namespace SK.Framework.Sockets
  5. {
  6. /// <summary>
  7. /// 服务器
  8. /// </summary>
  9. public class Server
  10. {
  11. //定义套接字
  12. private static Socket socket;
  13. //用于检测可读性的Socket列表
  14. private readonly static List<Socket> checkReadableList = new List<Socket>();
  15. //客户端Socket及客户端信息字典
  16. private readonly static Dictionary<Socket, Client> clients = new Dictionary<Socket, Client>();
  17. private static void Main(string[] args)
  18. {
  19. Init(8801);
  20. }
  21. //服务器初始化
  22. //port: 端口
  23. private static void Init(int port)
  24. {
  25. Console.WriteLine("服务器启动...");
  26. //Socket Tcp协议
  27. socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  28. //服务器IP地址
  29. IPAddress ipAddress = IPAddress.Parse("0.0.0.0");
  30. IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port);
  31. //Bind
  32. socket.Bind(ipEndPoint);
  33. //Listen 开启监听
  34. socket.Listen(0);
  35. //循环
  36. while (true)
  37. {
  38. //首先重置用于检测可读性的Socket列表
  39. OnCheckReadableListReset();
  40. //使用Select检测可读 实现non-block非阻塞方式
  41. //arg4: 超时值 单位毫秒 此处设置1000表示 1秒内没有可读消息时停止阻塞 返回空的列表
  42. Socket.Select(checkReadableList, null, null, 1000);
  43. //遍历检查可读对象
  44. for (int i = 0; i < checkReadableList.Count; i++)
  45. {
  46. Socket s = checkReadableList[i];
  47. if (s == socket) OnListenEvent(s);
  48. else OnClientEvent(s);
  49. }
  50. }
  51. }
  52. private static void OnCheckReadableListReset()
  53. {
  54. checkReadableList.Clear();
  55. //进行Select的列表包含监听套接字socket以及每个已经连接的客户端套接字
  56. checkReadableList.Add(socket);
  57. foreach (Client client in clients.Values)
  58. {
  59. checkReadableList.Add(client.socket);
  60. }
  61. }
  62. //监听事件
  63. private static void OnListenEvent(Socket s) {}
  64. //客户端消息事件
  65. private static void OnClientEvent(Socket s) {}
  66. }
  67. }

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


  
  1. //监听事件
  2. private static void OnListenEvent(Socket s)
  3. {
  4. try
  5. {
  6. //接受客户端连接
  7. Socket socket = s.Accept();
  8. Console.WriteLine($"客户端接入: {socket.RemoteEndPoint}");
  9. Client client = new Client(socket);
  10. //加入字典
  11. clients.Add(socket, client);
  12. }
  13. catch (SocketException error)
  14. {
  15. Console.WriteLine($"客户端接入失败: {error}");
  16. }
  17. }

OnClientEvent方法用于处理客户端发送来的消息,代码如下:


  
  1. //客户端消息事件
  2. private static void OnClientEvent(Socket s)
  3. {
  4. //从字典中获取该客户端信息类
  5. Client client = clients[s];
  6. //该客户端的读缓冲区
  7. ByteArray readBuff = client.readBuff;
  8. //如果缓冲区剩余空间不足 清除
  9. if (readBuff.remain <= 0)
  10. {
  11. OnReceiveData(client);
  12. readBuff.MoveBytes();
  13. }
  14. //如果依然不足 接收数据失败 关闭客户端连接 返回
  15. //缓冲区默认大小为1024 根据最大单条数据长度进行调整
  16. if (readBuff.remain <= 0)
  17. {
  18. Console.WriteLine($"接收数据失败,超出缓冲区长度。 {s.RemoteEndPoint}");
  19. //关闭客户端连接
  20. Close(client);
  21. return;
  22. }
  23. //接收数据长度
  24. int length = 0;
  25. try
  26. {
  27. length = s.Receive(readBuff.bytes, readBuff.writeIdx, readBuff.remain, 0);
  28. }
  29. catch (SocketException error)
  30. {
  31. Console.WriteLine($"接收数据失败: {error}. {s.RemoteEndPoint}");
  32. Close(client);
  33. return;
  34. }
  35. //客户端关闭
  36. if (length <= 0)
  37. {
  38. Close(client);
  39. return;
  40. }
  41. //处理数据
  42. readBuff.writeIdx += length;
  43. OnReceiveData(client);
  44. //移动缓冲区
  45. readBuff.CheckAndMoveBytes();
  46. }
  47. //数据处理
  48. private static void OnReceiveData(Client client) {}

其中的ByteArray类是用于处理数据的粘包半包问题而封装的用来操作读写缓冲区的相关内容,在后续章节中进行介绍。关于数据的粘包半包问题及处理方法在以往的文章中也有介绍,地址如下,本套框架中我们使用了长度信息法来处理粘包半包问题。

Socket TCP协议解决粘包、半包问题的三种解决方案

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

文章来源: coderz.blog.csdn.net,作者:CoderZ1010,版权归原作者所有,如需转载,请联系作者。

原文链接:coderz.blog.csdn.net/article/details/124051945

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。