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

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

介绍

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

一、通用服务端框架

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

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

        (三)、Protobuf 通信协议

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

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

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

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

 二、通用客户端网络模块

        (一)、Connect 连接服务端

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

        (三)、Send 发送数据

        (四)、Close 关闭连接

本篇内容:

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


      using System;
      namespace SK.Framework.Sockets
      {
         public class ByteArray
          {
             //默认大小
             private const int DEFAULT_SIZE = 1024;
             //初始大小
             private readonly int initSize = 0;
             //缓冲区
             public byte[] bytes;
             //读取位置
             public int readIdx = 0;
             //写入位置
             public int writeIdx = 0;
             //容量
             private int capacity = 0;
             //剩余空间
             public int remain { get { return capacity - writeIdx; } }
             //数据长度
             public int length { get { return writeIdx - readIdx; } }
             //构造函数
             public ByteArray(int size = DEFAULT_SIZE)
              {
                  bytes = new byte[size];
                  capacity = size;
                  initSize = size;
                  writeIdx = 0;
                  readIdx = 0;
              }
             //构造函数
             public ByteArray(byte[] defaultBytes)
              {
                  bytes = defaultBytes;
                  capacity = defaultBytes.Length;
                  initSize = defaultBytes.Length;
                  readIdx = 0;
                  writeIdx = defaultBytes.Length;
              }
             //重设尺寸
             public void ReSize(int size)
              {
                 if (size < length) return;
                 if (size < initSize) return;
                 int n = 1;
                 while (n < size)
                  {
                      n *= 2;
                  }
                  capacity = n;
                 byte[] newBytes = new byte[capacity];
                  Array.Copy(bytes, readIdx, newBytes, 0, writeIdx - readIdx);
                  bytes = newBytes;
                  writeIdx = length;
                  readIdx = 0;
              }
             //检查并移动数据
             public void CheckAndMoveBytes()
              {
                 if (length < 8)
                  {
                      MoveBytes();
                  }
              }
             //移动数据
             public void MoveBytes()
              {
                 if (length > 0)
                  {
                      Array.Copy(bytes, readIdx, bytes, 0, length);
                  }
                  writeIdx = length;
                  readIdx = 0;
              }
             //写入数据
             public int Write(byte[] bs, int offset, int count)
              {
                 if (remain < count)
                  {
                      ReSize(length + count);
                  }
                  Array.Copy(bs, offset, bytes, writeIdx, count);
                  writeIdx += count;
                 return count;
              }
             //读取数据
             public int Read(byte[] bs, int offset, int count)
              {
                  count = Math.Min(count, length);
                  Array.Copy(bytes, readIdx, bs, offset, count);
                  readIdx += count;
                  CheckAndMoveBytes();
                 return count;
              }
             //读取Int16
             public Int16 ReadInt16()
              {
                 if (length < 2) return 0;
                  Int16 ret = (Int16)((bytes[readIdx + 1]) << 8 | bytes[readIdx]);
                  readIdx += 2;
                  CheckAndMoveBytes();
                 return ret;
              }
             //读取Int32
             public Int32 ReadInt32()
              {
                 if (length < 4) return 0;
                  Int32 ret = (Int32)((bytes[readIdx + 3] << 24) |
                                      (bytes[readIdx + 2] << 16) |
                                      (bytes[readIdx + 1] << 8) |
                                      bytes[readIdx + 0]);
                  readIdx += 4;
                  CheckAndMoveBytes();
                 return ret;
              }
          }
      }
  
 

      using System.Collections.Generic;
      namespace SK.Framework.Sockets
      {
         /// <summary>
         /// 消息发布、订阅系统
         /// </summary>
         public class Messenger
          {
             public delegate void MessageEvent(params object[] args);
             private static readonly Dictionary<string, MessageEvent> msgDic = new Dictionary<string, MessageEvent>();
             /// <summary>
             /// 发布消息
             /// </summary>
             /// <param name="msgKey">消息Key值</param>
             /// <param name="arg">参数</param>
             public static void Publish(string msgKey, params object[] args)
              {
                 if (msgDic.ContainsKey(msgKey))
                  {
                      msgDic[msgKey].Invoke(args);
                  }
              }
             /// <summary>
             /// 订阅消息
             /// </summary>
             /// <param name="msgKey">消息Key值</param>
             /// <param name="messageEvent">订阅事件</param>
             public static void Subscribe(string msgKey, MessageEvent messageEvent)
              {
                 if (msgDic.ContainsKey(msgKey))
                  {
                      msgDic[msgKey] += messageEvent;
                  }
                 else
                  {
                      msgDic[msgKey] = messageEvent;
                  }
              }
             /// <summary>
             /// 取消订阅
             /// </summary>
             /// <param name="msgKey">消息Key值</param>
             /// <param name="messageEvent">订阅事件</param>
             public static void Unsubscribe(string msgKey, MessageEvent messageEvent)
              {
                 if (msgDic.ContainsKey(msgKey))
                  {
                      msgDic[msgKey] -= messageEvent;
                     if (msgDic[msgKey] == null)
                      {
                          msgDic.Remove(msgKey);
                      }
                  }
              }
          }
      }
  
 

      using System;
      using ProtoBuf;
      using System.IO;
      using System.Text;
      namespace SK.Framework.Sockets
      {
         /// <summary>
         /// 协议工具
         /// </summary>
         public static class ProtoUtility
          {
             /// <summary>
             /// 协议编码
             /// </summary>
             /// <param name="proto">协议</param>
             /// <returns>返回编码后的字节数据</returns>
             public static byte[] Encode(IExtensible proto)
              {
                 using (MemoryStream ms = new MemoryStream())
                  {
                      Serializer.Serialize(ms, proto);
                     return ms.ToArray();
                  }
              }
             /// <summary>
             /// 协议解码
             /// </summary>
             /// <param name="protoName">协议名</param>
             /// <param name="bytes">要解码的byte数组</param>
             /// <param name="offset">协议体所在起始位置</param>
             /// <param name="count">协议体长度</param>
             /// <returns>返回解码后的协议</returns>
             public static IExtensible Decode(string protoName, byte[] bytes, int offset, int count)
              {
                 using (MemoryStream ms = new MemoryStream(bytes, offset, count))
                  {
                      Type type = Type.GetType(protoName);
                     return (IExtensible)Serializer.NonGeneric.Deserialize(type, ms);
                  }
              }
             /// <summary>
             /// 协议名编码
             /// </summary>
             /// <param name="proto">协议</param>
             /// <returns>返回编码后的字节数据</returns>
             public static byte[] EncodeName(IExtensible proto)
              {
                 //名字bytes和长度
                 byte[] nameBytes = Encoding.UTF8.GetBytes(proto.GetType().FullName);
                  Int16 length = (Int16)nameBytes.Length;
                 //申请bytes数值
                 byte[] bytes = new byte[length + 2];
                 //组装2字节的长度信息
                  bytes[0] = (byte)(length % 256);
                  bytes[1] = (byte)(length / 256);
                 //组装名字bytes
                  Array.Copy(nameBytes, 0, bytes, 2, length);
                 return bytes;
              }
             /// <summary>
             /// 协议名解码
             /// </summary>
             /// <param name="bytes">要解码的byte数组</param>
             /// <param name="offset">起始位置</param>
             /// <param name="length">长度</param>
             /// <returns>返回解码后的协议名</returns>
             public static string DecodeName(byte[] bytes, int offset, out int length)
              {
                  length = 0;
                 //必须大于2字节
                 if (offset + 2 > bytes.Length) return string.Empty;
                 //获取长度
                  Int16 l = (Int16)((bytes[offset + 1] << 8) | bytes[offset]);
                 if (l <= 0) return string.Empty;
                 //长度必须足够
                 if (offset + 2 + l > bytes.Length) return string.Empty;
                 //解析
                  length = 2 + l;
                 string name = Encoding.UTF8.GetString(bytes, offset + 2, l);
                 return name;
              }
          }
      }
  
 

