好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

C# socket tcp通讯

C# socket tcp通讯

c# tcp通讯可以用tcplistener做服务器,tcpclient做客户端,这两个类在framework里已封装好,调用也比直接调用socket方便得多,但性能相对socket又差一些,如果想取得更好一点的性能,可以模仿tcplistener和tcpclient对socket进行封装。

         但不管是socket还是tcplistener与tcpclient,数据接收处理都需要自己进行处理,经过一段时间总结,我采用如下方法对数据进行封包:[包长 4个byte][aciton动作类型 4个byte][实体 N个byte][包长 4个byte]   这里最重要的是第一个包长4个字节,有了包长的信息,就可以对包进行拆分和解析,最简单的可以为:[包长 4个byte][实体 N个byte] ,更复杂的可以根据情况而做相应设计,但我觉得如上的设计一般都能满足项目的要求了。action相当于command,在实际项目中,可以把action转换为枚举类型,方便开发和编码工作,有了action,就可以对][实体 N个byte]进行反序列化得到相应的实体,最后的[包长 4个byte]对客户端来说可能没有什么意思,但服务可以根据这个进行简单的验证封包的合法法,如果不合法,将接收到的客户端发来的数据进行清空并从新接收,这样,如果客户端发送某个数据错误不会导致之后的所的数据包都无法解析的现象发生。 

        如果是使用tcplistener与tcpclient,使用如上的方法对接收到的数据进行解析就可以实现了数据的接收工作。对于tcpclient,简单的可以做同步阻塞接收和发送数据,也就是使用GetStream()方法取得网络流后,对流进行read或write操作,但对服务器而言,tcplistener accep到tcpclient后,使用这样的方式几乎不可行的,当客户端请求比较繁忙的时候,服务器处理不过来,我们可以使用BenginRead和BeginWrite方法进行数据的异步发送或异步接收,使用这些方法时,需要注册回调函数,当系统执行完这些操作后,自动调用注册的回调函数,相当于我们的程序把数据的接收的发送给系统来完成,系统完成之后只要通知我们一声就可以了,中途里,我们程序可以做其它的事情。 

        但即使使用了Bengin的异步操作,服务器还是有很大的优化空间,如果普通的tcp服务器客户连接数达到5000,优化后可以达到10000,那么就可以节省了一台服务器费用的开支,这个优化是很有必要和值得的。使用传统的AMP异步编程里,BeginXXX方法后都创建一个IAsyncResult上下文对象,在通讯频繁的服务器,由于大量的这样的对象的产生,给GC增加了很多工作量,从而降低了通讯的性能 , 在c# 2.0SP1以后,有SocketAsyncEventArgs对象,表示socket的异步操作,这个对象不需要每将发送和接收的时候都需要创建,也就是在创建完成之后,可以重复使用,但编写难度比BeginXXX又相对困难了许多,这里好比哲学里的矛盾一样。  

        使用SocketAsyncEventArgs结合Socket,封装类似的Tcplistener和tcpclient,难点在于使用SocketAsyncEventArgs对数据的发送操作。在BeginSend方法,只要把要发送的数据写入到网络流中,就可以完成发送,但SocketAsyncEventArgs就不是这样,还需要做些处理,SocketAsyncEventArgs有SetBuffer的方法,用来绑定和设计SocketAsyncEventArgs的发送缓冲区和缓冲区起始偏移量以及数据长度,但有一个要求是,如果更换缓冲区的地址,会很耗性能。因此,设置缓冲区后,最好不要改变它的地址,但可以调节它的起始偏移量和数据长度。而在实际项目中,数据包包的长度是无法预测的,我们不可能对SocketAsyncEventArgs的发送缓冲区设置为非常非常大,足以容纳项目中最大的数据包。所以一般当发送缓冲区设置为一个比较合适的固定大小,系统常用的默认值是8*1024个字节,如果数据包大于缓冲区时,需要对数据包进行拆分,然后多次发送。但由于发送是异步的,也就是不能在SocketAsyncEventArgs进行发送的时候又进行第二次发送操作,正确的方法是,第一次SocketAsyncEventArgs发送后,接收到系统的完成通知,然后再将剩余的数据进行第二将发送,依此类推,直接把一个数据包全部发送完成。还有一个可能,发送第一个数据包还未完成时,需要发送第二个数据包,其实,这个问题比较好处理,只需要把第二个数据包放到未完成发送的数据的后面就行,剩下的只是等待系统通知发送完成事件,接着继续发送未完成的就可以。 

         当发送和接收工作处理以后,tcp通讯主要的工作也完成了,剩下的工作就是利用池进通讯过程中各种可以回收利用的对象对进回收利用了。对Socket不同的封装,池的设计也可能不尽一样。有人用一个SocketAsyncEventArgs绑定一个Socket,相当于封装Tcpclient一样,但我不太看好这种设计思路,这种存在的弊端是发送和接收工作不能独立起来,也不能同时进行,有背于Tcp的双工思想,我是这样设计客户端的,命名为TcpClientEx,把一个Socket和两个SocketAsyncEventArgs对象复合起来,封装成像Tcpclient,其中,两个SocketAsyncEventArgs分别独立进行发送和接收工作;而服务器则用一个类对一个Socket和一个SocketAsyncEventArgs进行封装,命名为TcpServerEx,其中,这个SocketAsyncEventArgs专门进行Accept操作,Accept返回的socket,直接绑定到TcpclientEx对象上,就可以对socket进行接收和发送工作了,这种思路正是套用微软的Tcplistener和Tcpclient的封装理念。 

        对于TcpServerEx,需要设定一个最大连接数,第一时间把相应数量的TcpClientEx初始化,放到TcpClientEx池中,当accep到socket后,从池中取出一个TcpClientEx和socket绑定,当客户端关闭连接之后,把TcpClientEx推到池中,进行复用。复用了TcpClientEx,其里面的两个SocketAsyncEventArgs对象也自然而然地得到复用了。 

        由于可以创建了固定数据的的TcpClientEx,我们可以创建一个连续的byte数组,作为这些TcpClientEx所有SocketAsyncEventArgs对象的公共缓冲区,数组长度为SocketAsyncEventArgs数量*(SocketAsyncEventArgs发送缓冲区大小+SocketAsyncEventArgs接收缓冲区大小)  ,把各SocketAsyncEventArgs的起始偏移位置设置好即可,从而可以达到创建多个SocketAsyncEventArgs的缓冲区而不会出现内存分散的现象。 

        以上完成了性能优化过程,但这是一家之法,相信还有N多种方法可以达到更好的性能,只是俺水平有限。我觉得调用的方便性也是衡量封装好坏的一个重点,如果单纯地追求性能而忽略了方便性,我们完全可以沉到ip层,利用更低层的语言等,但这好像太极端了。对于这种设计,方不方便都主要集中体验在TcpClientEx的设计上了,在项目中,我们期待的是将实体(entity)直接发送出去,而收到的是实体。前者很容易实现,我们在实体发送和二进制数据发送间增加序列化工作就可以,但后者不好实现,原因是,如果是返回实体,TcpClientEx就必须依赖于实体工程,也就是先有实体,后有TcpClientEx,这不符合我们的要求,因为我们希望这个通讯组件是基础,可以为多种项目和解决方案提供服务。以下为这种矛盾提供一种中庸的方法,达到两者平衡效果。 

      我们可以在实体和byte数据之间做一个新的数据类类型,既能转换为实体,也能转换为byte数据,下面是这个类型的代码设计:

     ///   <summary> 
     ///   二进制数据
      ///   </summary> 
     public   class   Binary
    {
          ///   <summary> 
         ///   实体数据
          ///   </summary> 
         internal   byte [] Buffer =  null  ;

          ///   <summary> 
         ///   二进制数据
          ///   </summary> 
         ///   <param name="buffer"></param> 
         internal  Binary( byte  [] buffer)
        {
              this .Buffer =  buffer;
        }

          ///   <summary> 
         ///   反序列化为实体
          ///   </summary> 
         ///   <typeparam name="T">  实体类型  </typeparam>       
         ///   <returns></returns> 
         public  T ToEntity<T> ()
        {
              return  (T)SerializeTool.Deserialize( this  .Buffer);
        }


          ///   <summary> 
         ///   通过UTF-8编码转换为Json字符串再反序列化为实体
          ///   </summary> 
         ///   <typeparam name="T">  实体类型  </typeparam> 
         ///   <returns></returns> 
         public  T ToEntityJson<T> ()
        {
              return  SerializeTool.DeserializeFormJsonByte<T>( this  .Buffer);
        }
     

实际上,这里只完成了将byte数据转换为实体的封装,实体转为二进制是如何设计和实现呢?我们可以利用扩展方法来解决。因为我们不知道实体是怎么样,所以需要定义一个接口,这个接口什么契约也没有,但实体必须继承于它,这样的好处是避免扩展方法带来脏方法,扩展方法只对会实现这个接口的实体进行扩展,而扩展方法里,就是把实体序列化为byte数组:

  ///   <summary> 
     ///   IEntityWhere类型实体拓展方法
      ///   </summary> 
     public   static   class   ExtentMethod
    {
          ///   <summary> 
         ///    序列化为二进制数据
          ///   </summary> 
         ///   <param name="entity">  实体  </param> 
         ///   <returns></returns> 
         public   static  Binary ToBinary( this   IEntityWhere entity)
        {
              return   new   Binary(SerializeTool.Serialize(entity));
        }

          ///   <summary> 
         ///    序列化为二进制数据
          ///   </summary> 
         ///   <param name="entityArray">  实体集合  </param> 
         ///   <returns></returns> 
         public   static  Binary ToBinary( this  IEnumerable<IEntityWhere>  entityArray)
        {
              return   new   Binary(SerializeTool.Serialize(entityArray));
        }

          ///   <summary> 
         ///   序列化为Json字符串再通过UTF-8编码转换为二进制数据
          ///   </summary> 
         ///   <param name="entity">  实体  </param> 
         ///   <returns></returns> 
         public   static  Binary ToBinaryJson( this   IEntityWhere entity)
        {
              return   new   Binary(SerializeTool.SerializeToJsonByte(entity));
        }


          ///   <summary> 
         ///   序列化为Json字符串再通过UTF-8编码转换为二进制数据
          ///   </summary> 
         ///   <param name="entityArray">  实体集合  </param> 
         ///   <returns></returns> 
         public   static  Binary ToBinaryJson( this  IEnumerable<IEntityWhere>  entityArray)
        {
              return   new   Binary(SerializeTool.SerializeToJsonByte(entityArray));
        }
    } 

利用Binary 中间件做为TcpClientEx的接收到数据包的事件返回类型,上层的具体项目只要写好实体后,就可以方便的发送和接收数据包了。TcpClientEx的事件如下设计:

         ///   <summary> 
         ///   接收完成一个数据包事件
          ///   </summary> 
         public   event  EventHandler<SocketDataEventArgs> OnRecvComplete =  null  ;

          ///   <summary> 
         ///   连接断开事件    
          ///   </summary> 
         public   event  EventHandler<SocketEventArgs> OnDisconnect =  null  ;

          ///   <summary> 
         ///   发送一个数据包完成事件 
          ///   </summary> 
         public   event  EventHandler<SocketEventArgs> OnSendComplete =  null  ;
     


 

SocketDataEventArg:

     ///   <summary> 
     ///   包含数据包的对象
      ///   </summary> 
     public   class   SocketDataEventArgs : EventArgs
    {
          ///   <summary> 
         ///   获取或设置动作类型
          ///   </summary> 
         public   int  Action {  get ;  set  ; }

          ///   <summary> 
         ///   获取或设置实体二进制数据
          ///   </summary> 
         public  Binary Binary {  get ;  set  ; }

          ///   <summary> 
         ///   异步发送数据
          ///   </summary>       
         ///   <param name="action">  动作类型  </param> 
         ///   <param name="binaryData">  实体二进制数据  </param> 
         public  SocketDataEventArgs( int   action, Binary binaryData)
        {
              this .Action =  action;
              this .Binary =  binaryData;
        }

          ///   <summary> 
         ///   字符串显示方式
          ///   </summary> 
         ///   <returns></returns> 
         public   override   string   ToString()
        {
              return   this  .Action.ToString();
        }
    } 


 

这样,接收和发送就以Binary 作为数据类型桥梁存在,TcpClientEx类图:

   

        看有位牛人自己写了BufferWriter和BufferReader,相当于集成了序列化和反序列化工作,以及发送和接收数据保存的缓冲区,的确非常不简单,但也导致了调用也非常不简单的毛病,试想如果和java间通讯,java端也得写一对应的reader和wirter,相当麻烦。我则写了一个byteBuilder,功能类似于stringBuilder,只是提供写入和读取数据,只通讯组件内部使用,项目开时使用者不需要对这个进行read或write操作,集中精力写好entity就可以,在使用上方便一些,而且也大大的减少了代码量。 

      另外,提供了二进制序列化和Json序列化传输两种机制,前者用于两个C#工程之间的通讯,后者可以提供与Java等所有能解析Json格式的语言通讯,可以实现服务器C#,安卓Java客户端的通讯。

 

分类:  Socket

作者: Leo_wl

    

出处: http://HdhCmsTestcnblogs测试数据/Leo_wl/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权信息

查看更多关于C# socket tcp通讯的详细内容...

  阅读:71次