2009-10-22

TCP读取报文不完整的问题分析(粘包,拆包)

Views: 28862 | Add Comments

首先解释一下这个题目, "报文"指的是业务层自定义的报文, TCP是流式协议, 不像UDP那样是报文协议.

某两个系统间进行网络交互, 请求报文的格式为:

<json串>&sig=xxx

请求方用PHP的stream_socket_sendto()进行发送. 服务器端使用Python的twisted框架. 上线后, 出现问题, 服务器端接收到的报文不完整. 例如, json串只读了一半, 或者缺少"&sig=xxx", 缺少的数据是随机的, 但只缺少尾部, 已经接收到数据没有差错.

发送方代码:

$str = sprintf('json=%s&sig=%s', $json, $sig);
stream_socket_sendto($fp, $str);

接收方代码:

class XServer(Protocol):
    def dataReceived(self, data):
        #处理一个报文

factory = Factory()
factory.protocol = XServer
reactor.listenTCP(19009, factory)
reactor.run()

我们通过PHP和Python的API文档, 以及我们对网络协议和套接口的理解来分析.

问题1: 发送缓冲太小了吗?(发送方分包了吗?)

我们首先想到的原因会不会是, 发送方的发送缓冲比一个报文小, 所以"分包"了? 不过仔细一想, 这个问题出发点本身就是错误的. 因为TCP协议是流式协议, 不存在"分包"的问题, 因为TCP的用户接口只能看到字节流, 不认识报文(包). 所以, 发送方的发送缓冲大小不影响接收.

问题2: 接收缓冲太小了吗?

怀疑完发送缓冲, 现在来怀疑一下接收缓冲. 因为使用的是TCP协议, 所以, 就算是只读到一个字节的数据, dataReceived()也可能被调用一次, 所以, 在dataReceived()中把接收到的数据当作一个报文, 就犯了逻辑错误, 与接收缓冲的大小无关. 而且, dataReceived()的API文档提到: Please keep in mind that you will probably need to buffer some data.

问题1和问题2都有相同的错误, 那就是错误地把TCP当作是基于报文的协议. 使用TCP, 如果应用层协议不是基于报文的, 你可以在每一次读数据之后对数据进行处理, 比如, echo程序. 如果应用层协议是基于报文的, 那么, 你必须自己组装报文; 或者, 一次TCP会话只发送一个报文, 通过连接关闭来显式地声明报文发送完毕. 而一个报文可能需要多次读操作才能组装完毕, 也可能一次读包含了多于一个报文(如果不是停止等待应答的话).

结论:

1. 重新定义报文格式, 基于行的协议

基于TCP socket的程序, 有几种方式可用来实现报文协议:

1. 报文中声明报文数据的长度.
2. 使用分隔符.
3. 发送方发送完一个报文后关闭连接.

业务层本意上是想使用一种基于报文的协议, 但所定义的报文格式并没有提供报文分隔符或者长度字段, 这就要求程序进行语义分析, 增加了实现难度. 现在改成使用换行符作为分隔符.

2. 改写接收端代码, 使用基于行的接口

class XServer(LineServer):
    def lineReceived(self, data):
        #处理一个报文

前面提到, 我们必须自己组装报文, 但这不代表我们要自己写组装代码, 行IO库可以替我们做这项工作. LineServer实现了基于行的协议, 只有读到完整的一行, lineReceived()才会调用.

姊妹篇: 编写基于TCP的应用程序 http://www.ideawu.net/blog/?p=429

Related posts:

  1. C语言解析JSON
  2. Zend Framework 的缓存模块 Zend_Cache 使用
  3. 写自己的 http_build_query
  4. PHP 用 curl 读取 HTTP chunked 数据
  5. Prado 中解决 Ajax 中文乱码问题
Posted by ideawu at 2009-10-22 19:21:02 Tags:

Leave a Comment