Connect 连接服务端:

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


      using System;
      using UnityEngine;
      using System.Net.Sockets;
      using System.Collections.Generic;
      namespace SK.Framework.Sockets
      {
         public class NetworkManager : MonoBehaviour
          {
             //定义套接字
             private static Socket socket;
             //接收缓冲区
             private static ByteArray readBuff;
             //是否正在连接
             private static bool isConnecting = false;
             //是否正在关闭
             private static bool isClosing = false;
             /// <summary>
             /// 连接服务端
             /// </summary>
             /// <param name="ip">服务器IP地址</param>
             /// <param name="port">端口</param>
             public static void Connect(string ip, int port)
              {
                 //状态判断
                 if ((socket != null && socket.Connected) || isConnecting) return;
                 //初始化
                  Init();
                 //参数设置
                  socket.NoDelay = true;
                 //连接
                  isConnecting = true;
                  socket.BeginConnect(ip, port, ConnectCallback, socket);
              }
             //初始化状态
             private static void Init()
              {
                 //Socket
                  socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                 //接收缓冲区
                  readBuff = new ByteArray();
                 //是否正在连接
                  isConnecting = false;
                 //是否正在关闭
                  isClosing = false;
              }
             //Connect回调
             private static void ConnectCallback(IAsyncResult ar)
              {
                 try
                  {
                      Socket socket = (Socket)ar.AsyncState;
                      socket.EndConnect(ar);
                      isConnecting = false;
                      Debug.Log($"成功连接服务端.");
                     //发布消息
                      Messenger.Publish("连接服务端", true);
                     //TODO:开始接收数据 
                  }
                 catch (SocketException error)
                  {
                      Debug.Log($"连接服务端失败:{error}");
                      isConnecting = false;
                     //发布消息
                      Messenger.Publish("连接服务端", false);
                  }
              }
          }
      }
  
 

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

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

    全部回复

    上滑加载中

    设置昵称

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

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

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