木无亡法

  博客园  :: 首页  :: 新随笔  :: 联系 ::  :: 管理

(一)TcpListen类、TcpClient类

    TcpListener类和TcpClient类都是System.Net.Sockets命名空间下的类,利用TcpListener和TcpClient可以快速创建服务端和客户端;这两个类对Socket进行了一层封装,TcpListener把创建Socket、服务端的监听、接收客户端连入进行了封装;TcpClient类也把Socket的创建、连接服务端写在类内部,利用这两个类可以很简单地完成TCP网络系统;

var listener = new TcpListener(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9735));
listener.Start(5);
listener.BeginAcceptTcpClient(OnAcceptClient, listener);

private void OnAcceptClient(IAsyncResult result)
{
    var listener = (TcpListener)result.AsyncState;
    TcpClient client = listener.EndAcceptTcpClient(result);
}

    用TcpListener创建服务端,有多种接收客户端连入的方式,这里采用BeginAcceptTcpClient()方法进行异步连接,这个方法和之前BeginReceive接收数据方法类似,需要一个成功连接之后的回调方法,在回调方法OnAcceptClient()中,再调用TcpListener.EndAcceptTcpClient()方法,得到一个TcpClient类的实例,在服务端,它就是用来与客户端通信的关键;

    上面的TcpListener.BeginAcceptTcpClient()方法同样也有其阻塞版本,AcceptTcpClient()方法,这个方法是接收客户端连入的阻塞方法,它直接返回一个TcpClient;

    上述接收客户端连入的阻塞或者异步方法,得到的都是对通信Socket的包装类TcpClient,而使用AcceptSocket()或者BeginAcceptSocket(),就会得到Socket,意味着我们接下来的网络工作要直接使用这个Socket;

    使用TcpListener同样也可以直接操作底层Socket,TcpListener.Server :: Socket,类属性Server得到一个Socket类实例,利用它可以像前篇一样调用Accept()接收客户端连入;

    在服务端得到的TcpClient实例,它代表与对应客户端的网络通信,TcpClient.Client::Socket,这个属性得到的就是与客户端TCP通信的Socket,利用它可以进行数据收发;TcpClient.GetStream()方法可以得到一个网络流,通过这个网络流同样可以与对应客户端进行数据通信;

private void StartClient()
{
    TcpClient client = new TcpClient();
    client.Connect("127.0.0.1", 9735);
}

    这个客户端的创建,客户端与服务端的通信利用TcpClient,它的用法和它在服务端的用法基本类似,不同的是,在多客户端系统服务端,需要维护一个TcpClient集合,并对其进行合理的管理;

 

(二)网络交互参数类SocketAsyncEventArgs

    命名空间System.Net.Sockets下还有一个很有用的类,它是一个参数类SocketAsyncEventArgs,它在服务端应用异步连接AcceptAsync、还有数据的异步发送SendAsync和接收ReceiveAsync时起到重要的作用,它可以保存一个客户端接入时的Socket实例、远程客户端地址,以及设置连接状态等等;它应用在数据收发时,可以保存数据缓存引用、收发的数据大小,甚至分页数据收发时,数据的状态;

    可以在底层服务类ServerListener中会这么定义:

/// <summary> 客户端连接参数池 </summary>
private ConcurrentStack<SocketAsyncEventArgs> AcceptArgsPool;
/// <summary> 数据通信参数池 </summary>
private ConcurrentStack<SocketAsyncEventArgs> SocketArgsPool;

 

    在线程安全栈中保存SocketAsyncEventArgs的缓存集合,一般在程序初始化的时候,创建足够数量的实例放入线程安全栈(比如一万个),并进行合适的初始化;

private SocketAsyncEventArgs NewAcceptArgs
{
    get
    {
        var args = new SocketAsyncEventArgs();
        args.Completed += OnAcceptComplete;
        return args;
    }
}

public ServerListener()
{
    requestHandler = new RequestHandler();
    AcceptArgsPool = new ConcurrentStack<SocketAsyncEventArgs>();
    SocketArgsPool = new ConcurrentStack<SocketAsyncEventArgs>();
    for (int i = 0; i < 10000; i++)
    {
        AcceptArgsPool.Push(NewAcceptArgs);
    }
    for (int i = 0; i < 20000; i++)
    {
        var socketArgs = new SocketAsyncEventArgs();
        socketArgs.Completed += IO_Complete;
        socketArgs.SetBuffer(new byte[ConfigManager.BufferSize], 0, ConfigManager.BufferSize);
        socketArgs.UserToken = new DataToken();
        SocketArgsPool.Push(socketArgs);
    }
}

    在底层服务类ServerListener中初始化连接缓存池AcceptArgsPool和数据缓存池SocketArgsPool,客户端连接缓存池的初始化,参数的Comleted属性添加事件OnAcceptComleted方法,这个自定义方法是处理客户端连入的回调,把NewAcceptArgs属性得到的实例添加到AcceptArgsPool连接缓存池;

    同理数据收发参数缓存池SocketArgsPool的实例集合,其Comleted属性都添加事件回调IO_Comlete方法,这是数据接收或者发送成功之后的回调,此外数据参数还应调用SetBuffer方法预先开辟数据缓存,缓存大小可以定义一个静态类ConfigManager,或者用App.config配置文件定义;

    UserToken是一个自定义类型,这里定义一个补充的数据参数DataToken,该参数主要是在数据接收时,存放接收的数据引用;分页数据收发时,存放接收或发送数据的当前状态,比如已接收大小、已发送大小、总大小;还可以定义一个委托引用,在数据发送SendAsync时,保存上层传入的委托方法,发送成功之后取出这个这个回调方法,通知上层;

    有需要时,还可以在UserToken定义其它的辅助字段;

    AcceptArgsPool中的实例都是用来为客户端连入所准备的参数缓存池,当使用服务Socket接入客户端连入时,使用异步方法AcceptAsync:

private void BeginAccept()
{
    try
    {
        if (!AcceptArgsPool.TryPop(out SocketAsyncEventArgs acceptArgs))
        {
            acceptArgs = NewAcceptArgs;
        }
        // 异步接收客户端连入,同步接收成功返回false,挂起返回true
        if (!listenSocket.AcceptAsync(acceptArgs))
        {
            AcceptProcess(acceptArgs);
        }
    }
    catch (Exception) { }
}

    异步方法AcceptAsync传入一个SocketAsyncEventArgs类型参数,这时候可以调用AcceptArgsPool.Pop()取出缓存中准备好的实例(预先创建好可以减少运行时消耗),进入AcceptAsync异步方法时,如果有客户端接入它会直接返回false,它相当于同步方法,如上if判断通过,调用AcceptProcess方法处理客户端连入;

    如果没有立即成功接收到Tcp连入,方法会返回true,表示接收连入过程进入异步状态,此时acceptArgs参数中的Completed属性设置的值就会起作用(OnAcceptComplte),而在这个回调中,其实也是调用AcceptProcess方法的,所以不管同步还是异步,最终处理客户端连入的方法都是AcceptProcess方法;

