Netty介绍
1)Netty是由JBOSS提供的一个Java开源框架,现为Github上独立的项目
2)Netty是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠的网络IO程序
3)Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输应用
4)Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
5)要透彻理解Netty,需要先学习NIO,这样我们才能Netty源码

BIO
I/O模型基本说明
1) I/O模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
2)Java共支持3种网络编程模式:BIO、NIO、AIO
3)Java BIO:同步并阻塞(传统阻塞型)服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销

4)Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理

5)Java AIO:异步非阻塞,AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序去处理,一般用于连接数较多且连接时间较长的应用
BIO、NIO、AIO适用的场景分析(面试题)
1)BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前唯一的选择,但程序简单易理解
2)NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等,编程比较复杂,JDK1.4开始支持
3)AIO方式使用于连接数目多且链接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持
Java BIO(blocking io)基本介绍
1)Java BIO 就是传统的java io 编程,其相关的类和接口在java.io
2) BIO 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户端连接服务器)
3)BIO方式用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解
java BIO 应用实例
实例说明
1)使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯
2)要求使用线程池机制改善,可以连接多个客户端
3)服务器可以接收客户端发送的数据(telnet方式即可)
4)代码演示
Java BIO 问题分析
1)每个请求都需要创建独立的线程,与对应的客户端进行数据Read,业务处理,数据Write
2)当并发较大时,需要创建大量线程来处理连接,系统资源占用较大
3)连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在Read操作上,造成线程资源浪费
NIO
Java NIO基本介绍
1)Java NIO 全称 java non-blocking IO, 是指JDK提供的新API。从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,被统称为NIO(即New IO),是同步非阻塞的
2)NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。
3)NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
4)NIO是面向缓冲区,或者面向块编程的,数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就是增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
NIO和BIO的比较
1)BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多
2)BIO是阻塞的,NIO则是非阻塞的
3)BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入通道中。Selector(选择器)用于监听多个通道,因此使用单个线程就可以监听多个客户端通道
Selector、Channel和Buffer的关系
1)每个channel都会对应一个Buffer
2)Selector 对应一个线程,一个线程对应多个channel(连接)
3)程序切换到哪个channel是由事件决定的,Event就是一个重要的概念
4)Selector会根据不同的事件,在各个通道上切换
5)Buffer就是一个内存块,底层是有一个数组
6)数据的读取写入是通过Buffer, BIO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写,需要flip方法切换
7)channel是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的。
channel(通道)
NIO通道类似于流,但是有些区别如下:
通道可以同时进行读写,而流只能读或者只能写
通道可以实现异步读写数据
通道可以从缓冲读数据,也可以写数据到缓冲

