在TCP连接开始到结束连接,之间可能会多次传输数据,也就是服务器和客户端之间可能会在连接过程中互相传输多条消息。理想状况是一方每发送一条消息,另一方就立即接收到一条,也就是一次write对应一次read。但是,现实不总是按照剧本来走。

MINA官方文档节选:

TCP guarantess delivery of all packets in the correct order. But there is no guarantee that one write operation on the sender-side will result in one read event on the receiving side. One call of IoSession.write(Object message) by the sender can result in multiple messageReceived(IoSession session, Object message) events on the receiver; and multiple calls of IoSession.write(Object message) can lead to a single messageReceived event.

Netty官方文档节选:

In a stream-based transport such as TCP/IP, received data is stored into a socket receive buffer. Unfortunately, the buffer of a stream-based transport is not a queue of packets but a queue of bytes. It means, even if you sent two messages as two independent packets, an operating system will not treat them as two messages but as just a bunch of bytes. Therefore, there is no guarantee that what you read is exactly what your remote peer wrote.

上面两段话表达的意思相同:TCP是基于字节流的协议,它只能保证一方发送和另一方接收到的数据的字节顺序一致,但是,并不能保证一方每发送一条消息,另一方就能完整的接收到一条信息。有可能发送了两条对方将其合并成一条,也有可能发送了一条对方将其拆分成两条。所以在上一篇博文中的Demo,可以说是一个错误的示范。不过服务器和客户端在同一台机器上或者在局域网等网速很好的情况下,这种问题还是很难测试出来。

举个简单了例子(这个例子来源于Netty官方文档):

消息发送方发送了三个字符串:

但是接收方收到的可能是这样的:

那么问题就很严重了,接收方没法分开这三条信息了,也就没法解析了。

对此,MINA的官方文档提供了以下几种解决方案:

1、use fixed length messages

使用固定长度的消息。比如每个长度4字节,那么接收的时候按每条4字节拆分就可以了。

2、use a fixed length header that indicates the length of the body

使用固定长度的Header,Header中指定Body的长度(字节数),将信息的内容放在Body中。例如Header中指定的Body长度是100字节,那么Header之后的100字节就是Body,也就是信息的内容,100字节的Body后面就是下一条信息的Header了。

3、using a delimiter; for example many text-based protocols append a newline (or CR LF pair) after every message

使用分隔符。例如许多文本内容的协议会在每条消息后面加上换行符(CR LF,即"\r\n"),也就是一行一条消息。当然也可以用其他特殊符号作为分隔符,例如逗号、分号等等。

当然除了上面说到的3种方案,还有其他方案。有的协议也可能会同时用到上面多种方案。例如HTTP协议,Header部分用的是CR LF换行来区分每一条Header,而Header中用Content-Length来指定Body字节数。

下面,分别用MINA、Netty、Twisted自带的相关API实现按换行符CR LF来分割消息。

MINA:

MINA可以使用ProtocolCodecFilter来对发送和接收的二进制数据进行加工,如何加工取决于ProtocolCodecFactory或ProtocolEncoder、ProtocolDecoder,加工后在IoHandler中messageReceived事件函数获取的message就不再是IoBuffer了,而是你想要的其他类型,可以是字符串,Java对象。这里可以使用TextLineCodecFactory(ProtocolCodecFactory的一个实现类)实现CR LF分割消息。

public class TcpServer {  

    public static void main(String[] args) throws IOException {
IoAcceptor acceptor = new NioSocketAcceptor(); // 添加一个Filter,用于接收、发送的内容按照"\r\n"分割
acceptor.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"), "\r\n", "\r\n"))); acceptor.setHandler(new TcpServerHandle());
acceptor.bind(new InetSocketAddress(8080));
} } class TcpServerHandle extends IoHandlerAdapter { @Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
cause.printStackTrace();
} // 接收到新的数据
@Override
public void messageReceived(IoSession session, Object message)
throws Exception { // 接收客户端的数据,这里接收到的不再是IoBuffer类型,而是字符串
String line = (String) message;
System.out.println("messageReceived:" + line); } @Override
public void sessionCreated(IoSession session) throws Exception {
System.out.println("sessionCreated");
} @Override
public void sessionClosed(IoSession session) throws Exception {
System.out.println("sessionClosed");
}
}

Netty:

Netty设计上和MINA类似,需要在ChannelPipeline加上一些ChannelHandler用来对原始数据进行处理。这里用LineBasedFrameDecoder将接收到的数据按行分割,StringDecoder再将数据由字节码转成字符串。同样,接收到的数据进过加工后,在channelRead事件函数中,msg参数不再是ByteBuf而是String。

public class TcpServer {  

    public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ChannelPipeline pipeline = ch.pipeline(); // LineBasedFrameDecoder按行分割消息
pipeline.addLast(new LineBasedFrameDecoder(80));
// 再按UTF-8编码转成字符串
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8)); pipeline.addLast(new TcpServerHandler());
}
});
ChannelFuture f = b.bind(8080).sync();
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
} } class TcpServerHandler extends ChannelInboundHandlerAdapter { // 接收到新的数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { // msg经过StringDecoder后类型不再是ByteBuf而是String
String line = (String) msg;
System.out.println("channelRead:" + line);
} @Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("channelActive");
} @Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("channelInactive");
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

Twisted:

Twisted的设计和上面两者的设计不太一样,所以实现消息分割也不太一样。处理事件的类TcpServerHandle不再继承Protocol,而是继承Protocol的子类LineOnlyReceiver。接收到新数据的事件方法也不再是dataReceived,而是LineOnlyReceiver提供的lineReceived。看Twisted源码的话可以发现LineOnlyReceiver的内部实际上自己实现了dataReceived,然后将其按行分割,有新的一行数据就调用lineReceived。

# -*- coding:utf-8 –*-  

from twisted.protocols.basic import LineOnlyReceiver
from twisted.internet.protocol import Factory
from twisted.internet import reactor class TcpServerHandle(LineOnlyReceiver): # 新的连接建立
def connectionMade(self):
print 'connectionMade' # 连接断开
def connectionLost(self, reason):
print 'connectionLost' # 接收到新的一行数据
def lineReceived(self, data):
print 'lineReceived:' + data factory = Factory()
factory.protocol = TcpServerHandle
reactor.listenTCP(8080, factory)
reactor.run()

下面用一个Java客户端对三个服务器进行测试:

public class TcpClient {  

    public static void main(String[] args) throws IOException {  

        Socket socket = null;
OutputStream out = null; try { socket = new Socket("localhost", 8080);
out = socket.getOutputStream(); // 请求服务器
String lines = "床前明月光\r\n疑是地上霜\r\n举头望明月\r\n低头思故乡\r\n";
byte[] outputBytes = lines.getBytes("UTF-8");
out.write(outputBytes);
out.flush(); } finally {
// 关闭连接
out.close();
socket.close();
}
}
}

MINA服务器输出结果:

sessionCreated
messageReceived:床前明月光
messageReceived:疑是地上霜
messageReceived:举头望明月
messageReceived:低头思故乡
sessionClosed

Netty服务器输出结果:

channelActive
channelRead:床前明月光
channelRead:疑是地上霜
channelRead:举头望明月
channelRead:低头思故乡
channelInactive

Twisted服务器输出结果:

connectionMade
lineReceived:床前明月光
lineReceived:疑是地上霜
lineReceived:举头望明月
lineReceived:低头思故乡
connectionLost

当然,测试的时候也可以将发送的数据模拟成不按规则分割的情况,下面用一个更变态的客户端来测试:

public class TcpClient {  

    public static void main(String[] args) throws IOException, InterruptedException {  

        Socket socket = null;
OutputStream out = null; try{ socket = new Socket("localhost", 8080);
out = socket.getOutputStream(); String lines = "床前";
byte[] outputBytes = lines.getBytes("UTF-8");
out.write(outputBytes);
out.flush(); Thread.sleep(1000); lines = "明月";
outputBytes = lines.getBytes("UTF-8");
out.write(outputBytes);
out.flush(); Thread.sleep(1000); lines = "光\r\n疑是地上霜\r\n举头";
outputBytes = lines.getBytes("UTF-8");
out.write(outputBytes);
out.flush(); Thread.sleep(1000); lines = "望明月\r\n低头思故乡\r\n";
outputBytes = lines.getBytes("UTF-8");
out.write(outputBytes);
out.flush(); } finally {
// 关闭连接
out.close();
socket.close();
}
}
}

再次分别测试上面三个服务器,结果和上面的输出结果一样,没有任何问题。

MINA、Netty、Twisted一起学系列

MINA、Netty、Twisted一起学(一):实现简单的TCP服务器

MINA、Netty、Twisted一起学(二):TCP消息边界问题及按行分割消息

MINA、Netty、Twisted一起学(三):TCP消息固定大小的前缀(Header)

MINA、Netty、Twisted一起学(四):定制自己的协议

MINA、Netty、Twisted一起学(五):整合protobuf

MINA、Netty、Twisted一起学(六):session

MINA、Netty、Twisted一起学(七):发布/订阅(Publish/Subscribe)

MINA、Netty、Twisted一起学(八):HTTP服务器

MINA、Netty、Twisted一起学(九):异步IO和回调函数

MINA、Netty、Twisted一起学(十):线程模型

MINA、Netty、Twisted一起学(十一):SSL/TLS

MINA、Netty、Twisted一起学(十二):HTTPS

源码

https://github.com/wucao/mina-netty-twisted

