如果你不能用最简单的语言来描述,那你就是没有真正领悟。——爱因斯坦
有关Socket原理的文章,网络上可谓是铺天盖地,在这里我就不再赘述,直接上干货。
一、思维导图
先来通过一张脑图,将整体设计结构展现出来。
二、核心文件
整个框架共九个类文件,分别负责独立的业务逻辑。上图中已经简明的描述了每个类文件的作用。下面,我将列出几个核心文件的代码逻辑。
1、GameSocket
核心类文件,控制着socket连接的整体流程
public class GameSocket
{
//Socket连接回调函数
private static Dictionary<SocketEvent, Action> socketEventHandles = new Dictionary<SocketEvent, Action>();
//Socket实例对象;
public Socket socket;
//最大重连次数;
private int maxReTryTime = 2;
//当前重连次数;
private int reTryTime = 0;
//服务器地址;
private string serverIp;
//服务器端口;
private int port;
//连接状态标记;
private bool connected = false;
//是否正在重连;
private bool reConnect = false;
#region 回调相关函数
/// <summary>
/// 注册socket连接回调函数
/// </summary>
/// <param name="conSucess">连接成功回调</param>
/// <param name="conFaild">连接失败回调</param>
public void RegisterSocketEvent(SocketEvent eventName ,Action action)
{
if (!socketEventHandles.ContainsKey(eventName))
socketEventHandles.Add(eventName, action);
else
socketEventHandles[eventName] += action;
}
/// <summary>
/// 移除Socket连接事件回调
/// </summary>
/// <param name="type">协议实体类型</param>
/// <param name="action">回调逻辑</param>
public void UnRegisterSocketEvent(SocketEvent eventName, Action action)
{
if (socketEventHandles.ContainsKey(eventName))
{
socketEventHandles[eventName] -= action;
if (socketEventHandles[eventName] == null)
socketEventHandles.Remove(eventName);
}
}
/// <summary>
/// 执行Socket连接事件回调方法;
/// </summary>
/// <param name="type"></param>
public void ExcuteSocketEventHandle(SocketEvent eventName)
{
if (socketEventHandles.ContainsKey(eventName))
{
Action action = socketEventHandles[eventName];
action.Invoke();
}
}
#endregion
#region 启动与中断
/// <summary>
/// 请求连接
/// </summary>
/// <param name="ip"></param>
/// <param name="port"></param>
public void Connect(string serverIp, int port)
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
//记录当前的地址和端口
this.serverIp = serverIp;
this.port = port;
//只能识别ip
//IPAddress ipAddress = IPAddress.Parse(server);
//兼容ip与域名;
IPHostEntry host = Dns.GetHostEntry(serverIp);
IPAddress ip = host.AddressList[0];
//创建连接对象
IPEndPoint ipEndPoint = new IPEndPoint(ip, port);
Tracer.Log("启动连接 ip:" + ip + " port:" + port);
socket.Connect(ipEndPoint);
if (socket.Connected)
OnConnectSucess();
else
OnConnectFaild();
}
catch (Exception e)
{
OnConnectFaild(e);
}
}
/// <summary>
/// 重连;
///
/// TODO,该方法需要被重写;
/// </summary>
public void ReConnect()
{
if (maxReTryTime > reTryTime)
{
reConnect = true;
reTryTime++;
Tracer.Log("开始进行第" + reTryTime + "次重连");
Connect(serverIp, port);
}
else
{
reConnect = false;
OnConnectFaild();
}
}
/// <summary>
/// 停止socket连接
/// </summary>
public void Dispose()
{
if (socket != null)
{
if (socket.Connected)
//禁用Socket的发送和接收功能,
socket.Shutdown(SocketShutdown.Both);
//释放托管资源
socket.Close();
//释放非托管资源;
socket.Dispose();
socket = null;
}
}
#endregion
#region 连接状态
/// <summary>
/// 连接成功;
/// </summary>
private void OnConnectSucess()
{
Tracer.Log("连接成功");
connected = true;
//启动Socket的线程;
GameSocketMgr.Self.StartThread();
//执行连接成功的回调
ExcuteSocketEventHandle(SocketEvent.CONNECT_SUCESS);
}
/// <summary>
/// 连接失败;
/// </summary>
private void OnConnectFaild(Exception e = null)
{
if (reConnect)
ReConnect();
else
{
Tracer.Log("连接失败");
connected = false;
//执行连接失败的回调
ExcuteSocketEventHandle(SocketEvent.CONNECT_FAILD);
}
}
/// <summary>
/// 不明原因引起的连接中断,包含正常关闭以及非正常关闭
/// </summary>
public void OnConnectClose()
{
Tracer.Log("连接中断");
connected = false;
ExcuteSocketEventHandle(SocketEvent.CONNECT_CLOSE);
}
#endregion
/// <summary>
/// 是否连接中
/// </summary>
public bool BeConnected
{
get { return connected; }
}
}
2、SocketSender
发送线程类。该类将所有待发送的协议字节流存入了待发送列表,然后在独立的线程内进行循环,最终将数据送入Socket管道。
public class SocketSender
{
//发送线程;
private Thread sendThread = null;
//待发送列表;
private Queue<byte[]> sendList = new Queue<byte[]>();
/// <summary>
/// 启动发送线程
/// </summary>
public void Start()
{
//清空发送列表;
sendList.Clear();
Loom.RunAsync(
() =>
{
sendThread = new Thread(new ThreadStart(ProtoSend));
sendThread.IsBackground = true;
sendThread.Start();
}
);
}
/// <summary>
/// 将通讯协议压入待发送队列
/// </summary>
/// <param name="msg">消息结构体</param>
public void ProtoEnqueue(CmdNo num,IExtensible msg)
{
//将协议进行序列化处理;
byte[] buff = ConvertTools.ProtoToBytes(num,msg);
sendList.Enqueue(buff);
}
/// <summary>
/// 将待发送队列中的协议推入通信管道
/// </summary>
private void ProtoSend()
{
Socket socket = GameSocketMgr.Self.Socket;
while (GameSocketMgr.Self.BeConnected)
{
if (sendList.Count > 0)
{
byte[] buff = sendList.Dequeue();
socket.Send(buff, buff.Length, SocketFlags.None);
}
//重新分配CPU资源
Thread.Sleep(0);
}
}
/// <summary>
/// 释放发送线程
/// </summary>
public void Dispose()
{
try
{
Tracer.Log("释放发送线程");
sendThread.Abort();
sendThread.Join();
}
//线程异常,不需处理
catch (ThreadAbortException ex)
{
}
catch (Exception e)
{
Tracer.Log("E:" + e, LogLevel.ERROR);
}
}
}
3、SocketReceiver
接受线程类。该类将在独立的线程内将所有接受到的字节流存入了待处理列表,然后转化成协议体实例,执行回调逻辑。
public class SocketReceiver
{
//字节对象;
private BytesBuff bytesBuff = new BytesBuff();
//接受字节数组
private byte[] tmpReceiveBuff = new byte[ReadMax];
//接受线程
private Thread receiveThread = null;
//最大缓存
private const int ReadMax = 1024;
/// <summary>
/// 启动协议接受线程
/// </summary>
public void Start()
{
//清空接受列表
bytesBuff.Reset();
Loom.RunAsync(
() =>
{
receiveThread = new Thread(new ThreadStart(ProtoEnqueue));
receiveThread.IsBackground = true;
receiveThread.Start();
}
);
}
/// <summary>
/// 将通讯协议压入待处理队列
/// </summary>
private void ProtoEnqueue()
{
Socket socket = GameSocketMgr.Self.Socket;
while (GameSocketMgr.Self.BeConnected)
{
//socket已经断开,或者不存在可以读取的数据;
if (!socket.Connected || socket.Available <= 0)
continue;
try
{
int receiveLength = socket.Receive(tmpReceiveBuff, 0, ReadMax, SocketFlags.None);
if (receiveLength > 0)
{
//将接受的数据压入当前字节流;
bytesBuff.MergeByte(tmpReceiveBuff, (uint)receiveLength);
ProtoExcute();
}
}
catch (Exception e)
{
Debug.Log(e);
}
//重新分配CPU资源
Thread.Sleep(0);
}
}
/// <summary>
/// 释放协议接受线程
/// </summary>
public void Dispose()
{
try
{
Tracer.Log("释放接受线程");
receiveThread.Abort();
receiveThread.Join();
}
//线程异常,不需处理
catch (ThreadAbortException ex)
{
}
catch (Exception e)
{
Tracer.Log("E:" + e, LogLevel.ERROR);
}
}
/// <summary>
/// 执行协议的逻辑
/// </summary>
/// <param name="val"></param>
/// <param name="len"></param>
public void ProtoExcute()
{
while (bytesBuff.ReadValid)
{
byte[] bytes = bytesBuff.ReadByte();
IExtensible proto = ConvertTools.BytesToProto(bytes);
if (proto != null)
{
//切换至主线程,进行协议方法的回调
Loom.QueueOnMainThread(() =>
{
GameSocketMgr.Self.ExcuteProtoHandle(proto.GetType(), proto);
});
}
}
}
}
4、ConvertTools
转换工具类,将字节流与ProtoBuff协议实例进行相互转换。注意,本类中有关协议体的结构是依据我目前项目的结构而进行读取的。各位在使用时,需要根据自己项目中的协议结构进行变通。
public class ConvertTools
{
//协议解析器;
private static ProtobufSerializer protobufSerializer = new ProtobufSerializer();
#region 字节转化成Protobuff
/// <summary>
/// 将ProtoMessage转化为proto
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public static IExtensible BytesToProto(byte[] bytes)
{
//第三位开始的两字节代表协议号
CmdNo num = (CmdNo)BitConverter.ToUInt16(bytes, sizeof(ushort));
//将通讯管道中的字节流进行初步解析,读取其中协议体部分
byte[] buffer = DecodeByte(bytes);
//将真实的协议字节流转化为协议体对象
IExtensible proto = DecodeProto(num, buffer);
//协议实体可能存在缺失注册的情况;
if (proto == null)
Tracer.Log("尚未注册协议:" + num, LogLevel.ERROR);
else
Tracer.LogProto("【接受协议】", (int)num, proto);
return proto;
}
/// <summary>
/// 将通讯管道中的字节流进行初步解析,读取其中协议体部分
/// </summary>
/// <returns></returns>
private static byte[] DecodeByte(byte[] bytes)
{
ushort dataSize = GetCurDataSize(bytes);
//协议体的实际长度要减去头部的四个字节
int len = dataSize - sizeof(uint);
byte[] buffer = new byte[len];
Array.Copy(bytes, sizeof(int), buffer, 0, len);
return buffer;
}
/// <summary>
/// 将真实的协议字节流转化为协议体对象
/// </summary>
/// <param name="num"></param>
/// <param name="buffer"></param>
/// <returns></returns>
private static IExtensible DecodeProto(CmdNo num, byte[] buffer)
{
IExtensible proto = null;
Type type = GameSocketMgr.Self.GetProtobufType(num);
if (type != null)
{
using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
{
ms.Write(buffer, 0, buffer.Length);
ms.Seek(0, System.IO.SeekOrigin.Begin);
proto = protobufSerializer.Deserialize(ms, null, type) as ProtoBuf.IExtensible;
ms.Close();
}
}
return proto;
}
#endregion
#region Prptobuff转化成字节
/// <summary>
/// 将协议体转化为字节流
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
public static byte[] ProtoToBytes(CmdNo num, IExtensible msg)
{
Tracer.LogProto("【发送协议】", (int)num, msg);
//将协议实体转化为字节流对象
byte[] buffs = EncodeProto(msg);
//将协议字节流转化为最终要发送的字节流
byte[] bytes = EncodeBytes(num, buffs);
return bytes;
}
/// <summary>
/// 将协议实体转化为字节流对象
/// </summary>
/// <param name="num"></param>
/// <param name="msg"></param>
/// <returns></returns>
private static byte[] EncodeProto(IExtensible msg)
{
byte[] data = null;
using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
{
protobufSerializer.Serialize(ms, msg);
if (ms.Length > 0)
{
data = new byte[(int)ms.Length];
ms.Seek(0, System.IO.SeekOrigin.Begin);
ms.Read(data, 0, data.Length);
}
ms.Close();
}
if (data == null)
data = new byte[0];
return data;
}
/// <summary>
/// 将协议字节流转化为最终要发送的字节流
/// </summary>
/// <param name="message"></param>
private static byte[] EncodeBytes(CmdNo num,byte[] datas)
{
//获取协议字节长度,ushort占用两字节
ushort cmd_len = datas == null ? (ushort)0 : (ushort)datas.Length;
//获取协议号
ushort cmd_no = (ushort)num;
//定义头部长度
const int head_len = sizeof(int);
//定义整个包长
int pack_len = cmd_len + head_len;
//定义待发送字节长度;
byte[] send = new byte[pack_len];
//将协议长度转化成字节数组
byte[] len_byte = BitConverter.GetBytes(cmd_len);
//将协议号转化为字节数组;
byte[] no_byte = BitConverter.GetBytes(cmd_no);
//将协议长度写入最前面的两位
Array.Copy(len_byte, 0, send, 0, len_byte.Length);
//将协议号写入后两位;
Array.Copy(no_byte, 0, send, len_byte.Length, no_byte.Length);
if (datas != null)
Array.Copy(datas, 0, send, head_len, cmd_len);
return send;
}
/// <summary>
/// 获取指定字节中首包的长度;
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static ushort GetCurDataSize(byte[] bytes)
{
return (ushort)(BitConverter.ToUInt16(bytes, 0) + sizeof(uint));
}
#endregion
}
5、BytesBuff
字节流对象,主要用于接收协议时对数据进行解析处理。
public class BytesBuff
{
//最小尺寸
private uint minSize;
//字节流数组
private byte[] bytes;
//当前字节位置
private uint curPosition;
/// <summary>
/// 构造函数
/// </summary>
public BytesBuff()
{
Reset();
}
/// <summary>
/// 重置字节流
/// </summary>
public void Reset()
{
curPosition = 0;
minSize = 256;
bytes = new byte[minSize];
}
/// <summary>
/// 合并字节流
/// </summary>
/// <param name="val"></param>
/// <param name="len"></param>
public void MergeByte(byte[] val, uint len)
{
//如果当前剩余尺寸小于即将压入的字节尺寸
if (resetSize < len)
{
//对当前尺寸进行扩容
minSize += len;
//按照当前的新尺寸定义一个接受数组
byte[] newBuffer = new byte[minSize];
//将当前数组的内容拷贝至新的数组;
Array.Copy(bytes, 0, newBuffer, 0, curPosition);
//将新数组赋值给原数组,使原数组拥有更大的尺寸;
bytes = newBuffer;
}
//向当前字节数组存储新的数据;
Array.Copy(val, 0, bytes, curPosition, len);
//增加已使用的字节数
curPosition += len;
}
/// <summary>
/// 读取字节流;
/// </summary>
public byte[] ReadByte()
{
//获取最近一条协议的长度
ushort dataSize = ConvertTools.GetCurDataSize(bytes);
//定义当前协议的字节数组;
byte[] buffs = new byte[dataSize];
//将指定的长度拷贝至新的字节数组
Array.Copy(bytes, 0, buffs, 0, dataSize);
//================== 变更字节读取流 ========================//
//将当前字节位置减少消耗的长度;
curPosition -= dataSize;
//当前已经使用的字节数是否小于最小字节数
uint size = curPosition < minSize ? minSize : curPosition;
//创建新的字节数组
byte[] newBuff = new byte[size];
//将旧字节数组拷贝至新的字节数组
Array.Copy(bytes, dataSize, newBuff, 0, curPosition);
//赋值;
bytes = newBuff;
return buffs;
}
/// <summary>
/// 是否能够读取下一条协议;
/// </summary>
public bool ReadValid
{
get
{
//可能存在当前接收不完整,所以需要对包体大小进行判断;
//只有当前字节数组的位置数大于下一条协议的长度时,才可以进行读取;
if (curPosition != 0 && curPosition >= ConvertTools.GetCurDataSize(bytes))
return true;
return false;
}
}
/// <summary>
/// 当前剩余字节数
/// </summary>
private uint resetSize
{
get {
return minSize - curPosition;
}
}
}
本套框架对常见的Socket问题进行了处理,并使用独立的发送线程与接受线程进行优化。
完整框架代码下载地址
注:该代码因未上传协议体部分,故不可运行,仅供学习参考使用。