基于ProtoBuf的服务器与客服端的交互
1,Protocol Buffer 简称 ProtoBuf,是用于结构化数据串行化的灵活、高效、自动的方法,又如 XML。
简单来说,它就是一种二进制格式,是google发起的,目前广泛应用在各种开发语言中。具体的介绍可以参见:https://code.google.com/p/protobuf/ 。我们之所以选择protobuf,是基于它的高效,数据冗余少,编程简单等特性。关于C#的protobuf实现,网上有好几个版本,公认比较好的是Protobuf-net。
有些实验中ProtoBuf比xml序列化小5倍,比二进制也近小一倍,有人说ProtoBuf比xml可以小到20倍,根据数据的复杂度这是有可能的。ProtoBuf的数据格式做为数据报文有着绝对优势,当然也有个弊端,它是2进制报文,没有xml格式这样的可读性,要想看懂报文内容只能用ProtoBuf反序列化了。
安装:打开工具下的NuGet程序包管理器下的程序包管理器控制台如图所示:
在包管理器里输入
PM > Install-Package protobuf-net即可。
服务器端代码如下:
定义Perctent类
using ProtoBuf;
namespace Client
{
[ProtoContract]
public class Percent
{
[ProtoMember(1)]
public string name;
[ProtoMember(2)]
public int age;
[ProtoMember(3)]
public int sex;
}
}
server类封装成单例
class GameServer
{
private static GameServer _instance;
public static GameServer Instance
{
get
{
if (_instance==null)
{
_instance = new GameServer();
}
return _instance;
}
}
Dictionary clients = new Dictionary();
string ip = "127.0.0.1";
int port = 8500;
TcpListener listener;
public GameServer()
{
IPAddress address = IPAddress.Parse(ip);
listener = new TcpListener(address, port);
listener.Start(); //开始侦听
Console.WriteLine("服务器开始侦听");
void AcceptTcp(IAsyncResult ar)
{
try
{
TcpClient client = listener.EndAcceptTcpClient(ar);
ScocketClient socket = new ScocketClient(client);
string key = client.Client.RemoteEndPoint.ToString();
clients.Add(key, socket);
//再次异步接受客服端链接
listener.BeginAcceptTcpClient(AcceptTcp, null);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
public void BroadCast(string msg)
{
foreach (var item in clients)
{
item.Value.Send(msg);
}
}
public void BroadCast(byte[] buffer)
{
foreach (var item in clients)
{
item.Value.Send(buffer);
}
}
public void DidConnect(string clientkey)
{
clients.Remove(clientkey);
Console.WriteLine(clientkey + "断开连接");
}
}
写出ProtobufHelper类来实现传递Protobuf的序列化和反序列化 ,T为泛型。
class ProtobufHelper
{
//序列化
public static byte[] Serialize(T t)
{
using (MemoryStream stream = new MemoryStream())
{
Serializer.Serialize(stream, t);
return stream.ToArray();
}
}
//反序列化
public static T Deserialize(byte[] bytes)
{
using (MemoryStream ms = new MemoryStream(bytes))
{
T p = Serializer.Deserialize(ms);
return p;
}
}
}
main方法:
static void Main(string [] args)
{
GameServer server = GameServer.Instance;
while (true)
{
Console.WriteLine("按ESC退出");
ConsoleKey key = Console.ReadKey(true).Key;
if (key==ConsoleKey.Escape)
{
break;
}
}
}
ScocketClient类来实现发送广播和接收消息并解析
class ScocketClient
{
public const int bufferSize = 8192;
byte[] buffer = new byte[bufferSize];//定义buffer缓冲区
TcpClient client;
NetworkStream stream;
MemoryStream men;//缓存流
BinaryReader reader;//缓存流的读取器
public ScocketClient(TcpClient _client)
{
client = _client;
Console.WriteLine("客服端链接成功" + client.Client.RemoteEndPoint);
stream = client.GetStream();
men = new MemoryStream();
reader = new BinaryReader(men);
stream.BeginRead(buffer, 0, bufferSize, Read, null);
}
//子线程读取消息
void Read(IAsyncResult ar)
{
try
{
int readCount = stream.EndRead(ar);
if (readCount == 0) throw new Exception("读取异常");
OnReceive(buffer, readCount);
lock (client)
{
Array.Clear(buffer, 0, bufferSize);
stream.BeginRead(buffer, 0, bufferSize, Read, null);
}
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
OnDisConnect();
}
}
//逻辑层 ,解包,处理[长度+消息]
void OnReceive(byte[] bytes, int count)
{
men.Seek(0, SeekOrigin.End);//指针指向最后
men.Write(bytes, 0, count);//消息往后追加
men.Seek(0, SeekOrigin.Begin);//指针指向开头,开始读消息
while (ReMainLength() > 4)
{ //获取消息长度 指针后移4位
int length = reader.ReadInt32();
if (ReMainLength() >= length)//剩余的字节数是否大于消息长度
{
byte[] content = reader.ReadBytes(length);
OnMessage(content);
}
else
{
men.Seek(-4, SeekOrigin.Current);//把指针前移4位,保证消息的完整性[长度+消息]
break;
}
}
byte[] remain = reader.ReadBytes(ReMainLength());//把剩余的半包读取出来
men.SetLength(0);//清空缓存流
men.Write(remain, 0, remain.Length);//写入剩余的半包
}
//得到一条消息
void OnMessage(byte[] content)
{
//广播给所有的客服端
GameServer.Instance.BroadCast(content);
}
//解析二进制格式序列化
object ReadBinaryformat(byte[] bytes)
{
using (MemoryStream ms=new MemoryStream())
{
ms.Write(bytes, 0, bytes.Length);
ms.Flush();
ms.Position = 0;
var format = new BinaryFormatter();
var obj = format.Deserialize(ms);
return obj;
}
}
//获取剩余字节长度
int ReMainLength()
{
return (int)(men.Length - men.Position);
}
public void Write(byte[] content)
{
//获取消息和长度
byte[] length = BitConverter.GetBytes(content.Length);
//使用内存流做为缓存,拼接遵循[长度+消息]协议的消息
MemoryStream mem = new MemoryStream();
mem.Write(length, 0, 4);
mem.Write(content, 0, content.Length);
//发送最终消息
byte[] bytes = mem.ToArray();
stream.Write(bytes, 0, bytes.Length);
}
public void Send(string msg)
{
byte[] content = Encoding.UTF8.GetBytes(msg);
Write(content);
}
public void Send(byte[] content)
{
Write(content);
}
//断开连接
public void OnDisConnect()
{
GameServer.Instance.DidConnect(this.client.Client.RemoteEndPoint.ToString());
stream.Close();
client.Close();
}
}
客服端代码如下
写出ProtobufHelper类来实现传递Protobuf的序列化和反序列化 ,T为泛型。
代码同上方ProtobufHelper一样
定义Percent类,类中属性需与上方Percent一样
代码同上方上方Percent一样
SocketClient类来实现服务器的链接
class SocketClient
{
string ip = "127.0.0.1";
int port = 8500;
TcpClient client;
NetworkStream stream;
public const int bufferSize = 8192;
byte[] buffer = new byte[bufferSize];
MemoryStream men;//缓存流
BinaryReader reader;//缓存流的读取器
public SocketClient()
{
client = new TcpClient();
// client.Connect(ip, port);
client.BeginConnect(ip, port, OnConnect, null);//异步连接服务器
}
void OnConnect(IAsyncResult ar)
{
Console.WriteLine("服务器链接成功" + client.Client.RemoteEndPoint);
stream = client.GetStream();//获取网络流
men = new MemoryStream();
reader = new BinaryReader(men);
stream.BeginRead(buffer, 0, bufferSize, Read, null);//开启异步读取数据
}
..........剩余部分与上方代码相同。
}
客服端的Main方法
static void Main(string[] args)
{
SocketClient client = new SocketClient();//新建一个客服端链接
while (true)
{
Console.WriteLine("请输入消息:");
string msg= Console.ReadLine();
Percent p = new Percent();
p.name = "小明";
p.age = 20;
p.sex = 0;
//byte[] buffer = Binartformat(p);
byte[] buffer = ProtobufHelper.Serialize(p);
for (int i = 0; i < 10; i++)
{
client.Send(buffer);
}
}
最后运行结果如下: