Netty基本概念
Netty
Netty入门知识
1.1 Netty概述
1.1.1 Netty简介
- Netty是一个异步时间驱动的网络应用程序框架,用于快速开发可维护的高性能服务器和客户端。
- Netty是一个NIO客户机-服务器框架,它支持快速、简单地开发网络应用程序,如服务器和客户机。它大大简化了网络编程,如TCP和UDP套接字服务器。
- “快速和简单”并不意味着生成的应用程序将受到可维护性或性能问题的影响。Netty经过精心设计,并积累了许多协议(如ftp、smtp、http)的实施经验,以及各种二进制和基于文本的遗留协议。因此,Netty成功地找到了一种方法,在不妥协的情况下实现了易于开发、性能、稳定性和灵活性。
1.1.2 谁在使用Netty
Dobbo、zk、RocketMQ、ElasticSearch、Spring5(对HTTP协议的实现)、GRpc、Spark等大型开源项目都在使用Netty作为底层通讯框架。
1.1.3 Netty执行流程
1.1.4 Netty中核心概念
-
Channel
管道,其是对Socket的封装,其包含了一组API,大大简化了直接与Socket进行操作的复杂性。
-
EventLoopGroup
EventLoopGroup是一个EventLoop线程池,包含了很多的EventLoop。
Netty为每个Channel分配了一个EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件。EventLoop本身只是一个线程驱动,在其生命周期内只会绑定一个线程,让该线程处理一个Channel的所有IO事件。
一个Channel一旦与一个EventLoop相绑定,则在Channel的整个生命周期内是不会也不能发生变化。但一个EventLoop可以与多个Channel相绑定。
-
ServerBootStrap
用于配置整个Netty代码,将各个组件关联起来。服务端使用的是ServerBootStrap,而客户端使用的是BootStrap。
-
ChannelHandler(管道数据处理器)与ChannelPipeline(管线对象)
ChannelHandler是对Channel中数据的处理器,这些处理器可以是系统本身定义好的编码器,也可以是用户自定义的。这些处理器会被统一添加到一个ChannelPipeline的对象中,然后按照添加的顺序对Channel中的数据进行依次处理。
-
ChannelFuture
Netty中所有I/O操作都是异步的,即操作不会立即得到返回结果,所以Netty中定义了一个ChannelFuture对象作为这个异步操作的“代言人”,表示异步操作本身。如果想获取到该异步操作的返回值,可以通过该异步操作对象的addListener()方法为该异步操作添加监听器,为其注册回调:当结果出来后马上调用执行。
Netty的异步编程模型都是建立在Future与回调概念之上的。
1.2 Http服务端01-primary
通过该程序达到的目的是,对 Netty 编程的基本结构及流程有所了解。
该程序是通过 Netty 实现 HTTP 请求的处理,即接收 HTTP 请求,返回 HTTP 响应。
这个代码相当于“SpringMVC + Tomcat”。
1.2.1 创建工程
创建一个普通的Maven的java工程。
1.2.2 导入依赖
仅导入一个netty-all依赖即可。
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.targer>1.8</maven.compiler.targer>
</properties>
<dependencies>
<!-- netty-all 依赖 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.36.Final</version>
</dependency>
<!--lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
</dependencies>
1.2.3 定义服务器启动类
该服务器就是用于创建并初始化服务器启动对象ServerBootStrap。
/**
* 服务器启动类
*/
public class SomeServer {
public static void main(String[] args) {
//用于处理客户端连接请求,将请求发送给childGroup中的eventLoop
EventLoopGroup parentGroup = new NioEventLoopGroup();
//用于处理客户端请求
EventLoopGroup childGroup = new NioEventLoopGroup();
try {
//用于启动ServerChannel
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parentGroup,childGroup)//指定eventLoopGroup
.channel(NioServerSocketChannel.class)//指定使用NIO进行通信
.childHandler(new ChannelInitializer<SocketChannel>() {
//当Channel初始化创建完毕后就会触发该方法的执行,用于初始化Channel
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//从Channel中获取pipeline
ChannelPipeline pipeline = socketChannel.pipeline();
//将HttpServerCode处理器放入到pipeline的最后
//HttpServerCodec是什么?是HttpRequestDecoder与HttpResponseEncoder的复合体
//HttpRequestDecoder:http请求解码器,将Channel中的ByteBuf数据解码为HttpRequest对象
//HttpResponseEncoder:http响应编码器,将HttpResponse对象编码为将要在Channel中发送的ByteBuf数据
pipeline.addLast(new HttpServerCodec());
//将自定义的处理器放入到pipeline的最后
pipeline.addLast(new SomeServerHandler());
}
});//指定childGroup中的eventLoop所绑定的线程所处理的处理器
//指定当前服务器所监听的端口号
//bind()方法的执行时异步的
//sync()方法会使bind()操作与后续的代码的执行由异步变为同步。
ChannelFuture future = bootstrap.bind(8888).sync();
//只要放回的是ChannelFuture,那么这个方法就是异步执行的。
System.out.println("服务器启动成功。监听的端口号是8888!");
//关闭Chanel
//closeFuture()的指定是异步的
//当Channel调用了close()方法并关闭成功之后才会触发closeFurture()方法的执行
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//优雅关闭(没有处理完毕的等待处理完毕之后在关闭)
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
}
}
1.2.4 定义管道初始化器
/**
* 管道初始化器
* 当前类的实例在pipeline初始化完毕之后就会被GC
*/
public class SomeChannelInitialize extends ChannelInitializer<SocketChannel> {
//当Channel初始化创建完毕后就会触发该方法的执行,用于初始化Channel
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//从Channel中获取pipeline
ChannelPipeline pipeline = socketChannel.pipeline();
//将HttpServerCode处理器放入到pipeline的最后
//HttpServerCodec是什么?是HttpRequestDecoder与HttpResponseEncoder的复合体
//HttpRequestDecoder:http请求解码器,将Channel中的ByteBuf数据解码为HttpRequest对象
//HttpResponseEncoder:http响应编码器,将HttpResponse对象编码为将要在Channel中发送的ByteBuf数据
pipeline.addLast(new HttpServerCodec());
//将自定义的处理器放入到pipeline的最后
pipeline.addLast(new SomeServerHandler());
}
}
由于该类只需要初始化一次,所以一般直接在启动类利用匿名内部类的写法创建即可。
1.2.5 自定义服务端处理器
/**
* 自定义服务器处理器(这才是真正的业务处理部分)
* 需求:用户提交一个请求后,在浏览器就会看到Hello netty world
*/
public class SomeServerHandler extends ChannelInboundHandlerAdapter {
/**
* 当Channel中有来自于客户端的数据时,就会触发该方法的执行
* @param ctx 上下文对象
* @param msg 就是来自于客户端的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// System.out.println("msg = " + msg.getClass());
// System.out.println("客户端地址 = " + ctx.channel().remoteAddress());
if (msg instanceof HttpRequest){
HttpRequest request = (HttpRequest)msg;
System.out.println("请求方式: " + request.method().name());
System.out.println("请求URI:" + request.uri());
if ("/favicon.ico".equals(request.uri())){
System.out.println("浏览器发送的请求图标,我们不做处理!");
return;
}
//构造response的响应体
ByteBuf body = Unpooled.copiedBuffer("Hello netty world", CharsetUtil.UTF_8);
//生成响应对象
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, body);
//获取到response的头部后进行初始化
HttpHeaders headers = response.headers();
headers.set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
headers.set(HttpHeaderNames.CONTENT_LENGTH, body.readableBytes());
//将响应对象写入到Channel
//ctx.write(response);
//ctx.flush();
//ctx.writeAndFlush(response);
ctx.writeAndFlush(response)
//添加监听器,响应体发送完毕之后直接关闭Channel
.addListener(ChannelFutureListener.CLOSE);
}
}
/**
* 当Channel中的数据在处理过程中发生异常时就会触发该方法的执行
* @param ctx 上下文
* @param cause 发生的异常对象
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
//关闭Channel
ctx.close();
}
}
1.3 Socket编程02-socket
在netty中若要学习TCP的拆包与粘包,则首先要清楚基于TCP协议的Socket编程。
1.3.1 创建工程
复制 01-primary2 工程,在此基础上进行修改。
本例要实现的功能是:客户端连接上服务端后,服务端会向客户端发送一个数据。客户
端每收到服务端的一个数据后,便会再向服务端发送一个数据。而服务端每收到客户端的一
个数据后,便会再向客户端发送一个数据。如此反复,无穷匮也。
1.3.2 定义服务端
- 定义服务端启动类
/**
* 服务端启动类
*/
public class SomeServer {
public static void main(String[] args) {
EventLoopGroup parentGroup = new NioEventLoopGroup();
EventLoopGroup childGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parentGroup,childGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//从Channel中获取pipeline
ChannelPipeline pipeline = socketChannel.pipeline();
//StringDecoder:字符串解码器,将Channel中的Bytebuf数据解码转成String
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
//StringEncoder:字符串编码器,将String编码转成将要发送到Channel中的ByteBuf
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
//自定义处理器
pipeline.addLast(new SomeSocketServerHandler());
}
});
//指定服务的端口号
ChannelFuture future = bootstrap.bind(8888).sync();
System.out.println("服务器已启动,监听的端口为8888!");
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (parentGroup != null){
parentGroup.shutdownGracefully();
}
if (childGroup != null){
childGroup.shutdownGracefully();
}
}
}
}
- 定义服务端处理器
public class SomeSocketServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//将来自于客户端的数据显示在服务端控制台
System.out.println(ctx.channel().remoteAddress() + ", " + msg);
//向客户端发送数据
ctx.writeAndFlush("from server :" + UUID.randomUUID());
TimeUnit.MILLISECONDS.sleep(3000);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
1.3.3 定义客户端
- 定义客户端启动类
/**
* 客户端启动类
*/
public class SomeClient {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new SomeSocketClinetHandler());
}
});
ChannelFuture future = bootstrap.connect("localhost", 8888).sync();
System.out.println("已连接上服务器!");
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (group != null)
group.shutdownGracefully();
}
}
}
- 定义客户端处理器
public class SomeSocketClinetHandler extends SimpleChannelInboundHandler<String> {
//msg的消息类型与类中的泛型类型是一致的
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(ctx.channel().remoteAddress() + ", " + msg);
ctx.writeAndFlush("from client: " + LocalDateTime.now());
TimeUnit.MILLISECONDS.sleep(3000);
}
//当Channel被激活(创建好初始化之后紧接着就是被激活)后悔触发该方法的执行
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("from client: " + "begin talking!");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
1.3.4 两个处理器的区别
SimpleChannelInboundHandler中的channelRead0()方法会自动释放接受到来自对方的msg所占有的所有资源。
ChannelInboundHandlerAdapter中的channelRead()方法不会自动释放接受到来自对方的msg.
若对方没有向自己发送数据,则自定义处理器建议集成自ChannelInboundHandlerAdapter,因为若继承自SimpleChannelInboundHandler需要重写channelRead0()方法,而重写该方法的目的就是对来自对方的数据进行处理,因为对方根本没有发送数据,所以也没有必要重写channelRead()方法
若对方向自己发送了数据,而自己又需要将该数据在发给对方,则自定义处理器建议集成自ChannelInboundHandlerAdapter,因为write()方法的执行时异步的,且SimpleChannelInboundHandler中的channelRead0()方法会自定释放掉来自对方的msg。若write()方法中正在处理msg,而此时SimpleChannelInboundHandler中的channelRead0()方法执行完毕了,将msg释放了,此时就会报错。