Mina、Netty、Twisted一起学(二):TCP消息边界问题及按行分割消息的更多相关文章

  1. Mina、Netty、Twisted一起学(三):TCP消息固定大小的前缀(Header)

    在上一篇博文中,有介绍到用换行符分割消息的方法.但是这种方法有个小问题,如果消息中本身就包含换行符,那将会将这条消息分割成两条,结果就不对了. 本文介绍另外一种消息分割方式,即上一篇博文中讲的第2条: ...

  2. Mina、Netty、Twisted一起学(一):实现简单的TCP服务器

    MINA.Netty.Twisted为什么放在一起学习?首先,不妨先分别看一下它们官方网站对其的介绍: MINA: Apache MINA is a network application frame ...

  3. MINA、Netty、Twisted一起学(十二):HTTPS

    由于HTTPS协议是由HTTP协议加上SSL/TLS协议组合而成,在阅读本文前可以先阅读一下HTTP服务器和SSL/TLS两篇博文,本文中的代码也是由这两篇博文中的代码组合而成. HTTPS介绍 上一 ...

  4. Mina、Netty、Twisted一起学(八):HTTP服务器

    HTTP协议应该是目前使用最多的应用层协议了,用浏览器打开一个网站就是使用HTTP协议进行数据传输. HTTP协议也是基于TCP协议,所以也有服务器和客户端.HTTP客户端一般是浏览器,当然还有可能是 ...

  5. Mina、Netty、Twisted一起学(十):线程模型

    要想开发一个高性能的TCP服务器,熟悉所使用框架的线程模型非常重要.MINA.Netty.Twisted本身都是高性能的网络框架,如果再搭配上高效率的代码,才能实现一个高大上的服务器.但是如果不了解它 ...

  6. Mina、Netty、Twisted一起学(九):异步IO和回调函数

    用过JavaScript或者jQuery的同学都知道,JavaScript特别是jQuery中存在大量的回调函数,例如Ajax.jQuery的动画等. $.get(url, function() { ...

  7. Mina、Netty、Twisted一起学(七):发布/订阅(Publish/Subscribe)

    消息传递有很多种方式,请求/响应(Request/Reply)是最常用的.在前面的博文的例子中,很多都是采用请求/响应的方式,当服务器接收到消息后,会立即write回写一条消息到客户端.HTTP协议也 ...

  8. Mina、Netty、Twisted一起学(六):session

    开发过Web应用的同学应该都会使用session.由于HTTP协议本身是无状态的,所以一个客户端多次访问这个web应用的多个页面,服务器无法判断多次访问的客户端是否是同一个客户端.有了session就 ...

  9. Mina、Netty、Twisted一起学(五):整合protobuf

    protobuf是谷歌的Protocol Buffers的简称,用于结构化数据和字节码之间互相转换(序列化.反序列化),一般应用于网络传输,可支持多种编程语言. protobuf如何使用这里不再介绍, ...

随机推荐

  1. Leetcode 303 Range Sum Query - Immutable

    题意:查询一个数组在(i,j]范围内的元素的和. 思路非常简单,做个预处理,打个表就好 拓展:可以使用树状数组来完成该统计,算法复杂度为(logn),该数据结构强力的地方是实现简单,而且能完成实时更新 ...

  2. Unity中的协程是什么?

    什么是协程? 1.协程是一个分部执行,遇到条件(yield return 语句)会挂起,直到条件满足才会被唤醒继续执行后面的代码. 2.Unity在每一帧(Frame)都会去处理对象上的协程.Unit ...

  3. 写hive db的两种方法

    方法1tmp_channel_hive_file="/tmp/tmp_channel_hive_file"tmp_channel_hive_file_new="/tmp/ ...

  4. [原创]配置管理技术圈QQ群:129489184

    [原创]配置管理技术圈QQ群:129489184 配置管理技术圈QQ群:129489184,研究cvs,svn,git,cc等平台配置技术,涉及版本控制,持续集成,自动化构建等! 欢迎各位同学来,来时 ...

  5. Objective-C 语法之 static 关键字

    转:http://www.apkbus.com/android-593-1.html 学习过Java 或者 C 语言的朋友应该很清楚static关键字吧?在某个类中声明一个static 静态变量, 其 ...

  6. AsyncTask实现多线程断点续传

    前面一篇博客<AsyncTask实现断点续传>讲解了如何实现单线程下的断点续传,也就是一个文件只有一个线程进行下载.   对于大文件而言,使用多线程下载就会比单线程下载要快一些.多线程下载 ...

  7. Passwordless SSH Login

    原文地址:http://manjeetdahiya.com/2011/03/03/passwordless-ssh-login/ Consider two machines A and B. We w ...

  8. [转]揭秘webdriver实现原理

    转自:http://www.cnblogs.com/timsheng/archive/2012/06/12/2546957.html 通过研究selenium-webdriver的源码,笔者发现其实w ...

  9. Activemq消息持久化

    官方文档: http://activemq.apache.org/persistence.html ActiveMq持久化相关配置:/usr/local/apache-activemq-5.11.1/ ...

  10. HDU 5914 Triangle 数学找规律

    Triangle 题目连接: http://acm.hdu.edu.cn/showproblem.php?pid=5914 Description Mr. Frog has n sticks, who ...