今天,我们使用通用的应用程序或者类库来实现互相通讯,比如,我们经常使用一个 HTTP 客户端库来从 web 服务器上获取信息,或者通过 web 服务来执行一个远程的调用。
然而,有时候一个通用的协议或他的实现并没有很好的满足需求。比如我们无法使用一个通用的 HTTP 服务器来处理大文件、电子邮件以及近实时消息,比如金融信息和多人游戏数据。我们需要一个高度优化的协议来处理一些特殊的场景。例如你可能想实现一个优化了的 Ajax 的聊天应用、媒体流传输或者是大文件传输器,你甚至可以自己设计和实现一个全新的协议来准确地实现你的需求。
我们知道Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,由于 NIO 原生编程太过于复杂,Netty对其进行了优秀的封装。Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发。
通过 Netty 我们可以快速简单地开发网络应用程序,比如服务器(HTTP服务器,FTP服务器,WebSocket服务器,Redis的Proxy服务器等等)和客户端的协议。Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发。
Netty和Tomcat有什么区别?
Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器,但是Netty不一样,他能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能,这就是netty和tomcat最大的不同。
有人说netty的性能就一定比tomcat性能高,其实不然,tomcat从6.x开始就支持了nio模式,并且后续还有APR模式——一种通过jni调用apache网络库的模式,相比于旧的bio模式,并发性能得到了很大提高,特别是APR模式,而netty是否比tomcat性能更高,则要取决于netty程序作者的技术实力了。
Netty 4.x User Guide 中文翻译《Netty 4.x 用户指南》 Essential Netty in Action 《Netty 实战(精髓)
让我们从 handler (处理器)的实现开始,handler 是由 Netty 生成用来处理 I/O 事件的。
public class EchoServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { System.out.println(ctx.channel().remoteAddress() + "->Server :" + msg.toString()); ctx.write(msg); // (1) ctx.flush(); // (2) // final ChannelFuture future = ctx.writeAndFlush(msg); // final ChannelFuture future = ctx.write(msg + "\n"); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { System.out.println("server read complete"); ctx.flush(); TimeUnit.MILLISECONDS.sleep(200); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // 当出现异常就关闭连接 cause.printStackTrace(); ctx.close(); } }EchoServerHandler 继承自 ChannelInboundHandlerAdapter,这个类实现了 ChannelInboundHandler接口,ChannelInboundHandler 提供了许多事件处理的接口方法,然后你可以覆盖这些方法。现在仅仅只需要继承 ChannelInboundHandlerAdapter 类而不是你自己去实现接口方法。
这里我们覆盖了 chanelRead() 事件处理方法。每当从客户端收到新的数据时,这个方法会在收到消息时被调用。
ChannelHandlerContext 对象提供了许多操作,使你能够触发各种各样的 I/O 事件和操作。这里我们调用了 write(Object) 方法来逐字地把接受到的消息写入。
ctx.write(Object) 方法不会使消息写入到通道上,他被缓冲在了内部,你需要调用 ctx.flush() 方法来把缓冲区中数据强行输出。或者你可以用更简洁的 cxt.writeAndFlush(msg) 以达到同样的目的。
exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来并且把关联的 channel 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
目前为止一切都还不错,我们已经实现了服务器的一半功能,剩下的需要编写一个 main() 方法来启动服务端的 EchoServerHandler。
/** 应答服务器 */ public class EchoServer { private int port; public EchoServer(int port) { this.port = port; } public void run() throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1) EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); // (2) b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // (3) .childHandler( new ChannelInitializer<SocketChannel>() { // (4) @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new EchoServerHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) // (5) .childOption(ChannelOption.SO_KEEPALIVE, true); // (6) // 绑定端口,开始接收进来的连接 ChannelFuture f = b.bind(port).sync(); // (7) System.out.println("Server start listen at " + port); // 等待服务器 socket 关闭 。 // 在这个例子中,这不会发生,但你可以优雅地关闭你的服务器。 f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port; if (args.length > 0) { port = Integer.parseInt(args[0]); } else { port = 8080; } new EchoServer(port).run(); } }NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器,Netty 提供了许多不同的 EventLoopGroup 的实现用来处理不同的传输。在这个例子中我们实现了一个服务端的应用,因此会有2个 NioEventLoopGroup 会被使用。第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。如何知道多少个线程已经被使用,如何映射到已经创建的 Channel上都需要依赖于 EventLoopGroup 的实现,并且可以通过构造函数来配置他们的关系。
ServerBootstrap 是一个启动 NIO 服务的辅助启动类。你可以在这个服务中直接使用 Channel,但是这会是一个复杂的处理过程,在很多情况下你并不需要这样做。
这里我们指定使用 NioServerSocketChannel 类来举例说明一个新的 Channel 如何接收进来的连接。
这里的事件处理类经常会被用来处理一个最近的已经接收的 Channel。ChannelInitializer 是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel。也许你想通过增加一些处理类比如DiscardServerHandler 来配置一个新的 Channel 或者其对应的ChannelPipeline 来实现你的网络程序。当你的程序变的复杂时,可能你会增加更多的处理类到 pipline 上,然后提取这些匿名类到最顶层的类上。
你可以设置这里指定的 Channel 实现的配置参数。我们正在写一个TCP/IP 的服务端,因此我们被允许设置 socket 的参数选项比如tcpNoDelay 和 keepAlive。请参考 ChannelOption 和详细的 ChannelConfig 实现的接口文档以此可以对ChannelOption 的有一个大概的认识。
你关注过 option() 和 childOption() 吗?option() 是提供给NioServerSocketChannel 用来接收进来的连接。childOption() 是提供给由父管道 ServerChannel 接收到的连接,在这个例子中也是 NioServerSocketChannel。
我们继续,剩下的就是绑定端口然后启动服务。这里我们在机器上绑定了机器所有网卡上的 8080 端口。当然现在你可以多次调用 bind() 方法(基于不同绑定地址)。
恭喜!你已经熟练地完成了第一个基于 Netty 的服务端程序。
BootStrap 和 ServerBootstrap 类似,不过他是对非服务端的 channel 而言,比如客户端或者无连接传输模式的 channel。
如果你只指定了一个 EventLoopGroup,那他就会即作为一个 boss group ,也会作为一个 workder group,尽管客户端不需要使用到 boss worker 。
代替NioServerSocketChannel的是NioSocketChannel,这个类在客户端channel 被创建时使用。
不像在使用 ServerBootstrap 时需要用 childOption() 方法,因为客户端的 SocketChannel 没有父亲。
我们用 connect() 方法代替了 bind() 方法。
public class EchoClientHandler extends ChannelInboundHandlerAdapter { private final String firstMessage; /** Creates a client-side handler. */ public EchoClientHandler() { // firstMessage = Unpooled.buffer(EchoClient.SIZE); // for (int i = 0; i < firstMessage.capacity(); i ++) { // firstMessage.writeByte((byte) i); // } firstMessage.writeByte('\n'); firstMessage = "hello\n"; } @Override public void channelActive(ChannelHandlerContext ctx) { ctx.writeAndFlush(firstMessage); System.out.println("channel active."); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { System.out.println("channel read from server: " + msg); ctx.write(msg + "\n"); } @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); System.out.println("channel read complete"); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // Close the connection when an exception is raised. cause.printStackTrace(); ctx.close(); } }参考文章: Netty 4.x User Guide 中文翻译《Netty 4.x 用户指南》 Essential Netty in Action 《Netty 实战(精髓)