上篇文章我们讨论了Netty的基本原理,重要概念,并使用java代码描述了Netty的基本使用。当然Netty的技术涵盖点远远不是那一篇基础代码就可以全部概括的,但是至少可以给读者一个切入点。让大家去思考一个我们一直在讨论的问题:为什么有了JAVA NIO框架后我们还需要有Netty这样的框架对底层再次进行封装?
5-1、IO模型的封装
5-1-1、再次总结IO模型
在前文中我们已经提到了,几种典型的IO模型(参见系统间通信3、4、5这三篇文章中的介绍,这里再进行一次总结):
-
阻塞和非阻塞:这个概念是针对应用程序而言,是指应用程序中的线程在向操作系统发送IO请求后,是否一直等待操作系统的IO响应。如果是,那么就是阻塞式的;如果不是,那么应用程序一般会以轮询的方式以一定周期询问操作系统,直到某次获得了IO响应为止(轮序间隔应用程序线程可以做一些其他工作)。
-
同步和异步:IO操作都是由操作系统进行的(这里的IO操作是个广泛概念了:磁盘IO、网络IO都算),不同的操作系统对不同设备的IO操作都有不同的模式。同步和异步这两个概念都指代的操作系统级别,同步IO是指操作系统和设备进行交互时,必须等待一次完整的请求-响应完成,才能进行下一次操作(当然操作系统和设备本身也有很多技术加快这个反应过程,例如“磁盘预读”技术、数据缓存技术);异步IO是指操作系统和设备进行交互时,不必等待本次得到响应,就可以直接进行下一次操作请求。设备处理完某次请求后,会主动给操作系统相应的响应通知。
-
多路复用IO:多路复用IO,从本质上看还是一种同步IO,因为它没有100%消除IO_WAIT,操作系统也没有为它提供“主动通知”机制。但是多路复用IO的处理速度已经相当快了,利用设备执行IO操作的时间,操作系统可以继续执行IO请求。并同样采用周期性轮询的方式,获取一批IO操作请求的执行响应。操作系统支持的多路复用IO技术主要有select、poll、epoll、kqueue。
-
阻塞式同步IO模型:这个从字面上就很好理解了,应用程序请求IO操作,并一直等待处理结果;操作系统同时也进行IO操作,并等待设备的处理结果;可以看出,应用程序的请求线程和操作系统的内核线程都是等待状态。
-
非阻塞式同步IO模型:应用程序请求IO,并且不用一直等待返回结果就去做其他事情。隔一定的周期,再去询问操作系统上次IO操作有没有结果,直到某一次询问从操作系统拿到IO结果;操作系统内核线程在进行IO操作时,还是处理一直等待设备返回操作结果的状态。
-
非阻塞式多路复用IO模型:应用程序请求IO的工作采用非阻塞方式进行;操作系统采用多路复用模式工作。
-
非阻塞式异步IO模型:应用程序请求IO的工作采用非阻塞方式进行,但是不需要轮询了,因为操作系统异步IO其中一个主要特性就是:可以在有IO响应结果的时候,主动进行通知。
5-1-2、对IO模型的再次封装
以上这些IO工作模型,在JAVA中都能够找到对应的支持:传统的JAVA Socket套接字支持阻塞/非阻塞模式下的同步IO(有的技术资料里面也称为OIO或者BIO);JAVA NIO框架在不同操作系统下支持不同种类的多路复用IO技术(windows下的select模型、Linux下的poll/epoll模型);JAVA AIO框架支持异步IO(windows下的IOCP和Linux使用epoll的模拟AIO)
实际上Netty是对JAVA BIO 、JAVA NIO框架的再次封装。让我们不再纠结于选用哪种底层实现。您可以理解成Netty/MINA 框架是一个面向上层业务实现进行封装的“业务层”框架。而JAVA Socket框架、JAVA NIO框架、JAVA AIO框架更偏向于对下层技术实现的封装,是面向“技术层”的框架。
5-2、数据信息格式的封装
“技术层”框架本身只对IO模型技术实现进行了封装,并不关心IO模型中流淌的数据格式;“业务层”框架对数据格式也进行了处理,让我们可以抽出精力关注业务本身。
-
Protobuf数据协议的集成:Netty利用自身的Channelpipeline的设计(在《架构设计:系统间通信(6)——IO通信模型和Netty 上篇》中讲过),对Protobuf数据协议进行了无缝结合。
-
JBoss Marshalling数据协议的集成:JBoss Marshalling 是一个Java对象的序列化API包,修正了JDK自带的序列化包的很多问题,又保持跟 java.io.Serializable 接口的兼容。Netty通过封装这个协议,可以帮助我们在客户端-服务端简便的进行对象系列化和反序列化。
-
HTTP Request / HTTP Response 协议的集成:在Netty中,可以方便的接受和发送Http协议。也就是说,我们可以使用Netty搭建自己的WEB服务器,当然您还可以根据自己的业务要求,方便的设计类似于Struts、Spring MVC这样的WEB框架。
下面是一个使用Netty的Http编码/解码处理器,设计的一个简单的WEB服务器:
package testNetty
import java.net.InetSocketAddress
import java.nio.channels.spi.SelectorProvider
import java.util.concurrent.ThreadFactory
import io.netty.bootstrap.ServerBootstrap
import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelOption
import io.netty.channel.EventLoopGroup
import io.netty.channel.ChannelHandler.Sharable
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.codec.http.DefaultFullHttpResponse
import io.netty.handler.codec.http.FullHttpResponse
import io.netty.handler.codec.http.HttpContent
import io.netty.handler.codec.http.HttpHeaders
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpRequestDecoder
import io.netty.handler.codec.http.HttpResponseEncoder
import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion
import io.netty.util.AttributeKey
import io.netty.util.concurrent.DefaultThreadFactory
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.apache.log4j.BasicConfigurator
public class TestHTTPNetty {
static {
BasicConfigurator.configure()
}
public static void main(String[] args) throws Exception {
//这就是主要的服务启动器
ServerBootstrap serverBootstrap = new ServerBootstrap()
//=======================下面我们设置线程池(代码已经详细讲解过,就不再赘述了)
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1)
ThreadFactory threadFactory = new DefaultThreadFactory("work thread pool")
int processorsNumber = Runtime.getRuntime().availableProcessors()
EventLoopGroup workLoogGroup = new NioEventLoopGroup(processorsNumber * 2, threadFactory, SelectorProvider.provider())
serverBootstrap.group(bossLoopGroup , workLoogGroup)
//========================下面我们设置我们服务的通道类型(代码已经详细讲解过,就不再赘述了)
serverBootstrap.channel(NioServerSocketChannel.class)
//========================设置处理器
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//我们在socket channel pipeline中加入http的编码和解码器
ch.pipeline().addLast(new HttpResponseEncoder())
ch.pipeline().addLast(new HttpRequestDecoder())
ch.pipeline().addLast(new HTTPServerHandler())
}
})
serverBootstrap.option(ChannelOption.SO_BACKLOG, 128)
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true)
serverBootstrap.bind(new InetSocketAddress("0.0.0.0", 83))
}
}
@Sharable
class HTTPServerHandler extends ChannelInboundHandlerAdapter {
private static Log LOGGER = LogFactory.getLog(HTTPServerHandler.class)
private static AttributeKey<StringBuffer> CONNTENT = AttributeKey.valueOf("content")
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof HttpRequest) {
HttpRequest request = (HttpRequest)msg
HttpMethod method = request.getMethod()
String methodName = method.name()
String url = request.getUri()
HTTPServerHandler.LOGGER.info("methodName = " + methodName + " && url = " + url)
}
//如果条件成立,则在这个代码段实现http请求内容的累加
if(msg instanceof HttpContent) {
StringBuffer content = ctx.attr(HTTPServerHandler.CONNTENT).get()
if(content == null) {
content = new StringBuffer()
ctx.attr(HTTPServerHandler.CONNTENT).set(content)
}
HttpContent httpContent = (HttpContent)msg
ByteBuf contentBuf = httpContent.content()
String preContent = contentBuf.toString(io.netty.util.CharsetUtil.UTF_8)
content.append(preContent)
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
HTTPServerHandler.LOGGER.info("super.channelReadComplete(ChannelHandlerContext ctx)")
StringBuffer content = ctx.attr(HTTPServerHandler.CONNTENT).get()
HTTPServerHandler.LOGGER.info("http客户端传来的信息为:" + content)
//开始返回信息了
String returnValue = "return response"
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK)
HttpHeaders httpHeaders = response.headers()
//这些就是http response 的head信息咯,参见http规范。另外您还可以设置自己的head属性
httpHeaders.add("param", "value")
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain")
//一定要设置长度,否则http客户端会一直等待(因为返回的信息长度客户端不知道)
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, returnValue.length())
ByteBuf responseContent = response.content()
responseContent.writeBytes(returnValue.getBytes("UTF-8"))
//开始返回
ctx.writeAndFlush(response)
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
由于上篇文章中已经介绍了Netty的基本使用方法,所以以上的代码将其他不必要的注释、方法都去掉了,只做了实现web服务器的最简代码。但是这段代码本省是可以运行的。下面是运行效果:
5-3、解决了“技术层”框架中的技术问题
通过阅读Netty框架的代码,我们知道了Netty框架至少解决了JAVA NIO框架中的一些Bug:
sun.nio.ch.Util contains code which is not thread safe and can throw a NullPointerException:
private static String bugLevel = null;
static boolean atBugLevel(String bl) {
if (bugLevel == null) {
if (!sun.misc.VM.isBooted())
return false;
java.security.PrivilegedAction pa =
new GetPropertyAction("sun.nio.ch.bugLevel");
bugLevel = (String)AccessController.doPrivileged(pa);
if (bugLevel == null)
bugLevel = "";
}
return (bugLevel != null) && bugLevel.equals(bl);
}
Suppose that two threads enter the "if (buglevel == null)" body at the same time. The first one runs until the return line and gets scheduled out right after the (buglevel != null) check. The second one then runs until right after the doPrivileged() call, sets bugLevel to null and gets scheduled out. The first one continues and hits a NullPointerException while calling bugLevel.equals() with bugLevel being null.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
这个问题在Netty框架中,负责进行JAVA NIO Selector的NioEventLoop类中得到了解决。
A DESCRIPTION OF THE PROBLEM :
Trying to get all bindings from the transient nameserver brings orbd into a state where it consumes 100% CPU. Its interesting to note that the problem only occurs if the client is programmed in c++. I was not able to reproduce the problem with a client programmed in Java.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1) Get omniORB 4.1 from http://omniorb.sourceforge.net
2) Compile omniORB (requires python devel package installed)
cd /tmp
mkdir omni_local
tar xvzf omniORB-4.1.0.tar.gz
cd omniORB-4.1.0
./configure
make
make install
3) Compile the test program (binding_browser, source attached to this report)
g++ -I/tmp/omni_local/include -L/tmp/omni_local/lib -lomniORB4 -lomniDynamic4 -lomnithread -lpthread -lrt BindingBrowser.cc -o binding_browser
4) Start orbd
<JAVA_HOME>/bin/orbd -ORBInitialPort 12345
5) Start binding_browser (from another shell)
5a) export LD_LIBRARY_PATH=/tmp/omni_local/lib:$LD_LIBRARY_PATH
5b) ./binding_browser -ORBInitRef NameService=corbaloc::1.2@localhost:12345/TNameService
Repeat step 5b until orbd consumes 100% cpu.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
这个问题从官方的Bug Database中的描述看,是在JDK7的版本中被解决的。Netty框架在JDK 6+的环境下在JAVA NIO框架封装之上解决了这个Bug。
5-4、半包和粘包问题
我们考虑一下这样的情况:我们编写了一个机器人控制程序,通过一个遥控器(客户端)向机器人(服务器)建立了一个长连接,并通过这个连接连续不断的从遥控器发送控制指令给机器人。由于是连续控制指令,所以指令与指令之间没有间隔(实际上您还可以想想很多类似场景,例如:开发的Online对战游戏)。
我们使用JSON格式作为指令数据的承载格式。那么发送方和接收方的数据发送-接受过程可能如下图所示。
通过上图我们看到了接收方为了接受这两条连贯的指令,一共做了三次接受,第二次接收的时候,收到了一部分message1的内容和一部分message2的内容。这里要说明几个注意事项:
-
MSS:MSS属性是TCP连接双方在三次握手时所确认的每一个TCP报文段中数据字段的最大长度。注意,一是连接双方协商出来的;二是只是数据段的最大长度,不包括IP协议头和TCP协议头的最大长度。
-
半包是指接收方应用程序在接收信息时,没有接收到一个完成的信息格式块;粘包是指,接收方应用程序在接受信息时,除了接收到发送方应用程序发送的某一个完整数据信息描述外,还接受到了一下发送方应用程序发送的下一个数据信息的一部分。
-
半包和粘包是针对应用程序来说的,这个问题只会发生在TCP一些进行连续发送数据时(TCP长连接)。UDP不会出现这个问题,因为UDP都是有边界的数据报;TCP短连接也不会出现,因为发送完一个指令信息后连接就断开了,不会发送第二个指令数据。
-
半包和粘包问题产生的根本是因为TCP本质上没有“数据块”的概念,而是一连串的数据流。在应用程序层面上我们所定义的“数据块”在TCP层面上并不被协议认可。
-
半包/粘包是一个应用层问题。要解决半包/粘包问题,就是在应用程序层面建立协商一致的信息还原依据。常见的有两种方式:一是消息定长,即保证每一个完整的信息描述的长度都是一定的,这样无论TCP/IP协议如何进行分片,数据接收方都可以按照固定长度进行消息的还原。二是在完整的一块数据结束后增加协商一致的分隔符(例如增加一个回车符)。
在JAVA NIO技术框架中,半包和粘包问题我们需要自己解决,如果使用Netty框架,它其中提供了多种解码器的封装帮助我们解决半包和粘包问题。甚至针对不同的数据格式,Netty都提供了半包和粘包问题的现成解决方式,例如之前我们提到的ProtobufVarint32FrameDecoder解码器,就是专门解决Protobuf数据格式在TCP长连接传输时的半包问题的。
下文中我们会介绍FixedLengthFrameDecoder、DelimiterBasedFrameDecoder、LineBasedFrameDecoder来解决半包/粘包的问题。
由于上文中我们已经通过完整的代码演示了Netty的基本使用,所以下面的示例代码中为了节约篇幅,我只会列出重要的代码片段。
5-4-1、使用FixedLengthFrameDecoder解决问题
FixedLengthFrameDecoder解码处理器将TCP/IP的数据按照指定的长度进行重新拆分,如果接收到的数据不满足设置的固定长度,Netty将等待新的数据到达:
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ByteArrayEncoder());
ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
ch.pipeline().addLast(new TCPServerHandler());
ch.pipeline().addLast(new ByteArrayDecoder());
}
});
Netty上层的channelRead事件方法将在Channel接收到20个字符的情况下被触发;而如果剩余的内容不到20个字符,channelRead方法将不会被触发(但注意channelReadComplete方法会触发的啦)。
5-4-2、使用LineBasedFrameDecoder解决问题
LineBasedFrameDecoder解码器,基于最简单的“换行符”进行接收到的信息的再组织。windows和linux两个操作系统中的“换行符”是不一样的,LineBasedFrameDecoder解码器都支持。当然这个解码器没有我们后面介绍的DelimiterBasedFrameDecoder解码器灵活。
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ByteArrayEncoder());
ch.pipeline().addLast(new LineBasedFrameDecoder(100));
ch.pipeline().addLast(new TCPServerHandler());
ch.pipeline().addLast(new ByteArrayDecoder());
}
});
那么如果客户端发送的数据是:
this is 0 client \r\n request 1 \r\n”
那么接收方重新通过“换行符”重新组织后,将分两次接受到数据:
this is 0 client
request 1
5-4-3、使用DelimiterBasedFrameDecoder解决问题
DelimiterBasedFrameDecoder是按照“自定义”分隔符(也可以是“回车符”或者“空字符”注意windows系统中和linux系统中“回车符”的表示是不一样的)进行信息的重新拆分。
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ByteArrayEncoder());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1500, false, Delimiters.lineDelimiter()));
ch.pipeline().addLast(new TCPServerHandler());
ch.pipeline().addLast(new ByteArrayDecoder());
}
});
DelimiterBasedFrameDecoder有三个参数,这里介绍一下:
DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, ByteBuf... delimiters)
-
maxFrameLength:最大分割长度,如果接收方在一段长度 大于maxFrameLength的数据段中,没有找到指定的分隔符,那么这个处理器会抛出TooLongFrameException异常。
-
stripDelimiter:这个是一个布尔型参数,指代是否保留指定的分隔符。
-
delimiters:设置的分隔符。一般使用Delimiters.lineDelimiter()或者Delimiters.nulDelimiter()。当然您也可以自定义分隔符,定义成bytebuf的类型就行了。
5-5、专注于业务
Netty框架的特性,使我们不需要关心下层所工作的IO模型,利用Netty提供的面向事件驱动的方法结构,使我们更能集中精力关注应用层业务。
在这5篇文章中,我们重点介绍了几种典型的IO模型,并且介绍了JAVA语言对这几种IO模型的实现,最后我们简单介绍了一下Netty框架,并且比较了JAVA NIO框架和Netty框架侧重点。实际上这几篇文章我们讲述的问题只有一个“信息如何进行传递”。
从下一篇文章开始,我们开始介绍JAVA RMI技术,并从JAVA RMI技术引出一项系统间通信重要的技术RPC,我们还会降到RPC的重要实现 TaoBao-Dubbo框架,最后我们讲解ESB技术和几个典型的ESB实现。这些技术要解决的问题是“传递过程如果进行统筹管理”。