本篇主要是分享基于.Net的tcp服务端通信方案。其中基于unity的tcp客户端通信会在后面贴出来。另外关于tcp粘包断包的处理将放在这里分享《C# socket粘包断包处理》。
目录
定时器处理丢失连接的客户端并回调ClientSocket的定时器
整体设计
如图所示一共采用了三层封装来处理整个服务端的逻辑。
首先最上层的TcpServerMgr可以被业务层直接创建,并且通过事件系统将服务器收到的来自客户端的网络消息分发到各个业务系统中去。
其次TcpServer负责了整个tcp网络通信的创建,监听建立连接,关闭,心跳超时等等处理。对于正常接收到的消息通过一个回调函数传递给TcpServerMgr。
最后ClientSocket的设计主要是由于一个服务器要和多个客户端通信,因此单个客户端的通信相关逻辑(发送,接收,关闭,心跳超时)需要被封装起来供由TcpServer整理来管理。
TcpData-单条tcp通信数据
对于所有的tcp消息需要将其封装到一个单独的脚本里面。便于我们定义一条消息的结构,处理加密解密,处理构造和解析。
这里将TcpData的结构简单的分为了四块:
- byte header:数据头,占一个字节,主要用于标识一条tcp消息的开始。
- int len:消息长度,占四个字节,主要用于确定整个消息的长度(header+len+protocol+bytesData)。
- int protocol:协议号,占四个字节,这个是客户端和服务器约定的业务处理编号,比如1表示建立连接,2表示获取英雄数据等等
- byte[] bytesData:数据内容,占N个字节,取决于具体的业务需求。
为什么需要定义数据头,消息长度这些与业务无关的东西。主要还是为了解决tcp通信中的粘包和断包的问题。这个在这里会分享。
定义好数据之后再封装几个供外部调用的接口
- Read():将二进制的字节流转换为我们制定好的消息即header len protocol bytesData,供接收使用
- Get():将我们定义好的消息转换为二进制,供发送使用
其他的一些接口都是辅助这些的并部太重要。
这里是完整的代码,中间有一块关于加密的处理,具体加密的实现并不重要,有兴趣可以看下这里###。这里只加密了数据内容的部分(跟加密的方式有一点关系)。
namespace GYSQ.Net.Tcp
{
// tcp数据处理
public class TcpData : Poolable<TcpData>
{
public static readonly byte TCP_HEAD_MARK = 0b10011011;
public static readonly int TCP_HEADER_LEN = 9;
// 加密
private static Encry mEncry = new Encry("gysq_tcp_key-123456");
// 数据头标记
public byte header = TCP_HEAD_MARK;
// 数据长度
public int len;
// 协议号
public int protocol;
// 数据内容
public byte[] bytesData;
// 读取字节
public void Read(byte[] arryData)
{
using (MemoryStream ms = new MemoryStream(arryData))
{
using (BinaryReader br = new BinaryReader(ms))
{
header = br.ReadByte();
len = br.ReadInt32();
protocol = br.ReadInt32();
bytesData = br.ReadBytes(len - TCP_HEADER_LEN);
// 解密
mEncry.DoEncry(bytesData);
}
}
}
// 获取加密后的二进制
public byte[] Get()
{
using (MemoryStream ms = new MemoryStream())
{
using (BinaryWriter bw = new BinaryWriter(ms))
{
bw.Write(header);
bw.Write(len);
bw.Write(protocol);
if (bytesData != null)
{
bw.Write(bytesData);
}
// 加密
byte[] data = ms.ToArray();
mEncry.DoEncry(data, TCP_HEADER_LEN);
return data;
}
}
}
// 构建消息
public void Build(int protocol, byte[] bytesData = null)
{
header = TCP_HEAD_MARK;
len = TCP_HEADER_LEN + (bytesData == null ? 0 : bytesData.Length);
this.protocol = protocol;
this.bytesData = bytesData;
}
// 获取json
public string GetJsonContent()
{
return Encoding.UTF8.GetString(bytesData);
}
public void Build(int protocol, string strJson)
{
if (string.IsNullOrEmpty(strJson))
{
Build(protocol);
}
else
{
Build(protocol, Encoding.UTF8.GetBytes(strJson));
}
}
protected override void OnDispose()
{
bytesData = null;
base.OnDispose();
}
}
}
ClientSocket-单个客户端的tcp逻辑处理
ClientSocket被构造的前提是服务器已经和某个客户端建立好连接。这块的脚本主要是处理发送、接收、关闭连接、将收到的完整消息传递出去。
发送处理
首先构建一个发送队列,里面存放的是需要被发送的对象TcpData。
启动一个线程,从队列中取出需要被发送的对象并通过Socket.Send发送出去
外部发送的时候只需要向队列中添加构建好的TcpData即可
这里开线程主要是将一些二进制转换的处理放到线程里面避免对主线程造成阻塞。
// 发送消息线程
Thread threadSend = null;
private BlockQueue<TcpData> mSendQueue = new BlockQueue<TcpData>(50);
// 开启发送线程
private void StartHanle()
{
// 开启发送线程
threadSend = new Thread(HandleSend);
threadSend.IsBackground = true;
threadSend.Start();
}
// 发送处理
private void HandleSend()
{
while (bAlive)
{
try
{
if (mSendQueue.TryDequeue(out var tcpData))
{
byte[] bytesData = tcpData.Get();
socket.Send