private void AcceptProcess(SocketAsyncEventArgs acceptArgs)
{
    try
    {
        if (acceptArgs.SocketError != SocketError.Success)
        {
// 回收acceptArgs参数到缓存池AcceptArgsPool ReleaseSocketObject(acceptArgs);
return; } SocketArgsPool.TryPop(out SocketAsyncEventArgs socketArgs); socketArgs.AcceptSocket = acceptArgs.AcceptSocket; socketArgs.SetBuffer(0, ConfigManager.BufferSize); acceptArgs.AcceptSocket = null; ReleaseSocketObject(acceptArgs); // 开始异步接收数据 BeginReceiveMsg(socketArgs); } finally {
// 重新调用异步接收客户端连入 BeginAccept(); } }

    客户端连入成功后,acceptArgs的AcceptSocket属性会自动保存好客户端连接Socket实例,它用来与这个客户端进行通信;然后在SocketArgsPool数据缓存池取出一个实例,并调用BegingReceiveMsg方法开始异步接收客户端数据,同时,acceptArgs参数也可以回收缓存了;BeginReceiveMsg方法和之前的BeginAccept方法基本类似;

private void BeginReceiveMsg(SocketAsyncEventArgs socketArgs)
{
    if (!socketArgs.AcceptSocket.ReceiveAsync(socketArgs))
    {
        ReceiveProcess(socketArgs);
    }
}
private void ReceiveProcess(SocketAsyncEventArgs socketArgs)
{
  // 处理数据...
BeginReceiveMsg(socketArgs); }

 

    ReceiveProcess和之前的AcceptProcess类似,它是处理数据接收的主体,socketArgs参数中的Completed属性,它在初始化时添加的回调方法IO_Complete方法也是调用AcceptProcess处理数据的,在这个方法中在连接正常的情况下,最后同样要再次调用BeginReceiveMsg继续异步接收数据;

    SocketAsyncEventArgs参数类在数据的接收或发送有几个重要字段/属性,它们都是封装好的,直接取用就行:SocketError - 传输结果的状态枚举值,BytesTransferred - 传输的字节数,LastOperation - 最近一次操作的类型(比如连接、断开、发送、接收)等;

    数据的发送也是类似的Begin - CompleteCallBack -  Process 的模式进行数据循环发送;

 

private void IO_Complete(object sender, SocketAsyncEventArgs socketArgs)
{
    try
    {
        switch (socketArgs.LastOperation)
        {
            case SocketAsyncOperation.Receive:
                ReceiveProcess(socketArgs);
                break;
            case SocketAsyncOperation.Send:
                SendProcess(socketArgs);
                break;
            default: break;
        }
    }
    catch (ObjectDisposedException)
    {
        ReleaseIOArgs(socketArgs);
    }
    catch (Exception) { }
}

 

    这里数据IO的回调方法,综合处理数据的发送和接收,其中只读属性LastOperation判断该次调用是发送回调,或是接收回调,它们分别调用发送回调主体SendProcess(),和接收的回调主体ReceiveProcess()方法。

 

 

(三)Net层架构设计

 

    在这个简化的架构图中,网络底层服务类ServerListener就是前面描述的,主要是监听客户端连入、数据接收和发送,这些前面已经有了;

    另外,它还需要一个数据接收事件的回调,在架构图中这个回调方法调用请求接口类RequestHandler,首先会过滤掉非法数据(不符合数据传输约定),然后把数据简单地包装之后送到数据行为协议Action的工厂类Actionfatory,这个类从数据中解出Action协议号,实例化一个BaseAction的派生类,并把数据传入到该派生类中,而该BaseAction派生类反序列化字节序列数据,得到一个特定的结构数据,接下来就按照Action派生类的预先定义的逻辑代码处理这个Action派生类所负责的行为,比如注册账号,用户登录等等;

会话类Session

    Session类是服务端和客户端交互的接口类,每一个Session类实例代表一个客户端连接,Session实例保存了分配给该客户端的ID、绑定的Socket实例,以及远程地址等等;

    另外它还给ActionFactory工厂类提供消息推送接口,通过Session提供的推送接口可以实现从Action - Session - ServerListener的数据发送过程;

数据协议Action

    基类BaseAction派生一系列协议类,命名为如Action100、Action101等等Action协议类型,在静态类ActionFactory工厂中,提供一个根据协议号创建Action实例的方法(利用C#反射技术),创建出来的Action类实现了BaseAction基类中填充数据包并序列化、反序列化包的方法,以及相关的应用层逻辑;

 

(三)对象序列化和反序列化

    序列化的定义:序列化(Serialization)描述了持久化(或传输)一个对象的状态到流(如文件流和内存流)的过程,被持久化的数据次序包括所有以后需要用来重建(即反序列化)对象状态所需的信息。

    就是说,序列化是把一个对象中所有必须的字段、属性的数据,按某种规则保存到一个流中,这个流会把数据写入到绑定的文件(文件流),或者保存在内存中(内存流);这个流中保存好的数据可以很容易地转化为字节序列或者字符串序列,因此它可以很容易地写入文件,或者通过Socket网络传输到远程端;

    这种经过序列化之后的数据,经过相应的反序列化方法,可以还原原对象;即序列化得到的数据远程发送到另一终端,可以将它还原为原对象,对象字段和属性的值可以保持完整;

    序列化和反序列化技术是面向对象编程中网络传输的基础;

BinaryFormatter

    C#提供了三种内置的序列化方案,字节序列化BinaryFormatter,SOAP消息序列化SoapFormatter,XML序列化XmlSerializer,它们的完全限定名分别是:

    System.Runtime.Serialization.Formatters.Binary.BinaryFormatter;

    System.Runtime.Serialization.Formatters.Soap.SoapFormatter;

    System.Xml.Serialization.XmlSerializer;

使用SoapFormatter必须引用System.Runtime.Serialization.Formatters.Soap.dll程序集,再使用上面的命名空间;

它们的使用方法基本类似,比如有一个注册请求包:

 

[Serializable]
class RegisterReqPack
{
    public string PassportID;

    public string Password;

    public string NickName;

    public int Age;

    public int Sex;
}

 

    这个注册请求包,类里面定义了通行证账号、密码(加密的)、昵称、年龄、性别等几个字段,使用BinaryFormatter序列化类对象,须在类定义的类名之前加上Serializable属性标记,这个类的实例就可以通过BinaryFormatter进行序列化:

 

public void SaveToFile()
{
    var reqPack = new RegisterReqPack()
    {
        PassportID = "13912345678",
        Password = "!@#$%^&*",
        NickName = "Alice",
        Age = 22,
        Sex = 1
    };

    var binFormat = new BinaryFormatter();
    using (FileStream stream = new FileStream("ReqPack.dat", FileMode.Create, FileAccess.Write, FileShare.None))
    {
        binFormat.Serialize(stream, reqPack);
    }
}

 

    SaveToFile方法把对象数据序列化,并通过文件流写入文件,在main方法中调用这个方法之后,可以看到运行目录下多了文件ReqPack.dat,用记事本打开可以看到序列化之后的数据(类型信息以及字段数据的字节乱码);

    BinaryFormatter序列化得到的是紧凑的二进制序列,由对象序列化之后的结果体积较小,方便用来进行网络传输,比较节省网络负载;

    而且它是符合.Net公共语言规范,也就是说,通过BinaryFormatter序列化得到的数据,可以通过文件或者网络传输到实现.Net框架的其它语言编写的终端,比如VB程序,可以无障碍地反序列化数据,得到类型结构相同的VB类型对象;

    用[Serializable]标记的类,其中的字段或属性可以用 [NonSerialized]标记,表示该字段或属性不参与序列化,反序列得到该类的对象其对应值为类型的默认值;

Json序列化

    json序列化是一种基于字符串的序列化方式,序列化是以Key - Value的形式表示为字符串,并且支持引用类型递归迭代;

    比如上述的注册请求包用 Newtonsoft.dll Json序列化工具序列化之后的结果为:

    string json = "{"PassportID":"13912345678","Password":"!@#$%^&*","NickName":"Alice","Age":"22","Sex":"1"}";

    序列化结果是一个字符串序列,一个类结构用大括号{}包括在内,字段和值用冒号间隔;如果类中有引用的类型结构,其值也用大括号{}括起来;

    Json序列化的优点是序列化简单,而且可读性高,缺点是序列化结果数据体积大,对网络端的负载较大,不利于商业项目的网络传输;

(四)封装数据包

    利用序列化和反序列化,可以在框架上层Action实现对数据包的封装,而底层网络层ServerListener发送的字节数据不用管它是由什么类型的对象序列化而来的;

    因此,Acton协议发送数据时,只需对数据包(比如上述的注册包RegistReqpack)进行序列化或者反序列化,得到或者发送想要的数据包到网络终端,而网络终端只要采用同一种序列化方案,就可以很容易地发送或者还原数据包,从而进行相应的逻辑处理;

    利用这种数据包的封装技术,可以在复杂的多功能的应用环境中,极大地简化数据传输,很容易地实现多种逻辑功能;类似于注册请求包,登录请求包,聊天包...

    在BaseAction的派生类中,每一个Action协议实现(负责)一种功能,比如Action100实现注册,Action101实现登录等等;相应的,在Action协议类内部,也要对应的数据包,比如RegistReqpack.

posted on 2018-01-18 12:39  木无亡法  阅读(1848)  评论(0编辑  收藏  举报