前言

网络编程无处不在,Java JDK内置提供了OIO(BIO)、NIO的支持;没有看过Java BIO浅析Java NIO浅析的小伙伴可以去看下BIO/NIO的小示例。但说到Java 网络编程少不了Netty;自它问世起就一直火到现在,像阿里的Dubbo框架、Jetty等都使用了Netty框架做为网络通信框架。
上篇Netty框架:用Service与Client示例入门中通过实现一个服务端与客户端互发消息功能的示例讲解了Netty框架NIO模型的网络编程方法,通过这样一个小示例了解决了Netty的使用方法。

到此你是否有疑问,为什么要用Netty框架编写网络通信层呢?Netty对比Java JDK提供的网络通信API有哪些不同?

在继续阅读下文前如果对Java JDK编写网络通信层不清楚的同学可以去看下用Java JDK 写的BIO与NIO通信的示例:Java BIO浅析Java NIO浅析

统一的API接口

Netty框架的作用是实现网络通信,它提供了一套能用的网络通信API接口,这一套网络接口中包含了EventLoop、Channel、Buffer、Future等,通过这些接口可以统一规范的实现OIO、NIO、Epoll的网络缓和模型,在模型与模型之间迁移时提供了便利。Netty框架工作的网络模型中的传输层。
image-20210713170135447

Netty默认支持的网络传输协议有TCP、UDP、SCTP、UDT。

传 输 方 式 TCP UDP SCTP UDT
NIO ✔️ ✔️ ✔️ ✔️
Epoll(linux) ✔️ ✔️
OIO/BIO ✔️ ✔️ ✔️ ✔️

对于以上传输方式在Netty中分别使用如下类实现:

  • NIO: NioSocketChannel 实现channel功能,NioEventLoop实现EventLoop功能,NioEventLoopGroup实现EventLoopGroup功能
  • OIO:OioSocketChannel 实现channel功能,OioEventLoop实现EventLoop功能,OioEventLoopGroup实现EventLoopGroup功能
  • Epoll:EpollSocketChannel 实现channel功能,EpollEventLoop实现EventLoop功能,EpollEventLoopGroup实现EventLoopGroup功能

另外Netty还实现了Jvm内的通信,使用LocalSocketChannel、LocalEventLoop、LocalEventLoopGroup进行实现,另外还是embedded的方法用于测试ChannelHandler,Local与embedded的方式就不多讲了。

在Netty框架中NIO与OIO的底层实现还是使用Java jdk提供的API;Netty对这些Api进行了封装,具体后面再进行详细的分析。对于Epoll只在linux系统中有效,Netty官方也建议在Linux系统中使用Epoll的方式。

使用Netty框架时在NIO/OIO/Epoll模型上可以很轻松的进行迁移,使用NIO的实现可以看Netty框架:用Service与Client示例入门;现在打算将应用部署到linux环境中并改用epoll进行实现,那我们只需要将EventLoop与Channel修改成Epoll对应的类就可以了,修改后的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] arg) throws InterruptedException {
//将EventLoopGroup的类型由原来的NioEventLoopGroup修改为EpollEventLoopGroup
EpollEventLoopGroup group=new EpollEventLoopGroup()
Bootstrap bootstrap = new Bootstrap();
//将SocketChannel类型由原来的NioSocketChannel修改为EpollSocketChannel
bootstrap.channel(EpollSocketChannel.class);
bootstrap.group(group)
bootstrap.option(ChannelOption.SO_KEEPALIVE,true);
bootstrap.remoteAddress("127.0.0.1", 30888);
final MessageChannel channel = new MessageChannel();
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(channel);
}
});
ChannelFuture channelFuture = bootstrap.connect().sync();
channelFuture.channel().closeFuture().sync();
group.shutdownGracefully();
}

示例为客户端的代码,只需要简单的修改EventLoopGroup与SocketChannel的类型为Epoll对应的类型就可以完成切换,同样的修改成Oio也只需要修改这些点就可以了,这是Netty框架为统一Api后带来的好处。

零拷贝

Netty框架还有一个被神化的操作零拷贝;在网络编程中常常需要对传输的数据进行拷贝,例如从内核内存区拷贝到用户内存区(也就是数据的内核态转化为用户态);但Netty的零拷贝并不是减少了数据从内核态到用户态的转换,而是在Jvm中对于用户态缓存的数据操作进行优化,体现在5个方面:

  • Netty接收与发送的数据使用直接内存进行Socket读写,减少在缓存区的二次拷贝。
  • 文件传输使用FileRegion包装的transferTo方法直接将文件缓冲区的数据发送到目标Channel,避免循环文件缓冲区导致的内存拷贝
  • 提供CompositeByteBuf类,可以将多个ByteBuf逻辑上合并成一个ByteBuf,避免了需要合并数据时导致的拷贝
  • 通过wrap操作对byte[]、ByteBuf、ByteBuffer等类型包装成Netty的ByteBuf对象避免内存的拷贝
  • Netty的ByteBuf支持slice操作,通过slice可以将一个ByteBuf分解为多个共享同一存储区域的ByteBuf避免需要操作部分数据时的拷贝

上面这些优化都只是对在用户态的数据进行操作上的优化避免用户态数据操作间的拷贝,对于内核态到用户态的转换还是需要对数据进行拷贝。

选择Netty的理由

如果统一的API接口与零拷贝这两个最大的优点都无法吸引你选择Netty框架学习Netty框架的话,我相信加上这些会让你觉得在项目中应用Netty框架是一个不错的选择:

  • Netty框架封装了网络编程的复杂性,解决了网络编程中常常需要处理且难已处理的网络问题,例如网络闪断、客户端重复接入、安全认证、消息编解码、半包读等问题
  • JDK NIO的Bug,例如臭名昭著的epoll bug,它会使Selector空轮询导致CPU 100%,虽然官方称在1.6版本的update18修复了该问题,但在1.7版本中仍旧存在,只是发生的概率降低了。
  • 使用JDK的NIO API 需要团队过多的关注网络编程本身,团队中需要有足够的网络编程经验且对Selector、ServiceSocketChannel、SocketChannel、ByteBuffer足够熟悉的人才能编写与高质量的NIO程序。
  • 对团队编码水平整体要求高,需要有额外的知识储备,例如多线程编程