channel基本介绍
1)BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作
2)Channel在NIO中是一个接口
public interface Channel extends Closeable{}
3) 常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel
4)FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写
FileChannel类
public int read(ByteBuffer dst) //从通道读取数据并放到缓冲区
public int write(ByteBuffer src) //把缓冲区的数据写到通道中
public long transferFrom(ReadableByteChannel src, long position, long count) //从目标通道复制数据到当前通道
public long transferTo(long position, long count, WritableByteChannel target) //把数据从当前通道复制给目标通道
应用实例本地文件写数据
1)使用前面学习后的ByteBuffer(缓冲)和FileChannel(通道),将“hello,尚硅谷”写入到“file01.txt中”
2)文件不存在就创建
3)代码
package com.hzz.netty.nio;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @Author: curry
* @Date: Created in 15:38 2023/9/6
* @Modified By:
* @description:
*/
public class NIOFileChannel01 {
public static void main(String[] args) throws Exception {
String str = "hello,尚硅谷";
//创建一个输出流->channel
FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
//通过fileOutputStream 获取对应的 FileChannel
//这个fileChannel真实类型是 FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
//创建一个缓冲区 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将str放入byteBuffer
byteBuffer.put(str.getBytes());
//对byteBuffer 进行flip
byteBuffer.flip();
//将byteBuffer数据写入到fileChannel
fileChannel.write(byteBuffer);
fileOutputStream.close();;
}
}
本地文件内容读取的控制台
public class NIOFileChannel02 {
public static void main(String[] args) throws Exception {
//创建文件的输入流
File file = new File("d:\\file01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
//通过fileInputStream获取对应的FileChannel->实际类型 FileChannelImpl
FileChannel fileChannel =fileInputStream.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
//将通道的数据读入到Buffer
fileChannel.read(byteBuffer);
//将byteBuffer 的字节数据转成String
System.out.println(new String(byteBuffer.array()));
}
}
应用实例3-使用一个Buffer完成文件读取
实例要求:
1)使用FileChannel(通道)和方法 read write 完成文件的拷贝
2)拷贝一个文本文件1.txt,放在项目下即可
3)代码
package com.hzz.netty.nio;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @Author: curry
* @Date: Created in 16:34 2023/9/6
* @Modified By:
* @description:
*/
public class NIOFileChannel03 {
public static void main(String[] args) throws Exception {
File file = new File("d:\\file01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel fileChannel01 = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel fileChannel02 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while(true){
//这里有一个重要的操作,一定不要忘了
/*position = 0;
limit = capacity;
mark = -1;
return this;*/
byteBuffer.clear();
int read = fileChannel01.read(byteBuffer);
if (read ==-1){ //标识读完
break;
}
//将buffer中的数据写入到fileChannel02 --- 2.txt
byteBuffer.flip();
fileChannel02.write(byteBuffer);
}
}
}
Selector(选择器)
Selector是一个抽象类,常用方法和说明如下
public abstract class Selector implements Clossable{
public static Selector open(); //得到一个选择器对象
public int select(long timeout); //监控所有注册的通道,当其中有IO操作可以进行时将对应的的SelectionKey加入到内部集合并返回,参数用来设置超时时间
public Set<SelectionKey> selectKeys();//从内部集合中得到所有的SelectionKey
}
selector.select() //阻塞
selector.select(1000) //阻塞1000毫秒,在1000毫秒后返回
selector.wakeup() //唤醒selector
selector.selectorNow() //不阻塞,立马返回
NIO非阻塞网络相关的(Selector、SelectorKey、ServerScoketChannel和SocketChannel)
1、当客户端连接时,会通过ServerSocketChannel得到SocketChannel
2、将socketChannel注册到Selector上,registor(Selector sel, int ops) 一个selector上可以注册多个SocketChannel
3、注册后返回一个SelectorKey 会和该Selector关联(集合)
4、Selector进行监听select 方法返回有事件发生的通道的个数
5、进一步得到各个SelectorKey(有事件发生)
6、再通过SeletorKey反向获取SocketChannel,方法channel()
7、可以通过得到channel完成业务处理
ServerSocketChannel
1)ServerSocketChannel在服务器端监听新的客户端Socket连接
2)相关方法如下
public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel{
public static ServerSocketChannel open(); //得到一个ServerSocketChannel通道
public final ServerSocketChannel bind(SocketAddress local); //设置服务器端端口号
public final SelectableChannel configureBlocking(boolean block); //设置阻塞或非阻塞模式,取值false表示采用非阻塞模式
public SocketChannel accept(); //接受一个连接,返回代表这个连接的通道对象
public final SelectionKey register(Selector sel, int ops); //注册一个选择器并设置监听事件
}
群聊系统
服务器端代码
package com.hzz.netty.nio.groupchat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* @Author: curry
* @Date: Created in 14:54 2023/9/27
* @Modified By:
* @description:
*/
public class GroupChatServer {
//定义属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6667;
//构造器
//构造器初始化工作
public GroupChatServer() {
try {
//得到选择器
selector=Selector.open();
//初始化ServerSocketChannel
listenChannel = ServerSocketChannel.open();
//绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞模式
listenChannel.configureBlocking(false);
//将该listenChannel注册到selector
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
}catch (Exception e){
e.printStackTrace();
}
}
//监听
public void listen(){
try {
//循环处理
while (true){
int count = selector.select();
if (count>0){ //有事件处理
//遍历得到selectionKey集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
//取出selectorKey
SelectionKey key = iterator.next();
//监听到accept
if (key.isAcceptable()){
SocketChannel sc = listenChannel.accept();
sc.configureBlocking(false);
//将该sc注册到selector
sc.register(selector,SelectionKey.OP_READ);
//提示
System.out.println(sc.getRemoteAddress()+"上线");
}
if(key.isReadable()){ //通道发送read事件,即通道是可读的状态
//处理读(专门写方法...)
read(key);
}
//当前的key删除,防止重复处理
iterator.remove();
}
}else {
System.out.println("等待....");
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
}
}
/**
* 读取客户端消息
* @param key
*/
private void read(SelectionKey key){
//定义一个SocketChannel
SocketChannel channel = null;
try {
//得到channel
channel =(SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
//根据count的值做处理
if (count>0){
//把缓存去的数据转成字符串
String msg = new String(buffer.array());
System.out.println("form 客户端:"+msg);
//向其他客户端转发消息(去掉自己),专门写一个方法来处理
sendInfoToOtherClients(msg,channel);
}
}catch (IOException e){
try {
System.out.println(channel.getRemoteAddress()+"离线了...");
//取消注册
key.cancel();
//关闭通道
channel.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
/**
* 转发消息给其他客户端(通道)
* @param msg
* @param self
*/
private void sendInfoToOtherClients(String msg,SocketChannel self) throws IOException {
System.out.println("服务器转发消息中...");
//遍历 所有注册到selector 上的SocketChannel并排除self
for (SelectionKey key : selector.keys()) {
//通过key取出对应的SocketChannel
Channel targetChannel = key.channel();
//排除自己
if(targetChannel instanceof SocketChannel && targetChannel !=self){
//转型
SocketChannel dest = (SocketChannel)targetChannel;
//将msg存储到buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将buffer数据写入通道
dest.write(buffer);
}
}
}
public static void main(String[] args) {
//创建一个服务器对象
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
客户端代码
package com.hzz.netty.nio.groupchat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
/**
* @Author: curry
* @Date: Created in 15:37 2023/9/27
* @Modified By:
* @description:
*/
public class GroupChatClient {
//定义相关属性
private final String HOST="127.0.0.1"; //服务器的ip
private final int PORT = 6667; //服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String username;
//构造器,完成初始化工作
public GroupChatClient() throws IOException {
selector = Selector.open();
//连接服务器
socketChannel = socketChannel.open(new InetSocketAddress(HOST, PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//将channel注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
//得到username
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username+ " is ok...");
}
//向服务器发送消息
public void sendInfo(String info){
info = username + "说" +info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取服务器端回复的消息
*/
public void readInfo(){
try {
int readChannels = selector.select();
if (readChannels>0){ //有可以用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if (key.isReadable()){
//得到相关的通道
SocketChannel sc = (SocketChannel) key.channel();
//得到一个Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
//把读到的缓冲区的数据转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim()+"haha");
}
}
iterator.remove(); //删除当前的selectionKey,防止重复操作
}else{
System.out.println("没有可以用的通道...");
}
}catch (Exception e){
}
}
public static void main(String[] args) throws IOException {
//启动我们的客户端
GroupChatClient chatClient = new GroupChatClient();
//启动一个线程,每隔3秒,读取从服务器发送的数据
new Thread(){
public void run(){
while (true){
chatClient.readInfo();
try{
Thread.currentThread().sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}.start();
//发送数据给服务器
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String s = scanner.nextLine();
chatClient.sendInfo(s);
}
}
}
NIO与零拷贝
1)mmap适合小数据量读写,sendFile适合大文件传输
2)mmap需要4次上下文切换,3次数据拷贝,sendFile需要3次上下文切换,最少2次数据拷贝
3)sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)
netty底层原理
Reactor模式
单Reactor单线程
优点:模型简单,没有多线程,进程通信、竞争的问题,全部都在一个线程中完成
缺点:性能问题,只有一个线程,无法完全发挥多核CPU的性能。Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈
单Reactor多线程
主从Reactor多线程
netty线程模式
Netty主要基于主从Reactor多线程模型做了一定的改进,其中主从Reactor多线程模型有多个Reactor
netty模型

NettyServer
package com.hzz.netty.netty.simple;
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: curry
* @Date: Created in 15:43 2023/10/11
* @Modified By:
* @description:
*/
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
//创建BossGroup和WorkerGroup
//1.创建两个线程组bossGroup和workerGroup
//2.bossGroup只是处理连接请求,真正的和客户端处理业务,会交给workerGroup完成
//3.两个都是无限循环
//4.bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
//默认实际cpu核数 * 2
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(8);
try {
//创建服务器端的启动对象,配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来进行配置
bootstrap.group(bossGroup,workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioServerSocketChannel作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG,128) //设置线程池队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true) //设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() { //创建一个通道初始化对象(匿名对象)
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}); //给我们的workerGroup的EventLoop对应的管道设置处理器
System.out.println("...服务器 is ready...");
//绑定一个端口并且同步处理,生成一个ChannelFuture对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//对关闭通道进行监听
cf.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
NettyServerHandler
package com.hzz.netty.netty.simple;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;
import java.nio.charset.Charset;
/**
* @Author: curry
* @Date: Created in 16:22 2023/10/11
* @Modified By:
* @description: 1. 自定义一个handler需要继承netty规定好的某个HandlerAdapter
*
*/
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("服务器读取线程"+Thread.currentThread().getName());
System.out.println("server ctx =" + ctx);
System.out.println("看看channel和pipeline的关系");
Channel channel = ctx.channel();
ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链表,出站入站
//将msg转成一个ByteBuf
//ByteBuf 是 Netty 提供的,不是NIO的ByteBuffer
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送的消息是:"+buf.toString(CharsetUtil.UTF_8));
System.out.println("客户端地址:"+ctx.channel().remoteAddress());
}
/**
* 数据读取完毕
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//writeAndFlush 是write+flush
//将数据写入到缓存,并刷新
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.channel().close();
}
}
NettyClient
package com.hzz.netty.netty.simple;
import io.netty.bootstrap.Bootstrap;
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: curry
* @Date: Created in 16:49 2023/10/11
* @Modified By:
* @description:
*/
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//客戶端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是ServerBootStrap而是Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) //设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("客户端 ok...");
//启动客户端去连接服务器端
//关于ChannelFuture要分析,涉及到netty的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
NettyClientHandler
package com.hzz.netty.netty.simple;
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: curry
* @Date: Created in 15:15 2023/10/12
* @Modified By:
* @description:
*/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 当通道就绪就会触发该方法
* @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));
}
/**
* 当通道有读取事件就会触发
* @param ctx
* @param msg
* @throws Exception
*/
@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 {
cause.printStackTrace();
ctx.close();
}
}
Netty群聊
GroupChatServer
package com.hzz.netty.netty.groupchat;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
/**
* @Author: curry
* @Date: Created in 13:09 2023/10/20
* @Modified By:
* @description:
*/
public class GroupChatServer {
private int port; //监听端口
public GroupChatServer(int port) {
this.port = port;
}
public void run() throws InterruptedException {
//创建两个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workGroup = new NioEventLoopGroup(); //8个NioEventLoop
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childOption(ChannelOption.SO_KEEPALIVE,true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//获取到pipeline
ChannelPipeline pipeline = ch.pipeline();
//向pipeline中加入解码器
pipeline.addLast("decoder",new StringDecoder());
//向pipeline中加入编码器
pipeline.addLast("encoder",new StringEncoder());
//加入自己的业务处理handler
pipeline.addLast(new GroupChatServerHandler());
}
});
System.out.println("netty 服务器启动");
ChannelFuture channelFuture = bootstrap.bind(port).sync();
//监听关闭时间
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
new GroupChatServer(7000).run();
}
}
GroupChantServerHandler
package com.hzz.netty.netty.groupchat;
import com.hzz.signmode.interpreter.VarExpression;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @Author: curry
* @Date: Created in 13:27 2023/10/20
* @Modified By:
* @description:
*/
public class GroupChatServerHandler extends SimpleChannelInboundHandler {
//定义一个channel组,管理所有的channel
//GlobalEventExecutor.INSTANCE 是全局的事件执行器,是一个单例
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 一旦连接建立,第一个被执行
* 将当前channel加入到channelGroup
* @param ctx
* @throws Exception
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
//将该客户加入聊天的信息推送给在线的客户端
/**
* 该方法会将channelGroup中的所有channel遍历,并发送 我们不需要自己遍历
*/
channelGroup.writeAndFlush("[客户端]"+channel.remoteAddress()+"加入聊天"+sdf.format(new Date())+"\n");
channelGroup.add(channel);
}
/**
* 断开连接,将xxx客户离开的信息推送给当前在线的客户
*
* @param ctx
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
channelGroup.writeAndFlush("[客户端]"+channel.remoteAddress()+"离开了\n");
System.out.println("channelGroup size"+channelGroup.size());
}
/**
* 表示channel处于活动状态,提示xxx上线
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
System.out.println(channel.remoteAddress()+"上线了~");
}
/**
* 表示channel处于非活动状态,提示xxx下线了
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress()+"离线了~");
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
//获取到当前的channel
Channel channel = ctx.channel();
//这时我们遍历channelGroup,根据不同的情况,回送不同的消息
channelGroup.forEach(ch->{
if(!channel.equals(ch)){ //不是当前的channel,转发消息
ch.writeAndFlush("[客户]"+channel.remoteAddress()+"发送消息"+msg+sdf.format(new java.util.Date())+"\n");
}else{
ch.writeAndFlush("[自己发送了消息]"+msg+"\n");
}
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//关闭通道
ctx.close();
}
}
GroupChatClient
package com.hzz.netty.netty.groupchat;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.util.Scanner;
/**
* @Author: curry
* @Date: Created in 14:18 2023/10/20
* @Modified By:
* @description:
*/
public class GroupChatClient {
//属性
private final String host;
private final int port;
public GroupChatClient(String host, int port) {
this.host = host;
this.port = port;
}
public void run() throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//得到pipeline
ChannelPipeline pipeline = ch.pipeline();
//加入相关的handler
pipeline.addLast("decoder",new StringDecoder());
pipeline.addLast("encoder",new StringEncoder());
//加入自定义的
pipeline.addLast(new GroupChatClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
//得到channel
Channel channel = channelFuture.channel();
System.out.println("--------"+channel.localAddress()+"--------");
//客户端需要输入信息,创建一个扫描器
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String msg = scanner.nextLine();
channel.writeAndFlush(msg+"\r\n");
}
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new GroupChatClient("127.0.0.1",7000).run();
}
}
GroupChatClientHandler
package com.hzz.netty.netty.groupchat;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* @Author: curry
* @Date: Created in 14:59 2023/10/20
* @Modified By:
* @description:
*/
public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(msg.trim()+"SB");
}
}
Netty通过WebSocket编程实现服务器和客户端长连接
hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
var socket;
//判断当前浏览器是否支持websocket
if (window.WebSocket){
//go on
socket = new WebSocket("ws://localhost:7000/hello")
//相当于channelRead0, ev收到服务器端回送的消息
socket.onmessage = function (ev) {
var rt = document.getElementById("responseText")
rt.value = ev.data;
}
//相当于连接开启(感知到连接开启)
socket.onopen = function (ev) {
var rt = document.getElementById("responseText")
rt.value="连接开启了..."
}
//相当于连接关闭了(感知到连接关闭了)
socket.onclose = function (ev) {
var rt = document.getElementById("responseText")
rt.value=rt.value+"\n"+"连接关闭了..."
}
}else{
alert("当前浏览器不支持websocket")
}
function send(message) {
if (!window.socket){ //先判断socket是否创建好
return;
}
if (socket.readyState = WebSocket.OPEN){
//通过socket发送消息
socket.send(message)
}else{
alert("连接没有开启")
}
}
</script>
<form onsubmit=" return false">
<textarea name="message" style="height: 300px; width: 300px"></textarea>
<input type="button" value="发生消息" onclick="send(this.form.message.value)">
<textarea id="responseText" style="height: 300px; width: 300px"></textarea>
<input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
</form>
</body>
</html>
MyServer
package com.hzz.netty.netty.websocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* @Author: curry
* @Date: Created in 16:06 2023/10/24
* @Modified By:
* @description:
*/
public class MyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler())
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//因为基于http协议,使用http的编码和解码器
pipeline.addLast(new HttpServerCodec());
//是以块方式处理,添加ChunkedWriteHandler处理器
pipeline.addLast(new ChunkedWriteHandler());
/**
* 说明
* 1. http数据在传输过程中是分段的,httpObjectAggregator,就可以将多段聚合
* 2. 这就是为什么, 当浏览器发送大量数据时,就会发出多次http请求
*/
pipeline.addLast(new HttpObjectAggregator(8192));
/**
* 说明
* 1. 对于webSocket,它的数据是以帧(frame)形式传递的
* 2. 可以看WebSocketFrame下面有六个子类
* 3. 浏览器请求时: ws://localhost:7000/xxx 表示请求的uri
* 4. WebSocketServerProtocolHandler 核心功能将一个http协议升级为ws协议,保持长连接
*
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
//自定义handler处理业务逻辑
pipeline.addLast(new MyTextWebSocketFrameHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
MyTextWebSocketFrameHandler
package com.hzz.netty.netty.websocket;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.time.LocalDateTime;
/**
* @Author: curry
* @Date: Created in 16:57 2023/10/24
* @Modified By:
* @description:
*/
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("服务器接收到消息"+msg.text());
//回复消息
ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间"+ LocalDateTime.now()+""+msg.text()));
}
/**
* Do nothing by default, sub-classes may override this method.
* 当web客户端连接后,触发方法
*
* @param ctx
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
//id表示唯一的一个值 LongText是唯一的, asShortText不是唯一的
System.out.println("handlerAdded 被调用"+ctx.channel().id().asLongText());
System.out.println("handlerAdded 被调用"+ctx.channel().id().asShortText());
}
/**
* Do nothing by default, sub-classes may override this method.
*
* @param ctx
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved 被调用"+ctx.channel().id().asLongText());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常发生"+cause.getMessage());
ctx.close(); //关闭连接
}
}
netty出站和入站
从socket到channelPipeline叫入站
从channelPipeline到socket叫出站
netty自定义数据包解决粘包,拆包问题
1、创建myServer服务端,创建bossGroup,workerGroup,创建ServerBootStrap;
serverBootStrap绑定bossGroup,workerGroup,定义channel为NioServerChannel.class,定义 MyServerInitializer,serverBootStrap绑定端口
package com.hzz.netty.netty.protocoltcp;
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: curry
* @Date: Created in 15:06 2023/11/7
* @Modified By:
* @description:
*/
public class MyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class).childHandler(new MyServerInitializer()); //自定义一个初始化类
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
2、创建MyServerInintializer继承ChannelInitializer<SocketChannel> 重写ChannelInitializer的 initChannel方法,在initChannel中添加 解码器handler,自定义MyServerHandler
package com.hzz.netty.netty.protocoltcp;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
/**
* @Author: curry
* @Date: Created in 16:16 2023/11/8
* @Modified By:
* @description:
*/
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyMessageDecoder());
pipeline.addLast(new MyServerHandler());
}
}
3、创建解码器handler MyMessageDecoder 继承ReplayingDecorder<Void>
package com.hzz.netty.netty.protocoltcp;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;
import java.util.List;
/**
* @Author: curry
* @Date: Created in 15:38 2023/11/9
* @Modified By:
* @description:
*/
public class MyMessageDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("MyMessageDecoder 方法被调用");
//这里需要将得到的二进制字节码->MessageProtocol 数据包(对象)
int len = in.readInt();
byte[] content = new byte[len];
in.readBytes(content);
//封装成messageProtocol对象
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(len);
messageProtocol.setContent(content);
out.add(messageProtocol);
}
}
4、创建自定义MyServerHandler 继承SimpleChannelInboundHandle<要传输的数据格式>,重写 channeRead0方法
package com.hzz.netty.netty.protocoltcp;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
import java.util.UUID;
/**
* @Author: curry
* @Date: Created in 16:19 2023/11/8
* @Modified By:
* @description:
*/
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
private int count;
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
/*yte[] buffer = new byte[msg.readableBytes()];
msg.readBytes(buffer);
//将buffer转成一个字符串
String message = new String(buffer, Charset.forName("utf-8"));
System.out.println("服务器端接收到数据"+message);
System.out.println("服务器端接收到消息量="+(++this.count));
//服务器回送数据给客户端,回送一个随机id
ByteBuf responseByteBuf = Unpooled.copiedBuffer(UUID.randomUUID().toString(), Charset.forName("utf-8"));
ctx.writeAndFlush(responseByteBuf);*/
//接收到数据,并处理
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println("服务端接收到信息如下");
System.out.println("长度="+len);
System.out.println("内容="+new String(content,Charset.forName("utf-8")));
System.out.println("服务器接收到消息包数量="+(++this.count));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
5、创建myClient客户端,创建NioEventLoopGroup,创建Bootstrap,bootstrp绑定 NioEventLoopGroup,channel为NioSocketChannel.class,绑定MyClientInitializer,bootstrap 绑定主机名和端口
package com.hzz.netty.netty.protocoltcp;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
/**
* @Author: curry
* @Date: Created in 15:27 2023/11/7
* @Modified By:
* @description:
*/
public class MyClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class).
handler(new MyClientInitializer());
ChannelFuture cha = bootstrap.connect("localhost", 7000).sync();
cha.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
6、创建MyClientInitializer继承Initializer<SocketChannel> 重写initChannel方法,在initChannel方法中加入自定义handler和编码器handler
package com.hzz.netty.netty.protocoltcp;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
/**
* @Author: curry
* @Date: Created in 16:08 2023/11/8
* @Modified By:
* @description:
*/
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyMessageEncoder());
pipeline.addLast(new MyClientHandler());
}
}
7、创建编码器handler MyMessageEncoder继承MessageToByteEncoder<要传输的数据格式>
package com.hzz.netty.netty.protocoltcp;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
/**
* @Author: curry
* @Date: Created in 15:34 2023/11/9
* @Modified By:
* @description:
*/
public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
System.out.println("MyMessageEncoder 被调用");
out.writeInt(msg.getLen());
out.writeBytes(msg.getContent());
}
}
8、创建MyClientHandler继承SimpleChannelInboundHandler<要传输的数据格式>
package com.hzz.netty.netty.protocoltcp;
import com.hzz.signmode.interpreter.VarExpression;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
/**
* @Author: curry
* @Date: Created in 16:11 2023/11/8
* @Modified By:
* @description:
*/
public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
private int count;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i=0; i<5;i++){
String msg ="今天天气冷,吃火锅";
byte[] content = msg.getBytes(Charset.forName("utf-8"));
int length = msg.getBytes(Charset.forName("utf-8")).length;
//创建协议包
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setContent(content);
messageProtocol.setLen(length);
ctx.writeAndFlush(messageProtocol);
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
/* byte[] bytes = new byte[msg.readableBytes()];
msg.readBytes(bytes);
String message = new String(bytes, Charset.forName("utf-8"));
System.out.println("客户端接收的消息="+message);
System.out.println("客户端接收消息的数量="+(++this.count));*/
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常消息是"+cause.getMessage());
ctx.close();
}
}
9、要传输的数据格式MessageProtocol
package com.hzz.netty.netty.protocoltcp;
/**
* @Author: curry
* @Date: Created in 15:20 2023/11/9
* @Modified By:
* @description: 协议包
*/
public class MessageProtocol {
private int len; //关键
private byte[] content;
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
}
ChannelPipline调度handler的源码剖析
1)Context包装handler,多个Context在pipline中形成了双向链表,入站叫inbound,有head节点开始,出站叫outhead,有tail节点开始
2)而节点中的传递通过AbstractChannelHandlerContext类内部的fire系列方法,找到当前节点的下一个节点不断的循环传播,是一个过滤器形式完成handler的调度
3)pipeline首先会调用Context的静态方fire,并传入context
4)然后静态方法调用Context的invoker方法,而invoker方法内部会调用Context所包含的Handler真正到的方法,调用结束如果还需要继续向后传递,就调用Context的fire方法,往复循环
Netty框架及相关I/O模型介绍
968

被折叠的 条评论
为什么被折叠?



