一 客户端基础Socket连接¶
整体逻辑决定用异步的逻辑去完成。 - 建立NetManager - 建立ByteBuffer字节缓冲区
二 协议MsgBase¶
一般用Json或者ProtoBuf做演示:
2.1 ☆协议划分--TCP封包与解码 (处理粘包问题)¶
- 协议长度(2字节--short)
- 协议名长度(2字节--short)
- 协议名(根据协议名长度解码获得)
- 协议消息体(根据协议长度-协议名长度获得)
2.2 接受数据与发包¶
- 发送和接受都是异步完成的,注意消息列表和发送队列加锁。
- 发送或者接受完一次后,通过回调判断列表和队列是否为空继续发送或者递归相关函数。
三 服务端Socket连接¶
采用C# .net 8.0编写服务端,特点: - 不需要写main函数 - 引用默认不为空,需要加?或者宏定义 - jsonutility,debug等unity函数库无法使用。 - 可以用.Net的库添加相关的工具包
3.1 构成¶
部分工作其实和客户端是一致的,比如
- ByteBuffer:字节缓冲区
- MsgBase:消息处理,封包解包
但也有不一样的,比如NetManager就要重构了,并且需要将·socket
对应的fd和专门的ClientState
对应构成字典。
四 服务端连接¶
大部分服务器还是运行在Linux端的,目前的异步和Select也是暂时学一遍,后面肯定都用epoll。
4.2 连接Connect过程¶
- 建立服务端Socket
- 设置IP和端口
-
Bind、Listen
-
执行后续的逻辑
4.1多路复用Select¶
使用Select,通过轮询的方式进行多路复用。 - 初始化一个list,里面存放所有的socket(包括客户端和服务端的) - 循环检测while - 放入服务端socket到sockets列表中 - 遍历客户端字典,将客户端Socket放入到sockets列表中,后续进行IO多路复用 - 传入socket.select,先暂时只用设置第一个参数(三个参数:读、写、报错),目前只需要读。 - 遍历所有socket - 先判断当前socket为服务端或者客户端,分别执行不同的逻辑
4.2服务端Socket接受连接逻辑Accept¶
主要逻辑:Accept()并创建客户端对象 - 如果多路复用有,代表需要连接客户端了 - 先通过服务端socket的accept,创建新的socket对象,作为客户端的socket - 同时创建对应的客户端状态ClientState对象 - 作为键值对放入客户端字典当中
4.3 客户端Socket接受发送消息的连接逻辑Receive¶
- 主要逻辑:
- 判断缓冲区空间是否足足够
- 接受消息,更新字节缓冲数组,并拿到消息字节大小
- 传入对应客户端对象对消息进行处理
- 处理完后移动字节缓冲数组
4.4 服务端处理消息¶
逻辑上其实和客户端差不多,都是处理封包的过程,主要需要从对应的客户端对象上拿字节数组。 - 如果消息没处理完,继续调用消息处理方法。
4.5 服务端发送消息Send¶
原理也基本不变,但是用的时候需要指定客户端对象,用客户端的socket去发送Send消息。(因为客户端只用发送给服务器,而服务器需要发送给所有的客户端);
五 测试连接¶
- 解析消息后,就可以通过消息执行相关的逻辑了
- 消息解析类继承自MsgBase:但是两边都最好要有相同的类去处理。
六 客户端关闭Close().¶
注意,Close有两种关闭的情况
一般来说也不会直接关闭,会判断一些特殊情况。 - 正在连接中:isConnecting表示正在连接但还没连接完的状态。‘ - 此时再次调用Connect函数应该报错并返回,不应该多次连接。 - Close:在正在连接的时候,不应该关闭 - 正在关闭中:还有其他的消息未处理(发送)完(writeQueue.Count > 0) - 设置一个isClosing,用来记录已经开始关闭的流程 - 对于Send - isConnecting和isClosing都将阻止继续编码封包发送消息 - Send如果正在发送消息,先等他递归发送(SendCallback)将writeQueue处理完成。 - 最后如果正在isClosing,那么此时发送完了,就直接用socket.Close关闭流程了。 - 没有上述情况,就直接在函数里关闭Socket了。
服务端关闭¶
这个就多了,错误、失败都直接关闭就行。
七 网络事件管理¶
7.1 定义枚举网络事件NetEvent:¶
- ConnectSucc = 1,
- ConnectFail = 2,
- Close,
7.2 定义委托 EventListener¶
定义一个委托delegate,叫做EventListener,需要传入一个string参数作为错误信息。(但实际上可以不用这个参数,只有失败才会用)
7.3 定义字典¶
- 键值对为(网络事件,EventListener)
7.4 添加事件¶
传入类型和委托参数 - 先根据事件类型判断有无Key,有的话value +=。 - 没有的话ADD键值对
7.5 删除事件¶
参数传入类型和委托 - 有Key 则-=listener。 - 没key则字典Remove
7.6 分发(执行)事件¶
传入事件类型和字符串err
- 字典里有key就直接执行委托eventListener[netEvent](err)
7.7 写事件¶
注意,Close有两种关闭的情况 - 在Main函数里添加三个网络事件及其对应处理方式 - 替换之前直接的Debug.Log,用事件的方式输出对应连接结果
八 消息管理¶
整体其实和上面类似。 只不过负责字典是以【string,委托】的形式进行存储的,方便自定义消息。
处理消息¶
可以设置一帧处理消息的数量。比如一帧处理10个消息
九 客户端心跳机制¶
总的来说就是 - 客户端间隔Interval发送Ping,并更新lastPingtime - 服务端收到ping发送Pong回应 - 若超过间隔4倍时间没收到,断开对应的客户端连接 - 客户端收到Pong消息后,更新lastPongTime - 若超过间隔4倍时间没收到,断开自己的socket连接
测试:时间间隔改为2秒,平时30秒。
9.1 Bool变量定义ping、pong消息¶
新建个文件夹专门存消息,继承自Msgbase
9.2 心跳设置¶
- 是否启用心跳机制
- 上次Ping发送时间
- 上次收到Pong时间
- 心跳间隔时间
9.3 心跳更新PingUpdate¶
每帧都需要进行更新时间,若当前时间-上一帧更新时间>30s(自己设置的时间)。则发送Ping消息。 - 这个和之前的读消息函数MsgUpdate一起放到NetManager的Update里,交由Main函数的Update循环进行调用。 - 断开处理:和服务端lastpongTime的时间差大于间隔的四倍。
9.4 心跳机制,接受Pong消息¶
- 将pong消息处理函数一开始就作为函数,在Init时就传到Addlistener里面,实现方法注册,方便后续调用
十 服务端心跳机制¶
10.1 拷贝客户端的消息¶
ping、pong
10.2 消息处理类¶
用来封装各种消息的处理,确保参数都是【客户端+消息】的形式,方便通过反射快速处理。 - Ping,向对应客户端发送消息 - Pong,
10.3 通过反射调用客户端发过来的协议与方法¶
- MethodInfo:方法元信息。
- 通过protoName协议名来找到对应typeof(MsgHandler)里注册的消息处理方法mi。
- 如果mi不为空
- 将要传入的参数统一封装到一个Object类型的数组中
- mi.Invoke调用方法。
10.4 lastPingTime,超时间隔¶
服务端同样需要设置超时间隔,只不过lastPing时间就需要单独设置到服务端连接的每个客户端了。 - lastPingTime,直接 - 当超时大于服务端设置的间隔的四倍,直接关闭Close。 - 服务端这边关闭就:直接socket关闭+客户端socket字典就行。
十一 protoBuf协议¶
11.1 基本语法¶
- syntax:指定协议版本号,没有则默认proto2版本(尽量标记一下)
- message:定义消息类型,相当于struct。
- enum:定义枚举类型,相当于enum。
概述:¶
- message用于定义一个数据结构类型,由一系列字段构成
- 每个字段的定义由一定格式构成:修饰符+数据类型+字段名称 = 字段标识+字段默认值
- 其中数据类型、字段名称、字段标识是必须的
举例¶
- protoBuf文件:
syntax = "proto2"; message MsgPing { optional string protoName=1[default="MsgPing"]; }
11.2 run编译protobuf的bat文件¶
-i
:表示输入路径-o
:表示输出路径protogen.exe -i:proto\MsgPing.proto -o:cs\MsgPing.cs protogen.exe -i:proto\MsgPong.proto -o:cs\MsgPong.cs pause
或者用通配符,对于大量的proto文件进行批处理
@echo off
for %%i in (proto\*.proto) do (
protogen.exe -i:%%i -o:cs\%%~ni.cs
)
pause
11.3 导入项目¶
protobuf包¶
C#的NuGet库,找到protoBuf-Net插件
导入proto的C#文件¶
首先将生成的protobuf相关消息文件添加到项目的新的文件夹中
11.4 封包和解码¶
- 参数,继承自IExtensible(新的消息基类)
编码¶
- 用流的方式写入MemoryStream