Netty技术笔记【韩顺平课程】
文章目录
- 1、Java IO概述
- 2.Java IO模型介绍
- 2.1 基本IO说明
- 2.2 各个IO的使用场景
- 3. BIO工作机制
- 4. NIO编程
- 4.1 基本介绍
- 4.2 NIO 和 BIO 的比较
- 4.3 NIO三个核心概念
- 2. NIO核心概念详解
- 2 Buffer
- 2.1 Buffer是什么
- 2.2 Buffer类详细介绍
- 2.2.1 几个关键属性含义
- 2.2.1 MappedByteBuffer
- 2.2.2 Scatter和Gatter
- 3. Channel
- 3.1 什么是Channel
- 3.2 FileChannel
- 3.3 ServerSocketChannel 和 SocketChannel 类
- 4. Selector选择器
- 4.1 基本介绍
- 4.2 Selector特点说明
- 4.3 SelectorKey相关介绍
- 5. NIO的零拷贝
- 1. 传统的零拷贝
- 2. 传统IO提升版本**mmap优化的IO读写**
- 3. sendFile优化的IO读写
- 4. sendFile 优化版本
- 3. Netty概述
- 3.1 netty概述
1、Java IO概述
2.Java IO模型介绍
2.1 基本IO说明
I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
主要有三种模型:BIO、NIO、AIO
Java BIO
: 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。【俗话说,占着茅坑不拉屎,这个茅坑还是得被占着】
Java NIO
: 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器(Selector
)轮询到连接有I/O
请求就进行处理。【专门有一个老大哥在巡逻,没有事儿,别占着位置】
Java AIO
: 异步非阻塞,AIO
引入异步通道的概念,采用了Proactor
模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
2.2 各个IO的使用场景
BIO
方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4
以前的唯一选择,但程序简单易理解。NIO
方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4
开始支持。AIO
方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS
参与并发操作,编程比较复杂,JDK7
开始支持
3. BIO工作机制
是一种阻塞的IO,本质是一个连接一个线程。效率低、资源浪费程度高。
如何进行BIO编程?
服务器端启动一个
ServerSocket
客户端启动
Socket
对服务器进行通信,默认情况下服务器端需要对每个客户 建立一个线程与之通讯客户端发出请求后, 先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝
如果有响应,客户端线程会等待请求结束后,在继续执行
看上面的分析图,可以发现阻塞IO会有两个地方会发生阻塞,
- 如果没有客户端来建立连接,服务器会阻塞
- 如果客户端没有输入,服务器也会阻塞。
演示代码如下
简单的说明:
使用
BIO
模型编写一个服务器端,监听8886
端口,当有客户端连接时,就启动一个线程与之通讯。要求使用线程池机制改善,可以连接多个客户端.
服务器端可以接收客户端发送的数据(通过终端的
telnet
方式即可)。
/*** 用于处理客户端的连接*/
public void server () throws Exception{ExecutorService threadPool = Executors.newCachedThreadPool();// 1. 创建ServerSocket,并且指定服务器端的端口ServerSocket serverSocket = new ServerSocket(8887);System.out.println("Server is start......");// 传统的通过无限循环来感知连接while (true) {// 2. 监听,一直等待客户端连接final Socket socket = serverSocket.accept();System.out.println("a client is connect.....");// 3. 创建一个线程用于处理具体的连接之后的逻辑threadPool.execute(new Runnable() {@Overridepublic void run() {connectHandler(socket);}});}
}/*** 用于处理具体连接的handler.* 这个是建立连接之后处理的请求* @param socket*/
public static void connectHandler (Socket socket) {// 可以通过日志观察,每次创建一个新的连接,服务器端都会创建一个新的线程IdSystem.out.println("线程Id: " +Thread.currentThread().getId());try {// 3.2 提前new一个byte数组,用于存储从inputStream中读取的数据byte[] bytes = new byte[1024];// 4. 接受客户端输入InputStream inputStream = socket.getInputStream();while (true) {// 5. 从inputStream中读取数据,并且存储到提前指定的byte[]中。// 可能很多人对这种方法比较陌生,参数为写入的目的地int byteReadNum = inputStream.read(bytes);// 6. 判断是否有读取到数据if (byteReadNum != -1) {System.out.println(new String(bytes, 0 ,byteReadNum));} else {// 执行完毕,直接跳出break;}}} catch (IOException e) {System.out.println("print some exception.....");}
}
4. NIO编程
4.1 基本介绍
Java NIO
全称 java non-blocking IO
,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO
(即 New IO
),是同步非阻塞的
核心内容
Channel
(通道),Buffer
(缓冲区), Selector
(选择器)
特点
Java NIO
的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。并且不需要让线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
通俗理解:NIO
是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。
类似于NIO应用
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
4.2 NIO 和 BIO 的比较
BIO
以流的方式处理数据,而NIO
以块的方式处理数据,块I/O
的效率比流I/O
高很多BIO
是阻塞的,NIO
则是非阻塞的BIO
基于字节流和字符流进行操作,而NIO
基于Channel
(通道)和Buffer
(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector
(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
4.3 NIO三个核心概念
图中有三个核心组件(Selector、Channel、Buffer
),各个核心组件有如下关系:
- 每个
Channel
对应一个Buffer
。 Selector
对应一个线程,但是一个Selector
可以对应多个Channel
- 程序切换到哪个
Channel
是由事件(Event
)决定的,Event
是一个重要的概念。 Selector
会根据不同的事件在各个Channel
上切换Buffer
其实就是一个内存块,底层是由一个数组构成BIO
中数据的写入是单向的,要么输入,要么输出;而NIO
不同,可以读写,需要利用filp进行模式切换。Channel
本身是双向的
关于NIO更加深入的理解,可以参见第二章节。
2. NIO核心概念详解
2 Buffer
2.1 Buffer是什么
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
2.2 Buffer类详细介绍
2.2.1 几个关键属性含义
属性 | 含义 |
---|---|
mark |
用于记录当前position 的位置,可以通过reset()恢复position的位置
|
position | 下一次将要读、写的下标索引,该值一般小于等于capacity |
limit | 缓冲区中可以读、写的最后一个下标值,该值一般小于等于capacity |
capacity | 缓冲区最大的元素数量,一旦创建就无法改变 |
一般来说,四个属性的关系应该属于
0 <= mark <= position <= limit <= capacity
如何理解这五个字段
比如下面的例子:
public class BufferExample {public static void main(String[] args) {// 1. 创建一个Buffer,大小为5,即可以存放5个intIntBuffer intBuffer = IntBuffer.allocate(5);// 2. 向buffer中存放数据for (int i = 0; i < intBuffer.capacity(); i++) {intBuffer.put(i * 2);}// 3. 如何从buffer中读取数据,将buffer转换,读写切换intBuffer.flip();// 4. 读取数据while (intBuffer.hasRemaining()) {System.out.println(intBuffer.get());}}
}
第一步:Buffer
刚创建时,capacity = 5
,固定不变。limit
指针指向5
,position
指向0
,mark
指向-1
。
第二步:之后调用 intBuffer.put
方法,向buffer
中添加数据,会不断移动position
指针,最后position
变量会和limit
指向相同。
第三步:调用 buffer.flip()
实际上是重置了position
和limit
两个变量,将limit
放在position
的位置,position
放在0的位置。这里只是最后的position
和limit
位置相同,所以flip
后limit
位置没变。【limit
移到写入的position
】
// 简单看看filp()的源码
public final Buffer flip() {limit = position;position = 0;mark = -1;return this;
}
第四步:调用 intBuffer.get()
实际上是不断移动position
指针,直到它移动到limit
的位置。
冗长又无聊的Api介绍
api | 具体作用 |
---|---|
public final int capacity()
|
直接返回了此缓冲区的容量 |
public final int position()
|
直接返回了此缓冲区指针的当前位置 |
public final Buffer position(int newPosition);
|
设置此缓冲区的位置,设置position |
public final int limit();
|
返回此缓冲区的限制 |
public final Buffer limit(int newLimit);
|
设置此缓冲区的限制,设置limit |
public final Buffer clear();
|
清除此缓冲区,即将各个标记恢复到初识状态, position = 0;limit = capacity; mark = -1,但是并没有删除数据。 |
public final Buffer flip();
|
反转此缓冲区, limit = position;position = 0;mark = -1。 |
public final boolean hasRemaining();
|
告知当前位置和限制之间是否有元素。 |
public abstract boolean isReadOnly();
|
此方法为抽象方法,告知此缓冲区是否为只读缓冲区,具体实现在各个实现类中。 |
public abstract boolean hasArray();
|
告知此缓冲区是否具有可访问的底层实现数组 |
public abstract Object array();
|
返回此缓冲区的底层实现数组 |
Buffer
本身是一个抽象的类,根据不同的数据类型,有不同的实现,可以挑选出任意一个实现类,分析具体的方法。
API | 具体含义 |
---|---|
public static ByteBuffer allocateDirect(int capacity);
|
创建直接缓冲区 |
public static ByteBuffer allocate(int capacity) ;
|
设置缓冲区的初识容量 |
public abstract byte get();
|
从当前位置position上get数据,获取之后,position会自动加1 |
public abstract byte get(int index);
|
通过绝对位置获取数据。 |
public abstract ByteBuffer put(byte b);
|
从当前位置上添加,put之后,position会自动加1 |
public abstract ByteBuffer put(int index, byte b);
|
从绝对位置上添加数据 |
2.2.1 MappedByteBuffer
有什么作用?
MappedByteBuffer
可以让文件直接在内存中(堆外内存)修改,操作系统不需要拷贝一次。
主要用法是
参数1: FileChannel.MapMode.READ_WRITE,使用的读写模式
参数2: 0,可以直接修改的起始位置
参数3: 5,是映射到内存的大小(不是文件中字母的索引位置),即将 1.txt 的多少个字节映射到内存,也就是可以直接修改的范围就是 [0, 5)
MappedByteBuffer map(MapMode mode, long position, long size)
public void test() throws IOException {RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");//获取对应的文件通道FileChannel channel = randomAccessFile.getChannel();MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);mappedByteBuffer.put(0,(byte)'N');mappedByteBuffer.put(3, (byte)'M');//会抛出 IndexOutOfBoundsExceptionmappedByteBuffer.put(5, (byte)'Y');// 关闭文件randomAccessFile.close();
}
2.2.2 Scatter和Gatter
什么是Scatter?Gatter?
**分散(scatter)**从Channel
中读取是指在读操作时将读取的数据依次写入多个Buffer
中。
**聚集(gather)**写入Channel是指在写操作时依次将多个buffer的数据写入同一个Channel,
scatter和Gatter的演示
public static void testScannerAndGather () {// 利用ServerSocketChannel来演示try {ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();InetSocketAddress inetSocketAddress = new InetSocketAddress(6667);// 服务器绑定端口并且启动serverSocketChannel.socket().bind(inetSocketAddress);// 等待客户端连接SocketChannel socketChannel = serverSocketChannel.accept();// 创建一个Buffer数组ByteBuffer[] bufferArray = new ByteBuffer[2];bufferArray[0] = ByteBuffer.allocate(5);bufferArray[1] = ByteBuffer.allocate(3);// 利用循环不断给buffer数组中写入数据int maxLength = 8;while (true) {// 读取字节的长度int byteReadLength = 0;while (byteReadLength < maxLength) {// 将客户端读入的数据写入到buffer数组中long readLength = socketChannel.read(bufferArray);byteReadLength += readLength;System.out.println("Have read byteReadLength: " + byteReadLength);//使用流打印,看看当前这个buffer的position和limitfor (ByteBuffer buffer : bufferArray) {String s = "position=" + buffer.position() + ", limit = " + buffer.limit();System.out.println(s);}}//读取数据后需要将所有的buffer进行flipArrays.asList(bufferArray).forEach(Buffer::flip);// 将数据显示到客户端上long byteWriteLength = 0;while (byteWriteLength < maxLength) {long l = socketChannel.write(bufferArray);byteWriteLength += l;}// 将所有的 buffer 进行clear操作Arrays.asList(bufferArray).forEach(Buffer::clear);System.out.println("byteRead=" + byteReadLength + ", byteWrite=" + byteWriteLength+ ", msgLength=" + maxLength);}} catch (IOException e) {e.printStackTrace();}
}
3. Channel
3.1 什么是Channel
NIO的通道类似于流,但有些区别
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓存读数据,也可以写数据到缓存
有哪些Channel
?
- 常用的
Channel
类有:FileChannel
、DatagramChannel
、ServerSocketChannel
(类似ServerSocket
)、SocketChannel
(类似Socket
) FileChannel
用于文件数据的读写,DatagramChannel
用于UDP
数据的读写,ServerSocketChannel
和SocketChannel
用于TCP
数据读写
各个Channel关系如下
3.2 FileChannel
作用:一个连接到文件的通道,可以通过文件通道读写文件。用于读取、写入、映射、操作的channel
。
核心方法:
理解的时候可以站在Buffer
方向。
API | 具体作用 |
---|---|
public int read(ByteBuffer dst)
|
从通道读取数据并放到缓冲区中;此操作也会移动 Buffer 中的position指针,不断往position中放数据,read完成后position指向limit |
public int write(ByteBuffer src)
|
把缓冲区的数据写到通道中;此操作也会不断移动Buffer中的position位置直到limit,读取到的数据就是position到limit这两个指针之间的数据 |
public long transferFrom(ReadableByteChannel src, long position, long count)
|
从目标通道中复制数据到当前通道 |
public long transferTo(long position, long count, WritableByteChannel target)
|
把数据从当前通道复制给目标通道;该方法拷贝数据使用了零拷贝,通常用来在网络IO传输中,将FileChannel里面的文件数据直接拷贝到与客户端或者服务端连接的Channel里面从而达到文件传输。 |
例子:
/*** 将message写入到本地文件中** @param message 要写入的文本* @param descFilePath 本地目标文件*/
private static void writeToFile (String message, String descFilePath) {try {// 1. 创建一个用于写入文件的输出流FileOutputStream fileOutputStream = new FileOutputStream(descFilePath);// 2. 通过fileOutputStream获取对应的fileChannel,而底层实际上是FileChannelImplFileChannel channel = fileOutputStream.getChannel();// 3. 创建一个Buffer【解决内容存储的Buffer位置】ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 4. 将原message写入到buffer中byteBuffer.put(message.getBytes());// 5. 对ByteBuffer进行反转,开始读取, 将前几步写入的数据读取出来byteBuffer.flip();// 6. 将ByteBuffer数据写入到FileChannel,这一步会不断的移动position直到limitchannel.write(byteBuffer);// 7. 关闭fileOutputStream.close();} catch (IOException e) {e.printStackTrace();}
}
整体逻辑:
先看几个简单的例子
1、将文本写入到本地文件中
/*** 将message写入到本地文件中** @param message 要写入的文本* @param descFilePath 本地目标文件*/
private static void writeToFile (String message, String descFilePath) {try {// 1. 创建一个用于写入文件的输出流FileOutputStream fileOutputStream = new FileOutputStream(descFilePath);// 2. 通过fileOutputStream获取对应的fileChannel,而底层实际上是FileChannelImplFileChannel channel = fileOutputStream.getChannel();// 3. 创建一个Buffer【解决内容存储的Buffer位置】ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 4. 将原message写入到buffer中byteBuffer.put(message.getBytes());// 5. 对ByteBuffer进行反转,开始读取, 将前几步写入的数据读取出来byteBuffer.flip();// 6. 将ByteBuffer数据写入到FileChannel,这一步会不断的移动position直到limitchannel.write(byteBuffer);// 7. 关闭fileOutputStream.close();} catch (IOException e) {e.printStackTrace();}
}
2、从本地文件中读取文本内容
/*** 从本地文件中读取数据** @param filePath 本地文件路径*/
private static void readFromFile (String filePath) {try {// 1. 将path转化为File,并且创建文件输入流File file = new File(filePath);FileInputStream fileInputStream = new FileInputStream(file);// 2. 获取对应的FileChannelFileChannel fileChannel = fileInputStream.getChannel();// 3. 创建缓冲区,将数据写入到缓冲区中ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());// 4. 将通道读入到缓冲区中fileChannel.read(byteBuffer);// 5. 将结果输出成文字System.out.println(new String(byteBuffer.array()));fileInputStream.close();} catch (IOException e) {e.printStackTrace();}}
3、将一个文件内容读取到另一个文件
/*** 将数据从一个文件读取到另一个文件** @param fileFrom 数据源文件* @param fileTo 数据去向*/
private static void readFileToOtherFile (String fileFrom , String fileTo) {try {// 1. 从源文件中读取数据FileInputStream fileInputStream = new FileInputStream(fileFrom);FileChannel inChannel = fileInputStream.getChannel();// 2. 设置读取的文件ChannelFileOutputStream fileOutputStream = new FileOutputStream(fileTo);FileChannel outChannel = fileOutputStream.getChannel();// 3. 分配自己的buffer用于处理,也就是说每次能够分配多少byte用于传输数据ByteBuffer byteBuffer = ByteBuffer.allocate(512);while (true){// 清空buffer,由于循环的最后执行了 write 操作,会将 position 移动到 limit 的位置// 清空 Buffer的操作才为上一次的循环重置position的位置// 如果没有重置position,那么上次读取后,position和limit位置一样,读取后read的值永远为0byteBuffer.clear();// 将数据存入 ByteBuffer,它会基于 Buffer 此刻的 position 和 limit 的值,// 将数据放入position的位置,然后不断移动position直到其与limit相等;int read = inChannel.read(byteBuffer);System.out.println("read=" + read);// 表示读完if (read == -1) {break;}// 将buffer中的数据写入到 FileChannel02 ---- 2.txtbyteBuffer.flip();outChannel.write(byteBuffer);}//关闭相关的流fileInputStream.close();fileOutputStream.close();} catch (IOException e) {e.printStackTrace();}
}
3.3 ServerSocketChannel 和 SocketChannel 类
ServerSocketChannel
:主要用于在服务器监听新的客户端Socket
连接。
常见的方法有:
方法 | 解释 |
---|---|
public static ServerSocketChannel open()
|
得到一个 ServerSocketChannel 通道
|
public final ServerSocketChannel bind(SocketAddress local)
|
设置服务器监听端口 |
public final SelectableChannel configureBlocking(boolean block)
|
用于设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式 |
public abstract SocketChannel accept()
|
接受一个连接,返回代表这个连接的通道对象 |
public final SelectionKey register(Selector sel, int ops)
|
将Channel注册到选择器并设置监听事件,也可以在绑定的同时注册多个事件 channel.register(selector,Selectionkey.OP_READ | Selectionkey.OP_CONNECT)
|
SocketChannel
:网络IO
通道,具体负责进行读写操作。NIO
把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区
方法 | 含义 |
---|---|
public static SocketChannel open()
|
得到一个SocketChannel通道 |
public final SelectableChannel configureBlocking(boolean block)
|
设置阻塞或非阻塞模式,取值 false表示采用非阻塞模式 |
public abstract boolean connect(SocketAddress remote)
|
连接服务器 |
public boolean finishConnect()
|
完成连接操作 |
public int write(ByteBuffer src)
|
往Channel中写入数据 |
public int read(ByteBuffer dst)
|
将Channel 中的数据读到buffer 中
|
public final SelectionKey register(Selector sel, int ops, Object att)
|
注册Channel到选择器并设置监听事件 |
public final void close()
|
关闭通道 |
4. Selector选择器
4.1 基本介绍
Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到
Selector
(选择器)Selector
能够检测多个注册的通道上是否有事件发生(注意:多个Channel
以事件的方式可以注册到同一个Selector
),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
避免了多线程之间的上下文切换导致的开销
4.2 Selector特点说明
Netty
的IO
线程NioEventLoop
聚合了Selector
(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。当线程从某客户端
Socket
通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞
IO
的空闲时间用于在其他通道上执行IO
操作,所以单独的线程可以管理多个输入和输出通道。由于读写操作都是非阻塞的,这就可以充分提升
IO
线程的运行效率,避免由于频繁I/O
阻塞导致的线程挂起。一个
I/O
线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O
一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
如何工作?
1、 当客户端连接时,可以通过ServerSocketChannel
得到SocketChannel
。
2、可以通过register(Selector sel, int ops)
,将SocketChannel
注册到Selector
上,并且一个selector
上可以注册多个SocketChannel
。
3、注册之后会得到SelectionKey
,会与该Selector
进行关联。
4、Selector
进行监听select
方法,并且返回有事件发生的通道的个数。并且进一步得到有事件发生的SelectionKey
5、通过SelectorKey
的channel()
方法,反向获取SocketChannel
。
6、上一步可以得到Channel
,然后完成业务的处理。
实例代码演示:实现客户端和服务器端的通信
服务器端Demo
public class GroupChatServer {/*** 定义属性*/private Selector selector;/*** 定义ServerSocketChannel*/private ServerSocketChannel serverSocketChannel;/*** 定义端口*/private static final int SERVER_PORT = 6667;/*** 重写构造方法*/public GroupChatServer () {try {// 1. 得到选择器selector = Selector.open();// 2. 初始化服务器ChannelserverSocketChannel = ServerSocketChannel.open();serverSocketChannel.socket().bind(new InetSocketAddress(SERVER_PORT));// 3. 设置服务器端的阻塞模式serverSocketChannel.configureBlocking(false);// 4. 将Channel注册到Selector中,并且绑定监听事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);} catch (IOException e) {e.printStackTrace();}}/*** 写主核心方法** @param args*/public static void main(String[] args) {GroupChatServer groupChatServer = new GroupChatServer();groupChatServer.listen();}/*** 编写服务器的监听方法*/public void listen () {try {// 1. 通过轮询的方式监听while (true) {// 2. 通过select()获取事件个数int count = selector.select();if (count == 0) {System.out.println("No client connect……please wait.");continue;}if (count > 0 ) {// 3. 获取所有事件对应的keySet<SelectionKey> selectionKeys = selector.selectedKeys();// 4. 开始遍历所有的keyIterator<SelectionKey> iteratorKey = selectionKeys.iterator();while (iteratorKey.hasNext()) {// 5. 遍历取出对应的keySelectionKey selectionKey = iteratorKey.next();// 6. 监听是否是连接事件if (selectionKey.isAcceptable()) {// 7. 获取客户端的socketSocketChannel socketChannel = serverSocketChannel.accept();// 8. 将该socket设置为非阻塞的socketChannel.configureBlocking(false);// 9. 注册所有的读取事件socketChannel.register(selector, SelectionKey.OP_READ);System.out.println(socketChannel.getRemoteAddress() + " is connecting……");}// 10. 如果监听到有读取事件,则开始读取数据if (selectionKey.isReadable()) {// 11. 专门读取客户端的数据readDateFromClient(selectionKey);}iteratorKey.remove();}}}} catch (IOException e) {e.printStackTrace();}}/*** 读取客户端数据** @param selectionKey*/private void readDateFromClient(SelectionKey selectionKey) {SocketChannel channel = null;try {// 1. 通过key获取到Channelchannel = (SocketChannel) selectionKey.channel();// 2. 分配缓冲区接纳数据ByteBuffer buffer = ByteBuffer.allocate(1024);// 3. 开始读取数据,并且判断是否读取到数据int readCount = channel.read(buffer);if (readCount > 0) {// 4. 输出消息System.out.println("Read message from client: " + new String(buffer.array()));// 5. 同时将消息发送给其他客户端sendToOtherClient(buffer, channel);}} catch (IOException e) {// 如果系统抛出了异常,则需要专门处理try {System.out.println(channel.getRemoteAddress() + "is offlined");// 取消通道,并且关闭注册selectionKey.cancel();channel.close();} catch (IOException ex) {ex.printStackTrace();}}}/*** 将消息从一个客户端发送给你另一个客户端** @param buffer 消息buffer* @param fromChannel 客户端Channel*/private void sendToOtherClient(ByteBuffer buffer, SocketChannel fromChannel) {System.out.println("Send message to other client……");// 1. 遍历注册到Selector上面的所有的Channel,当时不要发送给自己了Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();// 2. 同样通过key获取到对应的ChannelChannel channel = key.channel();// 3. channel排除来源于自己if (channel instanceof SocketChannel && channel != fromChannel) {// 转化为对应的SocketChannelSocketChannel destChannel = (SocketChannel) channel;// 存储到buffer中,并且写出try {destChannel.write(buffer);} catch (IOException e) {e.printStackTrace();}}}}}
客户端Demo
public class GroupChatClient {/*** 服务器IP地址*/private static final String SERVER_HOST = "127.0.0.1";/*** 服务器端口号*/private static final int SERVER_PORT = 6667;/*** 对应的Selector*/private Selector selector;/*** 客户端的Channel*/private SocketChannel socketChannel;/*** 不知道干啥*/private String userName;/*** 重写构造方法*/public GroupChatClient () {try {selector = Selector.open();// 1. 连接到服务器socketChannel = SocketChannel.open(new InetSocketAddress(SERVER_HOST, SERVER_PORT));// 2. 设置阻塞状态socketChannel.configureBlocking(false);// 3. 将Channel注册到Selector中socketChannel.register(selector, SelectionKey.OP_READ);// 4. 获取客户端名称,并且打印客户端信息userName = socketChannel.getLocalAddress().toString();System.out.println(userName + " is ok!!!");} catch (IOException e) {e.printStackTrace();}}/*** Main方法** @param args*/public static void main(String[] args) {GroupChatClient groupChatClient = new GroupChatClient();// 启动一个线程用户处理服务器的消息new Thread(()->{while (true) {groupChatClient.readMessageFromServer();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}}).start();// 主线程用于发送数据给服务器端Scanner scanner = new Scanner(System.in);while (scanner.hasNextLine()) {String s = scanner.nextLine();groupChatClient.sendMessage(s);}}/*** 给服务器发送消息** @param message 消息内容*/public void sendMessage (String message) {message = userName + " say: " + message;try {socketChannel.write(ByteBuffer.wrap(message.getBytes()));} catch (IOException e) {e.printStackTrace();}}/*** 读取从服务器端回复的消息**/public void readMessageFromServer () {// 1. 获取对应的Channeltry {int readChannelCount = selector.select();if (readChannelCount == 0 ) {System.out.println("There is no message from server .");return;}if (readChannelCount > 0) {// 2. 获取所有的key,并且开始遍历所有的keySet<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectionKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();// 判断key是否是可读的if (key.isReadable()) {// 通过key获取对应的ChannelSocketChannel channel = (SocketChannel) key.channel();// 分配一个缓冲区ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 并且读取数据到缓冲区中channel.read(byteBuffer);// 把缓冲区数据转化为字符串String message = new String(byteBuffer.array());System.out.println("Message read from server, message: " + message);}}}} catch (Exception e) {e.printStackTrace();}}
}
4.3 SelectorKey相关介绍
有什么作用?
Selector
通过管理SelectionKey
的集合从而去监听各个Channel
。
当Channel
注册到Selector
上面时,会携带该Channel
关注的事件 (SelectionKey包含Channel以及与之对应的事件),并会返回一个SelectionKey的对象,Selector将该对象加入到它统一管理的集合中去,从而对Channel进行管理。
有哪些事件?
1、public static final int OP_READ = 1 << 0
表示读操作,代表本Channel
已经接受到其他客户端传过来的消息,需要将Channel
中的数据读取到Buffer
中去
2、public static final int OP_WRITE = 1 << 2
表示写操作,一般临时将Channel的事件修改为它,在处理完后又修改回去。
3、public static final int OP_CONNECT = 1 << 3
代表建立连接。一般在ServerSocketChannel
上绑定该事件,结合 channel.finishConnect()
在连接建立异常时进行异常处理
4、public static final int OP_ACCEPT = 1 << 4
表示由新的网络连接可以连接。与ServerSocketChannel
进行绑定,用于创建新的SocketChannel
,并把其注册到Selector
上去。
5. NIO的零拷贝
什么是零拷贝?
从操作系统的角度来看,文件的传输不存在CPU的拷贝,只存在DMA拷贝(直接内存拷贝,不使用CPU完成)。零拷贝是网络编程的关键,很多性能优化都离不开它。
有哪些零拷贝类型?
1、mmap(内存映射)
2、sendFile
分析零拷贝需要看看传统文件拷贝是如何做到的。
1. 传统的零拷贝
2. 传统IO提升版本mmap优化的IO读写
主要优化点有:
mmap
通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以与内存空间共享数据。在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。- 需要进行4次上下文切换,3次数据拷贝
- 适合小数据量的读写
3. sendFile优化的IO读写
- 优化原理:数据根本不经过用户态,直接从内核缓冲区进入到
SocketBuffer
,同时,由于和用户态完全无关,就减少了一次上下文切换。 - 需要2次上下文切换和最少2此数据拷贝。
- 适合大文件的传输。
4. sendFile 优化版本
Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket Buffer
的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝
注:这里其实有一次CPU拷贝,kernel buffer -> socket buffer。但是,拷贝的信息很少,只拷贝了数据的长度、偏移量等关键信息,消耗低,可以忽略不计。
最后来看看什么是零拷贝?
- 这里说的零拷贝,是从操作系统角度看,因为内核缓冲区之间,没有数据是重复的。(只有kernal buffer有一份数据)
- 零拷贝可以带来更少的数据复制,还能带来其他的性能优化,比如线程上下文切换、更少的CPU共享、无CPU校验和计算等
3. Netty概述
为什么需要Netty,或者说原NIO有什么问题呢?
NIO
的类库和API
繁杂,使用麻烦:需要熟练掌握Selector
、ServerSocketChannel
、SocketChannel
、ByteBuffer
等。需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到
Reactor
模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO
程序。开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
JDK NIO
的Bug
:例如臭名昭著的Epoll Bug
,它会导致Selector
空轮询,最终导致CPU 100%
。直到JDK 1.7
版本该问题仍旧存在,没有被根本解决。
3.1 netty概述
Netty有什么优点?
Netty对JDK自带的NIO的API进行了封装,解决了上述问题。
设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池.
安全:完整的 SSL/TLS 和 StartTLS 支持
高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。
Netty技术笔记【韩顺平课程】相关推荐
- 韩顺平.2011最新版.玩转oracle视频教程笔记,韩顺平.2011最新版.玩转oracle视频教程(笔记)...
韩顺平.2011最新版.玩转oracle视频教程 ORA-01045: user XIAOMING lacks CREATE SESSION privilege; logon denied 警告: 您 ...
- JAVA——Java后端技术体系韩顺平框架图_韩顺平Java基础学习路线图
第一阶段:Java基础 变量.控制结构.OOP(封装,继承,多态).数组.Java API.异常和处理.集合.泛型.IO.反射.网络通信 第二阶段:Java高级 Java多线程/高并发 1.1 并发基 ...
- 韩顺平c语言视频笔记,韩顺平视频笔记
1.添加字段 alter table aaa1 add (classid number (7,2)); 2.修改字段的长度 alter table aaa1 modify (xm varchar2(3 ...
- [学习笔记]韩顺平-快速学习github -github教程 github视频 github入门
Github实战:创建repository发布到公网,所有人能访问 效果:网站https://tnhsp2.github.io/ 就会看到 好大的一只鲸鱼 步骤1:按照规范创建一个repositroy ...
- 韩顺平 2021零基础学Java 学习笔记
韩顺平 2021零基础学Java 学习笔记(1)(自用)_丨shallow丨的博客-CSDN博客_韩顺平java笔记 韩顺平 2021零基础学Java 学习笔记(2)(自用)_丨shallow丨的博客 ...
- Java基础易忘重点内容笔记【附B站韩顺平老师课程链接】
B站课程链接:https://www.bilibili.com/video/BV1fh411y7R8?spm_id_from=333.999.0.0 1. 文档注释 用于对Java方法的注释,可据此生 ...
- 韩顺平mysql优化笔记_韩顺平 mysql优化笔记.doc
韩顺平 mysql优化笔记.doc 还剩 6页未读, 继续阅读 下载文档到电脑,马上远离加班熬夜! 亲,喜欢就下载吧,价低环保! 内容要点: ? 垂直分割表如果你的数据库的存储引擎是 MyISAM 的 ...
- 韩顺平Linux教程学习笔记
Linux系统学习笔记 新装了deepin v23系统,结果磁盘没设置好,玩崩了,百度半天修复不了,看看韩顺平老师的Linux操作课程,做做笔记(只记录对自己有用的). B站网址 基础篇·Li ...
- servletjsp入门.....韩顺平笔记
u 背景知识介绍 J2EE的13种技术 java->servlet->jsp [技术总是有一个演变过程] zip粘贴到word设置 u 回顾一下我们现有的技术 java 基础(面向对象,集 ...
最新文章
- sklearn中的cross_val_score交叉验证
- 96.总线里的异步通信
- 计算机组成原理考试复习
- linux安装qq_体验一下,看看 Linux 系统中的 QQ 是什么样子的
- iOS开发:创建真机调试证书
- QT学习-10/31/2012
- Google官方 详解 Android 性能优化【史诗巨著之内存篇】
- Android 网络管理
- 【ML小结14】条件随机场CRF
- zbbz的lisp_学习LISP语言的体会
- 当马队遭遇狼群,阿里华为长篇竞合剧基情开幕
- Delphi 金额转大写
- 基于Hadoop平台使用MapReduce统计某银行信用卡违约用户数量
- Scala和Kotlin脚本编程
- 如果非要回到古代,我会选择春秋战国
- 易企秀源码html,精仿易企秀源码
- 换服务器影响网站排名,网站更换服务器空间会影响排名吗
- 扎根基层一线 助力社区(村)发展
- 用原生JS实现网页调用系统自带的分享功能
- Flowable Unknown property used in expression: xxx
热门文章
- websocket 那些配置及具体的Demo
- Microsoft Releases .NET 7新功能
- Android 使用WebView 实现播放爱奇艺视频,可全屏(使用的腾讯的X5内核) (一)
- 使用RXTXcomm 报错 #Problematic frame: # C [rxtxSerial.dll+0x4465]
- 轩小陌的Python笔记-day06 数据类型
- 专业程序员路上用到的各种优秀资料、神器及框架
- 路由器频繁的死机的原因及解决技巧
- BMG5100千兆5G智能杆网关
- linux中more cmd.txt,cmd.txt · 究极贾露露/linux-file - Gitee.com
- 中级软件设计师2011上半年上午试题