Unity【Multiplayer 多人在线】- Socket 通用客户端网络模块(一)、Connect 连接服务端
介绍
在阅读了罗培羽著作的Unity3D网络游戏实战一书后,博主综合自己的开发经验与考虑进行部分修改和调整,将通用的客户端网络模块和通用的服务端框架进行提取,形成专栏,介绍Socket网络编程,希望对其他人有所帮助。目录如下:
一、通用服务端框架
二、通用客户端网络模块
(一)、Connect 连接服务端
本篇内容:
客户端网络模块中同样使用服务端框架中的通用缓冲区结构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
- 点赞
- 收藏
- 关注作者
评论(0)