使用TcpServerEx构建服务器
描述
TcpServerEx是从TcpServerBase<DataEventExArgs>派生,它与TcpServerBase用法非常相似,不同的是它对发送的数据进行封包处理,可以和客户端之间传送复杂的实体,同时对应的SocketAsync<DataEventExArgs>扩展了发送数据的方法SendCallback,该方法发送数据到另一端后,不需要事件接收返回的数据,而是通过回调得到数据,可以在一个方法内编写更符合一般逻辑的代码风格。封包是由DataEventExArgs来描述,其代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace NetWorkSocket.TcpExtention { /// <summary> /// TcpServerEx和TcpClientEx所使用的数据 /// [包长 4个byte][action command 4个byte][hashCode 4个byte][实体 N个byte][包长 4个byte] /// </summary> public class DataEventExArgs : DataEventArgs { /// <summary> /// 获取本次通讯回合的命令动作 /// </summary> public int Action { get; set; } /// <summary> /// 获取本次通讯回合的哈希值 /// </summary> internal int HashCode { get; set; } /// <summary> /// 转换为二进制数据 /// </summary> /// <returns></returns> public override byte[] ToByteArray() { int capacity = 16; //总包长4 + action 4 + hashCode 4 + 验校长 4 if (this.Binary != null && this.Binary.Buffer != null) { capacity = capacity + this.Binary.Buffer.Length; // +实体数据长 } ByteBuilder builder = new ByteBuilder(capacity); builder.Add(BitConverter.GetBytes(capacity)); builder.Add(BitConverter.GetBytes(this.Action)); builder.Add(BitConverter.GetBytes(this.HashCode)); if (this.Binary != null) { builder.Add(this.Binary.Buffer); } builder.Add(BitConverter.GetBytes(capacity)); return builder.GetBaseBuffer(); } } }
用图片说明上面代码描述的封包协议:
Total length、Action、HashCode都是int类型,各占4个byte(注意byte是无符号类型,关于int转byte[],java和c#高低位的差异请baidu相关资料),而有效实体数据才是真正的数据。
total length是整块byte[]数据的长度;
Action是指本次请求的命令,好比前台post数据到后台时常有个参数叫action或cmd之类的;
HashCode是本次请求的哈希码,如果连续多次请求一样的Action到服务器,服务返回的数据顺序可能是错乱的,这时需要hashCode进行匹配纠正。
最后的Total length是为了服务器验证数据的合法性,如里不一样,则从缓冲区中清除全部的数据,以确保前面数据的错误也不会影响到后面数据的解析。
契约
本次我们将实现客户端通过用户名向从服务器请求获取详细的用户信息的这个要求,接口类似于:UserInfo GetUserinfo(string username);
Userinfo实例如下
using System; using System.Collections.Generic; using System.Linq; using System.Text; using NetWorkSocket.Serialization; namespace Entity { /// <summary> /// 用户信息实体类 /// IEnityWhere用于扩展对UserInfo的序列化 /// </summary> [Serializable] public class Userinfo : IEntityWhere { /// <summary> /// 用户名 /// </summary> public string name { get; set; } /// <summary> /// 密码 /// </summary> public string password { get; set; } } }
上面的接口是同步调用方式的接口,如果是异步调用,则接口可以改为:void GetUserinfo(string username, Action<UserInfo> callBack);向服务器提交username后,不用等待返回,当有返回值后,自动触发委托callBack,就是回调的意思,为了更容易理解,这里先从开发客户端进行演示。
构建客户端
新建工程ClientEx,引用NetworkSocket.dll和Userinfo所在的Entity工程,引入NetworkSocket、NetWorkSocket.TcpExtention、NetWorkSocket.Serialization、Entity命名空间
using System; using System.Collections.Generic; using System.Linq; using System.Text; using NetWorkSocket; using NetWorkSocket.TcpExtention; using NetWorkSocket.Serialization; using System.Net; using Entity; namespace ClientEx { class Program { // 客户端实例 static TcpClientEx client = new TcpClientEx(); static void Main(string[] args) { // 连接到服务器 bool state = client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8181)); if (state == true) { while (true) { string username = Console.ReadLine(); Console.WriteLine("获取用户名为{0}的用户信息...", username); // 从服务器获取用户信息 GetUserinfo(username, (user) => { if (user == null) { Console.WriteLine("userinfo is null"); } else { Console.WriteLine("name:{0} password:{1}", user.name, user.password); } }); } } } /// <summary> /// 从服务器获取用户信息 /// </summary> /// <param name="username">用户名</param> /// <param name="callBack">回调</param> static void GetUserinfo(string username, Action<Userinfo> callBack) { // 将username包装为DataEventExArgs DataEventExArgs e = new DataEventExArgs() { Action = 0, Binary = SerializerExtention.ToBinary(username) }; // 发送到服务器并等待回调 client.SendCallBack(e, (bin) => { // 返回数据转换为Userinfo Userinfo user = bin.ToEntity<Userinfo>(); // 回调 callBack(user); }); } } }
构建服务器
新建工程ServerEx,引用NetworkSocket.dll和Userinfo所在的Entity工程,引入NetworkSocket、NetWorkSocket.TcpExtention、NetWorkSocket.Serialization、Entity命名空间
using System; using System.Collections.Generic; using System.Linq; using System.Text; using NetWorkSocket; using NetWorkSocket.Serialization; using NetWorkSocket.TcpExtention; using System.Net; using Entity; namespace ServerEx { class Program { // 服务Socket实例 static TcpServerEx server = new TcpServerEx(); // 模拟数据库的用户信息 static List<Userinfo> userlist = new List<Userinfo>() { new Userinfo (){ name ="ABC", password ="abc"}, new Userinfo (){ name ="EFG", password ="efg"}, }; static void Main(string[] args) { // 绑定接收数据事件 server.OnRecvComplete += new EventHandler<DataEventExArgs>(server_OnRecvComplete); // 绑定客户端连接事件 server.OnConnect += new EventHandler(server_OnConnect); // 启动服务 server.StartListen(new IPEndPoint(IPAddress.Any, 8181)); while (true) { Console.ReadLine(); } } /// <summary> /// 客户端连接事件 /// </summary> /// <param name="sender">客户端</param> /// <param name="e">事件</param> static void server_OnConnect(object sender, EventArgs e) { var client = sender as SocketAsync<DataEventExArgs>; Console.WriteLine("{0}连接进来...当前连接数:{1}", client.RemoteEndPoint, server.AliveClients.Count); } /// <summary> /// 接收数据完成事件 /// </summary> /// <param name="sender">客户端</param> /// <param name="e"></param> static void server_OnRecvComplete(object sender, DataEventExArgs e) { // sender就是当前发来数据的客户端 var client = sender as SocketAsync<DataEventExArgs>; switch (e.Action) { case 0: //GetUserInfo GetUserInfo(client, e); break; default: break; } } /// <summary> /// 需要返回UserInfo对象给客户端 /// </summary> /// <param name="client">客户端</param> /// <param name="e">收到的数据</param> static void GetUserInfo(SocketAsync<DataEventExArgs> client, DataEventExArgs e) { // 客户端发来的数据转换为字符串 // 对客户端而言服务接口是:Entity.Userinfo GetUserInfo(string username); string userName = e.Binary.ToEntity<string>(); // 从数据库中找出符合条件的用户信息 var user = userlist.Find((item) => item.name.Equals(userName)); // 这里直接用原来的e,保证e的Action和HashCode属性不变化 // ToBinary()是 NetWorkSocket.Serialization.SerializerExtention里的扩展方法 e.Binary = user.ToBinary(); // 发送数据给客户端 client.Send(e); } } }
运行效果