Unity【Multiplayer 多人在线】- Socket 通用客户端网络模块(一)、Connect 连接服务端

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

介绍

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

一、通用服务端框架

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

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

        (三)、Protobuf 通信协议

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

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

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

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

 二、通用客户端网络模块

        (一)、Connect 连接服务端

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

        (三)、Send 发送数据

        (四)、Close 关闭连接

本篇内容:

客户端网络模块中同样使用服务端框架中的通用缓冲区结构ByteArray,和消息的发布、订阅系统Messenger,以及通信协议工具类ProtoUtility,代码分别如下:


  
  1. using System;
  2. namespace SK.Framework.Sockets
  3. {
  4. public class ByteArray
  5. {
  6. //默认大小
  7. private const int DEFAULT_SIZE = 1024;
  8. //初始大小
  9. private readonly int initSize = 0;
  10. //缓冲区
  11. public byte[] bytes;
  12. //读取位置
  13. public int readIdx = 0;
  14. //写入位置
  15. public int writeIdx = 0;
  16. //容量
  17. private int capacity = 0;
  18. //剩余空间
  19. public int remain { get { return capacity - writeIdx; } }
  20. //数据长度
  21. public int length { get { return writeIdx - readIdx; } }
  22. //构造函数
  23. public ByteArray(int size = DEFAULT_SIZE)
  24. {
  25. bytes = new byte[size];
  26. capacity = size;
  27. initSize = size;
  28. writeIdx = 0;
  29. readIdx = 0;
  30. }
  31. //构造函数
  32. public ByteArray(byte[] defaultBytes)
  33. {
  34. bytes = defaultBytes;
  35. capacity = defaultBytes.Length;
  36. initSize = defaultBytes.Length;
  37. readIdx = 0;
  38. writeIdx = defaultBytes.Length;
  39. }
  40. //重设尺寸
  41. public void ReSize(int size)
  42. {
  43. if (size < length) return;
  44. if (size < initSize) return;
  45. int n = 1;
  46. while (n < size)
  47. {
  48. n *= 2;
  49. }
  50. capacity = n;
  51. byte[] newBytes = new byte[capacity];
  52. Array.Copy(bytes, readIdx, newBytes, 0, writeIdx - readIdx);
  53. bytes = newBytes;
  54. writeIdx = length;
  55. readIdx = 0;
  56. }
  57. //检查并移动数据
  58. public void CheckAndMoveBytes()
  59. {
  60. if (length < 8)
  61. {
  62. MoveBytes();
  63. }
  64. }
  65. //移动数据
  66. public void MoveBytes()
  67. {
  68. if (length > 0)
  69. {
  70. Array.Copy(bytes, readIdx, bytes, 0, length);
  71. }
  72. writeIdx = length;
  73. readIdx = 0;
  74. }
  75. //写入数据
  76. public int Write(byte[] bs, int offset, int count)
  77. {
  78. if (remain < count)
  79. {
  80. ReSize(length + count);
  81. }
  82. Array.Copy(bs, offset, bytes, writeIdx, count);
  83. writeIdx += count;
  84. return count;
  85. }
  86. //读取数据
  87. public int Read(byte[] bs, int offset, int count)
  88. {
  89. count = Math.Min(count, length);
  90. Array.Copy(bytes, readIdx, bs, offset, count);
  91. readIdx += count;
  92. CheckAndMoveBytes();
  93. return count;
  94. }
  95. //读取Int16
  96. public Int16 ReadInt16()
  97. {
  98. if (length < 2) return 0;
  99. Int16 ret = (Int16)((bytes[readIdx + 1]) << 8 | bytes[readIdx]);
  100. readIdx += 2;
  101. CheckAndMoveBytes();
  102. return ret;
  103. }
  104. //读取Int32
  105. public Int32 ReadInt32()
  106. {
  107. if (length < 4) return 0;
  108. Int32 ret = (Int32)((bytes[readIdx + 3] << 24) |
  109. (bytes[readIdx + 2] << 16) |
  110. (bytes[readIdx + 1] << 8) |
  111. bytes[readIdx + 0]);
  112. readIdx += 4;
  113. CheckAndMoveBytes();
  114. return ret;
  115. }
  116. }
  117. }

  
  1. using System.Collections.Generic;
  2. namespace SK.Framework.Sockets
  3. {
  4. /// <summary>
  5. /// 消息发布、订阅系统
  6. /// </summary>
  7. public class Messenger
  8. {
  9. public delegate void MessageEvent(params object[] args);
  10. private static readonly Dictionary<string, MessageEvent> msgDic = new Dictionary<string, MessageEvent>();
  11. /// <summary>
  12. /// 发布消息
  13. /// </summary>
  14. /// <param name="msgKey">消息Key值</param>
  15. /// <param name="arg">参数</param>
  16. public static void Publish(string msgKey, params object[] args)
  17. {
  18. if (msgDic.ContainsKey(msgKey))
  19. {
  20. msgDic[msgKey].Invoke(args);
  21. }
  22. }
  23. /// <summary>
  24. /// 订阅消息
  25. /// </summary>
  26. /// <param name="msgKey">消息Key值</param>
  27. /// <param name="messageEvent">订阅事件</param>
  28. public static void Subscribe(string msgKey, MessageEvent messageEvent)
  29. {
  30. if (msgDic.ContainsKey(msgKey))
  31. {
  32. msgDic[msgKey] += messageEvent;
  33. }
  34. else
  35. {
  36. msgDic[msgKey] = messageEvent;
  37. }
  38. }
  39. /// <summary>
  40. /// 取消订阅
  41. /// </summary>
  42. /// <param name="msgKey">消息Key值</param>
  43. /// <param name="messageEvent">订阅事件</param>
  44. public static void Unsubscribe(string msgKey, MessageEvent messageEvent)
  45. {
  46. if (msgDic.ContainsKey(msgKey))
  47. {
  48. msgDic[msgKey] -= messageEvent;
  49. if (msgDic[msgKey] == null)
  50. {
  51. msgDic.Remove(msgKey);
  52. }
  53. }
  54. }
  55. }
  56. }

  
  1. using System;
  2. using ProtoBuf;
  3. using System.IO;
  4. using System.Text;
  5. namespace SK.Framework.Sockets
  6. {
  7. /// <summary>
  8. /// 协议工具
  9. /// </summary>
  10. public static class ProtoUtility
  11. {
  12. /// <summary>
  13. /// 协议编码
  14. /// </summary>
  15. /// <param name="proto">协议</param>
  16. /// <returns>返回编码后的字节数据</returns>
  17. public static byte[] Encode(IExtensible proto)
  18. {
  19. using (MemoryStream ms = new MemoryStream())
  20. {
  21. Serializer.Serialize(ms, proto);
  22. return ms.ToArray();
  23. }
  24. }
  25. /// <summary>
  26. /// 协议解码
  27. /// </summary>
  28. /// <param name="protoName">协议名</param>
  29. /// <param name="bytes">要解码的byte数组</param>
  30. /// <param name="offset">协议体所在起始位置</param>
  31. /// <param name="count">协议体长度</param>
  32. /// <returns>返回解码后的协议</returns>
  33. public static IExtensible Decode(string protoName, byte[] bytes, int offset, int count)
  34. {
  35. using (MemoryStream ms = new MemoryStream(bytes, offset, count))
  36. {
  37. Type type = Type.GetType(protoName);
  38. return (IExtensible)Serializer.NonGeneric.Deserialize(type, ms);
  39. }
  40. }
  41. /// <summary>
  42. /// 协议名编码
  43. /// </summary>
  44. /// <param name="proto">协议</param>
  45. /// <returns>返回编码后的字节数据</returns>
  46. public static byte[] EncodeName(IExtensible proto)
  47. {
  48. //名字bytes和长度
  49. byte[] nameBytes = Encoding.UTF8.GetBytes(proto.GetType().FullName);
  50. Int16 length = (Int16)nameBytes.Length;
  51. //申请bytes数值
  52. byte[] bytes = new byte[length + 2];
  53. //组装2字节的长度信息
  54. bytes[0] = (byte)(length % 256);
  55. bytes[1] = (byte)(length / 256);
  56. //组装名字bytes
  57. Array.Copy(nameBytes, 0, bytes, 2, length);
  58. return bytes;
  59. }
  60. /// <summary>
  61. /// 协议名解码
  62. /// </summary>
  63. /// <param name="bytes">要解码的byte数组</param>
  64. /// <param name="offset">起始位置</param>
  65. /// <param name="length">长度</param>
  66. /// <returns>返回解码后的协议名</returns>
  67. public static string DecodeName(byte[] bytes, int offset, out int length)
  68. {
  69. length = 0;
  70. //必须大于2字节
  71. if (offset + 2 > bytes.Length) return string.Empty;
  72. //获取长度
  73. Int16 l = (Int16)((bytes[offset + 1] << 8) | bytes[offset]);
  74. if (l <= 0) return string.Empty;
  75. //长度必须足够
  76. if (offset + 2 + l > bytes.Length) return string.Empty;
  77. //解析
  78. length = 2 + l;
  79. string name = Encoding.UTF8.GetString(bytes, offset + 2, l);
  80. return name;
  81. }
  82. }
  83. }

