• C# socket 阻止模式与非阻止模式应用实例


    问题概述

    最近在处理一些TCP客户端的项目,服务端是C语言开发的socket. 实际项目开始的时候使用默认的阻塞模式并未发现异常。代码如下

     1   public class SocketService
     2     {
     3         public delegate void TcpEventHandler1(byte[] receivebody, int length);
     4         public event TcpEventHandler1 OnGetCS;
     5         Socket client = null;
     6         IPEndPoint endPoint = null;
     7         public SocketService(string ip, int port)
     8         {
     9             client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    10             //client.Blocking = false;默认是阻塞模式
    11             endPoint = new IPEndPoint(IPAddress.Parse(ip), port);
    12             IsRcv = true;
    13         }
    14 
    15         Thread rthr = null;//异步线程用于接收数据       
    16 
    17         /// <summary>
    18         /// 表示是否继续接收数据
    19         /// </summary>
    20         public bool IsRcv { get; set; }
    21         /// <summary>
    22         /// 打开连接
    23         /// </summary>
    24         /// <returns></returns>
    25         public bool Open()
    26         {
    27             if (client != null && endPoint != null)
    28             {
    29                 try
    30                 {
    31                     client.Connect(endPoint);
    32                     Console.WriteLine("连接成功");
    33 
    34                     //启动异步监听
    35                     rthr = new Thread(ReceiveMsg);
    36                     rthr.IsBackground = true;
    37                     rthr.SetApartmentState(ApartmentState.STA);
    38                     rthr.Start();
    39                     return true;
    40                 }
    41                 catch
    42                 {
    43                     AbortThread();
    44                     Console.WriteLine("连接失败!");
    45                 }
    46             }
    47             return false;
    48         }
    49 
    50         /// <summary>
    51         /// 关闭接收数据线程
    52         /// </summary>
    53         private void AbortThread()
    54         {
    55             if (rthr != null)
    56             {
    57                 rthr.Abort();
    58             }
    59         }
    60 
    61         /// <summary>
    62         /// 关闭连接
    63         /// </summary>
    64         public void Close()
    65         {
    66             if (client.Connected)
    67             {
    68                 client.Close();
    69             }
    70         }
    71 
    72         /// <summary>
    73         /// 接收数据
    74         /// </summary>
    75         private void ReceiveMsg()
    76         {
    77             byte[] arrMsg = new byte[1024 * 1024];
    78             try
    79             {
    80                 while (IsRcv)
    81                 {
    82                     int length = client.Receive(arrMsg);//阻塞模式,此次线程会停止继续执行,直到socket内核有数据
    83                     byte type;
    84                     if (length > 0)
    85                         OnGetCS(arrMsg, length); //出发数据接收事件                  
    86                 }
    87             }
    88             catch (Exception ex)
    89             {
    90                 rthr.Abort();
    91                 client.Close();
    92                 client = null;
    93                 Console.WriteLine("服务器断开连接");
    94             }
    95         }
    96     }
    阻止模式代码

    当客户运行久后就发现 从服务器端发过来的数据到处理完成整个环节消耗的时间比较多(比同行慢)。 

    使用TCP 监听助手,和客户端程序在OnGetCS处打印出时间比较分析,发现TCP助手显示收到的时间会比客户端程序显示的快500-800MS左右。

    .也就是说服务器已经吧数据发送到客户端TCP缓冲区了,只是客户端 int length = client.Receive(arrMsg); 并么有及时获得相应。

    查了很多资料都没有查到有类似的问题。最后我用C#模拟做了一个TCP服务端与自己的TCP客户端之间通信,则完全没有延迟。

    因此只能考虑语言特性的差别了。C#毕竟封装了很多信息。这个时候再查看TCP监听助手对比服务器是C的和C#的发现 C服务器在指令标记位没有PSH标记位,而C#的则有这个标记位,如下图(此处C#作为服务器的有兴趣的可以自己去试)

    查询网络上的一段解释如下

    PSH 的作用

    TCP 模块什么时候将数据发送出去(从发送缓冲区中取数据),以及 read 函数什么时候将数据从接收缓冲区读取都是未知的。

    如果使用 PSH 标志,上面这件事就确认下来了:

    • 发送端

    对于发送方来说,由 TCP 模块自行决定,何时将接收缓冲区中的数据打包成 TCP 报文,并加上 PSH 标志(在图 1 中,为了演示,我们假设人为的干涉了 PSH 标志位)。一般来说,每一次 write,都会将这一次的数据打包成一个或多个 TCP 报文段(如果数据量大于 MSS 的话,就会被打包成多个 TCP 段),并将最后一个 TCP 报文段标记为 PSH。

    当然上面说的只是一般的情况,如果发送缓冲区满了,TCP 同样会将发送缓冲区中的所有数据打包发送。

    • 接收端

    如果接收方接收到了某个 TCP 报文段包含了 PSH 标志,则立即将缓冲区中的所有数据推送给应用进程(read 函数返回)

    当然有时候接收缓冲区满了,也会推送。

    通过这个解释瞬间总算是明白了,早期C开发的很多TCP通信,都是不带PSH标记位的,后来的产品很多都遵守这个模式了,然后我们的C#默认就是使用PSH标记位。 因此就导致了数据接收延迟500-800MS(根据PSH的解释这个延迟具体多久是未知的)。 

    解决方案

     最简单的是服务器端增加这个标记位发送过来。一番讨论后,人家写这个服务器的人都已经离职了,没人会处理。那么客户是上帝,只能我们这边来处理了。这里就要用到非阻止模式的socket了。

    首先我在网上查到很多人说异步就是非阻止模式。这个完全是错误的。异步同步与阻止模式是没有关系的两个概念。 当阻塞模式下有一个线程不断在等待缓冲区把数据交给它处理,异步的话就是触发回调方法,同步的话就继续执行同步的业务代码。

    而非阻塞模式的逻辑是,客户端的连接,读取数据线程都不会被阻塞,也就是会立即返回。比如连接的逻辑是客户端发起connect连接,因为TCP连接有几次握手的情况,需要一定的时间,然而非阻塞要求立即返回,这个时候系统会抛一个异常(Win32Excetion)。

    我们则只需要在异常里处理这个TCP连接需要一定时间的问题。可以循环读取TCP连接状态来确认是否连接成功。client.Poll 方法来查询当前连接状态。同理读取的时候也是在该异常里循环读取。

      1     public class SocketService
      2     {
      3         public delegate void TcpEventHandler1(byte[] receivebody, int length);
      4         public event TcpEventHandler1 OnGetCS;
      5         Socket client = null;
      6         IPEndPoint endPoint = null;
      7         public SocketService(string ip, int port)
      8         {
      9             client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
     10             client.Blocking = false;//非阻塞模式,定时循环读取缓冲区的数据把它拼接到缓冲区数据队列 arrMsg
     11             endPoint = new IPEndPoint(IPAddress.Parse(ip), port);
     12         }
     13 
     14         Thread rthr = null;
     15         /// <summary>
     16         /// 表示是否继续接收数据
     17         /// </summary>
     18         public bool IsRcv { get; set; }
     19         /// <summary>
     20         /// 非阻塞模式
     21         /// </summary>
     22         /// <param name="timeout"></param>
     23         /// <returns></returns>
     24         public bool Open(int timeout = 1000)
     25         {
     26             bool connected = false;
     27             if (client != null && endPoint != null)
     28             {
     29                 try
     30                 {
     31                     client.Connect(endPoint);//此处不会阻塞,如果是正在连接服务器的话,则会跑出win32Excetion异常(这里如果是netcore在linux上的话,怎么也会抛出异常,具体异常自行查阅)
     32                     Console.WriteLine("连接成功");
     33                     //启动异步监听
     34                     connected = true;
     35                 }
     36                 catch (Win32Exception ex)
     37                 {
     38                     if (ex.ErrorCode == 10035) // WSAEWOULDBLOCK is expected, means connect is in progress
     39                     {
     40                         var dt = DateTime.Now;
     41                         while (true)//循环读取当前连接的状态,如果timeout时间内还没连接成功,则反馈连接失败。
     42                         {
     43                             if (dt.AddMilliseconds(timeout) < DateTime.Now)
     44                             {
     45                                 break;
     46                             }
     47                             connected = client.Poll(1000000, SelectMode.SelectWrite);//不会阻塞
     48                             if (connected)
     49                             {
     50                                 connected = true;
     51                                 break;
     52                             }
     53                         }
     54                     }
     55                 }
     56                 catch (Exception ex)
     57                 {
     58                     AbortThread();
     59                     Console.WriteLine("连接失败");
     60                 }
     61             }
     62             if (connected)
     63             {
     64                 StartReceive();//连接成功则启动数据读取线程
     65             }
     66             return connected;
     67         }
     68 
     69         private void StartReceive()
     70         {
     71             rthr = new Thread(ReceiveMsgNonBlock);
     72             rthr.IsBackground = true;
     73             rthr.SetApartmentState(ApartmentState.STA); //设置通信线程通信线程同步设置,才能在打开接受文件时 打开 文件选择框
     74             rthr.Start();
     75         }
     76 
     77         private void AbortThread()
     78         {
     79             if (rthr != null)
     80             {
     81                 rthr.Abort();
     82             }
     83         }
     84 
     85         public void Close()
     86         {
     87             if (client.Connected)
     88             {
     89                 client.Close();
     90             }
     91         }
     92 
     93         /// <summary>
     94         /// app端缓冲池
     95         /// </summary>
     96         byte[] arrMsg = new byte[1024 * 1024];
     97         /// <summary>
     98         /// 当前缓冲池的长度
     99         /// </summary>
    100         int currentlength = 0;
    101 
    102         /// <summary>
    103         /// 读取TCP缓冲数据
    104         /// </summary>
    105         private void ReceiveMsgNonBlock()
    106         {
    107             while (true)
    108             {
    109                 try
    110                 {
    111                     byte[] tempBytes = new byte[1024 * 1024];
    112 
    113                     int length = client.Receive(tempBytes);//此处不会阻塞,如果有数据则继续,如果没有数据则抛出Win32Exception异常(linux 下netcore自行查找异常类型 )
    114 
    115                     DealMsg(tempBytes, length);
    116                 }
    117                 catch (Win32Exception ex)
    118                 {
    119 
    120                     if (ex.ErrorCode == 10035) // WSAEWOULDBLOCK is expected, means connect is in progress
    121                     {
    122                         Thread.Sleep(50);
    123                     }
    124 
    125                 }
    126                 catch (Exception ex)
    127                 {
    128                     rthr.Abort();
    129                     client.Close();
    130                     client = null;
    131                     Console.WriteLine("服务器断开连接");
    132                     break;
    133                 }
    134             }
    135         }
    136 
    137         /// <summary>
    138         /// 把当前读取到的数据添加到app,并且根据自己的TCP约定的规则分析包头包尾长度校验等等信息,来确认在arrMsg中获取自己想要的数据包最后交给OnGetCS事件
    139         /// </summary>
    140         /// <param name="bytes"></param>
    141         /// <param name="length"></param>
    142         public void DealMsg(byte[] bytes, int length)
    143         {
    144             //先把数据拷贝到 全局数组arrMsg
    145             if (bytes.Length + this.currentlength > 1024 * 1024)
    146             {
    147                 byte[] arrMsg = new byte[1024 * 1024];
    148             }
    149 
    150             Array.Copy(bytes, 0, arrMsg, this.currentlength, length);
    151             this.currentlength += length;
    152 
    153 
    154             ///根据自己的包头包尾的规则来截取TCP数据包,因为实际运行当中要考虑到服务端发送特别大的数据包,以及服务器太忙的时候分段发送数据包的情况。因此不能盲目的以为读取的缓冲区的数据就是一个完成的数据包。
    155             ///最终生成tmpMsg。
    156             var tmpMsg = new byte[1000];
    157             OnGetCS(tmpMsg, tmpMsg.Length);          
    158         }
    159     }
    非阻止模式

    经过测试,通过循环主动去读取缓冲带完美的解决了客户端缓慢的问题,实际运行的时候读取缓冲区的时间间隔可以根据需求自行更改,本例中用了50ms。

  • 相关阅读:
    openwrt 更改 debug 等级(hostapd)
    openwrt 中procd
    openwrt增加串口登录需要密码
    openwrt设置语言的过程
    小程序感悟123
    如何用php实现分页效果
    如何利用h5将视频设置为背景
    关于js中定时器的返回值问题
    canvas二:绘制圆和其他曲线
    canvas一:基本认识
  • 原文地址:https://www.cnblogs.com/yibey/p/14335786.html
Copyright © 2020-2023  润新知