JDK 编程

感受了上面的 java 中的 BIO/NIO/AIO 详解,不知道你是否觉得 jdk 直接编程非常麻烦?

还有很多情况需要去考虑处理,还有性能相关的问题、稳定性问题,拓展性问题。

不选择Java原生NIO编程的原因

现在我们总结一下为什么不建议开发者直接使用JDK的NIO类库进行开发,具体原因如下。

(1) NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等

(2) 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。

这是因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序。

(3) 可靠性能力补齐,工作量和难度都非常大。

例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大。

(4) JDK NIO的BUG, 例如臭名昭著的epollbug,它会导致Selector空轮询, 最终导致CPU 100%。

官方声称在JDK1.6版本的update18修复了该问题, 但是直到JDK1.7版本该问题仍旧存在, 只不过该BUG发生概率降低了一些而已,它并没有得到根本性解决。

为什么选择 netty

Netty是业界最流行的NIO框架之一, 它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的, 它已经得到成百上千的商用项目验证, 例如Hadoop的RPC框架Avro就使用了Netty作为底层通信框架, 其他还有业界主流的RPC框架, 也使用Netty来构建高性能的异步通信能力。

通过对Netty的分析,我们将它的优点总结如下。

  • API使用简单, 开发门槛低;

  • 功能强大,预置了多种编解码功能,支持多种主流协议;

  • 定制能力强, 可以通过ChannelHandler对通信框架进行灵活地扩展;

  • 性能高, 通过与其他业界主流的NIO框架对比,Netty的综合性能最优;

  • 成熟、稳定,Netty修复了已经发现的所有JDKNIOBUG, 业务开发人员不需要再为NIO的BUG而烦恼;

  • 社区活跃, 版本迭代周期短, 发现的BUG可以被及时修复,同时, 更多的新功能会加入;

  • 经历了大规模的商业应用考验, 质量得到验证。Netty在互联网、大数据、网络游戏、企业应用、电信软件等众多行业已经得到了成功商用,证明它已经完全能够满足不同行业的商业应用了。

正是因为这些优点,Netty 逐渐成为了 Java NIO 编程的首选框架。

Netty

Netty 是一个NIO客户端服务器框架,可以快速轻松地开发协议服务器和客户端等网络应用程序。

它极大地简化并简化了TCP和UDP套接字服务器等网络编程。

“快速简便”并不意味着最终的应用程序会受到可维护性或性能问题的影响。

Netty经过精心设计,具有丰富的协议,如FTP,SMTP,HTTP以及各种二进制和基于文本的传统协议。

因此,Netty成功地找到了一种在不妥协的情况下实现易于开发,性能,稳定性和灵活性的方法。

components.png

快速入门

本章通过简单的示例浏览Netty的核心结构,以便您快速入门。

当您在本章末尾时,您将能够立即在Netty上编写客户端和服务器。

入门之前

本章中运行示例的最低要求仅为两个:最新版本的Netty和JDK 1.6或更高版本。

maven 引入

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.17.Final</version>
</dependency>

实例代码

编写丢弃服务器

世界上最简单的协议不是’Hello,World!’,而是丢弃。

它是一种在没有任何响应的情况下丢弃任何接收数据的协议。

要实现DISCARD协议,您唯一需要做的就是忽略所有收到的数据。

让我们直接从处理程序实现开始,它处理由Netty生成的I/O事件。

  • DiscardServerHandler.java
package com.github.houbb.netty.learn.four.discard;

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