Connect 连接服务端:

创建网络管理类NetworkManager,定义Socket套接字、读缓冲区、以及正在连接和关闭的标志位等字段,封装Connect连接函数,接收两个参数,参数一ip代表服务端的IP地址,参数二port代表端口:


  
  1. using System;
  2. using UnityEngine;
  3. using System.Net.Sockets;
  4. using System.Collections.Generic;
  5. namespace SK.Framework.Sockets
  6. {
  7. public class NetworkManager : MonoBehaviour
  8. {
  9. //定义套接字
  10. private static Socket socket;
  11. //接收缓冲区
  12. private static ByteArray readBuff;
  13. //是否正在连接
  14. private static bool isConnecting = false;
  15. //是否正在关闭
  16. private static bool isClosing = false;
  17. /// <summary>
  18. /// 连接服务端
  19. /// </summary>
  20. /// <param name="ip">服务器IP地址</param>
  21. /// <param name="port">端口</param>
  22. public static void Connect(string ip, int port)
  23. {
  24. //状态判断
  25. if ((socket != null && socket.Connected) || isConnecting) return;
  26. //初始化
  27. Init();
  28. //参数设置
  29. socket.NoDelay = true;
  30. //连接
  31. isConnecting = true;
  32. socket.BeginConnect(ip, port, ConnectCallback, socket);
  33. }
  34. //初始化状态
  35. private static void Init()
  36. {
  37. //Socket
  38. socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  39. //接收缓冲区
  40. readBuff = new ByteArray();
  41. //是否正在连接
  42. isConnecting = false;
  43. //是否正在关闭
  44. isClosing = false;
  45. }
  46. //Connect回调
  47. private static void ConnectCallback(IAsyncResult ar)
  48. {
  49. try
  50. {
  51. Socket socket = (Socket)ar.AsyncState;
  52. socket.EndConnect(ar);
  53. isConnecting = false;
  54. Debug.Log($"成功连接服务端.");
  55. //发布消息
  56. Messenger.Publish("连接服务端", true);
  57. //TODO:开始接收数据
  58. }
  59. catch (SocketException error)
  60. {
  61. Debug.Log($"连接服务端失败:{error}");
  62. isConnecting = false;
  63. //发布消息
  64. Messenger.Publish("连接服务端", false);
  65. }
  66. }
  67. }
  68. }

NoDelay参数含义:

将其设为true时,表示不使用Nagle算法,什么是Nagle算法?

        Nagle 算法旨在通过使套接字缓冲小数据包,然后在特定情况下将它们合并并发送到一个数据包,从而减少网络流量。 TCP 数据包包含40字节的标头以及要发送的数据。 当使用 TCP 发送小型数据包时,TCP 标头产生的开销可能会成为网络流量的重要部分。 在负载较重的网络上,由于这种开销导致的拥塞会导致丢失数据报和重新传输,以及拥塞导致的传播时间过大。 如果在连接上以前传输的数据保持未确认的情况,则 Nagle 算法将禁止发送新的 TCP 段。

        启用Nagle算法可以提升网络传输效率,但它要收集到一定长度的数据后才会把它们一起发送出去。这样就会降低网络的实时性,本套框架里我们关闭Nagle算法,将socket.NoDelay设为true。

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

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

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

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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