文章目录
- 涉及的一些基础
涉及的设计模式:观察者模式、命令模式、责任链模式
涉及的数据结构:链表(管道的底层使用了链表)
一.netty介绍
是一个java开源项目
是一个异步的、基于事件驱动的网络应用框架,用以开发高性能、高可用的网络io程序
异步是相对于同步而言的
同步:在同步中,当浏览器向服务器发送了一个请求1,需要等待服务器向浏览器返回了响应2后,浏览器才能进行操作3。
异步:在异步中,浏览器向服务器发起一个请求,但是它并不会因为响应没有到达,而发生阻塞,而是可以进行操作3,不需要等待响应2,提供了性能
高性能、高可用其实就是对java中的io进行了重写
主要针对于在TCP协议下,面向Clients的高并发应用,或者是peer-to-peer > 场景下的大量数据持续传输的应用
Netty本质是一个NIO框架,适用于服务通讯的多种应用场景,所以学好NIO。
二.Netty应用的场景
- 作为基础的通信组件被RPC框架使用,例如dubbo
- 游戏行业
- 大数据领域:AVRO实现数据文件的共享
三.IO模型
三种IO模型:用什么样的通道进行数据的发送和接受,很大程度上决定了程序通道的性能
BIO:同步并阻塞(传统阻塞),服务器实现模式为一个链接一个线程,即客户端有链接请求时服务器端就需要启动一个线程进行处理,如果这个链接不做任何事情就会造成不必要的线程开销
当一个服务器被多个客户端请求时,就会对服务器造成压力,求每个线程都会有一定的开销。
BIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(链接),即客户端发送的链接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理。
一个线程维护一个选择器,选择器可以对客户端的通道进行轮询操作,实现了可以处理更多的并发
AIO:异步非阻塞,AIO引入异步通道的概念,采用Procactor模式,简化了程序编写,有效的请求才启动线程,它的特点时由操作系统完成后才通知服务端程序开启线程去处理,一般适用于连接数较多且连接时间较长的应用(目前还不常用)
IO适用场景:
- BIO方式适用于连接数目较小且固定的框架,但是方式对服务器资源要求比较高,并发局限于应用中,jdk1.4以前的唯一选择,但是程序简单
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯、
- AIO方式适用于连接数目比较多且连接比较长的架构,比如相册服务器,充分调用OS参与并发操作,编程较为复杂,jdk7后开始支持
四.BIO讲解
同步阻塞,线程的开销大,可以通过线程池改善开销
编程流程:
- 服务器端创建一个serverSocket
- 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与通信
- 客户端发送请求后,先咨询服务器是否由线程响应,如果1没有则会等待,或者被拒绝
- 如果有响应,客户端线程会等待请求结束后,在继续执行
BIO相关案例:使用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 {
//创建一个线程池
ExecutorService newCachedThreadPool= Executors.newCachedThreadPool();
// 创建一个ServerScocket
ServerSocket serverSocket=new ServerSocket(6666);
System.out.println("服务启动了");
while(true){
//主线程
System.out.println("线程信息 id:"+Thread.currentThread().getId()+" 名字为:"+Thread.currentThread().getName());
final Socket socket=serverSocket.accept(); //可能会进行阻塞
System.out.println("有客户端进行连接了");
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
//与客户端进行通讯
handler(socket);
}
});
}
}
//编写一个Handler方法,与客户端进行通讯
public static void handler(Socket socket){
try{
byte[] bytes=new byte[1024];
InputStream inputStream = socket.getInputStream(); //可能会进行阻塞
//循环读取客户端发送的数据
while(true){
System.out.println("等待连接");
System.out.println("线程信息 id:"+Thread.currentThread().getId()+" 名字为:"+Thread.currentThread().getName());
System.out.println("read-------");
int read = inputStream.read(bytes); //返回读了多少个数据
if(read!=-1){
System.out.println(new String(bytes,0,read));//输出客户端发送的数据
}else {
break;
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("关闭连接");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
在cmd中使用
telnet 127.0.0.1 6666
ctrl+] 进入命令行界面
后使用send命令进行发送
send helloWorld
五.Java NIO基本介绍
-
全程java non-blocking IO ,是一系列改进的输入/输出的新特性,被统称为NIO,是同步非阻塞的
-
NIO相关类都被放在java.nio 包和其子包下,并且对原java.io包中很多类进行了改写
-
三大核心部分:Channel(管道) ,Buffer(缓冲区),Selector(选择器)
-
NIO是面向缓冲区,或者面向块编程的,数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中进行前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
-
java NIO 的非阻塞模式,是一个线程从某通道发送的请求或者读取数据,但是它仅能得到且目前可用的数据,如果目前没有数据可用时,就什么都不会获得,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情,而非阻塞也是如此,一个线程请求写入一些数据到某通道,但是不需要等待它完全写入,这个线程同时可以去做别的事情。
-
通俗理解:NIO是可以做到用一个线程来处理多个操作的,假设有10000个请求过来,根据实际情况,可以分配50个或100个,不会像阻塞IO那样,非得分配10000个
-
HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP 1.1大了好几个数量级
NIO的Buffer小案例:
文件1
import java.nio.IntBuffer;
public class BasicBuffer {
public static void main(String[] args) {
// 创建一个buffer,大小为5,可以存放5个int
IntBuffer intBuffer=IntBuffer.allocate(5);
// 向Bufer中存放数据
intBuffer.put(10);
intBuffer.put(11);
intBuffer.put(12);
intBuffer.put(13);
intBuffer.put(14);
// 如何向Buffer中读取数据
// 将bufer读写切换
intBuffer.flip();
while(intBuffer.hasRemaining()){
System.out.println(intBuffer.get());//取得后,索引后移
}
}
}
NIO与BIO的比较
- BIO以流的方式处数据,NIO以块的方式处理数据,块I/O的效率比流I/O高很多
- BIO是阻塞的,NIO则是非阻塞的
- BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取数据到缓冲区,或者从缓冲区写入到通道中,Selector(选择器)用于监听多个通道的事件(比如:连接请求、数据到达等),因此使用单个线程就可以监听多个客户端通道。
NIO三大核心
- 每个channel都对应一个Buffer
- Selector对应一个线程
- 一个线程可以对应多个channel,一个channel相当于一个连接
- 该图反应了有3个channel注册到了Selector
- 程序切换到哪个channel是由事件决定的,Event是一个很重要的事件
- Seletor会根据不同的事件在各个通道进行切换
- Buffer相当于就是一个内存块,底层是一个数组
- 数据的读取是通过Buffer,这个和BIO是完全不同的,BIO要么是输入流或者是输出流,不是双向的;但是NIO的Buffer是可以读也可以写,但是需要有flip进行切换
- channel是双向的,可以返回底层操作系统的情况,如linux底层的操作系统通道就是双向的
缓冲区Buffer
缓冲区本质上是一个可以读写数据的内存块,可以理解是一个容器对象(含数组),该对象提供了一组方法,可以更加轻松地使用内存块,缓冲区对像内置了一些机制,能够跟踪和记录缓冲区地状态变化情况,channel提供文件、网络读取数据地渠道,但是读取或写入地数据必须是经过Buffer,如图:
Buffer即其子类:
先看看Buffer源码地几个重要地成员属性:
属性 | 概述 |
---|---|
Capacity | 容量,即可以容纳地最大数据量;在缓冲区创建时被设定并且不能改变 |
Limit | 表示缓冲区地当前终点,不能对缓冲区超过极限地位置进行读写操作。且极限是可以修改的 |
Position | 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变值,为下一次读写操作准备 |
Mark | 标记 |
可以对 文件1 进行代码debug调式:
当position到达5时,就无法进行写的操作了
接下来我们使用filp进行反转,我们先看看这个方法中的源代码:
将限制limit定位到当前position,表示读操作时必须在limit范围内,接着将position置为0,进行新的读操作。
Buffer中常用的方法:
ByteBuffer是Buffer的子类,也是我们进行网络传输最常用的一个类
通道Channel
基本介绍:
1.BIO中的Stream是单向的,例如FileInputStream对象只能进行读取文件数据的操作,而NIO中的通道channel是双向的,可以读操作,也可以写操作。
2.Channel在NIO中是一个接口,常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel、SocketChannel
3.FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据的读写,ServerSocketChannel和SocketChannel用于TCP的数据读写
FileChannel是其中一个重要的类,常见的方法有:
int read(ByteBuffer dst) :从通道读取数据并放到缓冲区中
int write(ByteBuffer src) :把缓冲区的数据写到通道中
long transferFrom(ReadableByteChannel src,long position,long count): 从目标通道中复制数据到当前通道
long transferTo(long position,long count,WritiableByteChannel target) : 把数据从当前通道复制给目标通道
channel、buffer小案例
建议以下案例自行进行debug,观察buffer中那4个关键字段的变化
1.使用前面学习的ByteBuffer和FileChannel,将一段文字写入到file01.txt中
public class ChannelTest1 {
public static void main(String[] args) throws Exception{
String s="hello world";
//创建一个文件输出流
FileOutputStream fileOutputStream=new FileOutputStream("d://myNIO//file01.txt");
// 注意:虽然在NIO中我们没有怎么提到输出流,但是Channel实际是输出流的一个包装,所以我们可以通过输出流获得一个Channel
// fileChannel实际是fileChannelmpl
FileChannel channel = fileOutputStream.getChannel();
//创建一个缓冲区,用于内存数据到channel的一个缓冲区
//这个buffer是被写的
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将s放入到buffer中
buffer.put(s.getBytes());
// 现在我们需要将buffer中的数据读到channel中,需要对buffer进行反转
buffer.flip();
//将bytebuffer写入到channel
channel.write(buffer);
//关闭FileoutputStream即可关闭所有流
}
}
案例2:
使用channel和buffer读取文件file01.txt
/**
* 使用channel和buffer读取文件file01.txt
*/
public class ChannelTest2 {
public static void main(String[] args) throws Exception {
File file = new File("d://myNIO//file01.txt");
//创建文件输入流
FileInputStream fileInputStream = new FileInputStream("d://myNIO//file01.txt");
//获得通道
FileChannel channel = fileInputStream.getChannel();
// 创建缓冲区,因为我们文件已知长度,所以这里直接将缓冲区大小设置为和文件大小一样
ByteBuffer buffer = ByteBuffer.allocate((int)file.length());
//将通道的数据写入到缓冲区
int read = channel.read(buffer);
//从buffer中获取数据并将字节转化为字符串
//这里直接获取buffer底层的整个数组,与position无关,所以可以不用filp反转了
System.out.println(new String(buffer.array()));
fileInputStream.close();
}
}
案例3
使用一个buffer完成文件的读取
使用channel和read、write方法,完成文本文件的拷贝
public class ChannelTest3 {
public static void main(String[] args) throws Exception{
File file = new File("d://myNIO//file01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream fileOutputStream=new FileOutputStream("d://myNIO//file02.txt");
FileChannel fileInputStreamChannel = fileInputStream.getChannel();
FileChannel fileOutputStreamChannel = fileOutputStream.getChannel();
//获得一个buffer
ByteBuffer buffer = ByteBuffer.allocate(524);
while(true){
buffer.clear(); //重要操作,不然会无限循环
int read = fileInputStreamChannel.read(buffer);
if(read==-1){
break;
}
buffer.flip();
fileOutputStreamChannel.write(buffer);
}
fileInputStream.close();
fileOutputStream.close();
}
}
案例4:
使用FileChannel和transerFrom方法,完成对文件的拷贝
public class ChannelTest4 {
public static void main(String[] args) throws Exception{
// 创建输入流
FileInputStream fileInputStream = new FileInputStream("d://myNIO//p1.jpg");
//创建输出流
FileOutputStream fileOutputStream=new FileOutputStream("d://myNIO//p2.jpg");
FileChannel fileInputStreamChannel = fileInputStream.getChannel();
FileChannel fileOutputStreamChannel = fileOutputStream.getChannel();
//将目标通道的内容复制到该通道
fileOutputStreamChannel.transferFrom(fileInputStreamChannel,0,fileInputStreamChannel.size());
fileInputStreamChannel.close();
fileOutputStreamChannel.close();
}
}
通过以上的案例,这里做一个总结
buffer和channel的注意事项:
- ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用相应的数据类型取出来,否则可能会报错
BufferUnderflowException
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.putInt(3);
buffer.putChar('e');
buffer.flip();
buffer.getInt();
buffer.getChar();
- 可以将一个普通的Buffer转化成为一个只读的Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
//readonlyBuffer只可以读,buffer还是可读可写的
- NIO提供了MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成。
最重要的就是FileChannel中的一个方法可以获得这个直接内存修改的通道对象:
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
/**
* MappedByteBuffer 可以让文件直接在内存(对外内存)进行修改,操作系统不需要进行一次拷贝操作,减少了开销
*/
public class MappedByteBufferTest {
public static void main(String[] args)throws Exception{
RandomAccessFile randomAccessFile=new RandomAccessFile("d://myNIO//file01.txt","rw");
FileChannel channel = randomAccessFile.getChannel();
//参数一:FileChannel的模式选择,这里为读写模式
//参数二: 0 :可以直接修改文件的起始文件
//参数三: 5: 映射到内存的大小,即 1.txt从0开始的多少个字节可以进行直接修改,这里直接修改的范围是1~4
// mappedByteBuffer是一个抽象类,这里实际类型是directBytebuffer
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0,(byte)'E');
// mappedByteBuffer.put(5,(byte)'L');//会报错
randomAccessFile.close();
}
}
- 前面的操作我们可以看出基本都是通过一个Buffer进行操作的,NIO还支持通过多个BUffer(即Buffer数组来完成读写操作),即
Scattering
和Gathering
,即分散和聚集
- Scattering:将数据写到buffer时,可以采用Buffer数组,依次写入,即当一个buffer满后,可以使用下一个buffer
- Gathering:从buffer读取数据时,可以采用buffer数组,一次读取多个buffer
package com.atguigu.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
/**
* Scattering:将数据写入到 buffer 时,可以采用 buffer 数组,依次写入 [分散]
* Gathering:从 buffer 读取数据时,可以采用 buffer 数组,依次读
*/
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws Exception {
//使用 ServerSocketChannel 和 SocketChannel 网络
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
//绑定端口到 socket,并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//创建 buffer 数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
//等客户端连接 (telnet)
SocketChannel socketChannel = serverSocketChannel.accept();
int messageLength = 8; //假定从客户端接收 8 个字节
//循环的读取
while (true) {
int byteRead = 0;
while (byteRead < messageLength) {
long l = socketChannel.read(byteBuffers);
byteRead += l; //累计读取的字节数
System.out.println("byteRead = " + byteRead);
//使用流打印,看看当前的这个 buffer 的 position 和 limit
Arrays.asList(byteBuffers).stream().map(buffer -> "position = " + buffer.position() + ", limit = " + buffer.limit()).forEach(System.out::println);
}
//将所有的 buffer 进行 flip
Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
//将数据读出显示到客户端
long byteWirte = 0;
while (byteWirte < messageLength) {
long l = socketChannel.write(byteBuffers);//
byteWirte += l;
}
//将所有的buffer进行clear
Arrays.asList(byteBuffers).forEach(buffer -> {
buffer.clear();
});
System.out.println("byteRead = " + byteRead + ", byteWrite = " + byteWirte + ", messagelength = " + messageLength);
}
}
}
Selector(选择器)
- java的NIO,用非阻塞的IO方式,可以用一个线程,处理多个客户端的连接,就会使用到Selector
- Selector能够检测到多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector)