在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. 前端开发-Weex初试

    1 Weex介绍 weex是阿里2016年开源的一套跨移动端(Andriod/IOS/Wap)的前端框架,采用VUE,较React Native入门相对简单 官网地址 2 Weex安装与初始化 2.1 ...

  2. 重学JAVA基础(八):锁的基本知识

    1.线程状态 如上图,当我们新建一个线程,并start后,其实不一定会马上执行,因为只有操作系统调度了我们的线程,才能真正进行执行,而操作系统也随时可以运行其他线程,这时线程又回到可运行状态.这个过程 ...

  3. C#类、接口、虚方法和抽象方法0322

    虚拟方法和抽象方法有什么区别与联系: 1.抽象方法只有声明没有实现代码,需要在子类中实现:虚拟方法有声明和实现代码,并且可以在子类中重写,也可以不重写使用父类的默认实现. 2.抽象类不能被实例化(不可 ...

  4. 分布式Hadoop安装(二)

    二.集群环境安装Zookeeper 1.         hadoop0,namenode机器下,配置zookeeper,先解压安装包. 使用命令:tar -zxvf zookeeper-3.4.4. ...

  5. Mysql:Error Code 1235,This version of MySQL doesn’t yet support ‘LIMIT & IN/ALL/ANY/SOME 错误解决

    This version of MySQL doesn’t yet support ‘LIMIT & IN/ALL/ANY/SOME 错误解决 这次国庆节回来后的测试中,在一个Mysql表达式 ...

  6. PreparedStatement ResultSet

    public int searchProblemDistinctCount() throws Exception { DBOperator dbo = getDBOperator(); try { P ...

  7. 常用天气预报API接口整理(转)

    文章转自:http://www.nohacks.cn/post-35.html 自序: 由nohacks.cn 收集整理,来源于网络,版权归原作者所有,基本收集了网络上能使用的大部分天气API接口,作 ...

  8. mac下忘记mysql密码, 重新设置mysql密码

    step1: 苹果->系统偏好设置->最下边点mysql 在弹出页面中 关闭mysql服务(点击stop mysql server)step2:进入终端输入:cd /usr/local/m ...

  9. ITF Demo代码(用VBScript构建的接口测试框架)

    ITF Demo代码(用VBScript构建的接口测试框架) http://blog.csdn.net/testing_is_believing/article/details/20872629

  10. [LeetCode] Range Sum Query - Immutable

    The idea is fairly straightforward: create an array accu that stores the accumulated sum fornums suc ...