教程:Netty核心技术及源码剖析——尚硅谷
前置课程:JAVA OOP编程、多线程编程、IO编程、网络编程、常用设计模式
书籍:
《Netty IN ACTION》
《Netty 权威指南》(基于Netty5)
《Netty 进阶之路》
从上至下:
Netty
NIO
原生JDK io/网络
TCP/IP
[Socket] —— (read/write) —— [Thread]
流程:
用命令行窗口创建客户端
>telnet 127.0.0.1 portId
# 进入127.0.0.1窗口之后 输入 ctrl+]
> send hello #即可发送hello
客户端
问题分析:
NIO与网络编程
一个server,新建一个Thread,对应一个Selector,每个Selector可以在多个通道之间选择,一个通道对应一个Buffer,双向读写,每个Buffer对应一个连接。
NIO相关的Selector、 SelectionKey、ServerSocketChannel 和 SocketChannel 关系梳理
服务端:
public class NIOServer {
public static void main(String[] args) {
final int portId = 6666;
try (
// 创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
) {
System.out.println("服务器(监听端口:"+portId+")启动......");
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(portId));
//设为非阻塞
serverSocketChannel.configureBlocking(false);
// 注册选择器
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while (true){
System.out.println("等待客户端连接......");
//监听,等待客户连接
if (selector.select(1000)==0){//没有时间发生
// 或selectNow()
System.out.println("服务器等待了1秒,无连接。");
continue;
}
//如果返回的>0,表示以及获取到关注的事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
if (key.isAcceptable()){//有新的客户端连接
//给客户端生成一个socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//将当前socketChannel也注册到选择器,同时关联一个Buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接成功");
}else if (key.isReadable()){
//通过 key反向获取channel
SocketChannel channel = (SocketChannel) key.channel();
// 获取 关联buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("来自客户端"+new String(buffer.array()));
}
// 手动移除key,防止重复操作
keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
public class NIOClient {
public static void main(String[] args) {
try (SocketChannel socketChannel = SocketChannel.open()) {
socketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器,如果连接失败
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他工作");
}
}
//如果连接成功
String str = "hello,链接成功";
// 产生一个buffer,里面存储str,buffer的大小等于str字节数组的大小
ByteBuffer wrap = ByteBuffer.wrap(str.getBytes());
//发送数据,buffer数据写入channel
socketChannel.write(wrap);
//会阻塞在此处
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
出现了异常
Exception in thread "main" java.nio.channels.IllegalBlockingModeException
at java.nio.channels.spi.AbstractSelectableChannel.register(AbstractSelectableChannel.java:201)
at com.km.mpdemo001.netty.nio.NIOServer.main(NIOServer.java:59)
原因:未将连接的socketChannel设为非阻塞模式
//给客户端生成一个socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
有个问题:会重复打印接收数据?
解决:把给客户端生成的socketChannel 关掉, channel.close();
代码有蛮多问题,先不放。
傻瓜三歪让我教他「零拷贝」——敖丙
零拷贝是网络编程的关键,与性能优化有关。
DMA:direct memory access
0拷贝指的不是不拷贝,而是没有CPU copy
传统IO经过4次拷贝,3次切换
mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样在网络传输时,就可以减少内核空间到用户空间的拷贝次数。
sendFile优化
linux2.1版本提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到Socket Buffer,同时由于和用户态完全无关,减少了一次上下文切换。
Linux2.4版本。避免了从内核缓冲区拷贝到Socket buffer的操作,从而再一次减少了数据拷贝
mmap和sendFile的区别
NIO的零拷贝方式传递(transferTo)一个大文件
BIO | NIO | AIO | |
---|---|---|---|
IO模型 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
编程难度 | 易 | 难 | 难 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |
原生NIO存在的问题
https://netty.io/
知名的Elasticsearch Dubbo框架内部都采用了Netty
Netty的优点:
问题分析:
Reactor模式:反应器模式/分发者(dispatcher)模式/通知者(notifier)模式
针对传统阻塞IO服务模型的2个缺点,解决方案:
reactor模式,通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动)
服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,因此也叫分发者(dispatcher)模式。
reactor模式使用IO复用监听事件,收到事件后,分发给某个线程(进程),这点就是网络服务器高并发处理关键。
单reactor单线程
优点:模型简单
缺点:
使用场景:
客户端数量有限,业务处理非常快,如redis在业务处理时间复杂度O(1)的情况
主从模型在许多项目中广泛使用:Nginx、Memcached、Netty
实现:
server 的handler:
package com.km.mpdemo001.netty.tcp;
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;
import java.nio.charset.Charset;
/**
* @description: 自定义Handler
*
* @author: 大颗
* @time: 2020/10/11 19:35
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 读取数据
* @param ctx : 上下文对象,含有pipeline,channel,地址
* @param msg : 客户端发送的数据,默认Object类型
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx: "+ctx);
//将msg转成一个 ByteBuf,注意不是java.nio.ByteBuffer;
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 {
//将数据写入到缓存,并刷新 writeAndFlush(Object:msg)
//一般对msg进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
}
/**
* 处理异常,一般需要关闭通道
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
client的handler:
/**
* @description: 自定义Handler
*
* @author: 大颗
* @time: 2020/10/11 19:35
*/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 读取数
* @param ctx : 上下文对象,含有pipeline,channel,地址
* @param msg : 客户端发送的数据,默认Object类型
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx: "+ctx);
//将msg转成一个 ByteBuf,注意不是java.nio.ByteBuffer;
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 {
// //将数据写入到缓存,并刷新 writeAndFlush(Object:msg)
// //一般对msg进行编码
// ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端",CharsetUtil.UTF_8));
// }
/**
* 处理异常
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
/**
* 当通道就绪就会触发该方法
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client: " +ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello ,server",CharsetUtil.UTF_8));
}
}
server:
public class NettyServer {
public final static int PORT_ID = 6668;
public static void main(String[] args) {
/**
* 1. 创建两个线程组 bossGroup和workerGroup,无限循环
* workerGroup 连接请求
* bossGroup 读写请求
*/
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//创建服务器端的启动对象
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行设置
bootstrap.group(bossGroup,workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioSocketChannel作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG,128)//设置线程队列等待连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true) // 设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() { //创建一个通道初始化测试(匿名)对象
//给pipeline设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyServerHandler());
}
}); //给workerGroup的EventLoop对应的管道设置处理器
System.out.println("服务器 is ready .....");
//绑定一个端口并且同步,生成了一个 ChannelFuture 对象
//启动服务器
ChannelFuture cf = bootstrap.bind(PORT_ID).sync();
//对关闭通道进行监听
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
client:
public class NettyClient {
public final static int PORT_ID = 6668;
public final static String aInetHost = "127.0.0.1";
public static void main(String[] args) {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
// 注意:客户端是io.netty.bootstrap.Bootstrap,服务端是io.netty.bootstrap.ServerBootstrap;
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("客户端启动......");
ChannelFuture channelFuture = bootstrap.connect(aInetHost, PORT_ID).sync();
//对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
new NioEventLoopGroup()的children大小默认为CPU核心数*2,如果要设置,可以初始化时传入参数。
toString():
ctx: ChannelHandlerContext(NettyServerHandler#0, [id: 0x92760a4d, L:/127.0.0.1:6668 - R:/127.0.0.1:55693])
toString():
channel:[id: 0x92760a4d, L:/127.0.0.1:6668 - R:/127.0.0.1:55693]
pipeline: DefaultChannelPipeline{(NettyServerHandler#0 = com.km.mpdemo001.netty.tcp.NettyServerHandler)}
pipeline是一个双向链表
pipeline和channel是一一对应的关系
任务队列中的task有3种典型使用场景
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端; tag2\n",CharsetUtil.UTF_8));
}
});
类的关系:
taskQueue中的Runnable依次在一个线程中执行: poll and run
NioEventLoop extends SingleThreadEventLoop
.......
SingleThreadEventLoop extends SingleThreadEventExecutor
.......
public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
private final Executor executor;
private final Queue<Runnable> taskQueue;
......//略
@Override
public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}
boolean inEventLoop = inEventLoop();
if (inEventLoop) {
addTask(task);
} else {
startThread();
addTask(task);
if (isShutdown() && removeTask(task)) {
reject();
}
}
if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}
}
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端; tag4\n",CharsetUtil.UTF_8));
}
},5, TimeUnit.SECONDS);
SingleThreadEventExecutor extends AbstractScheduledEventExecutor...
public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor {
PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue;
@Override
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
ObjectUtil.checkNotNull(command, "command");
ObjectUtil.checkNotNull(unit, "unit");
if (delay < 0) {
delay = 0;
}
return schedule(new ScheduledFutureTask<Void>(
this, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay))));
}
}
基本概念:
Future-Listener机制
当有Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作。
/**
* 注册监听器
*/
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()){
System.out.println("监听端口"+PORT_ID+"成功。");
}else {
System.out.println("监听端口"+PORT_ID+"失败。");
}
}
});
server编写与上面基本一样,只是把ServerInitializer 写一个自定义的。
public class ServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//加入一个netty提供的 处理Http的编解码器
pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
pipeline.addLast("MyHttpServerHandler",new HttpServerHandler());
}
}
同时,handler的代码:
/**
* @description:
* SimpleChannelInboundHandler是ChannelInboundHandlerAdapter的子类
* HttpObject 客户端和服务器端相互通信的数据被封装成这个类型
* @author: 大颗
* @time: 2020/10/11 22:31
*/
public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
/**
* 读取客户端数据
* @param ctx
* @param msg
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
//判断msg是不是一个HttpRequest请求
if(msg instanceof HttpRequest){
System.out.println("msg Class type = "+msg.getClass());
System.out.println("客户端地址:"+ctx.channel().remoteAddress());
//回复信息给浏览器
ByteBuf content = Unpooled.copiedBuffer("hello, i'm a server", CharsetUtil.UTF_8);
//构建一个 http response
DefaultHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.OK, content);
response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());
//发送response
ctx.writeAndFlush(response);
}
}
}
有一个问题,浏览器每次发出请求,上面handler的channelRead0方法都会执行两次。
原因:浏览器点击一次,其实发出了两个请求,一个是网内页面的请求,一个是对网页图标的请求。那么可以尝试过滤掉第二个请求。
HTTP协议是无状态的协议,因此不同的请求对应的handler不是同一个。
一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务程序的启动引导类,均继承自AbstractBootstrap。
常用的方法有:
AbstractBootstrap:
public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable{
volatile EventLoopGroup group;
public B group(EventLoopGroup group)
public B channel(Class<? extends C> channelClass)
public <T> B option(ChannelOption<T> option, T value) {......}
public ChannelFuture bind() {
}
ServerBootstrap:
public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup){......}
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) {.......}
public ServerBootstrap childHandler(ChannelHandler childHandler) {......}
}
public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
public ChannelFuture connect(String inetHost, int inetPort) {......}
}
入站:数据流入channel
子类
ChannelPipeline:
Pipeline和ChannelPipeline: