Netty入门实例

最近读了许多基于Netty的代码,发现Netty实在是网络通信开发的不二之选,值得深入学习。它的性能和可靠性已经有许多应用实践的背书,单从代码来讲,也是极简洁优雅的。
本文提供一个完整入门实例,实现一个简单的客户端和服务器通信的例子:客户端发送数据到服务端,服务端稍微加工然后再发送回客户端。后面也简单提了下出现粘包拆包问题的原因,以及通过Netty编码解码器解决该问题的思路。

摘要

  • 摘要
  • Netty简介
  • Netty实例
      • Netty服务器
      • 用Telnet测试Netty服务器
      • Netty客户端
      • Netty实例测试结果
  • 粘包拆包问题
  • Netty编码解码器

Netty简介

Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。参见百度百科或Netty官网。


Netty实例

简单的服务器客户端例子。客户端向服务器发送10条记录(Hello Netty!),服务器接收到后发回给客户端,并加上服务器标识,如From Server: Hello Netty!
Github代码链接如下:https://github.com/prufeng/hellowork/tree/master/netty

Netty服务器

额,好简单,具体类和参数意义可以查看官网文档的例子。主要是运用Builder模式链式调用的形式,支持按需添加参数、编码解码器(Encoder/Decoder)和处理器(Handler)。
本例使用了StringDecoder,所以在Handler里可以直接将接收到的数据转换为String,否则要通过ByteBuf来转换。

/**
 * Created by PanRufeng on 2018/6/22.
 */
public class DemoServer {
    private final int port;

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

    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args.length == 1) {
            port = Integer.parseInt(args[0]);
        }
        new DemoServer(port).run();
    }

    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)
                    .option(ChannelOption.SO_BACKLOG, 128)          // (5)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer() { // (4)
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new StringDecoder()).addLast(new DemoServerHandler());
                        }
                    });

            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync(); // (7)

            // Wait until the server socket is closed.
            System.out.println(DemoServer.class.getName() + " started and listen on " + port);
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

DemoServerHandler负责事件处理,处理具体的业务逻辑,从方法名看就知道是完全的事件驱动模式。本例读取客户端输入后,后台显示出来,然后添加“From Server:”前缀,再发送回客户端。

/**
 * Created by PanRufeng on 2018/6/22.
 */
public class DemoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        String inStr = (String) msg;
        System.out.println("Received: " + inStr);
        String outStr = "From Server: " + inStr + System.lineSeparator();
        ByteBuf outBb = Unpooled.copiedBuffer(outStr.getBytes());
        ctx.write(outBb);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
}

用Telnet测试Netty服务器

PS. 客户端未完成时,可以使用telnet进行测试。telneto localhost 8080,[Ctrl]+],send hello
telnet发送数据:

Microsoft Telnet> send hello world1
发送字符串 hello world1
Microsoft Telnet> send hello world2
发送字符串 hello world2
Microsoft Telnet> send hello world3
发送字符串 hello world3

服务端返回:

From Server: hello world1
From Server: hello world2
From Server: hello world3

服务端日志:

Received: hello world1
Received: hello world2
Received: hello world3

Netty客户端

客户端定义所使用类名与服务端稍微不同,形式基本一致。另外因为是客户端,需要指定host,和定义对应的Handler。

/**
 * Created by PanRufeng on 2018/6/22.
 */
public class DemoClient {
    private String host;
    private int port;

    public DemoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public static void main(String[] args) throws Exception {
        String host = "localhost";
        int port = 8080;
        if (args.length == 2) {
            host = args[0];
            port = Integer.parseInt(args[1]);
        }
        new DemoClient(host, port).run();
    }

    public void run() throws Exception {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ChannelInitializer() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new StringDecoder()).addLast(new DemoClientHandler());
                        }
                    });

            // Start the client.
            ChannelFuture f = b.connect(host, port).sync(); // (5)
            System.out.println(DemoClient.class.getName() + " started and connected to " + host + ":" + port);
            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

DemoClientHandler负责处理客户端事件,注意父类跟服务端并不相同。方法channelRead0()读取服务端发送过来的数据。channelActive()当Channel连接成功就会调用。本例为了简单把客户端发送数据的逻辑放在这里,效果就是当客户端启动时,立即就往服务端发送10条数据。

/**
 * Created by PanRufeng on 2018/6/22.
 */
public class DemoClientHandler extends SimpleChannelInboundHandler {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        System.out.println("Channel Active");
        for (int i = 0; i < 10; i++) {
            ctx.writeAndFlush(Unpooled.copiedBuffer((i + " Hello Netty!").getBytes()));
        }
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, Object msg) {
        String inStr = (String) msg;
        System.out.println("Received: " + inStr);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

Netty实例测试结果

运行DemoServer,然后启动DemoClient,可得结果如下。
服务端:

pan.rufeng.netty.DemoServer started and listen on 8080
Received: 0 Hello Netty!
Received: 1 Hello Netty!2 Hello Netty!3 Hello Netty!4 Hello Netty!
Received: 5 Hello Netty!6 Hello Netty!7 Hello Netty!
Received: 8 Hello Netty!9 Hello Netty!

客户端:

Channel Active
pan.rufeng.netty.demo.DemoClient started and connected to localhost:8080
Received: From Server: 0 Hello Netty!
From Server: 1 Hello Netty!2 Hello Netty!3 Hello Netty!4 Hello Netty!
From Server: 5 Hello Netty!6 Hello Netty!7 Hello Netty!
From Server: 8 Hello Netty!9 Hello Netty!

至此,一个简单的可以跑起来的客户端服务器通信模型就完成了。

粘包拆包问题

但是,你可能已经发现了这个结果跟预期有些出入,而且,重启客户端,得到的结果又会不一样。结果是不可预测的!
我们可能更希望看到的是客户端每次发送到服务端都得到一次响应,依次打印出10行的结果,可是这里的有些结果却粘在一起,如1,2,3和4,这就是粘包问题。
粘包问题发生的原因是程序每次写入的数据小于Socket缓冲区的大小,这样网卡会把应用程序多次写入的数据一次发送。相应的,如果程序写入的数据大于Socket缓冲区,则会出现拆包问题。表现为完整的句子被断开。

Netty编码解码器

粘包拆包的问题,可以通过编码解码器来解决。具体方法有很多,比如可以定义每个包的前4个字节为包的长度,后面才是真正的数据。
Netty提供了很多内置的工具来解决此类问题,也包括Protobuf等序列化的方式。
对于字符串类型消息,可结合StringDecoder和以下其中一个Decoder来解决,其实质是通过特殊分隔符、分行符或固定长度来判断数据包的结束。
DelimiterBasedFrameDecoder
LineBasedFrameDecoder
FixedLengthFrameDecoder

其他比较有用的类如:
ObjectDecoder
ObjectEncoder

ProtobufDecoder
ProtobufVarint32FrameDecoder
ProtobufEncoder
ProtobufVarint32LengthFieldPrepender

MarshallingDecoder
MarshallingEncoder

ByteToMessageDecoder
ByteToMessageCodec

相关代码和更多方案可以在这个包下面找。
io\netty\handler\codec

你可能感兴趣的:(Architect)