本博客 猫叔的博客,转载请申明出处html
阅读本文约 “15分钟”java
适读人群:Java 中级git
学习笔记,Netty系列的学习教程,可能不少部份内容是摘抄,不过本身从新作了整理,相关案例也更新了本身的理解。github
官方解释:Netty是一个异步的事件驱动的网络应用程序框架,用于快速开发可维护的高性能的协议服务器和客户端。——摘自Netty:Homeapache
词义拆解:编程
零拷贝:TCP接收和发送缓冲区使用直接内存代替堆内存,避免了内存复制bootstrap
高效的并发编程:经过读写锁、volatile、线程安全容器等提高并发性能windows
无锁化的串行设计:为了尽量地避免锁竞争带来的性能损耗,能够经过串行化设计,既消息的处理尽量在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁api
持续维护:其修复了已经发现的JDK NIO BUG,下降了开发人员的编程难度数组
链路的有效性检测:TCP层面、协议层、应用层的心跳检测
规避NIO BUG(Netty的解决策略)
Netty能够适用的行业很是广,由于设计优雅、高性能,其能够在多个行业有所应用,好比在互联网行业通常会做为PRC框架使用,而游戏行业中也进场须要其做为通讯组件即多协议栈特色,能够在游戏行业发挥其高性能通讯,还有在通讯行业里,因其异步高性能、高可靠性等,它在互联网上也有许多开源或者教学的IM案例,等等...
若是你或你的团队在找寻一个高性能且成熟稳定的NIO框架,那么必定要选择Netty!
我在第一次了解Netty是由于项目须要支持TCP并发长链接的解决方案,而在互联网上找寻了许久,由于服务端是Java写的,且在了解Netty的机制与服务能力后,便开始了和Netty的不解之缘。
但愿你也能有所收获!
即便你是初学编程的小白,你能够跟着这个小节一步一步构建本身的开发环境,以便于后续学习Netty相关代码实战环境。
本小节选择的开发工具是IDEA,其是一款目前Java开发工程师比较经常使用的开发工具,而java与Maven的版本的选择,本着学有所成的目的,但愿你们能够和课程保持一致。
若是你的电脑已经安装了JDK,那么请你先验证一下它的版本。
打开电脑cmd,输入:
java -version
复制代码
以下是本次演示的java版本。
若是你还没安装JDK,那么能够到Oracle官网下载。
下载地址:www.oracle.com/technetwork…
注意:咱们仅需下载JDK便可
下载后进行安装便可。
安装后还要进行环境变量的配置,在系统变量中新建JAVA_HOME、CLASSPATH两个。
JAVA_HOME : C:\Program Files\Java\jdk1.8.0_191(Windows上安装的默认值)
CLASSPATH : .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tolls.jar;
还有在Path中添加两个地址
%JAVA_HOME%\bin
%JAVA_HOME%\jre\bin
在Linux上则将**${JAVA_HOME}/bin**添加到执行路径上。
以上配置好后,请再运行cmd,输入:Java -version,验证电脑的Java版本是否显示正常。
若是你的电脑已经安装了Maven,那么请你先验证一下它的版本。
打开电脑cmd,输入:
mvn -v
复制代码
以下是本次演示的Maven版本。
若是你还没安装Maven,那么能够到官网下载。
将文件解压到指定的目录下,如:F:\Maven\apache-maven-3.6.3
在系统变量中新建MAVEN_HOME
MAVEN_HOME : F:\Maven\apache-maven-3.6.3
还有在Path中添加一个地址
%MAVEN_HOME%\bin
在Linux上则将**${MAVEN_HOME}/bin**添加到执行路径上。
以上配置好后,请再运行cmd,输入:mvn -v,验证电脑的Maven版本是否显示正常。
以后能够再打开:F:\Maven\apache-maven-3.6.3\conf 下的settings,修改成阿里镜像
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
复制代码
本次教程使用IDEA,你们请根据本身的电脑状况下载对应的版本,本次教程的IDEA版本是Windows的Community。
若是你已经有本身熟练的IDE,那么也能够用于学习,并不会影响学习质量。
安装完成后,须要再配置下IDE的Maven,以下图,你能够修改Maven路径、settings文件及本地仓库。
用IDEA构建一个简单的SpringBoot项目,你们这时能够在Project SDK的配置上选择咱们一开始配置JDK,以下图
项目新建后,请打开pom.xml文件,引入Netty资源,关于相关框架的Maven资源均可以到如下网站搜索
Netty地址:mvnrepository.com/artifact/io…
netty-all的maven地址
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.44.Final</version>
</dependency>
复制代码
引入到Pom.xml文件中,以下
<dependencies>
<!--省略部分代码-->
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.44.Final</version>
</dependency>
<!--省略部分代码-->
</dependencies>
复制代码
以上,netty引入成功,环境搭建完成。
一、javac显示不是内部或外部命令
操做系统win十、win7
配置如上文所示时,能够将PATH的路径改成绝对路径
C:\Program Files\Java\jdk1.8.0_191\bin
C:\Program Files\Java\jdk1.8.0_191\jre\bin
二、mvn显示不是内部或外部命令
jdk的环境变量配置有错,或者是M2_HOME路径有错
操做系统win十、win7
配置如上文所示时,能够将PATH的路径改成绝对路径
F:\Maven\apache-maven-3.6.3\bin
三、系统存在两个JDK版本
将默认启动的JDK版本中java.exe所在路径加入到操做系统PATH的首位
四、maven版本不兼容
输入:mvn -v,若是报“Exception in thread "main" java.lang.UnsupportedClassVersionError: org/apache/maven/cli/MavenCli : Unsupported major.minor version 51.0”的错误,能够更新新版的maven,解决问题。
下图简单讲述一个Netty服务端构建的基本流程操做,并不涉及具体的业务逻辑。
以下展现部分代码与上图流程一致。
EventLoopGroup acceptorGroup = new NioEventLoopGroup();
try {
//一、ServerBootstrap引导类
ServerBootstrap b = new ServerBootstrap();
//二、NioEventLoopGroup接受新链接及读/写处理
b.group(acceptorGroup)
//三、指定传输类型Channel
.channel(NioServerSocketChannel.class)
//四、添加业务Handler
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(...业务Handler);
}
});
//五、绑定服务器及端口
ChannelFuture f = b.bind(port).sync();
//六、监听服务器Channel关闭
f.channel().closeFuture().sync();
}finally {
//六、释放资源
acceptorGroup.shutdownGracefully();
}
复制代码
对于一个基本的Netty服务端而言,他须要绑定到对应的服务器上同时在其上监听端口以保证能够接受传入的链接请求,还须要给他配置Channel,将入站消息及时通知给咱们所定义的业务Handler中进行处理。
咱们一开始建立了一个ServerBootStrap实例,他是Netty启动NIO服务端的辅助启动类,能够为咱们下降服务端的开发难度,同时构建了一个NioEventLoopGroup来接受和处理新的链接,固然你也能够建立两个Reactor线程组,一个用于服务端接受客户端的链接,一个用于进行SocketChannel的网络读写。咱们还指定了Channel的类型为NioServerSocketChannel,其功能对应JDK NIO 类库中的ServerSocketChannel类,在大部分状况下,你能够配置NioServerSocketChannel的TCP参数,好比将它的backlog设置为1024等。
复杂的点来了,绑定I/O事件的ChildChannelHandler类,其实它有点相似Reactor中的Handler类,主要用于处理网络I/O,例如对消息的编解码等。但是为何还有一个ChannelInitializer类呢?
这也是Netty美妙的地方,当一个新的链接被服务端接受时,一个新的子Channel会建立,而ChannelInitializer就会把咱们的业务Handler实例添加到这个子Channel的ChannelPipeline中。
接下来是绑定服务器及端口,经过sync()咱们能够阻塞当前的Thread,直到绑定操做完成为止,同时咱们还要继续使用sync()去监听服务端的Channel,直到它被关闭,咱们才能去关闭EventLoopGroup还有其余的全部资源。
以上也是Netty服务端的建立流程,相比其传统的NIO服务端,其大大简化了开发的复杂程度。
如下是下文演示案例的服务端代码。
package com.demo.timer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/** * @ClassName MkTimeServer * @Description 服务端 * @Author Java猫说 * @Date 2020/1/4 0004 14:02 **/
public class MkTimeServer {
public static void main(String[] args) throws Exception {
int port = 8080;
//启动服务端
new MkTimeServer().run(port);
}
void run(int port) throws Exception{
EventLoopGroup acceptorGroup = new NioEventLoopGroup();
try {
//引导类
ServerBootstrap b = new ServerBootstrap();
b.group(acceptorGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//MkTimeServerHandler属于业务Handler
socketChannel.pipeline().addLast(new MkTimeServerHandler());
}
});
//阻塞直到异步绑定服务器完成
ChannelFuture f = b.bind(port).sync();
//阻塞直到Channel关闭
f.channel().closeFuture().sync();
}finally {
acceptorGroup.shutdownGracefully();
}
}
}
复制代码
下图简单讲述一个Netty客户端构建的基本流程操做,并不涉及具体的业务逻辑。
以下展现部分代码与上图流程一致。
EventLoopGroup acceptorGroup = new NioEventLoopGroup();
try {
//一、Bootstrap引导类
Bootstrap b = new Bootstrap();
//二、NioEventLoopGroup建立链接及处理出/入站数据
b.group(group)
//三、指定传输类型为NioSocketChannel类型
.channel(NioSocketChannel.class)
//四、添加业务Handler
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(...业务Handler);
}
});
//五、链接远程指定的host、port节点
ChannelFuture f = b.connect(host,port).sync();
//六、监听服务器的Channel关闭
f.channel().closeFuture().sync();
}finally {
//六、释放资源
group.shutdownGracefully();
}
复制代码
对于一个基本的Netty客户端而言,其实咱们能够发现它与服务端的流程很类似,若是说ServerBootStrap实例,他是Netty启动NIO服务端的辅助启动类,能够为咱们下降服务端的开发难度,那么其实BootStrap也是Netty为咱们提供的NIO客户端的辅助启动类。
AbstractBootstrap类是ServerBootstrap及Bootstrap的基类,感兴趣的朋友能够看看后续章节的源码解析。
与服务端不一样的是,客户端的Channel须要设置为NioSocketChannel,一样你能够设置相关的选择,而后为其添加Handler。
当客户端完成并启动BootStrap辅助类,咱们须要调用connect()方法发起异步链接,而后调用sync()同步等待链接成功。
当客户端链接关闭时,释放线程资源,并退出客户端主函数。
如下是下文演示案例的服务端代码。
package com.demo.timer;
import io.netty.bootstrap.Bootstrap;
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.NioSocketChannel;
/** * @ClassName MkTimeClient * @Description 客户端 * @Author Java猫说 * @Date 2020/1/4 0004 14:03 **/
public class MkTimeClient {
public static void main(String[] args) throws Exception {
int port = 8080;
String host = "127.0.0.1";
new MkTimeClient().connect(port, host);
}
void connect(int port, String host) throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//MkTimeClientHandler属于业务Handler
socketChannel.pipeline().addLast(new MkTimeClientHandler());
}
});
ChannelFuture f = b.connect(host,port).sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
复制代码
在阅读本章以前,请先学习前两节,即Netty的服务端与客户端建立。
若是你能了解服务端与客户端的建立流程与步骤,那么接下来的学习能够更加高效。
这个例子以服务端和客户端的时间传输为例,给各位读者介绍Netty的服务端与客户端,让各位能够简单的搭建并运行。
编码过程涉及的部分知识点也会有所介绍,若是各位对于部分组件有兴趣也能够跳到对应的词条阅读相关的源码介绍。
如上图的项目架构逻辑图,本次学习的项目案例是使用Netty构建一个主动给链接发送时间戳的服务端,服务端的做用是启动后监听链接的客户端,每当有一个客户端链接时,新建一个Channel并在其ChannelPipeline后追加服务业务MkTimeServerHandler实例,业务Handler负责检测到新链接时主动向客户端发送当前系统时间戳。
客户端的做用是启动后根据指定的Host和Port去链接服务端,同时客户端Channel的ChannelPipeline也会有对应处理时间戳的客户业务MkTimeClientHandler实例,负责主动链接服务端,读取服务端发送的系统时间戳并打印到控制台并关闭客户端服务。
咱们的MkTimeServerHandler类第一步就是须要继承ChannelInboundHandlerAdapter类,首先了解下为何须要继承它,ChannelInboundHandlerAdapter是ChannelHandler的适配器之一,其对应的还有ChannelOutboundHandlerAdapter,其中ChannelInboundHandler负责处理处理进站数据和全部状态更改事件,而ChannelOutboundHandler负责处理出站数据,容许拦截各类操做。
由于服务端须要第一时间判断新链接并主动向客户端发送系统时间戳,所以咱们继承了ChannelInboundHandlerAdapter。
那么ChannelInboundHandlerAdapter负责的全部状态更改事件是挺多的,咱们今天就先学习演示Demo中的两个。
适配器提供的多个与Channel生命周期相关的方法之一,channelActive指当Channel处于活跃时,即Channel链接且准备就绪时。这也知足咱们业务场景的需求,在检测到链接的时候,获取当前的系统时间戳,并建立一个Netty定义的ByteBuf字节串用于保存时间戳,经过ChannelHandlerContext调用writeAndFlush方法发送给客户端。
这里的ChannelHandlerContext是ChannelPipeline用来直接管理ChannelHandler的“替身”。
通常咱们在写这一类的业务Handler时,为了不链接异常,通常都会实现exceptionCaught方法,其能够在捕获到异常时,打印并关闭这个Channel通道,这也是加强了代码的健壮性。
package com.demo.timer;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import java.util.Date;
/** * @ClassName MkTimeServerHandler * @Description 发送系统时间 * @Author Java猫说 * @Date 2020/1/4 0004 14:03 **/
public class MkTimeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("One Channel Connect");
//获取系统时间戳的字符串
String serverTime = new Date(System.currentTimeMillis()).toString();
//建立一个 ByteBuf 保存特定字节串
ByteBuf resp = Unpooled.copiedBuffer(serverTime.getBytes());
//将 ByteBuf 发送给客户端
ctx.writeAndFlush(resp);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
//异常关闭
ctx.close();
}
}
复制代码
在客户端的MkTimeClientHandler类中,咱们也一样继承了ChannelInboundHandlerAdapter,这里就再也不赘述了。
channelRead也是Channel生命周期相关的方法之一,当Channel读取到消息时调用,即服务端发送消息,咱们能够经过这个方法获取到服务端的时间消息。
咱们经过ByteBuf接收,由于在服务端发送时也同一个了这个类型,并建立一个等长的byte数组,同时在将ByteBuf中的内容传输到byte数组,最后转化为String打印在控制台。
在这个业务中,咱们在获取到服务端系统时间戳后就关闭了链接,咱们经过调用ChannelHandlerContext的close方法,关闭链接,并在操做完成后通知ChannelFuture,由于不管关闭成功或失败,都没法再使用这个链接。
package com.demo.timer;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/** * @ClassName MkTimeClientHandler * @Description 接受时间消息 * @Author Java猫说 * @Date 2020/1/4 0004 14:04 **/
public class MkTimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf m = (ByteBuf) msg;
byte[] req = new byte[m.readableBytes()];
m.readBytes(req);
String serverTime = new String(req);
System.out.println(serverTime + " From Server.");
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
复制代码
一、运行服务端,在IDEA中启动MkTimeServer的main函数。
启动成功!
二、运行客户端,在IDEA中启动MkTimeClient的main函数。
启动成功!
同时链接服务端,并接收到服务端发送的时间消息,打印到了控制台,且自动关闭客户端。
服务端在控制台也检测到客户端的链接并发送了系统时间,从控制台日志中能够看到,以下图。
normanmaurer:exceptionCaught(...) is only called for inbound exceptions. All outbound exceptions must by handled in a listener by design. If you always want to have exceptions handled in exceptionCaught(...) just add a ChannelOutboundHandler that will an listener for every outbound operation.
在这个issues中曾提过,exceptionCaught 只会捕获 inbound handler的exception, outbound exceptions 须要在writeAndFlush方法里加上listener来监听消息是否发送成功。
我是MySelf,还在坚持学习技术与产品经理相关的知识,但愿本文能给你带来新的知识点。
学习交流群:728698035
现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不按期干货。