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;
}
}
}
}
总结
这个网络库实现写了有一周左右吧,具体时间我也不知道,想起来就写,然后就测,改过很多地方,最后感觉有点样子。
最后,希望能多和您交流,如果您觉得有什么地方可以修改,希望您留言评论。