TCP客户端

TCP通信客户端-解决数据包接收不全的过程

背景:5个串口条码枪,通过MOXA Nport系列转换器,以TCPServer的形式推送扫描到的条码到客户端。5个条码枪均位于流水线上方的支架上,流水线货物流过的瞬间条码枪完成箱中12个或者4个货物的扫描(12个一箱时启动三个条码枪,4个一箱时启动两个条码枪),并推送出去。

问题:开发TCP客户端接收并解析条码

  1. 由于条码枪半自动模式下,自动识别条码时前排的货物条码总是漏掉,因此采用客户端循环发送读取命令的方式;

  2. 条码枪扫描推送的条码没有开始符和数据包长度,只有0x10,0x13两个结束符。

这是开发前连接的需求和信息。

TCP客户端版本一

本着不重复造轮子的思路,直接照搬之前局域网PLC通信的TCP实现,稍加改造,就ok了。

public void ReceiveServerTcpMsg()
{
    var buffer = new byte[4096];
    while (!BExit)
    {
        try
        {
            if (_socket != null && _socket.Connected)
            {
                if (_socket.Poll(-1, SelectMode.SelectRead))
                {
                    if (_socket.Available > 0)
                    {
						//接收条码
                        var recLength=_socket.ReceiveFrom(buffer, SocketFlags.None, ref _serverEndPoint);

                        //解析条码
                        var barcode = GetBarCodeFromAscii(buffer);

						//后续业务逻辑处理                            
                    }                        
                }
            }
        }
        catch (Exception e)
        {
            if (EquipMessage != null)
            {
                EquipMessage(EquipName, _barcodeNo, 0, string.Format("ReceiveServerTcpMsg异常。ErrorCode={0}", e.Message));
            }
        }
    }
}

写好了,拿个条码枪测试一下。
扫一个条码ok,扫一个条码ok,连扫几个条码问题出现了:

我扫的条码是6位的,但是总会出现2位或者4位的条码。

怎么会这样呢?这个实现在与PLC的通讯过程中是没有问题的呀,这里数据怎么会不全。当时手边没有资料,按照函数的说明一致在调整

_socket.Available>0

因为发送的可访问的字节数大于0就读,可能没有发送完毕,因此出现断码,于是调整为

_socket.Available>5

问题暂时解决了,但是真正使用时的条码也不是6位,而且即使这样只是断码的情况还是偶尔会出现。但是有几个疑问:

TCP一个数据包不是发送的很快吗?为什么会出现数据包不全的情况?

晚上开始查阅资料,发现了一些有用的东西:

  1. TCP底层存在着分包机制,网络传输过程中可能会出现粘包;
  2. 串口转网口通信是典型按照一定频率发送数据的;
  3. 更详细一点解释可以参照这两篇博客C# Socket Networkstream接收数据不全关于串口接收并解析数据

基于以上信息可以确认

  1. TCP数据传输是基于流的,一条完整的数据可能分包后需要发送多次,也会由于网路堵塞多个数据包粘在一起;
  2. TCP通讯数据包,客户端需要根据通讯协议约定的数据包头部、尾部、长度等信息,重新组合成一条完成的数据。

于是第二个版本的客户端就诞生了。

//接收函数
public void ReceiveServerTcpMsg()
{
    var buffer = new byte[4096];
    while (!BExit)
    {
        try
        {
            if (_socket != null && _socket.Connected)
            {
                if (_socket.Poll(-1, SelectMode.SelectRead))
                {
                    if (_socket.Available > 5)
                    {
                        var recLength=_socket.ReceiveFrom(buffer, SocketFlags.None, ref _serverEndPoint);
                        if(recLength<=0)continue;

                        //接收字节放入缓冲区
                        var recStr=new StringBuilder();

						//因为出队列和入队列是两个线程,所以需要加锁
						//接收线程只负责把数据放在缓冲队列
                        lock (_barcodeBuffer)
                        {
                            for (var i = 0; i < recLength; i++)
                            {
                                _barcodeBuffer.Enqueue(buffer[i]);
                                recStr.Append(buffer[i]);
                            }
                        }                            
                    }                        
                }
            }
        }
        catch (Exception e)
        {
            if (EquipMessage != null)
            {
                EquipMessage(EquipName, _barcodeNo, 0, string.Format("ReceiveServerTcpMsg异常。ErrorCode={0}", e.Message));
            }
        }
    }
}

处理函数:

private void receiveTimer_Elapsed(object sender, ElapsedEventArgs e)
{
    //没有收到数据
    if (_barcodeBuffer.Count <= 0) return;

    var buffer = new byte[1024];

	//加锁
    lock (_barcodeBuffer)
    {
        //剔除开头空字符
        while (_barcodeBuffer.Peek().Equals(10) || _barcodeBuffer.Peek().Equals(13))
        {
            _barcodeBuffer.Dequeue();
        }
        if (_barcodeBuffer.Count <= 0) return;
        
        var length = 0;

        //没有收到结束符不读取缓冲区
        if (!_barcodeBuffer.Contains(10) || !_barcodeBuffer.Contains(13)) return;
        byte item;
        while (!(item = _barcodeBuffer.Dequeue()).Equals(10))
        {
            buffer[length] = item;
            length++;
        }
        buffer[length] = item;
		
		//条码解析                
    }
            
}

//处理线程定时读取数据和处理数据
_receiveTimer = new Timer
{
    Interval = 100,
    Enabled = false,
    AutoReset = true
};
_receiveTimer.Elapsed += receiveTimer_Elapsed;

//接收线程负责接收数据
new Thread(ReceiveServerTcpMsg).Start();

ok,这个版本无论是在多个条码枪同时工作还是提高条码枪扫描频率情况下都可以正常工作。这算是基本满足现有的需求了吧。
但是在后续查阅了解到还有几个问题有待解决:

  1. Lock锁性能太差,.net4.0提供有异步队列,是线程安全的,而且锁的性能损耗更小(本人没有使用是由于本项目基于.net3.5);
  2. 另外是一个业务问题:由于流水线速度、条码枪触发时机等问题,货物存在漏读的现象。

这是第一篇博客,拖了这么长时间终于写完了。
刚开始准备写的时候,感觉要写的东西挺多的,但是由于选择写博客的客户端和工作上的事情耽搁了。导致现在感觉怎么写都有点干巴巴的,好吧万事开头难,我这也算开头了。

原文地址:https://www.cnblogs.com/zhangdk/p/5389233.html