本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1.1 简介
netty.io/ 官网
netty是对java nio api的封装,简化了nio程序的开发,jdk要求最低1.6
流行的网络编程通信框架,Dubbo Elasticsearch 等框架底层的网络通信框架都是 Netty
架构模型
版本
netty 共有 3.x 4.x 5.x三个大版本
3.x较老,5.x有重大bug,被官网废弃 现在主要使用4.x
1.2 线程模型
有以下几种线程模型
4.2.1 传统I/O阻塞模型
每一个链接都需要一个对应的线程进行处理,并且当链接建立后,如果当前链接没有数据传输时,此线程会被阻塞在read()方法
1.2.2 Reactor模式
原理图示如上
主要是针对了传统I/O模型一个连接会阻塞一个线程的问题进行了改进,当连接建立后都通过ServiceHandler调用线程池中的线程进行处理,这样就只用阻塞一个ServiceHandler线程,达到多路复用的目的
Reactor模式有三种实现方式
1.2.2.1单Reactor单线程
使用一个线程通过多路复用完成所有操作,包括读写连接
redis使用的就是这种模型 单线程
1.2.2.2单Reactor多线程
相对于单Reactor单线程,主线程不在进行业务处理,当有请求过来之后,具体的业务处理交与线程池中的线程处理,线程处理完成后再通过handler返回给Client
1.2.2.3 主从Reactor多线程
相比于单Reacotr,主从Reactor将Reactor分为MainReactor和SubReactor
MainReactor中负责分发和连接
SubReactor中负责读写
一个MainReactor可以对应多个SubReactor
1.2.3 Netty模型
简述Netty模型
- 角色
-
- BossGroup BossGroup的类型是NioEventLoopGroup,其中包含了很多NioEventLoop
- NioEventLoop nio事件循环,每个NioEventLoop中都有一个Selctor和一个任务队列
-
- WorkerGroup 类型是NioEventLoopGroup,与BossGroup类似,只不过功能不同,BossGroup只负责与客户端建立连接, WorkerGroup需要读写,处理业务
- PipeLine 管道,对Channel进行的封装,具体的业务处理是通过Pipline对Channel进行处理
- 具体流程
-
- 当客户端发送请求时,首先进入BossGroup,有NioEventLoop对请求进行事件轮询,如果是连接事件就进行处理
-
-
- 处理的步骤分为三步
- 轮询
-
-
-
- 注册 这里的注册指的是将生成的SocketChannel注册到workerGroup中的某个NioEventLoop中的Selector上
- 执行任务列表
-
-
- 当请求的事件是读写时,就有workerGroup对请求进行具体的业务处理
-
-
- 处理的步骤BossGroup类似
-
- 总结 由此可以看出,Netty的模型与主从Reactor模型类似,都是由一个主Reactor负责连接事件,由一个从Reactor负责读写事件
1.2.4 案例demo
1.2.4.1 服务端
1.2.4.1.1 NettyServer
package netty;
import io.netty.bootstrap.ServerBootstrap;
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.NioServerSocketChannel;
/**
* @author: zhangyao
* @create:2020-09-03 08:55
**/
public class NettyServer {
public static void main(String[] args) {
//创建BossGroup 和 WorkerGroup
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
ChannelFuture channelFuture = null;
try {
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
socketChannel.pipeline().addLast(new NettyServerHandler());
}
});
System.out.println("服务器就绪.....");
//绑定端口
channelFuture = serverBootstrap.bind(6668).sync();
}catch (Exception e){
e.printStackTrace();
}finally {
try {
channelFuture.channel().closeFuture().sync();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.2.4.1.2 NettyServerHandler
package netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @author: zhangyao
* @create:2020-09-03 09:12
**/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//读取数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送消息:"+ buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:"+ ctx.channel().remoteAddress());
}
//数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello 客户端",CharsetUtil.UTF_8));
}
//处理异常 关闭ctx
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
1.2.4.2 客户端
1.2.4.2.1 NettyClient
package netty;
import io.netty.bootstrap.Bootstrap;
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.NioSocketChannel;
/**
* @author: zhangyao
* @create:2020-09-03 09:52
**/
public class NettyClient {
public static void main(String[] args) {
EventLoopGroup executors = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.group(executors)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("客户端就绪........");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//关闭通道
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
try {
executors.shutdownGracefully().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.2.4.2.2 NettyClientHandler
package netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @author: zhangyao
* @create:2020-09-03 10:00
**/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//就绪时触发
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("ctx: "+ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,服务端", CharsetUtil.UTF_8));
}
//读取信息
//这里读取的是服务器返回的信息
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("服务端发送消息: "+ byteBuf.toString(CharsetUtil.UTF_8));
System.out.println("服务端地址: "+ ctx.channel().remoteAddress());
}
//异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
一个简单的TCP服务通信,客户端发送消息,服务端接收消息并返回消息给客户端
1.2.4.3 案例demo源码分析
1.2.4.3.1 NioEventGroup
public NioEventLoopGroup() {
this(0);
}
public NioEventLoopGroup(int nThreads) {
this(nThreads, (Executor)null);
}
可以看到,如果使用无参的NioEventGroup,默认传递的是0,也可以指定线程数
一层一层找下去:
private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); //NettyRuntime.availableProcessors()获取当前计算机的核数(逻辑处理器)
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
发现最后找到NioEventGroup父类的方法
如果指定了NioEventGroup的线程数,且不为0的时候,就使用指定的线程数
否则,**就使用当前计算机的核数2作为线程数
debug 看结果
电脑是12核,默认是24个线程
指定一个线程
就只有一个线程数
1.2.5 异步模型
上文中的案例Demo中的 ChannelFuture
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
异步模型Future是相对与同步来说的
异步指的是当一个异步调用发出后,不会立刻得到结果,而是通过回调,状态来通知调用者调用的结果
Netty 中的 connect 和 bind() sync方法就是返回了一个异步的结果,之后再通过监听获取到结果
也就是 Future-Listener机制
当Future对象刚创建的时候,处于未完成的状态,可以通过返回的ChannelFuture查看操作执行的状态,也可以注册监听函数来执行完成后的操作
isSucess()是否成功
isDone()是否完成
isCancelable() 是否取消
cause() 失败原因
addListener 增加监听器
//绑定端口
channelFuture = serverBootstrap.bind(6668).sync();
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if(channelFuture.isSuccess()){
System.out.println("监听端口成功");
}else {
System.out.println("监听端口失败");
}
}
});
4.2.6 Netty Http服务
做一个简单的demo 浏览器(客户端)访问服务器端7001端口,返回一个字符串信息
浏览器访问是http请求,服务器也需要相应一个httpResponse
1.2.6.1 服务端
NettyHttpServer 启动类
package netty.http;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* @author: zhangyao
* @create:2020-09-04 11:16
**/
public class NettyHttpServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new NettyHttpInitialize());
ChannelFuture channelFuture = serverBootstrap.bind(7001).sync();
channelFuture.channel().closeFuture().sync();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
NettyHttpInitialize 处理器类
对之前的ChannelInitialize(SocketChannel)进行封装
package netty.http;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
/**
* @author: zhangyao
* @create:2020-09-04 11:21
**/
public class NettyHttpInitialize extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//得到管道
ChannelPipeline pipeline = sc.pipeline();
//管道中加入处理器 主要是处理Http请求的,解析请求头之类的
pipeline.addLast("myDecoder",new HttpServerCodec());
//加入处理器
pipeline.addLast("myServerHandler",new NettyServerHandler());
}
}
NettyServerHandler 具体的处理(返回http响应)
package netty.http;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import org.springframework.http.HttpStatus;
/**
* @author: zhangyao
* @create:2020-09-04 14:01
**/
public class NettyServerHandler extends SimpleChannelInboundHandler<HttpObject> {
//读信息
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) throws Exception {
System.out.println(httpObject);
System.out.println("客户端地址+"+ channelHandlerContext.channel().remoteAddress());
ByteBuf byteBuf = Unpooled.copiedBuffer("hello , im 服务端", CharsetUtil.UTF_8);
//返回客户端信息
FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.OK,byteBuf);
fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");
fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH,byteBuf.readableBytes());
channelHandlerContext.writeAndFlush(fullHttpResponse);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
出现的问题记录:
- 第一次绑定6668端口,浏览器访问失败,换成7001就可以了
原因: 谷歌浏览器禁用了6665-6669以及其他一些不安全的端口
- 访问后第一次请求 出现异常,可以正常返回数据
原因: NettyServerHandler中没有对异常处理方法进行重写
加上这部分就可以了,报错信息也报的很明显
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
1.2.6.2 Http服务的过滤
可以对一些不希望处理的请求进行过滤,其实就是在对http请求的处理过程中判断一下请求的uri
在上文中 NettyServerHandler类中 加入以下代码,即可拦截/favicon.ico请求
HttpRequest re = (HttpRequest) httpObject;
String uri = re.uri();
if(uri.equals("/favicon.ico")){
System.out.println("不想处理,返回");
return;
}