粘包的解决-消息定界(转)

一.引子与协议说明

之前开发了一个项目——车载导航系统。遇到的第一个问题就是硬件设备如何与服务器通信。

关键在于通信协议!

众所周知:要想实现通信,首先通信双方就要达成通信协议。

话不多说,且看协议:

————————————————华丽的分割线—————————————————

以上的这些协议说明是不是看得很头大呢?

遵循如此这般的通信协议的硬件设备又如何才能与服务器以及PC顺利通信呢?

还请各位看官稍安勿躁!且听我娓娓道来!

二.基础知识-TCP与粘包

我们都知道,互联网的核心是TCP/IP协议簇。其中TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层协议。另外我们大概都听过一个词,叫做“粘包”,然而很多人对其内涵不甚了了。其实,粘包问题和TCP密切相关。因为TCP是面向连接的而且基于字节流,我们可以用一根水管来比喻TCP的工作方式。

字节流就跟水流一样,当两个消息一起读取时,你无法分别出二者的边界。

三.粘包的解决-消息定界

为了解决粘包问题,就需要对消息定界。

方法1:文本协议模式

方法2:二进制协议模式

文本协议模式通过在消息尾部加上特殊的标志来作为划分消息的依据;
二进制协议则将消息封装成消息头+消息体,通过解析定长消息头,然后从消息头中取得消息体长度,进一步解析出消息体
 
——从而粘包的问题得到了解决。

四.回到问题-协议选择

以上是硬件设备的消息结构,从中我们既能够看到文本协议的影子也能够看到二进制协议的影子。

因为含有标志位,所以可以采用文本协议。

然后我们将开头的标志位作为消息头的一部分,剩下的部分都当成消息体,那么就是一个二进制协议的形式。二进制协议的两个要求是:1.消息头定长 2.消息头中能解析出消息体长度。而这个消息结构是满足这个要求的。

五.文本协议实现示例

复制代码
    // 摘要:
    // 文本协议助手接口。
    public interface ITextContractHelper
    {
        // 摘要:
        // 消息结束标识符(经过编码后得到的字节数组)的集合。
        // 比如一般应用使、用"\0"作为消息结束标志,那么,集合中只有一个元素("\0"的二进制)。
        // 有的应用可能有多个标识符(如"\0"、"\n"及其它)都可以作为消息的结束标志,则集合中就有多个元素。
        // 如果设置为null,引擎则不进行消息完整性识别及构造,每次接收到数据,就直接触发MessageReceived事件。
        List<byte[]> EndTokens { get; }
    }
复制代码

文本协议助手接口定义了采用文本协议的最基本的规范——具备消息结束符。接下来我们来看该接口的一个简单的实现。

复制代码
    public class DefaultTextContractHelper : ITextContractHelper
    {        
        public DefaultTextContractHelper(params string[] endTokenStrs)
        {
            this.endTokens = new List<byte[]>(); ;
            if (endTokenStrs == null || endTokenStrs.Length == 0)
            {
                return;
            }

            foreach (string str in endTokenStrs)
            {
                this.endTokens.Add(System.Text.Encoding.UTF8.GetBytes(str));
            }
        }

        private List<byte[]> endTokens;
        public List<byte[]> EndTokens
        {
            get
            {
                return this.endTokens;
            }
        }
    }
复制代码

其实文本协议的本质就是:消息的结束符经过编码(比如UTF-8)后得到的字节数组作为字节流中识别消息边界的标志。

我们将其注入通信引擎,引擎即可根据我们设置的标志来分割出一个个消息。

  //初始化并启动客户端引擎(TCP、文本协议)
   this.tcpPassiveEngine = NetworkEngineFactory.CreateTextTcpPassiveEngine(this.textBox_IP.Text, int.Parse(this.textBox_port.Text), new DefaultTextContractHelper("\0"));

Demo中用“\0”来作为消息结束标志。我们知道,消息结束符是我们人为的加到消息尾部,而真正的消息是不具备这样的内容的。所以在收到消息时我们需要剔除这个结束符,也就是解封。

复制代码
 void tcpPassiveEngine_MessageReceived(System.Net.IPEndPoint serverIPE, byte[] bMsg)
{
    string msg = System.Text.Encoding.UTF8.GetString(bMsg); //消息使用UTF-8编码
    msg = msg.Substring(0, msg.Length - 1); //将结束标记"\0"剔除
    this.ShowMessage(msg);
}
复制代码

附:文本协议demo源码下载

 

六.二进制协议Demo实现示例

复制代码
    // 摘要:
    //     二进制协议助手接口。
    public interface IStreamContractHelper
    {
        // 摘要:
        //     消息头的长度。
        int MessageHeaderLength { get; }

        // 摘要:
        //     从消息头中解析出消息体的长度(注意,不是整个消息的长度,而是不包含消息头的Body的长度)。
        //
        // 参数:
        //   head:
        //     完整的消息头,长度固定为MessageHeaderLength
        int ParseMessageBodyLength(byte[] head);
    }
复制代码

