本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1.Netty 介绍和应用场景
1.1 介绍
- Netty 是jboss的一个开源框架
- Netty是一个异步的,基于事件驱动的网络应用框架
- 基于nio
1.2 应用场景
- Rpc 例如dubbo
- 游戏
- 大数据
涉及到网络通信的应用都可以使用netty
2. i/o模型
2.1 介绍
- bio 同步并阻塞 一个连接对应服务器一个线程 适用于连接数较少的架构 jdk1.4
- nio 同步非阻塞 服务器一个线程处理多个连接 适用于连接数较多连接时间短 jdk1.4
- aio(nio.2) 异步非阻塞 适用于连接数多且连接时间长 jdk1.7
2.2 bio
blocking i/o
2.2.1 简单demo
开发一个服务端,创建一个线程池,当客户端发送一个请求,服务端对应创建一个线程处理,当有多个客户端请求时,就会创建多个线程对应处理
这里demo的客户端用telnet模拟
public static void handler(Socket socket){
try(InputStream in = socket.getInputStream();){
System.out.println("线程信息: id "+Thread.currentThread().getId()+" name " + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
while (true){
int read = in.read(bytes);
if(read!=-1){
System.out.println("输出信息: "+new String(bytes,"UTF-8"));
}else {
break;
}
}
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
try(ServerSocket serverSocket = new ServerSocket(6666);) {
ExecutorService executorService = Executors.newCachedThreadPool();
System.out.println("线程信息: id "+Thread.currentThread().getId()+" name " + Thread.currentThread().getName());
while (true){
System.out.println("等待链接");
final Socket socket = serverSocket.accept();
System.out.println("链接到一个客户端");
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程信息: id "+Thread.currentThread().getId()+" name " + Thread.currentThread().getName());
handler(socket);
}
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
2.3 nio
non-blocking i/o 非阻塞
2.3.1 简介
三大核心
- channel 通道
- buffer 缓冲区
- selector 选择器
简述操作原理: selector 选择可用的channel, channel 与 buffer可以相互读写,应用程序并不直接对channel进行操作,而是通过对buffer进行操作,间接操作channel
一个线程中会有多个selector,一个selector中可以注册多个channel,如果并没有数据传输,线程还可以做其他事,并不会一直等待
2.4 nio与bio 的区别
- nio非阻塞 bio阻塞
- nio用块的方式处理io bio用流的方式处理io 块的方式比流的方式要快
- bio基于字节流/字符流 nio基于缓冲区和通道(channel) selector监听多个通道的事件,因此用一个线程就可以处理多个通道的数据
图示: nio
bio
3. nio详解
3.1 nio模型三大组件的关系
- 一个线程对应一个selector
- 一个selecor对应多个channel
- 一个channel对应一个buffer
- 一个线程对应多个channel
- channel与buffer都是双向的,就是既可以读也可以写 使用flip()方法切换
- buffer就是一个内存块,读写内存比较快
- selector会根据不同事件切换不同的channel
3.2 Buffer缓冲区
3.2.1 简介
本质是一个读写数据的内存块,可以理解成一个提供了操作内存块方法的容器对象(数组)
缓冲区中内置了一些机制,这些机制可以检测到缓冲区的数据变化,状态变化
channel读写的数据必须都经过Buffer
3.2.2 源码分析
常用的几个操作方法
public static void main(String[] args) {
//allocate 规定intbuffer的长度
IntBuffer buffer = IntBuffer.allocate(5);
//capacity()获取容量
//put()写入
for(int i = 0;i<buffer.capacity();i++){
buffer.put(i*2);
}
//flip()反转 由写转为读
buffer.flip();
//读取
//get()每次读取后 索引向后移动一位
for(int i = 0;i<buffer.capacity();i++){
System.out.println(buffer.get());
}
}
3.2.2.1 定义
IntBuffer中定义了一个int数组,其他类型的buffer类似
public abstract class IntBuffer
extends Buffer
implements Comparable<IntBuffer>
{
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
// values, which is especially costly when coding small buffers.
//
final int[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly; // Valid only for heap buffers
最顶层的Buffer类中定义了四个属性
public abstract class Buffer {
/**
* The characteristics of Spliterators that traverse and split elements
* maintained in Buffers.
*/
static final int SPLITERATOR_CHARACTERISTICS =
Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
// Invariants: mark <= position <= limit <= capacity
private int mark = -1; //标记
private int position = 0; //当前索引的位置,不能超过limit
private int limit;//最大能读写的长度
private int capacity;//容量 allocate定义的长度
3.2.2.2 反转
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
可以看到,反转之后,由读变为写,或者由写变为读
将索引归0,最大读写长度不能超过上次操作的索引
3.2 channel 通道
3.2.1 简介
- 通道类似于流/连接,但是流只能写入或者读取,通道可以即读取也写入
- 通道异步读写数据
- 通道可以读写数据到缓存区
3.2.2 层级关系
当有客户端发送请求时,服务端会创建一个ServerSocketChannel(实现类:ServerSocketChannelImpl) 再由ServerSocketChannel创建一个SocketChannel(实现类:SocketChannelImpl即真正读写数据的通道),这个SocketChannel就是与这个客户端请求所对应的
3.2.3 案例剖析
3.2.3.1 FileChannle 输出文件流
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @author: zhangyao
* @create:2020-08-25 14:50
**/
public class FileChannelTest {
public static void main(String[] args) {
FileOutputStream fileOutputStream = null;
try {
//文件输出流
fileOutputStream = new FileOutputStream("D:\file01.txt");
//文件输出流包装为FileChannel 此处FileChannel默认实现FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
//创建对应的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//数据写入缓冲区
byteBuffer.put("hello nio".getBytes());
//反转,因为接下来需要从缓冲区读取数据写入Channel
byteBuffer.flip();
//从缓冲区写入Channel
fileChannel.write(byteBuffer);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭文件流
if(fileOutputStream!=null){
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
整体流程就是把数据写入缓冲区,在读取缓存区写入通道Channel,在由文件输出流输出
图示如下
3.2.3.2 FileChanle 输入文件流
public static void main(String[] args) {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream("D:\file01.txt");
//获取Channel
FileChannel channel = fileInputStream.getChannel();
//创建byteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//从channel中读取数据写入buffer
channel.read(byteBuffer);
//反转 下一步需要从buffer中读取数据输出
byteBuffer.flip();
//输出
byte[] array = byteBuffer.array();
System.out.println(new String(array));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
与上面的例子刚好相反,从文件中读取数据,通过通道写入buffer缓冲区,在输出
图示
3.2.3.3 FileChannel 拷贝文件
其实就是上面两个例子结合,把一个文件中的数据复制到另外一个文件中
public static void main(String[] args) {
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
try {
fileInputStream = new FileInputStream("D:\file01.txt");
fileOutputStream = new FileOutputStream("D:\file02.txt");
FileChannel channel = fileInputStream.getChannel();
FileChannel channel1 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true){
//将byteBuffer复位
byteBuffer.clear();
int read = channel.read(byteBuffer);
if(read==-1){
break;
}
byteBuffer.flip();
channel1.write(byteBuffer);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fileInputStream.close();
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里使用了byteBuffer.clear()方法
因为ByteBuffer缓冲区是有长度的,当读取的文件超过缓冲区的长度时,如果不对缓冲区进行清空,当进行下一次读取时,就会从上一次读取的位置开始读取,会出现死循环的情况
3.2.3.4 FileChannel 拷贝文件之TransferFrom
public static void main(String[] args) {
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
try {
fileInputStream = new FileInputStream("D:\file01.txt");
fileOutputStream = new FileOutputStream("D:\file02.txt");
FileChannel channel = fileInputStream.getChannel();
FileChannel channel1 = fileOutputStream.getChannel();
//从channel通道拷贝到 channel1通道
channel1.transferFrom(channel, 0, channel.size());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fileInputStream.close();
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.2.4 Buffer的分散和聚集
上面的例子都是使用单个buffer进行数据的读写,如果数据过大,也可用使用多个buffer(buffer数组)进行数据的读写,即用空间换时间
3.3 Selector选择器
3.3.1 基本简介
一个selector管理多个channel通道,使用异步的方式处理io
只有读写真正的发生时,才会处理数据,减小了线程的压力,不用每个请求都维护一个线程
避免了多线程之间的上下文切换导致的开销
3.3.2 selector的api
Selector:
- select() 阻塞
- select(Long timeout) 有超时时间
- selectNow() 非阻塞
- wakeup() 立即唤醒selector
3.3.2 selecor的工作流程
其实是Selector SelectionKey ServerSocketChannel SorkcetChannel的工作原理
- 当客户端链接时,通过ServerSockertChannel 得到SocketChannel 并且注册到 Selector上
-
- 注册源码
public abstract SelectionKey register(Selector sel, int ops)
throws ClosedChannelException;
这是SocketChannel注册到Selector上的方法,第一个参数为要注册的Selector对象,第二个参数为事件驱动的类型
public abstract class SelectionKey {
public static final int OP_READ = 1;
public static final int OP_WRITE = 4;
public static final int OP_CONNECT = 8;
public static final int OP_ACCEPT = 16;
private volatile Object attachment = null;
- 当注册完成后返回一个Selectionkey,这个selectionKey会和SocketChannel关联
- Selector通过select方法监听Channel,如果有事件发生,返回对应的selectionKey集合
-
- 源码
public int select(long var1) throws IOException {
if (var1 < 0L) {
throw new IllegalArgumentException("Negative timeout");
} else {
return this.lockAndDoSelect(var1 == 0L ? -1L : var1);
}
}
public int select() throws IOException {
return this.select(0L);
}
public int selectNow() throws IOException {
return this.lockAndDoSelect(0L);
}
private int lockAndDoSelect(long var1) throws IOException {
synchronized(this) {
if (!this.isOpen()) {
throw new ClosedSelectorException();
} else {
int var10000;
synchronized(this.publicKeys) {
synchronized(this.publicSelectedKeys) {
var10000 = this.doSelect(var1);
}
}
return var10000;
}
}
}
- 通过得到的selectionKey可以反向获取Channel
-
- 源码
public abstract SelectableChannel channel();
- 最后通过channel处理业务
3.3.3 案例
服务端思路:
- 创建serverSocketChannel绑定端口6666,把这个channel注册到Selector上,注册事件是OP_ACCEPT
- 循环监听,判断是否channel中是否有事件发生,如果有事件发生,判断不同的事件类型进行不同的链接,读/写操作
客户端思路
- 创建一个SocketChannel,连接上服务器之后,发送消息,并保持链接不关闭
3.3.3.1 server端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* @author: zhangyao
* @create:2020-08-26 16:55
**/
public class ServerChannel {
public static void main(String[] args) {
try {
//生成一个ServerScoketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置为非阻塞的
serverSocketChannel.configureBlocking(false);
//serverSocket监听6666端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//创建Selector
Selector selector = Selector.open();
//serverSocketChannel注册到Selector
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待链接
while (true){
//如果没有事件发生,就继续循环
if(selector.select(1000) == 0){
System.out.println("等待1s,无连接");
continue;
}
//如果有事件驱动,就需要遍历事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//如果事件是连接
if(key.isAcceptable()){
try {
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
SelectionKey register = channel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("链接成功");
} catch (IOException e) {
e.printStackTrace();
}
}
//如果是读取数据
if(key.isReadable()){
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
try {
int read = channel.read(byteBuffer);
byte[] array = byteBuffer.array();
System.out.println("读取数据:"+ new String(byteBuffer.array()));
} catch (IOException e) {
e.printStackTrace();
}
}
iterator.remove();
};
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.3.3.2 客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @author: zhangyao
* @create:2020-08-26 17:24
**/
public class ClientChannel {
public static void main(String[] args) {
//创建一个SocketChannel
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
if(!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
System.out.println("服务器连接中,线程并不阻塞,可以进行其他操作");
}
}
//连接成功
socketChannel.write(ByteBuffer.wrap("hello ,server".getBytes()));
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}