本文参与「新人创作礼」活动,一起开启掘金创作之路。
下一篇:Netty基础总结(2)
Netty基础总结
1、IO类型及说明
| 类型 | 含义 | 说明 | 使用场景 |
|---|---|---|---|
| BIO | 同步并阻塞IO | 服务器实现模式为一个连接对应一个线程, 客户端连接过多会严重影响性能 | 连接数少且固定的架构,对服务器资源要求高,并发局限于应用中,如数据库连接 |
| NIO | 同步非阻塞IO | 服务器实现模式为一个线程处理多个连接, 即客户端发送的连接请求都会注册到多路复用上; 多路复用器轮询到有IO请求就进行处理 | 连接数多且连接较短的架构,如聊天、弹幕、服务器间通讯 |
| AIO | 异步非阻塞IO | 无论是客户端的连接请求还是读写请求都会异步执行; | 连接数较多且连接较长的架构,充分调用操作系统参与 |
2、BIO基本介绍
1、传统的Java io编程、其相关的类和接口在java.io包下
2、同步阻塞。服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
3、执行流程:
服务器端启动一个ServerSocket
客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户端建立一个线程与之通讯
客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待或者被拒绝 如果有响应,客户端会等待请求结束后才继续执行
4、模型:
3、NIO基本介绍
1、原生jdk io上的一个改进 同步非阻塞!
2、三大核心组件:
| 类型 | 含义 | 理解 | |
|---|---|---|---|
| Channel | 通道 | 一个Channel就代表一个连接 | |
| Buffer | 缓冲区 | 用于临时存储的一块内存 | |
| Selector | 选择器 | 对Channel的一个管理器,监听通道的事件 |
3、模型:
4、特性:
a. 面向缓冲,数据读取到一个它稍后处理的缓冲区,需要时可以在缓冲区中前后移动,这就增加了处理过程中的灵活性,可以提供非阻塞的高伸缩性网格;
b. 非阻塞;
c. 通过选择器来模拟多线程;
4、NIO和BIO区别
1、BIO以流的方式处理数据,NIO以块的方式处理数据,块的效率 > 流的效率
2、BIO阻塞 NIO非阻塞
3、BIO基于字节流和字符流进行操作;NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道内读取到缓冲区或者从缓冲区写入到通道。
4、BIO监听一个连接需要一个线程,NIO中一个线程可以监听多个连接
5、NIO三大核心原理
1、每个channel都对应一个buffer
2、selector对应一个线程,一个线程对应多个channel
3、selector选择哪个channel是由事件决定的
4、数据的读写都是操作的buffer
5、BIO中要么是输入流要么是输出流,NIO中的Buffer可读也可写
6、Buffer和Channel的注意细节
1、ByteBuffer支持类型化的put和get,put放入的是什么数据类型 get就应该使用相应的类型来取出
2、可以改变Buffer的可读性
3、NIO提供MappedByteBuffer,可以直接让文件在内存中修改(堆外内存),相当于直接在操作系统上操作文件而不用再拷贝一份进行操作,效率快
4、读写操作可以通过一个Buffer来操作,NIO还支持通过多个Buffer操作即Buffer数组形式操作
7、NIO部分demo代码
package com.dwk.nio;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Set;
/**
* nio 案例实现
*/
public class NioDemo {
public static void main(String[] args) {
//buffer的基本使用
//intBuffTest(5);
//channel的基本使用
//channelTest();
//文件拷贝
//transferFormTest("","");
//buffer的存取
//bufferPutGet();
//buffer的只读
//readOnlyBuffer();
//使用byteBuffer数组读取进行数据交互
bufferArray();
}
public static void intBuffTest(int capacity) {
//创建一个容量大小为capacity的buff
IntBuffer allocate = IntBuffer.allocate(capacity);
for (int i = 0; i < capacity; i++) {
allocate.put(i * 2);
}
//将buffer切换,读写切换 处理buffer内的标记
allocate.flip();
//指定开始读取的位置
//allocate.position(1);
//指定结束读取的位置
//allocate.limit(2);
while (allocate.hasRemaining()){
System.out.println(allocate.get());
}
}
public static void channelTest(){
//DatagramChannel 用于UDP数据的读写,ServerSocketChannel/SocketChannel用于TCP的数据读写
//文件写通道
fileChannelWriteTest();
//文件读通道
fileChannelReadTest();
//使用一个通道完成文件的读写 - 文件拷贝
fileChannelWriteAndReadTest();
}
/**
* 文件写入
*/
public static void fileChannelWriteTest(){
FileOutputStream fileOutputStream = null;
FileChannel fileChannel = null;
ByteBuffer byteBuffer;
try {
String str = "fileChannelTest";
fileOutputStream = new FileOutputStream("C:\\duwk\\code\\myself\\frame-master\\netty\\src\\main\\resources\\file\\FileChannel.txt");
//获取通道
fileChannel = fileOutputStream.getChannel();
//创建缓冲区
byteBuffer = ByteBuffer.allocate(1024);
//写入缓冲区
byteBuffer.put(str.getBytes("UTF-8"));
//缓冲区索引重置
byteBuffer.flip();
//缓冲区数据写入通道
fileChannel.write(byteBuffer);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
fileOutputStream.close();
fileChannel.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* 文件读取
*/
public static void fileChannelReadTest(){
FileInputStream fileInputStream = null;
ByteBuffer byteBuffer = null;
FileChannel channel = null;
try {
String filePath = "C:\\duwk\\code\\myself\\frame-master\\netty\\src\\main\\resources\\file\\FileChannel.txt";
File file = new File(filePath);
fileInputStream = new FileInputStream(file);
//通道读取文件
channel = fileInputStream.getChannel();
//缓冲区读取通道
byteBuffer = ByteBuffer.allocate((int) file.length());
channel.read(byteBuffer);
byteBuffer.flip();
//缓冲区数据输出
System.out.println(new String(byteBuffer.array(),"UTF-8"));
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
fileInputStream.close();
channel.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* 文件拷贝
*/
public static void fileChannelWriteAndReadTest(){
FileOutputStream outputStream = null;
FileInputStream inputStream = null;
ByteBuffer byteBuffer = null;
try {
String fileName = "C:\\duwk\\code\\myself\\frame-master\\netty\\src\\main\\resources\\file\\FileChannel.txt";
String targetFileName = "C:\\duwk\\code\\myself\\frame-master\\netty\\src\\main\\resources\\file\\FileChannel副本.txt";
File file = new File(fileName);
File fileClone = new File(targetFileName);
if (fileClone.exists()) {
fileClone.delete();
fileChannelWriteAndReadTest();
}
inputStream = new FileInputStream(file);
//读取源文件流到通道
FileChannel inChannel = inputStream.getChannel();
//通道中的数据流写入到缓冲区
byteBuffer = ByteBuffer.allocate(1024);
inChannel.read(byteBuffer);
byteBuffer.flip();
//将缓冲区中的数据流写入到另一个通道
outputStream = new FileOutputStream(fileClone);
FileChannel outChannel = outputStream.getChannel();
outChannel.write(byteBuffer);
}catch (Exception e){
e.printStackTrace();
}finally {
try {
outputStream.close();
inputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* buffer的类型存取 按顺序存取
*/
public static void bufferPutGet(){
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
byteBuffer.putInt(1);
byteBuffer.putLong(1L);
byteBuffer.putChar('A');
byteBuffer.putShort((short) 1);
byteBuffer.flip();
//正常取出
int anInt = byteBuffer.getInt();
long aLong = byteBuffer.getLong();
char aChar = byteBuffer.getChar();
short aShort = byteBuffer.getShort();
System.out.println(anInt);
System.out.println(aLong);
System.out.println(aChar);
System.out.println(aShort);
System.out.println("======================");
//乱序取出 有异常
short bShort = byteBuffer.getShort();
char bChar = byteBuffer.getChar();
long bLong = byteBuffer.getLong();
int bnInt = byteBuffer.getInt();
System.out.println(bnInt);
System.out.println(bLong);
System.out.println(bChar);
System.out.println(bShort);
}
/**
* 设置buffer只读
*/
public static void readOnlyBuffer(){
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
byteBuffer.putInt(1);
//设置只读
byteBuffer.asReadOnlyBuffer();
int anInt = byteBuffer.getInt();
System.out.println("buffer只读 ==>" + anInt);
}
/**
* 使用通道的transferFrom方法拷贝文件
* @param sourcePath 源文件路径
* @param targetPath 目标文件路径
*/
public static void transferFormTest(String sourcePath,String targetPath){
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
FileChannel inputStreamChannel = null;
FileChannel outputStreamChannel = null;
try {
//创建文件流
inputStream = new FileInputStream(sourcePath);
outputStream = new FileOutputStream(targetPath);
//信道
inputStreamChannel = inputStream.getChannel();
outputStreamChannel = outputStream.getChannel();
//拷贝 参数:src = 源通道 position = 文件内开始转移的位置,必须是非负数 count = 最大的转换字节数,必须非负数
outputStreamChannel.transferFrom(inputStreamChannel,0,inputStreamChannel.size());
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
//关闭通道和流
inputStreamChannel.close();
outputStreamChannel.close();
inputStream.close();
outputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* 零拷贝
* mappedByteBuffer - 可以让文件直接在内存(堆外内存)中进行修改,操作系统不必拷贝一次 NIO同步到文件
* 相当于直接操作源文件,性能高,但是不安全
*/
public static void mappedByteBufferTest(){
RandomAccessFile randomAccessFile = null;
try {
//name : 文件名 mode:模式(r,rw,rws,rwd)
randomAccessFile = new RandomAccessFile("", "");
//获取通道
FileChannel channel = randomAccessFile.getChannel();
//MapMode mode 模式, long position 可以直接修改的起始位置, long size 映射到内存的大小(不是索引位置)即文件的多少个字节映射到内存
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
//修改文件
map.put(0,(byte) 'A');
map.put(10,(byte) 'B');
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
randomAccessFile.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* buffer的分散和聚集
* buffer数组操作 Scattering:将数据写入到buffer时可以采用buffer数组依次写入 Gathering:从buffer读取数据时依次读取buffer数组
*/
public static void bufferArray(){
//服务端通道
ServerSocketChannel serverSocketChannel = null;
//客户端通道
SocketChannel socketChannel = null;
try {
//创建服务端通道
serverSocketChannel = ServerSocketChannel.open();
//指定端口
InetSocketAddress inetSocketAddress = new InetSocketAddress(6666);
//绑定端口并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//服务端的buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
//初始化buffer大小
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
//等待客户端连接
socketChannel = serverSocketChannel.accept();
//每次从客户端通道读取的字节数
int countByte = 8;
//获取客户端发送的数据,,循环读取
while (true){
//统计读取的字节数
int byteRead = 0;
while (byteRead < countByte){
//从客户端通道读取字节到buffer数组
long read = socketChannel.read(byteBuffers);
byteRead += read;
System.out.println("累计读取的字节:" + byteRead);
Arrays.stream(byteBuffers).map(buffer -> "position = " + buffer.position() + " limit = " + buffer.limit()).forEach(System.out::println);
}
//将所有的buffer反转
Arrays.stream(byteBuffers).forEach(buffer -> {buffer.flip();});
//将数据读出显示到客户端
long byteWrite = 0;
while (byteWrite < countByte){
long writeByte = socketChannel.write(byteBuffers);
byteWrite += writeByte;
}
//将所有的buffer清除
Arrays.stream(byteBuffers).forEach(buffer -> {buffer.clear();});
System.out.println("byteRead : " + byteRead + " byteWrite : " + byteWrite + " byteCount : " + countByte);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
serverSocketChannel.close();
socketChannel.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
8、Selector选择器基本介绍
1、Java的NIO用非阻塞的IO方式。可以用一个线程处理多个客户端的连接,就会使用到Selector(选择器)
2、Selector能检测多个通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
3、只用在真正有读写事件发生时,才会进行读写,减少系统开销,不用为每个连接都创建一个线程
4、避免了线程上下文切换的问题
5、Selector示意图和特点说明
9、NIO非阻塞网络编程原理分析
Selector、SelectorKey、ServerSocketChannel、SocketChannel之间的关系如图;
1、当客户端连接时,会通过ServerSocketChannel得到SocketChannel;
2、将SocketChannel注册到指定的Selector上,一个Selector上可以注册多个SocketChannel;
3、注册后返回一个SelectionKey会和该Selector关联(集合);
4、Selector进行监听select方法,返回有事件发生的通道的个数;
5、进一步得到各个SelectionKey(有事件发生);
6、再通过SelectionKey反向获取SocketChannel;
7、可以通过得到的SocketChannel完成业务处理;
8、demo代码:
package com.dwk.netty;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Set;
/**
* netty demo
*/
public class NettyDemo {
public static void main(String[] args) {
chatting();
}
/**
* 非阻塞实现服务端和客户端之间通信
*/
public static void chatting () {
Thread thread = new Thread(() -> {
serverTest();
});
Thread thread1 = new Thread(() -> {
clientTest();
});
thread.start();
//等待两秒启动客户端
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
thread1.start();
}
/**
* 服务器端
*/
public static void serverTest(){
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(6666);
serverSocketChannel.socket().bind(inetSocketAddress);
//设置非阻塞
serverSocketChannel.configureBlocking(false);
//服务端的socketChannel注册到selector 并设置监听事件为准备连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while (true){
//阻塞一秒后事件数量若为0则没有连接事件发生
boolean nothing = selector.select(1000) == 0;
if (nothing){
System.out.println("服务器等待了1秒,无连接");
continue;
}
//有事件发生,获取到事件的selectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//通过selectionKey反向获取通道
selectionKeys.forEach(selectionKey -> {
//判断事件类型
//客户端连接事件
boolean acceptable = selectionKey.isAcceptable();
//
boolean connectable = selectionKey.isConnectable();
//客户端写事件
boolean writable = selectionKey.isWritable();
//
boolean valid = selectionKey.isValid();
//客户端读事件
boolean readable = selectionKey.isReadable();
if (acceptable){
//处理连接事件
try {
//客户端连接事件,,给客户端生成一个非阻塞的SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
//将socketChannel注册到selector,设置监听事件为准备读事件,并关联一个Buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (readable){
try {
//处理读取事件
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//获取channel关联的buffer
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
socketChannel.read(buffer);
System.out.println("客户端发送的数据:" + new String(buffer.array()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//移除集合中的selectionKey,防止重复操作
selectionKeys.remove(selectionKey);
});
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 客户端
*/
public static void clientTest(){
String data = "我是数据!";
try {
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞
socketChannel.configureBlocking(false);
//设置服务器端的ip和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
boolean connect = socketChannel.connect(inetSocketAddress);
if (!connect){
System.out.println("客户端正在连接...");
while (!socketChannel.finishConnect()){
//客户端还没有连接成功,可以先处理其他逻辑
System.out.println("客户端还没有连接成功,还在连接!");
}
}
System.out.println("客户端连接成功!发送数据给服务端...");
ByteBuffer byteBuffer = ByteBuffer.wrap(data.getBytes());
int write = socketChannel.write(byteBuffer);
if (write == data.getBytes().length){
System.out.println("客户端数据发送完成!");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
10、ServerSocketChannel和SocketChannel
| ServerSocketChannel | SocketChannel | |
|---|---|---|
| 作用 | 在服务器端监听新的客户端Socket连接,偏向连接 | 网络IO通道,具体负责读写操作,NIO把缓冲区的数据写入通道或者把通道内的数据读入缓冲区 偏向数据的读写、有分散和聚集操作 |
| 类图 | ||
| 方法 |