/**
 * <p> 信息处理器 </p>
 *
 * <pre> Created: 2019/9/18 8:24 PM  </pre>
 * <pre> Project: netty-learn  </pre>
 *
 * @author 老马啸西风
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 直接丢弃信息
        ((ByteBuf) msg).release();
    }

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

}
  • DiscardServer.java

核心启动流程。

package com.github.houbb.netty.learn.four.discard;

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

public class DiscardServer {

    public static void main(String[] args) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workGroup)
                    //在这里,我们指定使用NioServerSocketChannel类,该类用于实例化新Channel以接受传入连接。
                    .channel(NioServerSocketChannel.class)
                    //此处指定的处理程序将始终由新接受的Channel评估。
                    .childHandler(new DiscardServerHandler())
                    // 设置套接字选项信息
                    .option(ChannelOption.SO_BACKLOG, 128)
//            option()用于接受传入连接的NioServerSocketChannel。
//            childOption()用于父ServerChannel接受的Channels,在这种情况下是NioServerSocketChannel。
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            //bind
            ChannelFuture channelFuture = serverBootstrap.bind(8888).syncUninterruptibly();

            // 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.
            channelFuture.channel().closeFuture().syncUninterruptibly();
        } finally {
            workGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

EventLoopGroup 可以理解为线程池。

打印信息

我们如果启动程序,最简单的方式是使用 telnet localhost 8888 进行验证。

  • ReceivedByteServerHandler.java
package com.github.houbb.netty.learn.four.discard;

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

/**
 * <p> 打印收到的字节信息 </p>
 *
 * <pre> Created: 2019/9/18 8:24 PM  </pre>
 * <pre> Project: netty-learn  </pre>
 *
 * @author 老马啸西风
 */
public class ReceivedByteServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf) msg;

        try {
            while (in.isReadable()) {
                System.out.println(in.readChar());
                System.out.flush();
            }
        } finally {
            // 这个是可选的
            ReferenceCountUtil.release(in);
        }
    }

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

我们将 Server 中的 handler 换成这个,进行试验。

测试验证

  • 命令行
192:~ 老马啸西风$ telnet localhost 8888
Trying ::1...
Connected to localhost.
Escape character is '^]'.
5
1234
  • 服务端日志
5

1
2
3
4

Echo Server

将客户端的信息,直接返回给客户端。

public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 写入并且刷新
        ctx.writeAndFlush(msg);
    }

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

}

测试验证

将 server 的 handler 换成这个类。

启动服务端。

  • 命令行
192:~ 老马啸西风$ telnet localhost 8888
Trying ::1...
Connected to localhost.
Escape character is '^]'.
1
1
2
2
3
3
4
4

每次输入一个字符,都会得到相同的反馈信息。

时间服务端

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;

/**
 * <p> 时间戳响应服务端 </p>
 *
 * @author 老马啸西风
 */
public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        final ByteBuf byteBuf = ctx.alloc().buffer(4);
        final long time = System.currentTimeMillis();

        // 将时间戳写入 buffer
        byteBuf.writeLong(time);

        // 将 buffer 写入到 ctx
        ChannelFuture channelFuture = ctx.writeAndFlush(byteBuf);
        // 监听事件完成事件
        channelFuture.addListener((ChannelFutureListener) future -> {
            ctx.close();
        });
        // 也可以使用下面更加优雅的方式。
//        channelFuture.addListener(ChannelFutureListener.CLOSE);
    }

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

}

时间客户端

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

public class TimeClient {

    public static void main(String[] args) {
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            ChannelFuture channelFuture = bootstrap.group(workGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new TimeClientHandler())
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .connect("localhost", 8888)
                    .syncUninterruptibly();

            // 关闭客户端
            channelFuture.channel().closeFuture().syncUninterruptibly();
        } finally {
            workGroup.shutdownGracefully();
        }
    }
}
  • TimeClientHandler.java
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * <p> 时间戳响应客户端 </p>
 */
public class TimeClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        long value = byteBuf.readLong();
        System.out.println("Client receive time: " + value);

        ctx.close();
    }

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

}

测试验证

启动服务端

启动客户端

  • 客户端日志
Client receive time: 1568812068058

Process finished with exit code 0

感受

Netty 真的是非常的强大,api 也封装的非常优雅,很值得深入学习。

希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。

我是老马,期待与你的下次相遇。

拓展阅读

丢弃协议

参考资料

《Netty 权威指南》

Netty 官方