Netty从零开始(一)

需要用到netty,之前就当年实习的时候用过Mina,netty没用过,所以加急学习了一下,感觉还不错,不多说,从官网入手,官网地址:http://netty.io/wiki/user-guide-for-4.x.html

有兴趣的朋友可以自己去查看。


前言:

问题

现在我们使用通用应用程序或包进行通信。例如,我们经常使用HTTP客户端库从Web服务器检索信息,并通过Web服务调用远程过程调用。然而,通用协议或其实现有时不能很好地扩展。这就像我们不使用通用HTTP服务器来交换大量文件,电子邮件和近实时消息,如财务信息和多人游戏数据。需要的是高度优化的协议实现,专门用于特殊目的。例如,您可能希望实现针对基于AJAX的聊天应用程序,媒体流或大型文件传输进行了优化的HTTP服务器。你甚至可以设计和实施一个全新的协议,这个协议是根据你的需要而定制的。另一个不可避免的情况是当您必须处理旧版专有协议以确保与旧系统的互操作性。在这种情况下重要的是我们能够快速实现该协议,而不会牺牲最终应用程序的稳定性和性能。

方案

Netty项目是为了快速开发可维护的高性能高可扩展性协议服务器和客户端而努力提供异步事件驱动的网络应用程序框架和工具。换句话说,Netty是一个NIO客户端服务器框架,可以快速轻松地开发诸如协议服务器和客户端之类的网络应用程序。它大大简化了网络编程流程,如TCP和UDP套接字服务器开发。

“快速和容易”并不意味着由此产生的应用程序将遭受可维护性或性能问题的困扰。Netty经过精心设计,实现了许多协议,如FTP,SMTP,HTTP以及各种基于二进制和基于文本的传统协议。因此,Netty成功地找到了一种方法来实现轻松的开发,性能,稳定性和灵活性,而无需妥协。

有些用户可能已经找到了声称具有相同优势的其他网络应用程序框架,您可能想问问Netty与他们的区别。答案是它建立的哲学。Netty旨在为您提供API和执行方面最舒适的体验,从第一天开始。这不是有形的东西,但你会意识到,这个哲学将使你的生活更容易,当你阅读本指南和玩Netty的时候。

好了,以上就是关于netty的一个官网的初步介绍吧。下面进入搭建最简单的服务器的环节,我这里会按照官网的思路走,不过不会完全一点不差。好了,我们开始:

建立项目

首先我们需要建立项目,如下图所示:

项目名称是NettyDemo,官网建议使用JDK1.6以上,我这里使用的JDK1.8,然后加入使用maven导入Netty依赖:
<dependencies>
    <!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.6.Final</version>
    </dependency>
</dependencies>
那么现在我们可以正式开始我们的项目编写了。

编写一个Discard服务器(按我理解就是啥也不干的服务器,别着急反驳,往下看)

世界上最简单的协议不是“hello world”,而是。。。。什么也不做的协议Discard,丢弃的意思,服务端丢弃,那就是啥也不做的协议呗(尝试把协议理解为用户自定义功能)。
想要实现一个Discard协议,那么我们唯一需要做的就是忽略所有接收到的数据。让我们从处理器实现开始,它处理由netty生成的I/O事件。
首先我们创建一个java包:netty_beginner,然后在里面创建一个类DiscardServerHandler
类的内容如下:
package netty_beginner;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;


/**
 * Created by moon on 2017/4/5.
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // (2)
//        super.channelRead(ctx, msg);
        ((ByteBuf) msg).release(); // (3)
//        ByteBuf in = (ByteBuf) msg;
//        try {
//            while (in.isReadable()) {
//                System.out.print((char) in.readByte());
//                System.out.flush();
//            }
//        } finally {
//            ReferenceCountUtil.release(msg);
//        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // (5)
//        super.exceptionCaught(ctx, cause);
        cause.printStackTrace();
        ctx.close();
    }
}

  1. DiscardServerHandler 继承自ChannelInboundHandlerAdapter,它是 ChannelInboundHandlerChannelInboundHandler的实现。提供可以覆盖的各种事件处理程序方法。现在,只需要扩展ChannelInboundHandlerAdapter即可,而不是自己实现处理程序接口。
  2. 在这里,我们重写通道读取channelRead()事件处理方法。每当从客户端收到新数据时,都会使用接收到的消息调用此方法。在这个例子中,接收到的消息的类型是ByteBuf。
  3. 为了实现DISCARD协议,处理程序必须忽略收到的消息。ByteBuf是一个引用计数对象,必须通过release()方法显式释放。请记住,处理程序有责任释放传递给处理程序的引用计数对象。通常,channelRead()处理方法的实现方式如下:
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            // Do something with msg
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
  4. 当由于I / O错误或由于在处理事件时抛出异常而使得Netty抛出异常时,exceptionCaught() 事件将会被Throwable抛出。在大多数情况下,应该记录捕获到的异常,并在此关闭其关联的通道,虽然这种方法的实现可以根据你想要处理的异常情况而有所不同。例如,您可能希望在关闭连接之前发送带有错误代码的响应消息。
到目前位置一切顺利。我们已经实现了DISCARD服务器的前半部分。现在剩下的是写入使用DiscardServerHandler启动服务器的main()方法。 我们创建另外一个类:DiscardServer如下:
package netty_beginner;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * Created by moon on 2017/4/5.
 */
