GameNet 遊戲網絡庫 C#實現
介紹
很久沒有寫博客,面試了幾次發現自己實現的東西有點少,面試官問覺得自己就是搬磚,沒什麼意思,就寫點東西。
協議用protobuf-net
.net 版本用4.5
socket用SocketAsyncEventArgs
實現網絡庫
寫服務器和客戶端測試程序
代碼本身不難,很多零碎的知識點穿插起來難度就會提升。每一個模塊我儘量列舉實現的原因,有可能遇到的坑。
先看測試程序。
服務器監聽一個端口
客戶端維護64個連接與服務器通信
服務器測試程序
using System;
using ZyGame.GameNet.Server;
using ZyGame.GameNet.Componet;
using System.Threading.Tasks;
using ProtoBuf;
//需要引用庫文件ZyGame.GameNet.dll 和 protobuf-net.dll
namespace TestServer
{
// 信道id,將協議分類,
// 會話接收字節流後解析的協議集合
// server-client連接是一個信道
// server-server連接是一個信道
//信道id需要大於1,1庫內部已經佔用
internal static class TestServerChannel
{
//自己作爲服務器的信道
public const int Server = 2000;
}
//協議號
internal static class TestServerProtoType
{
public const int Test = 200;
}
//ProtoContract - conprotobuf-net知識,說明是protobuf-net解析的類型
//Proto 協議特性 說明 協議的協議號 所屬信道,
// 默認既是接受協議又是發送協議 若是純發送協議 本特性可以不寫
[ProtoContract]
[Proto(TestServerProtoType.Test, TestServerChannel.Server)]
public class ProtoTest : Protocol//繼承庫協議
{
//protobuf-net 成員屬性 爲測試程序服務 寫了一條
//若是發送協議則需要用戶賦值屬性 若是接收協議 屬性值會被內部賦值
[ProtoMember(1)]
public string Info { get; set; }
//構造函數 需要實現基類構造
public ProtoTest()
:base(TestServerProtoType.Test)
{
}
//協議處理 發送協議不用實現 接收協議必須實現
public override void Process(Session session)
{
Console.WriteLine(string.Format("Session id = {0} info = {1}",
session.SessionId, Info));
}
}
internal class Program
{
async Task Run()
{
//監聽器 需要制定信道id
Listener listen = new Listener(TestServerChannel.Server);
//監聽地址
listen.StartListen("127.0.0.1", 9898);
while (true)
{
//爲測試程序服務 不停的發送一條消息
ServerSessionManager.Foreach((session)=> {
var proto = new ProtoTest();
proto.Info = string.Format("Hello client, time = {0}",
DateTime.Now.ToString());
session.Send(proto);
});
await Task.Delay(50);
}
}
//主函數
static void Main(string[] args)
{
new Program().Run().Wait();
}
}
}
服務器測試程序代碼很少,只是爲了說明如何使用這個庫。
客戶端測試程序
客戶端發起64個連接,一個會話維持一個連接,
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ProtoBuf;
using ZyGame.GameNet.Client;
using ZyGame.GameNet.Componet;
//需要引入ZyGame.GameNet.dll 和 protobuf-net.dll
namespace TestClient
{
//信道id 服務器測試程序有解釋,
// 這個不用和服務器對應相等,只要大於1即可
internal static class TestClientChannel
{
public const int Client = 1000;
}
//協議號,可以多個,寫一個是爲了測試,多寫無益
internal static class TestClientProtoType
{
public const int Test = 200;
}
//服務器測試程序有解釋
[ProtoContract]
[Proto(TestClientProtoType.Test, TestClientChannel.Client)]
public class ProtoHello : Protocol
{
[ProtoMember(1)]
public string Info { get; set; }
public ProtoHello()
:base(TestClientProtoType.Test)
{
}
public override void Process(Session session)
{
Console.WriteLine(string.Format("Session id = {0} info = {1}",
session.SessionId, Info));
}
}
class Program
{
async Task Run()
{
//一個程序建立64個會話
//64個已經說明一些問題了,寫一兩個連接沒有出錯不能說明可以用到生產環境。
List<ClientSession> sessionList = new List<ClientSession>();
for (int idx = 0; idx < 64; idx++)
{//創建會話
ClientSession session = new ClientSession(TestClientChannel.Client);
session.Open("127.0.0.1", 9898);
sessionList.Add(session);
}
while (true)
{
foreach (var item in sessionList)
{//爲了測試,每條會話都在不停的發送消息
var proto = new ProtoHello();
proto.Info = string.Format("Hello Server, time = {0}", DateTime.Now.ToString());
item.Send(proto);
}
await Task.Delay(50);
}
}
static void Main(string[] args)
{
new Program().Run().Wait();
}
}
}
測試程序自評
測試程序我寫了三個版本,幾乎每個版本都是爲了如何方便使用程序,如何方便實現協議來改進的。
第一個版本修改協議特性,第二個版本修改協議註冊器。
達到的效果是
第一個版本去掉了讓用戶實現一個自己的特性的繁瑣步驟【proto特性來源】
第二個版本去掉了讓用戶實現一個自己的註冊器的步驟【信道來源】
前面的版本我不可能發出來了,是爲了記錄一下爲什麼使用這個庫這麼簡單,並且說明一下 特性和信道的來因。
網絡庫介紹
網絡庫實現很精巧,自我感覺。
網絡庫一共四個名空間,每一個名空間我會介紹,代碼我也會上傳。
從簡單到複雜介紹。
最簡單的是Misc 雜項的意思,單詞【miscellaneous】很多有名的庫也是取這個名字,典型的就是DirectX,也有一些庫會取別的名字,例如helper、tools、utils等一些。主要說的就是一些不好分類的小部件工具類。
Misc名空間
logger類和netsetting很簡單,望文知意。
logger類
using System;
namespace ZyGame.GameNet.Misc
{
public static class Logger
{
public static Action<string> LogErrorMethod { get; set; }
public static Action<string> LogWarnMethod { get; set; }
public static Action<string> LogInfoMethod { get; set; }
static Logger()
{
}
public static void LogError(string message)
{
if (LogErrorMethod != null)
{
LogErrorMethod(message);
}
else
{
Console.WriteLine(message);
}
}
public static void LogWarn(string message)
{
if (LogWarnMethod != null)
{
LogWarnMethod(message);
}
else
{
Console.WriteLine(message);
}
}
public static void LogInfo(string message)
{
if (LogInfoMethod != null)
{
LogInfoMethod(message);
}
else
{
Console.WriteLine(message);
}
}
}
}
網絡設置類
網絡需要的一些常量,在靜態和常量之間我衡量了半天,覺得靜態有肯能被用戶修改,如果確定需要修改還是重新編譯比較好。所以設定爲常量。
namespace ZyGame.GameNet.Misc
{
public static class NetSetting
{
public const int DecodeBufferSize = 2048;
public const int BufferPrefixSize = 4;
public const int MsgPrefixSize = 4;
public const int ProtoPrefixSize = BufferPrefixSize + MsgPrefixSize;
public const int SocketBufferSize = 1024;
public const int MaxConnect = 10000;
public const int InterChannel = 1;
}
}
ProtobufUtils類
字節流和某個類型的轉換器。protobuf-net的知識。
using System;
using System.IO;
using ProtoBuf.Meta;
namespace ZyGame.GameNet.Misc
{
public static class ProtoBufUtils
{
private static RuntimeTypeModel TypeModel { get; set; }
static ProtoBufUtils()
{
TypeModel = RuntimeTypeModel.Create();
TypeModel.UseImplicitZeroDefaults = false;
}
public static object Deserialize(byte[] bytes, Type param)
{
using(var stream = new MemoryStream(bytes))
{
return TypeModel.Deserialize(stream, null, param);
}
}
public static byte[] Serialize(object value)
{
using (var stream = new MemoryStream())
{
TypeModel.Serialize(stream, value);
return stream.ToArray();
}
}
}
}
misc名空間就是瑣碎的小部件。
Componet命空間
庫核心名空間
Attribute 協議特性類
Connecter socket連接封裝
Internal 庫內部邏輯,主要是爲了建立連接後服務器和客戶端同步會話id。這個會話id有不少網絡庫是不同步的,客戶端和服務器會話id是不一樣的。不過id有對應關係就好了。 例如客戶端會話id=12對應服務器會話id=45 這個數字是運行時確定的。
Protocol 網絡協議,實現極其簡單。
Recver 接收器,字節流解碼爲協議
Register 註冊器,協議的註冊器,字節流解釋成協議類的介紹者
Sender 發送器 協議編碼爲字節流
Session 會話,抽象會話,服務器和客戶端會繼承本類,客戶端和服務器是有多態性的,但是抽象來看都是一個會話。
Attribute文件
namespace ZyGame.GameNet.Componet
{
public enum ProtoType
{
ProtoSend = 0,
ProtoRecv = 1,
}
public class ProtoAttribute : System.Attribute
{
public int Id { get; set; }
public int Channel { get; set; }
public ProtoType ProtoType { get; set; }
public ProtoAttribute(int id, int channel, ProtoType protoType = ProtoType.ProtoRecv)
{
Id = id;
Channel = channel;
ProtoType = protoType;
}
}
}
Connector文件
網絡庫最核心的類實現
using System;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using ZyGame.GameNet.Misc;
using System.Collections.Generic;
namespace ZyGame.GameNet.Componet
{
public class Connector
{
private byte[] RecvBuffer { get; set; }
private byte[] SendBuffer { get; set; }
public Socket ConnectSocket { get; set; }
private IPEndPoint EndPoint { get; set; }
private SocketAsyncEventArgs RecvArgs { get; set; }
private ManualResetEvent RecvResetEvent { get; set; }
private SocketAsyncEventArgs SendArgs { get; set; }
private ManualResetEvent SendResetEvent { get; set; }
private Sender Sender { get; set; }
private Recver Recver { get; set; }
public Connector(string ip, int port, Session session)
: this(new IPEndPoint(IPAddress.Parse(ip), port), session)
{
}
public Connector(IPEndPoint endpoint, Session session)
{
try
{
EndPoint = endpoint;
ConnectSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
RecvBuffer = new byte[NetSetting.SocketBufferSize];
SendBuffer = new byte[NetSetting.SocketBufferSize];
RecvArgs = new SocketAsyncEventArgs();
RecvArgs.Completed += IOCompleted;
RecvArgs.UserToken = session;
RecvArgs.SetBuffer(RecvBuffer, 0, NetSetting.SocketBufferSize);
RecvResetEvent = new ManualResetEvent(true);
SendArgs = new SocketAsyncEventArgs();
SendArgs.Completed += IOCompleted;
SendArgs.UserToken = session;
SendArgs.SetBuffer(SendBuffer, 0, NetSetting.SocketBufferSize);
SendResetEvent = new ManualResetEvent(true);
Sender = new Sender();
Recver = new Recver();
Connect(session);
}
catch (Exception ex)
{
Logger.LogError(ex.ToString());
}
}
public Connector(Socket socket, Session session)
{
try
{
ConnectSocket = socket;
RecvBuffer = new byte[NetSetting.SocketBufferSize];
SendBuffer = new byte[NetSetting.SocketBufferSize];
RecvArgs = new SocketAsyncEventArgs();
RecvArgs.Completed += IOCompleted;
RecvArgs.SetBuffer(RecvBuffer, 0, NetSetting.SocketBufferSize);
RecvArgs.UserToken = session;
RecvResetEvent = new ManualResetEvent(true);
SendArgs = new SocketAsyncEventArgs();
SendArgs.Completed += IOCompleted;
SendArgs.SetBuffer(SendBuffer, 0, NetSetting.SocketBufferSize);
SendArgs.UserToken = session;
SendResetEvent = new ManualResetEvent(true);
Sender = new Sender();
Recver = new Recver();
PostRecv();
}
catch (Exception ex)
{
Logger.LogError(ex.ToString());
}
}
public void Connect(Session session)
{
try
{
if (ConnectSocket != null && !ConnectSocket.Connected)
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs
{
UserToken = session
};
args.Completed += IOCompleted;
args.RemoteEndPoint = EndPoint;
if (!ConnectSocket.ConnectAsync(args))
{
ConnectCompleted(args);
}
}
}
catch (Exception ex)
{
Logger.LogError(ex.ToString());
}
}
private void IOCompleted(object sender, SocketAsyncEventArgs args)
{
switch (args.LastOperation)
{
case SocketAsyncOperation.Connect:
ConnectCompleted(args);
break;
case SocketAsyncOperation.Disconnect:
DisconnectCompleted(args);
break;
case SocketAsyncOperation.Receive:
RecvCompleted(args);
break;
case SocketAsyncOperation.Send:
SendCompleted(args);
break;
default:
Logger.LogWarn(string.Format("Connector IOCompleted error, args.LastOperation = {0}", args.LastOperation.ToString()));
break;
}
}
public void Disconnect()
{
try
{
if (ConnectSocket != null && ConnectSocket.Connected)
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += IOCompleted;
if (!ConnectSocket.DisconnectAsync(args))
{
DisconnectCompleted(args);
}
}
else
{
ConnectSocket.Close();
}
}
catch (Exception ex)
{
Logger.LogWarn(ex.ToString());
}
}
private void DisconnectCompleted(SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
Close();
}
else
{
Close();
string message = string.Format("Connector DisconnectCompleted error, BytesTransferred = {0}, ErrorCode = {1}", args.BytesTransferred, args.SocketError.ToString());
Logger.LogError(message);
}
}
private void ConnectCompleted(SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
if (args.UserToken is Session session)
{
session.OnConnectSuccess(ConnectSocket.RemoteEndPoint.ToString());
}
PostRecv();
}
else
{
if (args.UserToken is Session session)
{
string message = string.Format("ErrorCode = {0}, Remote address = {1}", args.SocketError, args.RemoteEndPoint.ToString());
session.OnConnectError(message);
}
}
}
private void PostRecv()
{
try
{
RecvResetEvent.WaitOne();
if (ConnectSocket != null && ConnectSocket.Connected)
{
RecvResetEvent.Reset();
if (!ConnectSocket.ReceiveAsync(RecvArgs))
{
RecvCompleted(RecvArgs);
}
}
}
catch (Exception ex)
{
RecvResetEvent.Set();
Logger.LogError(ex.ToString());
}
}
private void RecvCompleted(SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
if (args.BytesTransferred > 0)
{
string message = string.Format("Connectoer RecvCompletd, BytesTransferred = {0}", args.BytesTransferred);
if (args.UserToken is Session session)
{
List<Protocol> protoList = Recver.Decode(args.Buffer, args.BytesTransferred, session.Register);
if (protoList != null)
{
foreach (var item in protoList)
{
item.Process(session);
}
}
session.OnRecvSuccess(message);
}
RecvResetEvent.Set();
PostRecv();
}
else
{
string message = string.Format("Connectoer RecvCompletd, BytesTransferred = {0}", args.BytesTransferred);
if (args.UserToken is Session session)
{
session.OnRecvSuccess(message);
}
RecvResetEvent.Set();
PostRecv();
}
}
else
{
if (args.SocketError == SocketError.ConnectionReset)
{
if (args.UserToken is Session session)
{
session.OnRemoteDisconnect(ConnectSocket.RemoteEndPoint.ToString());
}
}
else
{
string message = string.Format("Connector RecvCompleted error, BytesTransferred = {0}, ErrorCode = {1}", args.BytesTransferred, args.SocketError.ToString());
if (args.UserToken is Session session)
{
session.OnRecvError(message);
}
Logger.LogError(message);
}
RecvResetEvent.Set();
}
}
public void PostSend(Protocol proto)
{
try
{
SendResetEvent.WaitOne();
if (ConnectSocket != null && ConnectSocket.Connected)
{
byte[] buffer = Sender.Encoded(proto);
Array.Copy(buffer, 0, SendArgs.Buffer, 0, buffer.Length);
SendArgs.SetBuffer(0, buffer.Length);
SendResetEvent.Reset();
if (!ConnectSocket.SendAsync(SendArgs))
{
SendCompleted(SendArgs);
}
}
}
catch (Exception ex)
{
SendResetEvent.Set();
Logger.LogError(ex.ToString());
}
}
private void SendCompleted(SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
string message = string.Format("Connector SendCompleted, BytesTransferred = {0}", args.BytesTransferred);
if (args.UserToken is Session session)
{
session.OnSendSuccess(message);
}
}
else
{
string message = string.Format("Connector SendCompleted error, BytesTransferred = {0}, ErrorCode = {1}", args.BytesTransferred, args.SocketError.ToString());
if (args.UserToken is Session session)
{
session.OnSendError(message);
}
}
SendResetEvent.Set();
}
public void Close()
{
try
{
if (ConnectSocket != null)
{
if (ConnectSocket.Connected)
{
ConnectSocket.Shutdown(SocketShutdown.Both);
}
ConnectSocket.Close();
}
}
catch (Exception ex)
{
Logger.LogWarn(ex.ToString());
}
}
}
}
這個類裏面三個構造函數。 兩個是爲客戶端準備的,一個是爲服務器準備的,調用不能混了。
當然用戶不應該直接調用本類。用戶能看到只是session
Internal類
網絡庫內部系統,同步sessionid使用
using ProtoBuf;
using System.Reflection;
using ZyGame.GameNet.Misc;
namespace ZyGame.GameNet.Componet
{
internal static class InterProtoType
{
internal const int SyncSession = 1;
internal const int MaxLimit = 100;
}
[Proto(InterProtoType.SyncSession, NetSetting.InterChannel)]
[ProtoContract]
internal class InterProtoSyncSession : Protocol
{
[ProtoMember(1)]
internal int SessionId { get; set; }
internal InterProtoSyncSession()
: base(InterProtoType.SyncSession)
{
}
public override void Process(Session session)
{
session.OnCreateSuccess(SessionId, string.Empty);
}
}
internal static class InternalRegister
{
internal static Register Register { get; private set; }
static InternalRegister()
{
Register = new Register();
var assembly = Assembly.GetExecutingAssembly();
foreach (var item in assembly.GetTypes())
{
var attr = item.GetCustomAttribute<ProtoAttribute>();
if (attr != null)
{
if (attr.Channel == NetSetting.InterChannel)
{
if (attr.ProtoType == ProtoType.ProtoSend)
{
Register.SendDict.Add(attr.Id, item);
}
else
{
Register.RecvDict.Add(attr.Id, item);
}
}
}
}
}
}
}
Protocol 文件
協議實現文件,您一定驚歎實現的簡單
namespace ZyGame.GameNet.Componet
{
public class Protocol
{
public int Id { get; private set; }
public Protocol(int id)
{
Id = id;
}
public virtual void Process(Session session)
{
}
}
}
我看着都很清爽。代碼簡單明瞭,就是爲了說明協議是有協議號的。
Recver類
利用ProtobufUtil和Registe將字節流解釋成爲協議流。
一次socket接受不一定是一條協議,有可能是很多條協議一起接受到的,所以是協議流
using System;
using System.Collections.Generic;
using ZyGame.GameNet.Misc;
namespace ZyGame.GameNet.Componet
{
public class Recver
{
int pos;
int length;
byte[] buffer;
byte[] bufferPrefix;
byte[] msgPrefix;
public Recver()
{
buffer = new byte[NetSetting.DecodeBufferSize];
bufferPrefix = new byte[NetSetting.BufferPrefixSize];
msgPrefix = new byte[NetSetting.MsgPrefixSize];
Reset();
}
private void Reset()
{
pos = 0;
length = 0;
}
public List<Protocol> Decode(byte[] recvbuffer, int grow, Register register)
{
try
{
List<Protocol> list = new List<Protocol>();
Array.Copy(recvbuffer, 0, buffer, pos, grow);
pos += grow;
while (pos > 4)
{
if (length <= 0)
{
Array.Copy(buffer, 0, bufferPrefix, 0, NetSetting.BufferPrefixSize);
length = BitConverter.ToInt32(bufferPrefix, 0);
}
int curLength = pos - NetSetting.ProtoPrefixSize;
if (curLength >= length)//夠一個消息了
{
Array.Copy(buffer, NetSetting.BufferPrefixSize, msgPrefix, 0, NetSetting.MsgPrefixSize);
int msgid = BitConverter.ToInt32(msgPrefix, 0);
byte[] msgBuffer = new byte[length];
Array.Copy(buffer, NetSetting.ProtoPrefixSize, msgBuffer, 0, length);
int len = pos - NetSetting.ProtoPrefixSize - length;
if (len > 0)
{
Array.Copy(buffer, NetSetting.ProtoPrefixSize + length, buffer, 0, len);
pos = len;
length = 0;
}
else
{
Reset();
}
if (msgid < InterProtoType.MaxLimit)
{
if (ProtoBufUtils.Deserialize(msgBuffer, InternalRegister.Register.RecvDict[msgid]) is Protocol proto)
{
list.Add(proto);
}
}
else
{
if (ProtoBufUtils.Deserialize(msgBuffer, register.RecvDict[msgid]) is Protocol proto)
{
list.Add(proto);
}
}
}
else
{
break;
}
}
return list;
}
catch (Exception ex)
{
Logger.LogWarn(ex.ToString());
return null;
}
}
}
}
Register文件
這個就是一個dictionary,通過協議號找到對應的協議類型
using System;
using System.Collections.Generic;
using ZyGame.GameNet.Misc;
namespace ZyGame.GameNet.Componet
{
public class Register
{
public Dictionary<int, Type> SendDict { get; private set; }
public Dictionary<int, Type> RecvDict { get; private set; }
public Register()
{
SendDict = new Dictionary<int, Type>();
RecvDict = new Dictionary<int, Type>();
}
public bool RegistSendProto(int id, Type proto)
{
try
{
SendDict.Add(id, proto);
return true;
}
catch (Exception ex)
{
Logger.LogWarn(ex.ToString());
return false;
}
}
public bool RegistRecvProto(int id, Type proto)
{
try
{
RecvDict.Add(id, proto);
return true;
}
catch (Exception ex)
{
Logger.LogWarn(ex.ToString());
return false;
}
}
}
}
Session 文件
庫第二核心文件,對conncter進行封裝,讓用戶可以有自己的實現。嵌入網絡庫。
把網絡庫設想成一臺機子,當發生某些事件時用戶想做出某些對應行爲就需要繼承這個類,實現自己的會話行爲。
using ZyGame.GameNet.Misc;
namespace ZyGame.GameNet.Componet
{
public class Session
{
public Connector Connector { get; protected set; }
public int SessionId { get; private set; }
public Register Register { get; protected set; }
public object User { get; set; }
public bool Opened { get; private set; }
public Session()
{
}
public void Send(Protocol proto)
{
if (Opened && Connector != null)
{
Connector.PostSend(proto);
}
}
protected void Open()
{
Opened = true;
}
public virtual void Close()
{
if (Opened)
{
if (Connector != null)
{
Connector.Disconnect();
}
Opened = false;
}
else
{
if (Connector != null)
{
Connector.Close();
Connector = null;
}
}
}
public virtual void OnCreateSuccess(int sessionId, string message)
{
SessionId = sessionId;
if (string.IsNullOrEmpty(message))
{
Logger.LogInfo(string.Format("Session Create success, id = {0}", SessionId));
}
else
{
Logger.LogInfo(string.Format("Session Created success, id = {0}, remote address = {1}", SessionId, message));
}
}
public virtual void OnConnectSuccess(string message)
{
Open();
Logger.LogInfo(string.Format("Session Connect success, remote address = {0}", message));
}
public virtual void OnConnectError(string message)
{
Close();
Logger.LogInfo(string.Format("Session Connected Failed, {0}", message));
}
public virtual void OnSendSuccess(string message)
{
}
public virtual void OnSendError(string message)
{
Close();
}
public virtual void OnRecvSuccess(string message)
{
}
public virtual void OnRecvError(string message)
{
Close();
}
public virtual void OnRemoteDisconnect(string message)
{
Logger.LogInfo(string.Format("Session Remote disconnect, id = {0}, remote address = {1}", SessionId, message));
Close();
}
}
}
Componet名空間是網絡庫的核心,用這個庫大部分情況需要引入這個命空間。
Client名空間
客戶端程序用到
裏面只有一個文件,主要是爲了變化打開會話,實現很簡單。
這個裏面會解釋信道的作用,通過信號標記的協議類型來確定本會話的協議集合。
using System.Reflection;
using ZyGame.GameNet.Componet;
namespace ZyGame.GameNet.Client
{
public class ClientSession : Session
{
public ClientSession(int channel)
{
Register = new Register();
var assembly = Assembly.GetEntryAssembly();
if (assembly != null)
{
foreach (var item in assembly.GetTypes())
{
var attr = item.GetCustomAttribute<ProtoAttribute>();
if (attr == null)
{
continue;
}
if (attr.Channel != channel)
{
continue;
}
if (attr.ProtoType == ProtoType.ProtoSend)
{
Register.SendDict.Add(attr.Id, item);
}
else
{
Register.RecvDict.Add(attr.Id, item);
}
}
}
}
public bool Open(string ip, int port)
{
if (Register == null)
{
return false;
}
Connector = new Connector(ip, port, this);
return true;
}
}
}
Server 名空間
服務器程序用到
裏面三個文件,ServerSession和ServerSessionManager是一組。listener是服務用到的。
Listener文件
監聽器,需要信道,通過本監聽器建立的會話共享一個信道,協議集合。
如果用戶實現了自己的ServerSession【繼承自ServerSession】,則可以設置CreateSessionMethod 構建自定義會話。
using System;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using ZyGame.GameNet.Componet;
using ZyGame.GameNet.Misc;
using System.Reflection;
namespace ZyGame.GameNet.Server
{
public class Listener
{
private Socket AcceptSocket { get; set; }
public Register Register { get; private set; }
public Func<Register, ServerSession> CreateSessionMethod { get; set; }
private ManualResetEvent AcceptResetEvent { get; set; }
private SocketAsyncEventArgs AcceptArgs { get; set; }
public Listener(int channel)
{
Register = new Register();
var assembly = Assembly.GetEntryAssembly();
if (assembly != null)
{
foreach (var item in assembly.GetTypes())
{
var attr = item.GetCustomAttribute<ProtoAttribute>();
if (attr == null)
{
continue;
}
if (attr.Channel != channel)
{
continue;
}
if (attr.ProtoType == ProtoType.ProtoSend)
{
Register.SendDict.Add(attr.Id, item);
}
else
{
Register.RecvDict.Add(attr.Id, item);
}
}
}
}
public bool StartListen(string ip, int port)
{
return StartListen(new IPEndPoint(IPAddress.Parse(ip), port));
}
public bool StartListen(IPEndPoint endpoint)
{
try
{
if (Register != null)
{
if (CreateSessionMethod == null)
{
Logger.LogWarn("Listener CreateSessionMethod is null, will create ServerNet default Session.");
}
AcceptArgs = new SocketAsyncEventArgs();
AcceptArgs.Completed += IOCompleted;
AcceptResetEvent = new ManualResetEvent(true);
AcceptSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
AcceptSocket.Bind(endpoint);
AcceptSocket.Listen(NetSetting.MaxConnect);
PostAccept();
Logger.LogInfo(string.Format("Start listen ip = {0}, port = {1}", endpoint.Address.ToString(), endpoint.Port));
return true;
}
else
{
Logger.LogError("Listener Register is null");
return false;
}
}
catch (Exception ex)
{
Logger.LogError(ex.ToString());
return false;
}
}
private void PostAccept()
{
try
{
AcceptResetEvent.WaitOne();
AcceptResetEvent.Reset();
AcceptArgs.AcceptSocket = null;
if (!AcceptSocket.AcceptAsync(AcceptArgs))
{
AcceptCompleted(AcceptArgs);
}
}
catch (Exception ex)
{
Logger.LogError(ex.ToString());
}
}
private void IOCompleted(object sender, SocketAsyncEventArgs args)
{
switch (args.LastOperation)
{
case SocketAsyncOperation.Accept:
AcceptCompleted(args);
break;
default:
Logger.LogWarn(string.Format("The last operation completed on the socket was not a valid operation, error = {0}", args.LastOperation.ToString()));
break;
}
}
private void AcceptCompleted(SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
if (CreateSessionMethod != null)
{
var session = CreateSessionMethod(Register);
if (session != null)
{
session.Open(args.AcceptSocket);
}
}
else
{
var session = new ServerSession(Register);
session.Open(args.AcceptSocket);
}
AcceptResetEvent.Set();
PostAccept();
}
else
{
AcceptResetEvent.Set();
Logger.LogError(string.Format("AcceptCompleted error, BytesTransferred = {0}, error = {1}", args.BytesTransferred, args.SocketError.ToString()));
}
}
}
}
ServerSession和ServerSessionManager
這兩個文件是對服務器端會話管理的。
ServerSession是服務器監聽器建立的會話抽象
using System.Net.Sockets;
using ZyGame.GameNet.Componet;
namespace ZyGame.GameNet.Server
{
public class ServerSession : Session
{
private static int SessionIdSeed = 0;
public ServerSession(Register register)
{
Register = register;
}
private int GenerateSessionId()
{
return ++SessionIdSeed;
}
public bool Open(Socket socket)
{
if (Register == null)
{
return false;
}
Open();
Connector = new Connector(socket, this);
OnCreateSuccess(GenerateSessionId(), socket.RemoteEndPoint.ToString());
ServerSessionManager.AddSession(this);
return true;
}
public override void OnCreateSuccess(int sessionId, string message)
{
base.OnCreateSuccess(sessionId, message);
var proto = new InterProtoSyncSession
{
SessionId = SessionId
};
Send(proto);
}
public override void Close()
{
base.Close();
ServerSessionManager.RemoveSession(SessionId);
}
}
}
ServerSessionManager是對所有ServerSession 的管理,線程安全。
using System.Collections.Generic;
using System.Threading;
using System;
using ZyGame.GameNet.Componet;
namespace ZyGame.GameNet.Server
{
public static class ServerSessionManager
{
private static Dictionary<int, ServerSession> SessionDict { get; set; }
private static ManualResetEvent SessionResetEvent { get; set; }
static ServerSessionManager()
{
SessionDict = new Dictionary<int, ServerSession>();
SessionResetEvent = new ManualResetEvent(true);
}
public static void AddSession(ServerSession session)
{
SessionResetEvent.WaitOne();
SessionResetEvent.Reset();
SessionDict.Add(session.SessionId, session);
SessionResetEvent.Set();
}
public static void RemoveSession(int sessionId)
{
SessionResetEvent.WaitOne();
SessionResetEvent.Reset();
SessionDict.Remove(sessionId);
SessionResetEvent.Set();
}
public static void Foreach(Action<Session> func)
{
SessionResetEvent.WaitOne();
SessionResetEvent.Reset();
foreach (var item in SessionDict)
{
func(item.Value);
}
SessionResetEvent.Set();
}
public static ServerSession GetServerSession(int id)
{
SessionResetEvent.WaitOne();
SessionResetEvent.Reset();
if (SessionDict.ContainsKey(id))
{
SessionResetEvent.Set();
return SessionDict[id];
}
else
{
SessionResetEvent.Set();
return null;
}
}
}
}
總結
這個網絡庫實現寫了有一週左右吧,具體時間我也不知道,想起來就寫,然後就測,改過很多地方,最後感覺有點樣子。
最後,希望能多和您交流,如果您覺得有什麼地方可以修改,希望您留言評論。