介绍

在阅读了罗培羽著作的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;}//读取Int16public Int16 ReadInt16(){if (length < 2) return 0;Int16 ret = (Int16)((bytes[readIdx + 1]) << 8 | bytes[readIdx]);readIdx += 2;CheckAndMoveBytes();return ret;}//读取Int32public 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);//组装名字bytesArray.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(){//Socketsocket = 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版)罗培羽 著

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

  1. Unity【Multiplayer 多人在线】- Socket 通用客户端网络模块(二)、Receive 接收并处理数据

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

  2. Unity【Multiplayer 多人在线】- Socket 通用服务端框架(七)、时间戳和心跳机制

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

  3. 多线程+SOCKET编程实现qq群聊的服务端和客户端

    多线程+SOCKET编程实现qq群聊的服务端和客户端 标签(空格分隔): 多线程 网络编程 线程同步 一.设计思路 1.服务端 每来一个客户端连接,服务端起一个线程维护: 将收到的消息转发给所有的客户 ...

  4. 编写Java程序,使用 Socket类模拟用户加入 QQ 群时,QQ 小冰发送欢迎消息的场景(用户充当客户端,QQ 小冰充当服务端)

    查看本章节 查看作业目录 需求说明: 小冰是微软公司研发的人工智能机器人,被腾讯公司加入 QQ 群后,立即受到千万网友的喜爱.现在使用 Socket类模拟用户加入 QQ 群时,QQ 小冰发送欢迎消息的 ...

  5. Socket通信C#项目,完整的服务端和客户端

    Socket通信C#项目,完整的服务端和客户端,让您绕过最难写的Socket管理,是困难的多线程处理变成简单的事件处理,非常容易上手. 功能带有断线重连,实时侦测设备状态,简单实用,适合初学者或有迫切 ...

  6. UE4 TCP通信 (UE客户端与网络调试助手服务端、python服务端通信)

    目录 一.使用UE4建立TCP客户端 二.使用网络调试助手建立服务端 三.基于网络调试助手的服务端与UE客户端通信 四.基于python的TCP服务端与UE客户端通信 一.使用UE4建立TCP客户端 ...

  7. 学习 ET(1)- 开源的游戏客户端(基于 unity3d)服务端双端框架

    我: 客户端程序员,15+ 以上 C++ 编码经历, 还算扎实.Unity 编码经历 1年,C# 没有单独学过.真不想离开C++的世界,大形势驱使进入了Unity+C#世界. ET - 开源的游戏客户 ...

  8. 金蝶KS客户端无法连接服务端

    1.编写连接服务端注册表脚本: Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\Software\Kingdee\KDReg\DESKT ...

  9. oracle 客户端无法连接,Oracle客户端无法连接服务端解决方法及步骤

    客户端无法连接服务端数据库,原因有网络,配置,文件访问权限,数据库服务是否已启动 等问题导致,一般排查的步骤如下: 1.检查操作系统级别网络是否通畅 ping 192.168.10.1 2.检查数据库 ...

最新文章

  1. 如何优雅地实施持续交付部署
  2. .net中close和dispose及关闭流操作
  3. python编程django项目中ModuleNotFoundError: No module named ‘django.core.urlresolvers‘解决方法
  4. 为什么jsp写script代码报错_JSP 报错:ReferenceError: $ is not defined
  5. Windows 11 大更新!可安装超千款 Android 应用
  6. 97. Interleaving String 交错字符串
  7. 项目部署—Linux命令安装redis
  8. SQL Server中使用的公共数据类型
  9. java 文件上传终止_java文件上传
  10. hbase 2.0.5的下载及安装
  11. 自动驾驶之多传感器融合实践(1)------激光雷达点云数据处理
  12. WINDOWS图片和传真查看器找不到
  13. 如何绕过mac地址过滤_Maccms V8 后台Getshell #2(绕过过滤)
  14. Linux--文件操作
  15. mysql utf8mb4 错_MYSQL保存特殊字符失败,用编码utf8mb4解决错误 Incorrect string value ...解决方法...
  16. YOLOv7 训练前手动计算锚定框
  17. 架构设计文档模板之1:备选方案模板
  18. 3、jQuery插件之datetimepicker时间插件
  19. JavaScript之作用域链 1
  20. 小红书产品经理在线笔试题目

热门文章

  1. Vue路由跳转报错Avoided redundant navigation to current location: “/xxxxxx“.
  2. Windows下安装cab文件
  3. webrtc 海思芯片解码出错的解决办法
  4. Symbian 开源 免费下载
  5. 虎牙java工资_【虎牙直播工资】用户运营待遇-看准网
  6. linux命令行前面内容修改
  7. 为什么我设置了Targeted Device Family为Iphone还是ipad模拟器
  8. 1. 域控服务器部署
  9. FC小游戏合集网页版HTML源码
  10. 长夜梦中惊坐起,Vue的双向绑定到底是个什么东西?