public class DiscardServer {
    private int port;

    public DiscardServer(int port) {
        this.port = port;
    }

    public void run() throws InterruptedException {
        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 DiscardServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)          // (5)
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)

            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync(); // (7)
            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to gracefully
            // shut down your server.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new DiscardServer(port).run();
    }
}

  1. NioEventLoopGroup是一个处理I / O操作的多线程事件循环。Netty为不同类型的传输提供了各种EventLoopGroup实现。在这个例子中,我们正在实现一个服务器端应用程序,因此将使用两个NioEventLoopGroup。第一个,通常称为“老板”,接受传入的连接。第二个,通常称为“工人”,一旦老板接受连接并将接受的连接注册给工作人员,就处理接受的连接的流量。使用多少线程以及它们如何映射到创建的通道取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。
  2. ServerBootstrap是一个帮助类,用于设置服务器。您可以直接使用Channel设置服务器。但是请注意,这是一个繁琐的过程,在大多数情况下您不需要这样做。
  3. 在这里,我们指定使用NioServerSocketChannel类来实例化一个新的Channel来接受传入的连接。(可以这么理解,每个客户端连接我们服务端,我们都会为他们创建一个channel,那么这个channel对于面向对象的我们来说就是一个类,我们同意对于我们接受到的连接都初始化为:NioServerSocketChannel
  4. 这里指定的处理程序将始终由新接受的Channel进行评估。ChannelInitializer是一个特殊的处理程序,旨在帮助用户配置新的Channel。很可能您想通过添加一些处理程序(如DiscardServerHandler)来配置新Channel的ChannelPipeline来实现您的网络应用程序。随着应用程序的复杂化,您可能会在管道中添加更多的处理程序,并将这个匿名类最终提取到顶级类中。(个人感觉说白了就是想自己实现包含自己处理逻辑的Channel,但是又需要包含一些通用的原有功能,咋办,继承呗,这就是为什么上面的DiscardServerHandler继承netty的类)
  5. 您还可以设置特定于Channel实现的参数。我们正在编写一个TCP / IP服务器,因此我们可以设置套接字选项,如tcpNoDelay和keepAlive。请参阅ChannelOption的apidocs和特定的ChannelConfig实现,以获得有关支持的ChannelOptions的概述。
  6. 你有没有注意到option()和childOption()?option()用于接受传入连接的NioServerSocketChannel。childOption()用于在这种情况下由父级ServerChannel接受的通道,即NioServerSocketChannel。(我的理解就是前者用于配置我们父级Channel,后者用于配置我们自定义的子级Channel)。
  7. 我们现在准备好了。剩下的是绑定到端口并启动服务器。这里,我们绑定机器中所有NIC(网络接口卡)的端口到8080。您现在可以根据需要调用bind()方法多次(具有不同的绑定地址)。

恭喜,到了现在这个阶段我们已经完成了。下面可以进行尝试,那么在尝试之前,我要说一句,这个例子非常好,就是一点比较费解,即使我开始测试,往本机8080端口发送内容,我们根本看不出来是否成功,因为我们把内容忽略了 - -!。所以改一下,我们的DiscardServerHandler改成如下,打印收到的字符:

package netty_beginner;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;


/**
 * Created by moon on 2017/4/5.
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//        super.channelRead(ctx, msg);
//        ((ByteBuf) msg).release();
        ByteBuf in = (ByteBuf) msg;
        try {
            while (in.isReadable()) {
                System.out.print((char) in.readByte());
                System.out.flush();
            }
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//        super.exceptionCaught(ctx, cause);
        cause.printStackTrace();
        ctx.close();
    }
}

然后我们启动运行我们的main方法:


由于我用的win,不是linux,所以下面的操作可能有人不好使,因为有的win默认没有启动telnet,这个自己网上百度,很容易。我们打开cmd,输入telnet,进入一个新的窗口:





然后我们可以查看帮助,输入?/help,查看win下的使用方式:

注意o,也就是open是我们所需的,我们使用命令:open localhost 8080如下图:

这说明已经连上了,别跟我似的一直以为在连接ing。。。。。  
那么我们现在就可以联系了,由于我们的逻辑是一个字符一个字符输出,所以我们输入hello,在idea控制台会挨个字符输出:

那么到这里,说明我们的服务端小demo成功。

写一个Echo服务器

到目前为止,我们一直都在假设服务端是没有响应的。然而,服务器通常应该响应请求。让我们学习如何通过实现ECHO协议向客户端写入响应消息,其中任何接收到的数据都将被发回。
与前面部分实现的Discard服务器的唯一区别在于它将接收到的数据发回,而不是将接收的数据输出到控制台.因此,再次修改channelRead()方法就行了:
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ctx.write(msg); // (1)
    ctx.flush(); // (2)
}

ChannelHandlerContext对象提供了各种可以触发各种I / O事件和操作的操作。在这里,我们调用write(Object)来逐字写入接收到的消息。请注意,我们没有像DISCARD示例中那样发布接收的消息。这是因为,当Netty发布给电子邮件时,Netty会为您报告。
如果再次运行telnet命令,您将看到服务器发送回发送给它的任何内容。(自行查看)

编写一个时间服务器

本节中实现的协议是TIME协议。它与前面的示例不同之处在于,它发送一个包含32位整数的消息,而不接收任何请求,并在发送消息后关闭连接。在此示例中,您将学习如何构建和发送消息,并在完成时关闭连接。
因为我们不是将忽略任何接收到的数据,而是在建立连接后立即发送消息,这次我们不能使用channelRead()方法。相反,我们应该覆盖channelActive()方法。所以我们创建一个新的类TimeServerHandler,以下是实现:
package netty_beginner;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * Created by moon on 2017/4/5.
 */
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(final ChannelHandlerContext ctx) throws Exception {
        final ByteBuf time = ctx.alloc().buffer(4);
        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));

        final ChannelFuture f = ctx.writeAndFlush(time);
        f.addListener(new ChannelFutureListener() {
            public void operationComplete(ChannelFuture future) throws Exception {
                assert f == future;
                ctx.close();
            }
        });

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
    }
}

  1. 如上所述,当建立连接并准备好发送时,将调用channelActive()方法。我们来写一个32位整数,表示这个方法当前的时间。
  2. 要发送一条新消息,我们需要分配一个包含消息的新缓冲区。我们要写一个32位整数,因此我们需要一个容量至少为4个字节的ByteBuf。通过ChannelHandlerContext.alloc()获取当前的ByteBufAllocator,并分配一个新的缓冲区。
  3. 像往常一样,我们编写构造的消息。但等等,翻转的地方在哪里?在NIO发送消息之前,我们是否曾经调用过java.nio.ByteBuffer.flip()?ByteBuf没有这样的方法,因为它有两个指针;一个用于读操作,另一个用于写操作。当你写入东西到ByteBuf中,写索引增加,而读索引并没有改变。读者索引和作者索引分别表示消息的开始和结束位置。相比之下,如果不调用flip方法,NIO缓冲区不能提供干净的方式来确定消息内容的起始和结束位置。当您忘记翻转缓冲区时,您将会遇到麻烦,因为你会发送不正确甚至是空的内容。在Netty中不会发生这样的错误,因为我们针对不同的操作类型有不同的指针。你会发现它使你的生活更容易,当你习惯了 - 一个没有翻转的生活!另外要注意的是,ChannelHandlerContext.write()(和writeAndFlush())方法返回一个ChannelFuture。ChannelFuture表示尚未发生的I / O操作。这意味着任何请求的操作可能尚未执行,因为所有操作在Netty中都是异步的。例如,即使在发送消息之前,以下代码也可能会关闭连接:
    Channel ch = ...;
    ch.writeAndFlush(message);
    ch.close();
    因此,您需要在ChannelFuture完成之后调用close()方法,该方法由write()方法返回,并且在写入操作完成后通知其监听器。请注意,close()也可能不会立即关闭连接,并且它返回ChannelFuture。
  4. 当写请求完成时,我们如何得到通知?这就像将ChannelFutureListener添加到返回的ChannelFuture一样简单。在这里,我们创建了一个新的匿名ChannelFutureListener,当操作完成时关闭通道。或者,您可以使用预定义的监听器简化代码:
    f.addListener(ChannelFutureListener.CLOSE);
要测试我们的时间服务器是否按预期工作,可以使用UNIX rdate命令:
$ rdate -o <port> -p <host>

由于我的是win我就不测试了。
好了,这次就先到这里,明天继续接下来的内容。

猜你喜欢

转载自blog.csdn.net/qq_23660243/article/details/69258687