介绍
在阅读了罗培羽著作的Unity3D网络游戏实战一书后,博主综合自己的开发经验与考虑进行部分修改和调整,将通用的客户端网络模块和通用的服务端框架进行提取,形成专栏,介绍Socket网络编程,希望对其他人有所帮助。目录如下,链接为对应的CSDN博客地址:
一、通用服务端框架
(一)、定义套接字和多路复用
https://blog.csdn.net/qq_42139931/article/details/124051945?spm=1001.2014.3001.5501
(二)、客户端信息类和通用缓冲区结构
https://blog.csdn.net/qq_42139931/article/details/124053571?spm=1001.2014.3001.5502
(三)、Protobuf 通信协议
https://blog.csdn.net/qq_42139931/article/details/124054972?spm=1001.2014.3001.5501
(四)、数据处理和关闭连接
https://blog.csdn.net/qq_42139931/article/details/124055227?spm=1001.2014.3001.5501
(五)、Messenger 事件发布、订阅系统
https://blog.csdn.net/qq_42139931/article/details/124055392?spm=1001.2014.3001.5501
(六)、单点发送和广播数据
https://blog.csdn.net/qq_42139931/article/details/124055482?spm=1001.2014.3001.5501
(七)、时间戳和心跳机制
https://blog.csdn.net/qq_42139931/article/details/124055856?spm=1001.2014.3001.5501
二、通用客户端网络模块
(一)、Connect 连接服务端
https://blog.csdn.net/qq_42139931/article/details/124091349?spm=1001.2014.3001.5502
(二)、Receive 接收并处理数据
https://blog.csdn.net/qq_42139931/article/details/124092588?spm=1001.2014.3001.5502
(三)、Send 发送数据
https://blog.csdn.net/qq_42139931/article/details/124094323?spm=1001.2014.3001.5502
(四)、Close 关闭连接
https://blog.csdn.net/qq_42139931/article/details/124094895?spm=1001.2014.3001.5502
本篇内容:
客户端网络模块中同样使用服务端框架中的通用缓冲区结构ByteArray,和消息的发布、订阅系统Messenger,以及通信协议工具类ProtoUtility,代码分别如下:
代码语言:javascript复制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;
}
}
}
代码语言:javascript复制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);
}
}
}
}
}
代码语言:javascript复制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代表端口:
代码语言:javascript复制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版)罗培羽 著