尚硅谷B站Netty教程:尚硅谷Netty视频教程(B站超火,好评如潮)_哔哩哔哩_bilibili
目录
1.需求:透彻理解需要会使用 NIO,才可以阅读 Netty源码
4.了解I/O模型:https://blog.csdn.net/weixin_45506581/article/details/12300357
7.4、关于 Buffer 和 Channel的注意事项和细节
1、SelectionKey,表示Selector 和网络通道的注册关系,共四种
10.ServerSocketChannel 和 SocketChannel相关的API
1.需求:透彻理解需要会使用 NIO,才可以阅读 Netty源码
-
Netty介绍
-
是由 JBOSS 提供的一个Java开源框架,现为 Github 上的独立项目
-
是一个异步的、基于事件驱动的网络应用框架,用于快速开发高性能。高可靠性的网络 IO 程序 [ 异步:发送请求后,即使没有等到回复,程序仍可以接着走,不会在那里一直等待。Ajax异步原理。 事件驱动:网页上点击按钮触发的方法或函数。]
-
主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Per场景下的大量数据持续传输的应用。
-
Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
2.Netty框架结构
底层 0:TCP/IP协议,网络通讯基石
1:java原生IO、网络开发
2:NIO的网络开发
3:在NIO的基础上,进行封装优化。才是Netty
示意图:
3.Netty应用场景
3.1、互联网行业
3.1.1:互联网行业,在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少, Ntty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。
3.1.2:典型的应用有:阿里分布式服务框架 Dubbo的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信
3.2、游戏行业
3.2.1:无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用
3.2.2:Netty作为高性能的基础通信组件,提供了TCP/UDP 和HTTP协议栈,方便定制和开发私有协议栈,账号登录服务器
3.2.3:地图服务器之间可以方便的通过Netty进行高性能的通信
3.3、大数据领域
3.3.1:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信
3.3.2:它的Netty Service 基于 Netty框架二次封装实现
其它开源项目使用到Netty
网址:https://netty.io/wiki/related-projects.html
4.了解I/O模型:https://blog.csdn.net/weixin_45506581/article/details/12300357
5.Java BIO
5.1.基本介绍
5.1.1:Java BIO就是传统的 java io 编程,其相关的类和接口在java.io
5.1.2:BIO(blocking I/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时,服务器就需要启动一个线程进行处理,如果这个连接不作任何事情就会造成不必要的开销,可通过线程池机制改善(并不能减少线程,实现多个客户连接服务器)
5.1.3:BIO编程简单流程
1.服务器端启动一个ServerSocket
2.客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个请求(客户)建立一个线程与之通讯
3.客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝
4.如果有响应,客户端线程会等待请求结束后,再继续执行
5.2.BIO实例
实例说明:
(1).使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯
(2).要求使用线程池机制改善,可以连接多个客户端
(3).服务器端可以接收客户端发送的数据(telnet 方式即可)
代码:
package com.wanshi.bio;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BioServer {
public static void main(String[] args) throws IOException {
//线程池机制
/*
* 思路
* 1.创建线程池
* 2.如果有客户端连接,就创建一个线程与之通讯(单独写一个方法)
*/
//创建线程池
ExecutorService newCachedThreadPool= Executors.newCachedThreadPool();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("====服务器已启动====");
while (true){
//监听,等待客户端连接
//!!!问题:如果没连接到客户端,则会在此阻塞,任何事情都做不了
Socket socket = serverSocket.accept();
//创建一个线程与之通讯(单写一个方法)
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() { //run方法可以重写
//可以和客户端通讯
//调用handler方法,执行通讯操作
handler(socket);
}
});
}
}
//编写一个handler方法,与客户端通讯,服务端与客户端通讯时需要拿到一个Socket,否则无法通讯
public static void handler(Socket socket){
try {
//打印线程信息,验证BIO的通讯机制:一个客户端一个线程
System.out.println("客户端已连接,线程id:"+Thread.currentThread().getId() +" 线程名称:"+Thread.currentThread().getName());
byte[] bytes = new byte[1024];
// 通过socket,获取客户端发过来的输入流
InputStream inputstream = socket.getInputStream();
int read=0;
//!!!问题:如果客户端没有数据传入,则会在此阻塞,不会进入循环和向下运行,性能很低
while ((read=inputstream.read(bytes))!=-1){ //发送的数据大小未知,循环读取客户端发送的数据
//输出客户端发送的数据,转换为字符串,从0开始,转到它所有的长度
System.out.println("线程 "+Thread.currentThread().getName()+":"+new String(bytes,0,read));
}
}catch (Exception e){
//输出异常
e.printStackTrace();
}finally {
//无论异常与否,都需要关闭socket与服务端的连接
try {
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
5.3.BIO应用实例执行效果
通过cmd执行 telnet 127.0.0.1 6666 命令后,可连接服务器,并且打印如下内容:
通过命令行窗口发送字符串,服务端接收到数据并打印:
新建一个连接后,可以看到BIO是通过新建一个线程,来处理此次连接:
两个线程操作互不影响:
运行方式示意图:
5.4.BIO的问题
1.每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理与数据 Write
2.当并发数较大时,需要创建大量的线程来处理链接,系统资源占用较大
3.链接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
6.Java NIO
6.1.NIO介绍
1、Java NIO 全称 Java non-blocking IO,是指 JDK提供的新 API。从 JKD 1.4 开始,Java提供了一系列改进的输入/输出的新特性,被称为 NIO(New IO),是同步非阻塞的
2、NIO 相关的类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写
3、NIO 有三大核心部分:Channel(通道),Buffer(缓冲区、块),selector(选择器)
流程图:
4、NIO 是 面向缓冲区,或者 面向块 编程的,数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,并且缓冲区是双向的,既可以读取数据,又可以写入数据,不像BIO的单向数据流。使用它可以提供非阻塞式的高伸缩性网络
5、Java NIO的非阻塞模式, 使一个线程从某通道发送请求或读取数据,但是它仅能得到目前可用数据,如果目前没有数据可用,就什么都不会获取,而不是保持线程阻塞,所以直至数据变得可读取之前,该线程可以继续做其他事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情
6、通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有1000个请求过来,根据实际情况,可以分配50或者100个线程来处理,没必要像BIO那样非得分配1000个线程
7、HTTP2.0使用了多路复用的技术,做到同一个链接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级
6.2.Buffer了解实例
package com.wanshi.nio;
import java.nio.CharBuffer;
import java.nio.IntBuffer;
public class BasicBuffer {
public static void main(String[] args) {
//举例说明 Buffer 的使用
//创建一个 Buffer,大小为5,即可以存放5个int
IntBuffer intBuffer = IntBuffer.allocate(5);
//向Buffer 中存放数据
for (int i=0;i<intBuffer.capacity();i++){
intBuffer.put(i);
}
//从Buffer 中读取数据
intBuffer.flip(); //!重要。将Buffer转换,读写切换:Buffer即可读,也可写,但必须切换
while (intBuffer.hasRemaining()){//如果有剩余数据,则循环
System.out.println(intBuffer.get()); //输出数据,这个get维护了一个索引,每get一次,索引就向后移动一次
}
}
}
运行结果:
6.3.NIO 与 BIO 的比较
(1)、BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多
(2)、BIO 是阻塞的,NIO 则是非阻塞的,Buffer对非阻塞起到了重大作用。
非阻塞原因:原先BIO程序直接对通道进行连接处理,现在在它们中间有了Buffer,数据在Buffer中有了一定的量才回去读取,这样就做到了非阻塞
(3)、BIO基于字节流与字符流进行操作,而NIO则基于 Channel(管道) 与 Buffer(缓冲区) 进行操作,数据总是从管道读取到缓冲区中,或者从缓冲区写入到通道中
(4)、Selector(选择器):用于监听通道,如果通道发生了数据处理事件,则selector会去处理这个通道,没有发生则会去做其他事情,因此使用单个线程就可以监听多个客户端通道
6.4.NIO三大核心组件关系
6.4.1.关系示意图
6.4.2.关系
(1).每个 Channel 都会对应一个 Buffer
(2).Selector 对应一个线程,一个线程对应多个Channel(连接)
(3).上图反映了有三个 channel 注册到 该selector //程序
(4).程序切换到哪个 channel 是由事件决定的,Event是一个重要的概念
(5).Selector会根据不同的事件,在各个通道上切换
(6).Buffer 就是一个内存块,底层是有一个数组
(7).数据的读取写入是通过Buffer,这个和BIO有本质区别。BIO中要么是输入流、要么是输出流,不能是双向流,但NIO则可读也可写,但是需要是由 flip 方法进行切换
(8).channel 是双向的,可以返回底层操作系统的情况,比如 Linux,底层的操作系统通道就是双向的
6.5.Buffer(缓冲区)
6.5.1.基本介绍
缓冲区(Buffer),本质上是一个可以读写数据的内存块,可以理解是一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer
6.5.2.Buffer类及其子类
理解:Position相当于下标,Capacity相当于总大小,Limit相当于终点,如果有五个空间,但只存了三个,那么Limit就是三,即可用数据为三
7.Channel(通道)
7.1、基本介绍
1.NIO的通道类似于流, 但有区别:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲:
2.BIO中的stream是单向的,例如 FileinputStream 对象只能进行读取数据操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作,效率高。
3.代码角度:Channel在NIO中是一个接口—public interface Channel extends Closeable{}
4.常用的 Channel 类有:FileChannel(对文件进行操作)、DatagramChannel、ServerSocketChannel 和 SocketChannel。【ServerSocketChannel 类似 ServerSocket,SocketChannel 类似 Socket】
理解:ServerSocketChannel在Server中,当客户端连接Server时,为当前客户端生成一个对应的SocketChannel(通道),让客户端通过这个SocketChannel与服务器进行连接。 (ServerSocketChannel真实类型为ServerSocketChannelImpl,SocketChannel真实类型为SocketChannelImpl)
5.FIleChannel 用于文件数据的读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写
7.2、FileChannel类常见方法
主要用于对本地文件进行 IO 操作,常见方法有
- public int read(ByteBuffer dst) 从通道中读取数据并放到缓冲区中
- public int write(ByteBuffer src) 把缓冲区的数据写到通道中
- public long transferFrom(ReadableByteChannel src,long position,long count) 从目标通道中复制数据到当前通道,用于做文件的拷贝。参数说明:(通道对象、开始传输位置、结束传输位置\文件总大小) 目标通道为:ReadableByteChannel
- public long tansferTo(long position,long count,WritableByteChannel target) 把数据从当前通道复制给目标通道,目标通道为:WritableByteChannel
7.3、实例
7.3.1:实例1:输出数据到文件
实例要求:
- 使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道),将"Hello world"写入到file01.txt中
- 文件不存在就创建
实例代码:
package com.wanshi.nio;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
public class NIOFileChannel01 {
//NIO实例1:意义:可以看到整个数据流程,以及NIO与原生java IO的关系
public static void main(String[] args) throws IOException {
//创建写入数据
String str="Hello 世界!";
// 创建一个输出流 -> channel
FileOutputStream fileOutputStream = new FileOutputStream("D:\\file01.txt");
//通过 fileOutputStream 获取 对应的 FileChannel
//这个 fileChannel 真是类型为 FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
//现在是NIO操作,与数据进行操作需要通过缓存区
//创建一个缓冲区 ByteBuffer
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//将 str 放入到 ByteBuffer 中
byteBuffer.put(str.getBytes(StandardCharsets.UTF_8));
//进行读写反转
byteBuffer.flip();
// 将 缓冲区(ByteBuffer) 数据写入到 通道(FileChannel) 中
fileChannel.write(byteBuffer);
//关闭输出流
fileOutputStream.close();
/*总结:
1.创建数据
2.创建输出流 fileOutputStream,指定数据输出的位置,如果指定文件在指定文职没有,则会自动创建
3.根据 fileOutputStream 获取一个 fileChannel 通道,它的真实类型为 FileChannelImpl
4.创建直接缓冲区 ByteBuffer
5.将数据写入到缓冲区当中
6.输出,进行缓冲区的读写切换 ByteBuffer.flip()
7.用 FileChannel 常用方法:public int write(ByteBuffer src), 将ByteBuffer 中的数据写入到通道 fileChannel 中
8.关闭输出流
流程:源数据->ByeBuffer(缓冲区)->FileChannel(通道)->本地位置,由 fileOutputStream 指定
*/
}
}
流程图:
7.3.2:实例2:读取文件数据到页面
实例要求
- 使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道),将 file01.txt 中的数据读入到程序,并显示在控制台屏幕
- 假设文件已存在
实例代码:
package com.wanshi.nio;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel02 {
//NIO实例2:从文件中读取数据到缓冲区并打印
public static void main(String[] args) throws IOException {
//拿到要输入的源数据
FileInputStream fileInputStream=new FileInputStream("D:\\file01.txt");
try {
//通过 fileInputStream 获取对应的 FileChannel -> 实际类型 FileChannelImpl
FileChannel fileChannel=fileInputStream.getChannel();
//创建缓冲区
ByteBuffer byteBuffer=ByteBuffer.allocate(fileInputStream.available()); //技巧:1024会造成资源浪费,现在是读取,故可以知道文件大小
//通道 fileChannel 是 fileInputStream创建的,故该通道内已有 fileInputStream 所对应的文件的数据了
//使用 通道 自带的 read方法,将数据读入到缓冲区中
fileChannel.read(byteBuffer);
// array方法:将缓冲区的数据以数组方式全部读出
System.out.println(new String(byteBuffer.array()));
// //执行缓冲区切换读写
// byteBuffer.flip();
//
// //根据 limit 拿到实际有效(有数据,不为空)的空间创建一个byte数组
// byte bt[]=new byte[byteBuffer.limit()];
// //循环读取byteBuffer中的字节数据
// while (byteBuffer.hasRemaining()){
// //每读一个,放入到byte数组当中
// bt[byteBuffer.position()]=byteBuffer.get();
// }
// //转换为String类型并打印
// System.out.println(new String(bt));
}catch (Exception e){
e.printStackTrace();
}finally {
fileInputStream.close();
}
}
}
流程图:
7.3.3:实例3:将一个文件数据读取到另一个文件中
实例要求
- 使用 FileChannel(通道) 和方法 read,write,完成文件的拷贝(将一个文件内容拷贝到另一个文件)
- 拷贝一个文本文件1.txt,放在项目下即可
实例代码:
package com.wanshi.nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel03 {
//NIO实例2:从文件中读取数据到缓冲区并打印
public static void main(String[] args) throws IOException {
//拿到要输入的源数据
FileInputStream fileInputStream=new FileInputStream("D:\\file01.txt");
//获取一个输入通道 fileInputChannel
FileChannel fileInputChannel=fileInputStream.getChannel();
//创建一个 ByteBuffer
ByteBuffer byteBuffer=ByteBuffer.allocate(fileInputStream.available());
//清空buffer,将关键的属性重置
byteBuffer.clear();
//将 fileChannel1 数据存入缓冲区
fileInputChannel.read(byteBuffer);
//关闭输入流 fileInputStream
fileInputStream.close();
/*输入信息结束*/
//创建输出流
FileOutputStream fileOutputStream=new FileOutputStream("2.txt");
//获取一个输出通道
FileChannel fileOutputChannel=fileOutputStream.getChannel();
//进行缓冲区 byteBuffer 读写切换
byteBuffer.flip();
//写入缓冲区数据到 1.txt 中
fileOutputChannel.write(byteBuffer);
//关闭输出流
fileOutputStream.close();
}
}
流程图:
7.3.4:实例4:拷贝文件
实例要求
- 使用 FileChannel(通道) 和方法 transferFrom,完成文件的拷贝
- 拷贝一张图片
实例代码:
package com.wanshi.nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class NIOFileChannel04 {
//NIO实例4:拷贝图片文件
public static void main(String[] args) throws IOException {
//创建输入流
FileInputStream fileInputStream=new FileInputStream("C:\\Users\\K\\Desktop\\BlogFengMian.jpg");
//获取输入流通道
FileChannel fileInputChannel = fileInputStream.getChannel();
//创建输出流
FileOutputStream fileOutputStream=new FileOutputStream("good.jpg");
//获取输出流通道
FileChannel fileOutputChannel = fileOutputStream.getChannel();
//使用 transferFrom 方法,将目标通道数据拷贝到此通道中
fileOutputChannel.transferFrom(fileInputChannel,0,fileInputChannel.size());
//关闭相关的通道和流
fileInputChannel.close();
fileOutputChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
}
7.4、关于 Buffer 和 Channel的注意事项和细节
1、ByteBuffer 支持数据类型化的put 和 get,put 放入的是什么数据类型,get就应该使用相应的数据类型,否则可能会 BufferUnderflowException 异常。
package com.wanshi.nio;
import java.nio.ByteBuffer;
public class NIOByteBufferPutGet {
//ByteBuffer 支持数据类型化的 put 和 get,put 放入的是什么数据类型,get 就应该使用相应的数据类型,否则可能会 BufferUnderflowException 异常。
public static void main(String[] args) {
//创建一个Buffer
ByteBuffer byteBuffer=ByteBuffer.allocate(64);
//类型化方式放入数据
byteBuffer.putInt(100);
byteBuffer.putLong(900000);
byteBuffer.putChar('学');
byteBuffer.putShort((short) 4);
//取出
byteBuffer.flip();
//第一个放入的为 int 类型,需要按照 类型 取出
System.out.println(byteBuffer.getInt());
System.out.println(byteBuffer.getLong());
System.out.println(byteBuffer.getChar());
System.out.println(byteBuffer.getShort());
//这样则会抛出异常
System.out.println(byteBuffer.getInt());
System.out.println(byteBuffer.getLong());
System.out.println(byteBuffer.getLong());
System.out.println(byteBuffer.getShort());
}
}
2、可以将一个普通的Buffer 转成 只读Buffer(该Buffer只允许读取,不允许写入)
package com.wanshi.nio;
import java.nio.ByteBuffer;
public class ReadOnlyBuffer {
//将一个普通的Buffer 转成 只读Buffer
public static void main(String[] args) {
//创建一个Buffer
ByteBuffer byteBuffer=ByteBuffer.allocate(64);
//for循环放入一些数据
for (int i=0;i<15;i++){
byteBuffer.putInt(i);
}
//读取
byteBuffer.flip();
//得到一个只读Buffer,使用 asReadOnlyBuffer 方法获取一个只读Buffer
ByteBuffer byteBufferReadOnly = byteBuffer.asReadOnlyBuffer();
//现在读取没有问题
while (byteBufferReadOnly.hasRemaining()){
System.out.println(byteBufferReadOnly.getInt());
}
//插入数据则抛出 ReadOnlyBufferException 异常
byteBufferReadOnly.putInt(111);
}
}
3、NIO 还提供了 MappedByteBuffer,可以让文件直接在内存(对外内存) 中进行修改,而如何同步到文件由NIO来完成
package com.wanshi.nio;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/*
说明
1.MappedByteBuffer 可以让文件直接在内存(堆外内存)中修改,操作系统不需要进行拷贝,性能高
*/
public class MappedByteBufferTest {
public static void main(String[] args) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw"); //参数:所操作的文件,模式
//获取对应的文件通道,此通道直接跟 randomAccessFile 关联的,可以直接操作
FileChannel fileChannel = randomAccessFile.getChannel();
/*
参数1:模式,FileChannel.MapMode.READ_WRITE 使用的是读写模式
参数2:0:可以直接修改的起始位置
参数3:5:是映射到内存的大小,把文件的第几个位置映射到内存,即将文件 1.txt 的多少个字节映射到内存
可以直接修改的范围就是 0-5,此处 5 代表字节大小,不是索引位置
实际类型:DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
//调用相关方法
//降低一个位置的数据改为 H,第四个位置改为 9
mappedByteBuffer.put(0,(byte) 'H');
mappedByteBuffer.put(3,(byte) '9');
//目录文件不会刷新,去根目录查看
//关闭 randomAccessFile
randomAccessFile.close();
System.out.println("修改成功");
}
}
4、前面的读写操作,都是通过一个 Buffer 完成的,NIO 还支持多个Buffer(即 Buffer 数组)完成读写操作,即 Scattering 和 Gathering
package com.wanshi.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
public class Test {
/*
scattering:将数据写入到Buffer时,可以采用Buffer数组依次写入
Gathering:从Buffer读取数据时,也可采用Buffer数组,依次读
*/
public static void main(String[] args) throws IOException {
//此次使用ServerSocketChannel 和 SocketChannel 网络
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(8080);
//绑定端口到socket并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//创建Buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0]=ByteBuffer.allocate(5);
byteBuffers[1]=ByteBuffer.allocate(3);
//等待客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
//假定丛客户端最多读取8个字节
int maxLength=8;
//循环读取
while(true){
int byteRead=0; //统计读取的字节数
while(byteRead<maxLength){
long read = socketChannel.read(byteBuffers);//用socketChannel去读,会自动分散到多个Buffer
byteRead+=read; //累计读取的字节数
System.out.println("byteRead:"+byteRead);
Arrays.asList(byteBuffers).stream().map(buffer -> "position="+buffer.position() + ",limit="+buffer.limit()).forEach(System.out::println);
}
//将所有的Buffer进行反转
Arrays.asList(byteBuffers).stream().forEach(n->{n.flip();});
//将数据显示到客户端
long byteWrite=0;
while(byteWrite < maxLength){
long write = socketChannel.write(byteBuffers);
byteWrite+=write;
}
Arrays.asList(byteBuffers).forEach(bt->bt.clear());
System.out.println("byteRead="+byteRead+",byteWrite="+byteWrite+",maxLength="+maxLength);
}
}
}
理解:服务端也有Buffer,开启Buffer数组,当一个Buffer存满后,继续存储到另一个Buffer中
8.Selector(选择器)
8.1、基本介绍
1、Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个客户端的链接,就会使用到Selector(选择器)
2、Selector 能够检测到多个注册的通道上是否有事件发生(Selector 能够注册通道,注:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求
示意图:
3、只有在连接(通道) 真正有读写事件发生时,才会进行读写,大大减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
4、避免了多线程之间的上下文切换导致的开销
Selector示意图和特点说明
8.2、Selector类相关方法
说明:每个Selector关联一个Thread(线程),关联后 Selector 会调用 select 方法,返回一个selectionKey 的集合,我们可以通过 selectionKey 得知通道发生的什么操作(读、写或连续事件),再通过 selectionKey 取到对应的 Channel
注意事项
补充:
Selector如何创建:通过 open 方式得到
selector.select方法:其实是在监听,直到关注的Channel有事件发生,才会返回,返回后会立即把对应的SelectKey加入到内部集合,然后就可以得到了(此方法是阻塞的,没有任何事件发生时会一直阻塞)
8.2、NIO 非阻塞网络编程原理分析图
NIO非阻塞网络编程相关的(Selector、SelectionKey、ServerSocketChannel和SocketChannel)关系梳理图
梳理图说明:
1、当客户端连接时,会通过 ServerSocketChannel 得到对应的SocketChannel
2、selector开始监听
3、将得到的 SocketChannel 注册到Selector上,具体是这个方法:register(Selector sel,int ops),一个 Selector 上可注册多个SocketChannel
4、注册后,返回一个SelectionKey,会和该 Selector 关联(集合)
5、Selector 进行监听 Select 方法,返回有事件发生的通道的个数
6、进一步得到各个 SelectionKey(有事件发生的)
7、再通过 SelectionKey 反向获取 SocketChannel(你注册的那个Channel),方法:channel()
8、可以通过 得到的 channel,完成业务处理
8.3、NIO 非阻塞 网络编程快速入门
案例要求:
1、编写一个 NIO 入门案例,实现服务器端与客户端之间的数据简单通讯(非阻塞)
2、目的:理解NIO非阻塞网络编程机制
代码示例
服务端:
package com.wanshi.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Set;
/*
NIO 非阻塞 网络编程快速入门:1
理解NIO非阻塞网络编程机制
服务端-
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
//1、创建ServerSocketChannel 类似 ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2、得到一个 Selector 对象
Selector selector = Selector.open();
//3、绑定端口:8080,在服务端监听
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//4、把 serverSocketChannel 注册到 selector,关心的事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//5、循环等待客户端连接
while(true){
System.out.println("等待连接中。。。");
//此处等待客户端连接,如果没有事件发生,进入下一次循环
if(selector.select(1000) == 0){ //阻塞1秒,如果没有事件发生,直接返回
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的>0,获取到相关的 selectionKey 集合
//1、如果返回的>0,表示已经获取到关注的事件
//2、selector.selectedKeys() 这个方法是返回关注事件的集合
//3、通过 selectedKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历 Set<SelectionKey>,使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
//获取到 SelectionKey
SelectionKey key = keyIterator.next();
//根据这个key 对应的 通道发生的事件作相应的处理
if(key.isAcceptable()){ //如果发生 OP_ACCEPT(连接事件)
//生成该客户端的 SocketChannel 用于交互
SocketChannel socketChannel = serverSocketChannel.accept(); //此方法是阻塞的,但人家都连接进来了,故不需要阻塞直接创建
System.out.println("客户端连接成功,id:"+socketChannel.hashCode());
//将SocketChannel设置为非阻塞
socketChannel.configureBlocking(false);
//将 socketChannel 注册到 selector ,关注事件为 OP_READ(读),同时给该 socketChannel 关联一个Buffer
socketChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024));
}
//如果发生OP_READ(读) 事件
if(key.isReadable()){
//通过 key 获取对应 channel
SocketChannel channel = (SocketChannel)key.channel();
//获取该channel关联的buffer
ByteBuffer byteBuffer =(ByteBuffer) key.attachment();
//将当前 通道 数据读入到 buffer 中
channel.read(byteBuffer);
System.out.println("from 客户端:"+new String(byteBuffer.array()));
byte bt[]= new byte[byteBuffer.position()];
}
//手动从集合中移除当前的 selectionKey,目的:防止重复操作
keyIterator.remove();
}
}
}
}
客户端:
package com.wanshi.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
/*
NIO 非阻塞 网络编程快速入门:2
理解NIO非阻塞网络编程机制
客户端-
*/
public class NIOClient {
public static void main(String[] args) throws IOException {
//1、得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//2、设置非阻塞模式
socketChannel.configureBlocking(false);
//3、提供服务器端的 ip 与 端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8080);
//4、连接服务器
//如果连接不成功
if(!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
System.out.println("因为连接需要时间,客户端不会阻塞,可以做些其他工作");
}
}
//如果连接成功,就发送数据
String info="hello 世界~";
ByteBuffer wrap = ByteBuffer.wrap(info.getBytes(StandardCharsets.UTF_8)); //根据数据字节大小,创建的ByteBuffer的字节大小就是多少
//发送数据,将 buffer 数据写入 channel
socketChannel.write(wrap);
System.in.read();
}
}
9.SelectionKey
1、SelectionKey,表示Selector 和网络通道的注册关系,共四种![](https://img-blog.csdnimg.cn/3b4d92a1440242b2b35332699344f20d.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzQ1NTA2NTgx,size_13,color_FFFFFF,t_70,g_se,x_16)
2、SelectionKey相关方法![](https://img-blog.csdnimg.cn/656d313268404e6bb926d4b162ddfb70.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzQ1NTA2NTgx,size_20,color_FFFFFF,t_70,g_se,x_16)
10.ServerSocketChannel 和 SocketChannel相关的API
10.1、ServerSocketChannel
1、ServerSocketChannel 在服务器端监听新的客户端Socket连接
2、相关方法如下:
10.2、SocketChannel
1、SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区数据写入通道,或者把通道里的数据读到缓冲区
2、相关方法如下:
11.NIO 网络编程应用实例-群聊系统
实例要求:
1.编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
2.实现多人群聊
3.服务端:可以检测用户上线,离线,并实现消息转发功能
4.客户端:通过Channel可以无阻塞发送消息给其它所有用户,同时可以收到其它用户发送的消息(由服务器转发得到)
5.目的:进一步理解NIO非阻塞网络编程机制
效果:
1)、服务器端:
1.1:服务器启动并监听 8080
1.2:服务器接收客户消息,并实现转发(上线和离线)
代码:
package com.wanshi.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Set;
/*
NIO 非阻塞 网络编程快速入门:1
理解NIO非阻塞网络编程机制
服务端-
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
//1、创建ServerSocketChannel 类似 ServerSocket,用于创建SocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2、得到一个 Selector 对象
Selector selector = Selector.open();
//3、绑定端口:8080,在服务端监听
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//4、把 serverSocketChannel 注册到 selector,关心的事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//5、循环等待客户端连接
while(true){
System.out.println("等待连接中。。。");
//此处等待客户端连接,如果没有事件发生,进入下一次循环
if(selector.select(1000) == 0){ //阻塞1秒,如果没有事件发生,直接返回
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的>0,获取到相关的 selectionKey 集合
//1、如果返回的>0,表示已经获取到关注的事件
//2、selector.selectedKeys() 这个方法是返回关注事件的集合
//3、通过 selectedKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历 Set<SelectionKey>,使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
//获取到 SelectionKey
SelectionKey key = keyIterator.next();
//根据这个key 对应的 通道发生的事件作相应的处理
if(key.isAcceptable()){ //如果发生 OP_ACCEPT(连接事件)
//生成该客户端的 SocketChannel 用于交互
SocketChannel socketChannel = serverSocketChannel.accept(); //此方法是阻塞的,但人家都连接进来了,故不需要阻塞直接创建
System.out.println("客户端"+socketChannel.getRemoteAddress()+"连接成功!");
//将SocketChannel设置为非阻塞
socketChannel.configureBlocking(false);
//将 socketChannel 注册到 selector ,关注事件为 OP_READ(读),同时给该 socketChannel 关联一个Buffer
socketChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024));
}
//如果发生OP_READ(读) 事件
if(key.isReadable()){
//通过 key 获取对应 channel
SocketChannel channel = (SocketChannel)key.channel();
try {
//获取该channel关联的buffer
ByteBuffer byteBuffer =(ByteBuffer) key.attachment();
//将当前 通道 数据读入到 buffer 中
channel.read(byteBuffer);
System.out.println("from 客户端:"+new String(byteBuffer.array()));
}catch (Exception e){
System.out.println(channel.getRemoteAddress()+" 离线了");
key.cancel();
//关闭通道
channel.close();
break;
}
}
//手动从集合中移除当前的 selectionKey,目的:防止重复操作
keyIterator.remove();
}
}
}
}
2)、客户端:
2.1:连接服务器
2.2:发送消息
2.3:接收服务器端消息
代码:
package com.wanshi.NIOGroupChatSystem;
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.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Scanner;
/*
NIO网络编程群聊系统-Client端
*/
public class GroupChartClient {
//定义相关属性
private final String Host="127.0.0.1"; //服务器ip
private final int Port=8080; //服务器端口
private Selector selector;
private SocketChannel socketChannel; //客户端连接通道
private String userName;
//构造器
public GroupChartClient() throws IOException {
//初始化选择器-Selector
selector=Selector.open();
try {
//连接服务器
socketChannel = socketChannel.open(new InetSocketAddress(Port));
//设置非阻塞
socketChannel.configureBlocking(false);
// 将通道注册到selector,关注 OP_READ 事件
socketChannel.register(selector, SelectionKey.OP_READ);
//得到userName
userName = socketChannel.getLocalAddress().toString().substring(1);
System.out.println("已连接至服务器。。。");
}catch (Exception e){
System.out.println("服务器连接失败,原因:服务器未启动");
}
}
//向服务器发送消息
public void sendInfo(String info){
info=userName +":"+ info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes(StandardCharsets.UTF_8)));
}catch (Exception e){
e.printStackTrace();
}
}
//读取服务器端回复的消息
public void readInfo(){
try {
// 选择通道是否有事件发生
int readChannels = selector.select();
if(readChannels > 0){ // 有可以用的通道
//拿到所有注册到 selector 上面的 key
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
if(key.isReadable()){ // 如果是可读的
// 得到相关的通道
SocketChannel sc = (SocketChannel)key.channel();
// 得到一个Buffer
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
// 从通道读数据到 buffer
try {
sc.read(byteBuffer);
}catch (Exception e){
System.out.println("已断开服务器连接");
key.channel();
socketChannel.close();
}
//把读到的数据转成字符串
String msg = new String(byteBuffer.array());
System.out.println(msg.trim());
}
iterator.remove(); //删除当前的selectionKey,防止重复操作
}
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
// 启动客户端
GroupChartClient groupChartClient = new GroupChartClient();
// 启动一个线程,每隔3秒从服务器端读取可能发送的数据
new Thread(){
public void run(){
while (true){ //不停地去读取
groupChartClient.readInfo();
try { //每隔3s读取一次
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}.start();
//发送数据
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String msg = scanner.next();
System.out.println("我说:"+msg);
groupChartClient.sendInfo(msg);
}
}
}
启动服务器,在启动多个客户端,可以互相聊天
效果:
多个client启动方式:点击Run—> Edit Configurations —> 找到要启动多个的文件 —> 按下 Alt+M —>选中Allow multiple instances
12.NIO与零拷贝
12.1、基本介绍
1、零拷贝是网络编程的关键,很多性能优化都离不开
2、在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。
mmap 优化:
1.mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间拷贝的次数
2.mmap示意图
sendFile 优化:
1.Linux 2.1版本提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换
2.Linux 2.1示意图
提示:零拷贝是从操作系统角度,不是指不拷贝,而是没有CPU拷贝
3.Linux 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如图:
这里其实有一次 CPU拷贝,kemel buffer —> socket buffer,但拷贝的信息量很少,比如length、offset,消耗很低可以不计
再次理解:
1.所谓零拷贝,就是从操作系统角度来看,因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer有一份数据)
2.零拷贝不仅带来了更少的数据复制,还能带来其他性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算
3.mmap 和 sendFile的区别
1.mmap 适合小数据量读写,sendFile 适合大文件传输
2.mmap 需要4次上下文切换,3 次数据拷贝;sendFile需要3次上下文切换,最少2次数据拷贝
3.sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)
12.2、NIO 零拷贝案例
案例要求:
1、使用传统IO方法传递一个大文件
2、使用NIO 零拷贝方式传递(transferTo) 一个大文件
3、观测两种传递方式消耗时间分别是多少
传统IO拷贝代码:
OldIoServer:
package com.wanshi.NIOZeroCopy;
import java.io.DataInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class OldIoServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket=new ServerSocket(8080);
while(true){
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
FileOutputStream fileOutputStream=new FileOutputStream("C:\\Users\\K\\Desktop\\OldIo.mp4");
try {
byte bt[]=new byte[4096];
long l=0;
while ((l = dataInputStream.read(bt,0,bt.length))>0){
fileOutputStream.write(bt);
}
}catch (Exception e){
e.printStackTrace();
}
dataInputStream.close();
fileOutputStream.close();
}
}
}
OldIoClient:
package com.wanshi.NIOZeroCopy;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
public class OldIoClient {
public static void main(String[] args) throws IOException {
Socket socket=new Socket("127.0.0.1",8080);
InputStream inputStream=new FileInputStream("C:\\Users\\K\\Desktop\\test.mp4");
DataOutputStream dataOutputStream=new DataOutputStream(socket.getOutputStream());
byte bt[]=new byte[4096];
long readCount=0;
long total=0;
long startTime = System.currentTimeMillis();
while((readCount = inputStream.read(bt))>0){
total+=readCount;
dataOutputStream.write(bt);
}
System.out.println("发送字节数:"+total+",耗时:"+((float)(System.currentTimeMillis()-startTime)/1000)+"s");
inputStream.close();
dataOutputStream.close();
}
}
执行效果:
NIO零拷贝代码:
NewIoServer:
package com.wanshi.NIOZeroCopy;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NewIoServer {
public static void main(String[] args) throws IOException {
InetSocketAddress address = new InetSocketAddress(7080);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
//绑定地址
serverSocket.bind(address);
//创建Buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
FileOutputStream fileOutputStream=new FileOutputStream("C:\\Users\\K\\Desktop\\tes02.mp4");
while(true){
SocketChannel socketChannel = serverSocketChannel.accept(); //等待客户端连接
try {
int readCount=0;
while((readCount = socketChannel.read(byteBuffer))>0){
fileOutputStream.write(byteBuffer.array());
byteBuffer.rewind(); // 倒带,position=0,mark 作废
}
}catch (Exception e){
System.out.println(socketChannel.getRemoteAddress()+" 离线了。。");
socketChannel.close();
}
}
}
}
NewIoClient:
package com.wanshi.NIOZeroCopy;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class NewIoClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
//建立连接
socketChannel.connect(new InetSocketAddress("127.0.0.1",7080));
//获取一个文件输入流通道
FileChannel fileChannel = new FileInputStream("C:\\Users\\K\\Desktop\\test.mp4").getChannel();
//准备发送
//获取开始时间
long startTime=System.currentTimeMillis();
//在 Linux下,一个transferTo方法就可以完成传输
// 在windows下,一次调用 transferTo 只能发送8m,就需要分段传输文件,而且要注意传输时的位置
//参数说明:开始位置,终点位置(文件大小),socketChannel
long count=(fileChannel.size()/(1024*1024*8))+(fileChannel.size()%(1024*1024*8)==0?0:1);
int i=0;
while (i<count){
fileChannel.transferTo(((long) (1024*1024*8)*i),fileChannel.size(),socketChannel);
i++;
}
System.out.println("总共花费了:"+((float)(System.currentTimeMillis()-startTime)/1000)+" s");
}
}
执行效果:
BIO、NIO、AIO对比表
举例说明:
1、同步阻塞:到理发店理发,就一直等理发师,直到轮到自己理发。
2、同步非阻塞:到理发店理发,发现前面有其它人理发,给理发师说下,先干其他事情,一会过来看是否轮到自己
3、异步非阻塞:给理发师打电话,让那个理发师上门服务,自己干其他事情,理发师自己来家给你理发
13、Netty概述
13.1、原来NIO存在的问题(学习成本高,开发速度慢)![](https://img-blog.csdnimg.cn/72ffe52f68884a7d964662a6ca79dcf7.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzQ1NTA2NTgx,size_20,color_FFFFFF,t_70,g_se,x_16)
13.2、Netty官网说明
Netty是一个异步,基于网络事件驱动的应用程序框架,用于快速开发高性能的服务端和客户端
1、Netty是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序
2、Netty 可以帮助你快速、简单的开发出一个网络应用,相当于简化和流程化了 NIO 的开发过程
3、Netty 是目前最流行的 NIO 框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的 Elasticsearch、Dubbo框架内部都采用了 Netty
13.3、Netty的优点
Netty对 JDK 自带的 API 进行了封装,解决了上述问题
1、设计优雅,适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可拓展的事件类型,可以清晰的分离关注点;高度可定制的线程模型 -单线程,一个或多个线程池
2、使用方便:详细记录的 JavaDoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x) 或 6(Netty 4.x)就足够了
3、高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。
4、安全:完整的 SSL/TLS 和 StartTLS 支持。
5、社区活跃、不断更新:社区活跃,版本迭代周期短,发现的 Bug 可以被及时的修复,同时,更多的新功能会被加入
13.4、Netty版本说明
13.4.1、netty版本分为 netty3.x 和 netty 4.x、netty 5.x
13.4.2、因为netty5出现重大bug,已经被官网遗弃了,目前推荐使用netty4.x的稳定版本
13.4.3、目前在官网可下载的版本为netty4.0.x 和 4.1.x
14、Netty 高性能架构设计
14.1、线程模型基本介绍
1、不同的线程模式,对程序的性能有很大影响
2、目前存在的线程模型有:传统阻塞 I/O 服务模型、Reactor模式
3、根据 Reactor 的数量和处理资源池线程的数量不同,有3种典型的实现
单 Reactor 单线程; 单 Reactor 多线程;主从 Reactor 多线程
4、Netty 线程模式(Netty 主要基于主从 Reactor 多线程模型做了一定的改进,其中主从Reactor多线程模型有多个 Reactor)
14.2、传统阻塞 I/O 服务模型
14.3、Reactor模式
针对传统阻塞 I/O 服务模型的2个缺点,解决方案:
1、基于I/O复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有链接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理 。Reactor 对应的叫法:1.反应器模式 2.分发者模式(Dispatcher) 3.通知者模式(notifier)
2、 基于线程池复用线程资源:不必为每个链接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务
3、说明图
4、Reactor 模式中核心组成:
1.Reactor:Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人
2.Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作
5、Reactor 模式分类:
根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现
1.单 Reactor 单线程
2.单 Reactor 多线程
3.主从 Reactor 多线程
14.4、单 Reactor 单线程![](https://img-blog.csdnimg.cn/e6614db5f0d14a0190f53399f39d2d38.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzQ1NTA2NTgx,size_20,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/963d2baf80f6494eb58a21c8ea17ded7.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzQ1NTA2NTgx,size_20,color_FFFFFF,t_70,g_se,x_16)
14.5、单 Reactor 多线程![](https://img-blog.csdnimg.cn/a3c3733743224254a261122fb8236009.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzQ1NTA2NTgx,size_20,color_FFFFFF,t_70,g_se,x_16)
优点:可以充分的利用 多核CPU 的处理能力
缺点:多线程数据共享和访问比较复杂,reactor 处理所有的事件的监听和响应,在单线程运行,在高并发应用场景容易出现性能瓶颈
14.6、主从 Reactor 多线程
三种模式用生活案例理解:
1、单 Reactor 单线程:前台接待员与服务员是同一人,全程为顾客服务
2、单 Reactor 多线程:1个前台接待员,多个服务员,接待员只负责接待
3、主从 Reactor 多线程:多个前台接待员、多个服务员
优缺点:
15、Netty 模型![](https://img-blog.csdnimg.cn/331fd56339744dceb00a7f19dcdd488e.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAd2VpeGluXzQ1NTA2NTgx,size_18,color_FFFFFF,t_70,g_se,x_16)
Netty模型工作原理示意图-详细:
1、Netty 抽象出两组线程池 BossGroup(专门负责接收客户端的连接),WorkerGroup(专门负责网络的读写)
2、BoosGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
3、NioEventLoopGroup 相当于一个事件循环组,这个组中包含多个事件循环,每一个事件循环是 NioEventLoop
4、NioEventLoop 表示一个不断循环的执行任务处理的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 网络通讯
5、NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop
6、每个 BossNioEventLoop 执行的步骤有3步
- 轮询accept事件
- 处理accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某个 workerNioEventLoop 上的 selector
- 处理任务队列的任务,即 runAllTasks
7、每个 Worker NIOEventLoop 循环执行的步骤
- 轮询read,write 事件
- 处理 I/O 事件,即read.write事件,在对应的 NIOSocketChannel 处理
- 处理任务队列的任务,即 runAllTasks
8、每个 WorkerNIOEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中 包含了 channel,即通过 pipeline 可以获取对应管道,管道中维护了很多处理器
16、Netty开速入门实例-TCP服务
- 实例要求:使用IDEA创建Netty项目
- Netty 服务器在 6668 端口监听,客户端能发送消息给服务器 “hello,服务器~”
- 服务器可以回复消息给客户端“hello,客户端~”
- 目的:对Netty线程模型有一个初步认识,便于理解Netty模型理论
- 步骤:1、编写服务器。 2、编写客户端。 3、对netty程序进行分析,看看netty模型特点
Maven项目引入Netty包:File—>Project Structure—>Moudules—>Dependencies—>+—>Libraries—>New Library—>From Maven—>输入 io.netty:netty-all,选择4.1.20.Final—>等待下载—>完成后选中并点击 Add Selected
引入netty-jar包坐标:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.20.Final</version>
</dependency>
实例代码:
nettyServer:
package com.wanshi.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.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
//第一个netty实例:server
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
// 创建 BoosGroup 和 workerGroup
/*
说明
1.创建两个线程组 boosGroup 和 workerGroup
2.boosGroup 只是处理连接请求,真正的与客户端业务处理,会交给 workerGroup完成
3.两个都是无限循环
4.boosGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
默认是 cpu核数*2
*/
NioEventLoopGroup boosGroup = new NioEventLoopGroup(); //可以写参数,参数表示线程组数量
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
//创建服务器端的启动对象,可以设置启动的参数
ServerBootstrap bootstrap = new ServerBootstrap();
try {
//使用链式编程来进行设置
bootstrap.group(boosGroup,workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //设置服务器通道实现使用 NioServerSocketChannel
.option(ChannelOption.SO_BACKLOG,128) //设置线程队列等待连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true) //设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() { //创建一个通道初始化对象(匿名对象),只要有一个客户端连入,就执行一遍
//给 pipeline 设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//加入处理器Handler,可以自定义
ch.pipeline().addLast(new NettyServerHandler());
}
}); //给我们的 workerGroup 的 EventLoop 对应的管道设置处理器。可以自定义
System.out.println("服务器准备完毕。。");
// 绑定端口号,并且同步,生成了一个 ChannelFuture对象
// 启动服务器 并 绑定端口
ChannelFuture cf = bootstrap.bind(3070).sync();
// 对关闭通道进行监听,通道关闭时触发并执行此操作
cf.channel().closeFuture().sync();
}finally {
boosGroup.shutdown();
workerGroup.shutdown();
}
}
}
nettyServerHandler:
package com.wanshi.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;
import java.nio.charset.StandardCharsets;
/*
1、我们自定义一个Handler,需要继承 netty 规定好的某个 HandlerAdapter
2、这时我们自定义一个Handler,才能称为一个handler
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter { // 入栈Handler适配器
// 读取数据的事件(这里我们可以读取客户端发送的消息)
/*
1.ChannelHandlerContext ctx:上下文对象,含有 管道pipeline,通道,地址,基本所有可能用到的信息都可以在此找到
2.Object msg:客户端发送的数据,默认以Object对象方式发送,是一个字节数组
当有事件读取时,channelRead就会被触发
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器读取线程:"+Thread.currentThread().getName());
System.out.println("看看 channel 和 pipeline关系");
// 将 msg 转成 ByteBuf
// ByteBuf是 Netty 提供的,不是 NIO 的 Bytebuffer
ByteBuf buffer= (ByteBuf) msg; //这是一个字节数组
System.out.println("客户端发送消息是:"+ buffer.toString(CharsetUtil.UTF_8));
}
// 数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 把数据 写到一个缓冲 同时 刷新缓冲,是write + flush
// 一般来说,我们对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端~".getBytes(StandardCharsets.UTF_8)));
}
//处理异常方法,一般需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
nettyClient:
package com.wanshi.netty.simple;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
//第一个netty实例:client
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
// 客户端需要一个事件循环组
NioEventLoopGroup executors = new NioEventLoopGroup();
// 创建一个 客户端 启动对象
// 客户端启动对象与服务端的 ServerBootstrap 不一样,应该用 Bootstrap
Bootstrap bootstrap = new Bootstrap();
try {
// 相关参数设置
bootstrap.group(executors) // 设置线程组
.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("127.0.0.1", 3070).sync();
//给关闭通道进行监听,因为不知道什么时候关闭
channelFuture.channel().closeFuture().sync();
}finally {
executors.shutdown();
}
}
}
nettyClientHandler:
package com.wanshi.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;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//当通道就绪时触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client:"+ctx);
//向服务器发送消息
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,server:O(∩_∩)O~~".getBytes(StandardCharsets.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 channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
System.out.println("发生了写方法");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
17、Netty模型—任务队列 TaskQueue
解决当遇到耗时业务时,异步执行业务,不再让客户端与服务器等待
任务队列中的 Task 有3种典型使用场景
1、用户程序自定义的普通任务
2、用户自定义定时任务
3、非当前 Reactor 线程调用 Channel 的各种方法
例如在推送系统的业务线程中,根据用户的标识,找到对应的Channel引用,然后调用Write类方法向该用户推送消息,就会进入到这种场景。最终的 Write 会提交到任务队列中被异步消费
实例1:用户程序自定义的普通任务
// 解决方案1 用户程序自定义的普通任务
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,客户端01~".getBytes(StandardCharsets.UTF_8)));
}
});
// 在此添加一个任务,它所需要的时间就是 上一个任务执行时间 + 本次任务执行时间,也就是 30s
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(20 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端01~".getBytes(StandardCharsets.UTF_8)));
}
});
System.out.println("发送完毕。。");
不会再阻塞,而是会直接打印发送完毕。。 10s后发送消息:hello,客户端~。异步执行
如若再次添加一个任务,第二个任的务执行开始时间是上一个任务和本次任务执行时间的总和,也就是30s
实例2:用户自定义定时任务
// 解决方案2:用户自定义定时任务 ——> 该任务是提交到 scheduleTaskQueue 中
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端02~".getBytes(StandardCharsets.UTF_8)));
}
},5, TimeUnit.SECONDS);
此方法仍是在执行完成上面任务后才会被调用,并且不是添加进入 taskQueue,而是进入scheduledTaskQueue,并且也需要其他任务完成后,才会轮到它
实例3:非当前 Reactor 线程调用 Channel 的各种方法
具体操作:在 NettyServer 的 加入处理器Handler 操作时,添加任务到任务队列
//给 pipeline 设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//加入处理器Handler,可以自定义
System.out.println("客户对应的 sockChannel hasCode="+ch.hashCode()); //可使用一个集合管理所有的 SocketChannel,在推送消息时,
// 可以将业务加入到各个 channel 对应的NIOEventLoop 的 TaskQueue 或者 scheduleTaskQueue
ch.pipeline().addLast(new NettyServerHandler());
//添加任务
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000*5);
ch.writeAndFlush(Unpooled.copiedBuffer("hello,客户端02~".getBytes(StandardCharsets.UTF_8)));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
方案再说明
1、Netty 本质为 抽象出两组线程池,BoosGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写操作
2、NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个selector,用于监听绑定在其上的 socket 网络通道(包含一个消息队列)。
3、NioEventLoop 内部采用串行化设计,从消息的读取->处理->编码->发送,始终由 IO 线程 NioEventLoop 负责。所以此处如果有长期的业务处理,可能会导致阻塞,所以要用异步方式加入队列处理事件
- NioEventLoopGroup 下包含多个 NioEventLoop
- 每个 NioeventLoop 中包含有一个 Selector,一个 taskChannel
- 每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel
- 每个 NioChannel 只会绑定在唯一的 NioEverLoop 上
- 每个 NioChannel 都绑定有一个自己的 ChannelPipeline。ChannelPipeline 与 NioChannel 相互包含,可以通过 ChannelPipeline 反向拿到 NioChannel ,反之也可
关系图
18、异步模型(Netty中的一个机制)
基本介绍
- 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者
- Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect等操作先会简单的返回一个 ChannelFuture
- 调用者并不能立刻获得结果,而是通过 Future-Listener机制(Future:未来,Listener:监听器,操作成功或失败可通过监听器来获得结果),用户可以方便的主动获取或者通过通知机制获得 IO 操作结果
- Netty 的异步模型是建立在 future 和 callback 之上的。callback 就是回调(类似于ajax异步模型,在返回后会调用回调 .then(res=>{}) 等)。重点说Future,它的核心思想是:假设一个方法 fun,计算过程可能非常耗时,等待fun返回显然不可取。那么可以再调用 fun 的时候,立马返回一个 Future,后续通过Future 去监控方法 fun 的处理过程(即:Future-Listener机制)
Future 说明
- 表示异步的执行结果,可以通过它提供的方法来检测执行是否完成,比如检索计算等。
- Future 下 有个实现子类 ChannelFuture,是一个接口:public interface ChannelFuture extends Future<Void>,我们可以添加监听器,当监听的事件发生时,就会通知到监听器。案例说明:
工作原理示意图
1、在使用 Netty 进行编程时,拦截操作和转换出入站数据,只要你提供 callback 或利用 future 即可。这使得链式操作简单、高效,并有利于编写可重用的、通用的代码
2、Netty 框架的目的,就是让你的业务逻辑从网络基础应用编码中分离出来、解脱出来。
Future-Listener 机制
1、当 Future 对象刚刚创建的时候,处于非完成状态,调用者可以通过返回的 ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作
2、常见有以下操作
- 通过 isDone 方法判断当前操作是否完成
- 通过 isSuccess 方法判断已完成的当前操作是否成功
- 通过 getCause 方法获得当前完成的操作失败的原因
- 通过 isCancelled 方法判断当前操作是否被取消
- 通过 addListener 方法来注册监听器,当前操作已完成(isDone 方法返回完成),将会通知指定的监听器,如果 Future 对象已完成,则通知指定的监听器
3、举例说明:
演示:绑定端口是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑
ChannelFuture cf = bootstrap.bind(3070).sync();
//给 cf 注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if(future.isSuccess()){
System.out.println("绑定完成!");
}else{
System.out.println("绑定失败!原因:"+future.cause().getMessage());
}
}
});
小结:相比传统阻塞 I/O,执行 I/O 操作后线程会被阻塞住,指导操作完成;异步处理的好处是不会造成线程阻塞,线程在 I/O 操作期间可以执行别的程序,在高并发情形下会更稳定和更高的吞吐量
快速入门实例-HTTP服务
1、实例要求:使用 IDEA 创建Netty项目
2、Netty服务器在 6668 端口监听,浏览器发出请求 “http://localhost:6668/”
3、服务器可以回复消息给客户端 “Hello!我是服务器5”,并对特定请求资源进行过滤
4、目的:Netty 可以做Http服务开发,并且理解Handler实例和客户端及其请求的关系
代码示例:
TestServer
package com.wanshi.netty.http;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class TestServer {
public static void main(String[] args) throws Exception{
NioEventLoopGroup boosGroup = new NioEventLoopGroup(); //可以写参数,参数表示线程组数量
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//创建服务器端的启动对象,可以设置启动的参数
ServerBootstrap serverBootstrap= new ServerBootstrap();
serverBootstrap.group(boosGroup,workerGroup).channel(NioServerSocketChannel.class) //设置通道类型
.childHandler(new TestServerInitializer()); //创建一个通道初始化对象,这个对象是自定义的,这个Handler
// 包含一个对 Http 请求做处理的自定义Handler:TestHttpServerHandler
//绑定端口
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
//对 channelFuture 关闭进行异步处理,关闭时触发
channelFuture.channel().closeFuture().sync();
}finally {
boosGroup.shutdown();
workerGroup.shutdown();
}
}
}
TestServerInitlalizer
package com.wanshi.netty.http;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
//这个类负责完成 通道初始化,并且再此当中加入对 Http请求做处理的自定义Handler类:TestHttpServerHandler
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//向 管道 加入处理器
//得到管道
ChannelPipeline pipeline = ch.pipeline();
//加入一个 netty 提供的 httpServerCodec codec =>[coder - decoder] 编解码器
//HttpServerCodec:netty提供的处理 Http 的编码解码器
pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
//增加一个自定义的Handler
pipeline.addLast("MyTestHttpServerHandler",new TestHttpServerHandler());
}
}
TestHttpServerHandler
package com.wanshi.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 java.net.URI;
//接收和发送信息
/*
SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter的子类,
HttpObject 表示客户端和服务器端相互通讯的数据被封装成 HttpObject
*/
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
//有读取事件时,触发此事件
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
//判断 msg 是不是一个 HttpObject请求
if(msg instanceof HttpRequest){
//对特定资源进行过滤——服务过滤资源
HttpRequest httpRequest= (HttpRequest) msg;
// 获取到请求资源的 url
URI uri = new URI(httpRequest.uri());
if("/favicon.ico".equals(uri.getPath())){ //判断是否是要过滤的资源
System.out.println("请求了 favicon.ico,不作反应");
return;
}
System.out.println("msg:"+msg.getClass());
System.out.println("客户端地址:"+ctx.channel().remoteAddress());
//回复信息给浏览器 [Http协议]
ByteBuf count= Unpooled.copiedBuffer("hello,我是服务器", CharsetUtil.UTF_8);
//构造一个http的响应,即 httpresponse
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,count);
// 设置返回类型
response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain;charset=UTF-8");
// 设置长度
response.headers().set(HttpHeaderNames.CONTENT_LENGTH,count.readableBytes());
//将构建好的 response 返回
ctx.writeAndFlush(response);
}
}
}
2
运行效果:
在浏览器输入 localhost:8080后,效果如下:
控制台打印效果:
之所以会出现两次,是因为请求时会发送两次请求,1是获取资源,2是获取网页图标资源,所以对图标资源的请求进行过滤,不作处理
执行流程:
流程图
每当一个 client (浏览器) 发送请求时,s (服务器) 会给其生成一个对应的 h (Handler),并且 pipeline 也是相互独立的。类似于大家 独享pipeline 同时 独享Handler,每个 client 都有一个对应且不共享的的 Handler 和 pipeline
19、Netty 核心模块组件
Bootstrap、ServerBootstrap
1、Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的引导类,ServerBootStrap 是服务端引导类
2、常见方方法
-
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup),该方法用于服务器端,用来设置两个 EventLoopGroup
-
public B group(EventLoopGroup group),该方法用于客户端,用来设置一个 EventLoopGroup
-
public B channel(Class<? extends C> channelClass),该方法用来设置一个服务器端的通道实现
-
public <T> B option(ChannelOption<T> option, T value),用来给 ServerChannel 添加配置
-
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value),用来给接收到的通道添加配置
-
public ServerBootstrap childHandler(ChannelHandler childHandler),该方法用来设置业务处理类(自定义的 Handler)
-
public ChannelFuture bind(int inetPort),该方法用于服务端,用来设置占用的端口号
-
public ChannelFuture connect(String inetHost, int inetPort),该方法用于客户端,用来连接服务器
Future、ChannelFuture
1、Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但可以过一会等它执行完成后或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时,它就会自动触发注册的监听事件
2、常见的方法
- Channel channel(),返回当前正在进行 IO 操作的通道
- ChannelFuture sysn(),等待异步操作执行完毕
Channel
1、Netty 网络通讯的组件,能够用于执行网络 I/O 操作
2、通过 Channel 可获得网路连接的通道状态
3、通过 Channel 可获得网路连接的配置参数(例如接搜缓冲区大小)
4、Channel 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成
5、调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以让 I/O 操作成功、失败或取消时回调通知调用方
6、支持关联 I/O 操作与对应的处理程序
7、不同协议、不同的阻塞类型的链接都有不同的 Channel 类型与之对应,常用的 Channel类型:
- NioSocketChannel,异步的客户端 TCP Socket链接。
- NioServerSocketChannel,异步的服务器端 TCP Socket链接。
- NioDatagramChannel,异步的 UDp 链接
- NioSctpChannel,异步的客户端 Sctp 链接
- NioSctpServerChannel,异步的 Sctp 服务端链接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO
Channel
1、Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件
2、当向一个 Selector 中注册 Channel 后,Selector 内部的机制就
可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读、可写,网络连接完成等),这样程序就可以很简单的使用一个线程高效地管理多个 Channel
3、Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件
ChannelHandler 及其实现类
1、ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链) 中的下一个处理程序
2、ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承其他的子类
3、ChannelHandler 及其实现类一览图(后)
出站和入站:ChannelPipeline 提供了 ChannelHandler 链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,我们称这些事件为出站,即客户端发送消息给服务端的数据会通过 pipeline 中的一系列 ChannelOutboundHandler,并被这些 Handler 处理,反之则称为入站
4、我们经常需要自定义一个 Handler 类来 继承 channellnboundhandleradapter,然后重写相应的方法来实现业务逻辑,接下来我们通常会看到需要重写哪些方法
//当前channel注册到EventLoop
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelRegistered();
}
//当前channel从EventLoop取消注册
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelUnregistered();
}
//通道就绪 事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
}
//断开连接后 事件
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelInactive();
}
//通道发生读取数据 事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
//通道读取数据完成 事件
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelReadComplete();
}
//用户事件触发的时候
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
ctx.fireUserEventTriggered(evt);
}
//写状态变化的时候触发
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelWritabilityChanged();
}
//发生异常后触发
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.fireExceptionCaught(cause);
}
Pipeline 和 ChannelPipeline
ChannelPipeline 是一个重点:
1、Channelpipeline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和操作,相当于一个贯穿 Netty 的链。(也可以这样理解:ChannelPipeline 是保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作)
2、ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互
3、在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下
- 一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个有 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler(ChannelHandlerContext 真实类型 为 DefaultChannelHandlerContext)
- 入站事件和出站事件在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰
4、常用方法
- ChannelPipeline addFirst(ChannelHandler..handlers),把一个业务处理类(handler)添加到链中的第一个位置
- ChannelPipeline addLast(ChannelHandler..handlers),把一个业务处理类(handler)添加到链中的最后一个位置
ChannelHandlerContext
1、保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象
2、即ChannelHandlerContext 中包含一个具体的事件处理器ChannelHandler,同时ChannelHandlerContext中也绑定了对应的 pipeline 和 Channel 的信息,方便对 ChannelHandler 进行调用
3、常用方法:
- ChannelFuture.close(),关闭通道
- ChannelOutboundinvoker flush,刷新
- ChannelFuture writeAndFlush(Object msg),将数据写到 ChannelPipeline 中,当前ChannelHandler 的下一个 ChannelHandler 开始处理(出站)
ChannelOption
1、Netty 在创建 Channel 实例后,一般都需要设置 ChannelOption 参数。
2、CHannelOption 参数如下:
ChannelOption.SO_BACKLOG
对应的TCP/IP协议 listen 函数中的 backlog 参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog 参数指定了队列大小
ChannelOption.SO_KEEPALIVE
一直保持连接活动状态
EventLoopGroup 和其实现类 NioEventLoopGroup
1、EventLoopGroup 是一组 EventLoop 的抽象,Netty 为了更好地利用多核 CPU 资源,一般会有多个 EventLoop 同时工作,每个 EventLoop 维护着一个 Selector 实例
2、EventLoopGroup 提供next 接口,可以从组里面按照一定规则获取其中一个 EventLoop 来处理任务,在 Netty服务器端编程中,我们一般都需要提供两个 EventLoopGroup,例如:BoosEventLoopGroup 和 WorkerEventLoopGroup
3、通常一个服务端口即一个 ServerSocketChannel 对应一个 Selector 和一个EventLoop线程,BoosEventLoop负责接收客户端的链接并将 SocketChannel 交给 WorkerEventLoopGroup 来进行 IO 处理,如下图所示
4、常用方法:
- public NioEventLoopGroup,构造方法
- public Future<?> shutdownGracefully(),断开连接,关闭线程
Unpooled类
1、Netty 提供一个专门用来操作缓冲区(即Netty的数据容器)的工具类
2、常用方法如下: 3、举例说明Unpooled 获取 Netty 的数据容器 ByteBuf 的基本使用
UnpooledApi案例使用1:
package com.wanshi.netty.buf;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class NettyByteBuf01 {
public static void main(String[] args) {
//创建一个 ByteBuf
/*
说明
1、创建对象,该对象包括一个数组 arr,是一个byte[10]
2、在netty的buffer中,不需要 flip 进行反转
为什么不需要flip?
因为底层维护了 readerIndex 和 writerIndex
3、通过 readerindex 和 writerindex 和 capacity,将 buffer 分成三个区域
0 -- readerindex:已经读取的区域
readerindex -- writerIndex:可读区域
writerIndex -- capacity,可写区域
*/
ByteBuf buffer = Unpooled.buffer(10);
for (int i=0;i<10;i++){
buffer.writeByte(i);
}
for (int i=0;i<buffer.capacity();i++){
System.out.println(buffer.getByte(i));
}
}
}
UnpooledApi案例使用2:
package com.wanshi.netty.buf;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
import java.nio.charset.Charset;
public class NettyByteBuf02 {
public static void main(String[] args) {
//创建一个 Bytebuf
ByteBuf byteBuf = Unpooled.copiedBuffer("Hello,world!", Charset.forName("utf-8"));
//使用相关API(方法):
if(byteBuf.hasArray()){ //查看buf中是否有数组
System.out.println("bytebuf="+byteBuf);
// 偏移量
byteBuf.arrayOffset(); //0
//读取下标
byteBuf.readerIndex(); //0
//写入下标
byteBuf.writerIndex(); //12
//可读取的字节数
byteBuf.readableBytes(); //12
//按照某个范围读取,返回字符集
//参数:起始下标位置、读取长度、编码格式
byteBuf.getCharSequence(0, byteBuf.readableBytes(), CharsetUtil.UTF_8);
}
}
}
Netty应用实例-群聊系统
实例要求:
- 编写一个 Netty 群聊系统,实现服务器和客户端之间的数据简单通讯 (非阻塞)
- 实现多人群聊
- 服务器端:可以检测用户上线,离线,并实现消息转发功能
- 客户端:通过 channel 可以无阻塞发送消息给其他所有用户,同时可以接收其他用户发送的消息(由服务器转发)
- 目的:进一步理解 Netty 非阻塞网络编程机制
实例代码:
Server
package com.wanshi.netty.NettyGroupChatSystem;
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;
//netty 群聊系统-服务器
public class Server {
private final static int port = 8080; //端口号
public static void main(String[] args) throws InterruptedException {
EventLoopGroup boosGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boosGroup, workerGroup).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 {
ch.pipeline().addLast(new ServerInializer()); //添加自定义的handler
}
});
ChannelFuture channelFuture = bootstrap.bind(port).sync();
System.out.println("服务器准备完毕");
channelFuture.channel().closeFuture().sync();
} finally {
boosGroup.shutdown();
workerGroup.shutdown();
}
}
}
ServerInitalizer
package com.wanshi.netty.NettyGroupChatSystem;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
public class ServerInializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new ServerHandler());
}
}
ServerHandler
package com.wanshi.netty.NettyGroupChatSystem;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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.CharsetUtil;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
public class ServerHandler extends SimpleChannelInboundHandler {
//保留所有与服务器建立连接的channel对象
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("服务器接收到用户 " + subStringId(ctx.channel()) + " 发送的消息,内容:" + byteBuf.getCharSequence(0, byteBuf.readableBytes(), CharsetUtil.UTF_8));
if (byteBuf.getCharSequence(0, byteBuf.readableBytes(), CharsetUtil.UTF_8).equals("search")) {
ctx.writeAndFlush(setInfo(getUsersInfo(ctx.channel())));
} else if (byteBuf.getCharSequence(0, byteBuf.readableBytes(), CharsetUtil.UTF_8).toString().contains("#")) {
ctx.writeAndFlush(setInfo(PrivateChat(ctx.channel(), byteBuf.getCharSequence(0, byteBuf.readableBytes(), CharsetUtil.UTF_8).toString())));
} else {
String myInfo = "我说:" + byteBuf.getCharSequence(0, byteBuf.readableBytes(), CharsetUtil.UTF_8);
String userInfo = subStringId(ctx.channel()) + ":" + byteBuf.getCharSequence(0, byteBuf.readableBytes(), CharsetUtil.UTF_8);
Channel channel = ctx.channel();
channelGroup.forEach(ch -> {
if (channel != ch) {
ch.writeAndFlush(setInfo(userInfo));
} else {
ch.writeAndFlush(setInfo(myInfo));
}
});
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//数据读取完成后进行的操作
}
//每个客户端连接后,放入到 ChannelGroup 中以便操作
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println(subStringId(ctx.channel()) + " 上线了");
channelGroup.writeAndFlush(setInfo(subStringId(ctx.channel()) + "上线了"));
channelGroup.add(ctx.channel());
ctx.writeAndFlush(setInfo(getUsersInfo(ctx.channel())));
}
//获取所有用户信息
public String getUsersInfo(Channel _channel) {
String msg = "当前用户如下:\n";
//利用迭代器获取到所有用户
Iterator<Channel> iterator = channelGroup.iterator();
while (iterator.hasNext()) {
Channel channel = iterator.next();
if (channel == _channel) {
msg += "\t我\n";
} else {
msg += "\t" + subStringId(channel) + "\n";
}
}
return msg;
}
public ByteBuf setInfo(String msg) {
return Unpooled.copiedBuffer(msg.getBytes(StandardCharsets.UTF_8));
}
//私聊方法
public String PrivateChat(Channel _channel, String info) {
String id = info.substring(info.indexOf("#") + 1, info.indexOf("#") + 6);
String msg = info.substring(info.indexOf(id) + 6);
Iterator<Channel> iterator = channelGroup.iterator();
boolean send = false;
while (iterator.hasNext()) {
Channel channel = iterator.next();
String infoId = channel.remoteAddress().toString();
if (infoId.substring(infoId.indexOf(":") + 1).equals(id)) {
channel.writeAndFlush(setInfo("(私聊)" + subStringId(_channel) + ":" + msg));
send = true;
break;
}
}
if (send) {
return "我 ->" + id + ":" + msg;
} else {
return "发送失败,未找到此客户!";
}
}
public String subStringId(Channel channel) {
return channel.remoteAddress().toString().substring(channel.remoteAddress().toString().indexOf(":") + 1);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
String msg = "用户:" + subStringId(ctx.channel()) + " 下线了";
channelGroup.writeAndFlush(setInfo(msg));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
Client
package com.wanshi.netty.NettyGroupChatSystem;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
//netty群聊系统-客户端
public class client {
private final static int port = 8080; //要监听的端口号
public static void main(String[] args) throws InterruptedException {
//客户端只需要一个时间循环组
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//加入客户端 自定义handler
ch.pipeline().addLast(new clientHandler());
}
});
Channel channel = bootstrap.connect("127.0.0.1", port).sync().channel();
//标准输入
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
//利用死循环,不断读取客户端在控制台上的输入内容
for (; ; ) {
channel.writeAndFlush(Unpooled.copiedBuffer(bufferedReader.readLine().getBytes(StandardCharsets.UTF_8)));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
workerGroup.shutdown();
}
}
}
ClientHandler
package com.wanshi.netty.NettyGroupChatSystem;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
public class clientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端已连接!输入 search 查询当前人数与信息");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println(byteBuf.getCharSequence(0, byteBuf.readableBytes(), CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
System.out.println("与服务器断开连接。。。");
}
}