赞
踩
介绍
在阅读了罗培羽著作的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版)罗培羽 著
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。