文本协议助手接口定义了采用文本协议的最基本的规范——消息头定长,从消息头中能够解析出消息体长度。接下来我们看一个该接口的简单实现:消息头规定为8个字节,其中头4个字节存放一个int类型的消息体长度。

复制代码
    public class StreamContractHelper : IStreamContractHelper
    {
        public int MessageHeaderLength
        {
            get { return 8; }
        }

        public int ParseMessageBodyLength(byte[] head)
        {
            return BitConverter.ToInt32(head, 0);
        }
    }
复制代码

我们将其注入通信引擎,引擎即可识别消息头、取出消息体长度来分割出一个个消息。

 //初始化并启动客户端引擎(TCP、文本协议)
this.tcpPassiveEngine = NetworkEngineFactory.CreateStreamTcpPassivEngine(this.textBox_IP.Text, int.Parse(this.textBox_port.Text), new StreamContractHelper());

附:二进制协议demo源码下载

 

七.总结

本文旨在介绍文本协议设计的一般方法,通过对于文本协议与二进制协议的本质的掌握,大家就能根据实际的需要来针对性的实现其通信协议了。大家可以根据这个一般的方法自己来实现一开始我给出来的卫星定位系统的通信协议。

有许多需要与硬件设备通信的应用,诸如与GPS设备通信、与工厂车间的一些单片机设备通信、与物联网设备通信等等,大家只要掌握了这些设备的通信协议,或采用文本协议或采用二进制协议,将其实现,那么通信的问题就迎刃而解了!

最后,随着HTML5 WebSocket技术的日益成熟与普及,B/S架构的应用于C/S架构的应用的通信也逐渐走向大一统。有兴趣的朋友可以参考:打通B/S与C/S!让HTML5 WebSocket与.NET Socket共用一个服务端!

 

http://www.cnblogs.com/aoyeyuyan/p/5383042.html

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
st_asio_wrapper是一组类库,功能是对boost.asio的包装(调试环境:boost-1.51.0),目的是简化boost.asio开发; 其特点是效率高、跨平台、完全异步(当然这是从boost.asio继承而来)、自动重连,数据透明传输,自动解决分包粘包问题(你可以像udp一样使用它); 注:只支持tcp协议; 教程:http://blog.csdn.net/yang79tao/article/details/7724514 2.3版更新内容: 消息(std::string包装)不再用boost::shared_ptr包装,之前有过度使用智能指针之嫌。效率上,std::string如果支持引用记数,或者编译器支持std::move语义,是没有损失的(因为也不存在内存的拷贝,反而省了智能指针使用上的开销),幸好vc支持std::move语义(虽然它不支持引用记数,linux则都支持)。这样带来一个问题,原来所有的接口中的boost::shared_ptr<std::string>数据类型,全部换成了std::string引用,升级到2.3的朋友要注意修改之前重写虚函数的签名,如果不改,则重写肯定不生效,变成了新增加虚函数了(因为签名不一样)。这样向大家道歉,接口签名以后应该不会变化了,但可能增加接口; 修复使用std::advance的一个BUG,此BUG在linux下不存在,这里顺便向大家说一下,std::advance在vc和gcc下面,语义一样,但处理方式有些不同,一定要注意; 增加了个专门用于服务端压力测试的客户端框架st_test_client,并写了一个demo test_client,可以在performance_test目录下面找到; 把连接服务端逻辑从st_client剥离出来,定义了一个新的类st_connector,st_client和st_test_client将从它继承; 增加对vc2010的支持,和编译时对编译器版本的检测,如果达不到vc2010及其以上的版本,st_asio_wrapper将直接报错。
st_asio_wrapper是一组类,功能是对boost.asio的包装(调试环境:boost-1.50.0),目的是简化boost.asio开发; 其特点是效率高、跨平台、完全异步,当然这是从boost.asio继承而来; 自动重连,数据透明传输,自动解决分包粘包问题(你可以像udp一样使用它); 注:只支持tcp协议; 教程:http://blog.csdn.net/yang79tao/article/details/7724514 1.1版更新内容: 增加了自定义数据模式的支持,可用于st_asio_wrapper server与其它客户端的通信、或者st_asio_wrapper client与其它服务端的通信;当然,两端都是st_asio_wrapper的话,就用透明传输即可(1.0版已经支持了)。 1.2版更新内容: 修复BUG:当stop_service之后,再start_service时,client_base内部某些成员变量可能没有得到复位; 服务端增加修改监听地址功能,当然仍然要在start_service之前调用set_server_addr函数。 1.3版更新内容: 增加自定义消息格式的发送,这个本来是在1.1版本实现的,结果我漏掉了,只实现了自定义消息格式的接收。 1.4版更新内容: 将打包与解包器从client_base分离出来,以简化这个日益复杂的基类; 可以在运行时修改打包解包器。 1.5版更新内容: 增加ipv6支持,默认是ipv4,服务端和客户端都通过设置一个ipv6的地址来开启这个功能; 增加了一些服务端helper函数,小改了一下客户端set_server_addr函数签名(调换了两个参数的位置以保持和服务端一样)。 1.6版更新内容: 增加了接收消息缓存(改动较大,on_msg的语义有所变化,请看开发教程第三篇)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值