目录
Netty简介
Netty 的介绍
- Netty 是由 JBOSS 提供的一个 Java 开源框架,现为 Github 上的独立项目。
- Netty 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络 IO 程序。
- Netty 主要针对在
TCP
协议下,面向 Client 端的高并发应用,或者 Peer-to-Peer 场景下的大量数据持续传输的应用。 - Netty 本质是一个 NIO 框架,适用于服务器通讯相关的多种应用场景。
- 这里的异步讲的是,我们发布一个消息,并不需要收到回复之后才能继续发送消息。
- 这里的事件驱动,指的是当有事件发生的时候,会触动到对应处理的机制,如果没有事件的发生,程序智慧保持当前的状态(这里仅仅是个人理解)
这里我们看一下它的体系结构:因为netty底层还是基于网络的,所以会有tcp/ip
作为底层的基石,在底层的基础上是原生的jdk的io网络层
,在这个基础上包了一层NIO
,最后才是netty
。
IO编程
IO模型
-
io模型:io模型简单理解就是:用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。,在java中有三种模型:BIO、NIO、AIO(这个先不管)
-
BIO:同步阻塞IO模型,对于每一个客户端,都会开一个新的线程来和服务器连接,因为线程之间的切换损耗大,而且客户端和服务器并不是时时刻刻都在传输数据的会造成不必要的资源浪费,【简单示意图】
-
NIO:异步非阻塞IO模型,它的特点是加上一个选择器,每一次选择器都会轮询下游的客户端的连接,如果发现有连接事件产生,它就会选择当前的连接,并且对连接事件进行处理。于
BIO
相比,这种方式大大减少了线程的消耗。【简单示意图】
- NIO 有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器) 。
- NIO 是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
- Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。
- HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP 1.1 大了好几个数量级。【图解示例】
所以实际上的,客户端或者服务器不是直接和通道来交换数据,而是通过缓冲区来和通道进行数据交互的。
三种IO模型的使用场景
- BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序简单易理解。
- NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持。
- AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。
对IO模型具体讲解
BIO应用实例
首先给大家梳理一下BIO
编程的流程
- 服务器端启动一个
ServerSocket
。 - 客户端启动
Socket
对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯。 - 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
- 如果有响应,客户端线程会等待请求结束后,再继续执行。
- 下面演示一个具体的实例:实例要求使用线程池来连接多个客户端,而客户端的连接可以使用
telnet
方式来连接。
package com.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 Exception{
// 创建一个线程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
// 创建一个serverSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while(true){
// 启动监听,等待客户端连接
final Socket socket=serverSocket.accept();
System.out.println("连接到了一个客户端");
// 新建一个线程
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
//在这里和客户端通讯
handler(socket);
}
});
}
}
public static void handler(Socket socket){
byte[] bytes = new byte[1024];
try {
// 通过一个socket 获取输入流
InputStream inputStream = socket.getInputStream();
// 循环读取输入流的数据
while(true){
int read=inputStream.read(bytes);
if (read!=-1){
System.out.println(new String(bytes,0,read));
}else{
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
System.out.println("关闭连接");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 现在我们来模拟一下客户端:
win+r
打开命令行,输入telnet 127.0.0.1 6666
,既可以连接上客户端,按照程序走向我们会受到提示说连接到一个客户端
,键盘快捷键输入ctrl+]
,就会看到命令行的变化,输入命令send hello
就可以发送数据,自然服务器也会对数据进行处理。【如下结果】
BIO模型的问题:
- 每个请求都需要创建独立的线程,与对应的客户端进行数据
Read
,业务处理,数据Write
。 - 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。
NIO和BIO的比较
- BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。
- BIO 是阻塞的,NIO 则是非阻塞的。
- BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
。 - Buffer和Channel之间的数据流向是双向的。
NIO三大核心的关系
- 每一个通道都会对应一个缓冲区。
- 一个线程可以对应多个通道。这个图就反映了有三个通道注册到这个
selector
程序 - 至于切换到哪一个通道是由事件决定的。
- 选择器可以根据事件来在各个通道上切换。
buffer
就是一个内存块,底层是由数组组成的。NIO
的buffer
是可以读也可以写的,但是需要用函数flip
方法进行切换。- 通道是可以双向的。
Buffer 缓冲区
基本介绍
- 缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
buffer
类有7种,除了boolean
没有对应的buffer
,其他都有对应的缓冲区.
缓冲区的四个属性
- capacity:容量,也就是可以容纳的最大的数据量,在缓冲区创建的时候被设定且不可改变。
- limit:表示缓冲区的当前重点。不能对缓冲区超过极限的位置进行读写,而且极限是可以修改的。你可以直观的理解,如果我们创建一个缓冲容量为5的缓冲区,那么
limit=5
,也就是小标为5的位置不能被访问。 - position:位置,下一个要被读或者写的元素的缩影,每一次读写缓冲区都会改变数值,为下一次做好准备。在这里我们应该可以知道,
position≤5
,执行反转之后,会回到0的位置。 - mark:标记。
- 下面我们展示一下
buffer
的相关方法。
byteBuffer
channel 通道
NIO 的通道类似于流,但有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲
BIO
中的Stream
是单向的,例如FileInputStream
对象只能进行读取数据的操作,而NIO
中的通道(Channel
)是双向的,可以读操作,也可以写操作。Channel
在NIO
中是一个接口public interface Channel extends Closeable{}
常用的Channel
类有:FileChannel
、DatagramChannel
、ServerSocketChannel
和SocketChannel
。【ServerSocketChanne
类似ServerSocket
、SocketChannel
类似Socket
】
FileChannel
用于文件的数据读写,DatagramChannel
用于UDP
的数据读写,ServerSocketChannel
和SocketChannel
用于TCP
的数据读写。
FileChannel类
FileChannel 主要用来对本地文件进行 IO 操作,常见的方法有:
- public int read(ByteBuffer dst),从通道读取数据并放到缓冲区中
- public int write(ByteBuffer src),把缓冲区的数据写到通道中
- public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
- public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道
案例说明
- 这个案例主要是实现写数据到本地中的文件,如果文件不存在可创建。
package com.nio;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel {
public static void main(String[] args) throws Exception{
// 准备要写入的数据
String str="hello,how are you!";
// 创建一个输出流-》channel
FileOutputStream fileOutputStream = new FileOutputStream("E:\\mavenWorkspace\\number1\\src\\main\\java\\com\\nio\\t.txt");
// 通过输出流获取对应文件channel
FileChannel fileChannel = fileOutputStream.getChannel();
// 创建一个缓冲区
ByteBuffer allocate = ByteBuffer.allocate(1024);
// 将数据放入到缓冲区中
allocate.put(str.getBytes());
// 反转
allocate.flip();
//buffer数据写入到channel中
fileChannel.write(allocate);
// 关闭流
fileOutputStream.close();
}
}
它的工作流程是这样的:
- 这个案例主要是演示从本地文件中读数据的流程
package com.nio;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel02 {
public static void main(String[] args) throws Exception{
// 创建一个文件输入流对象,对通道来讲确实是输入流
File file = new File("E:\\\\mavenWorkspace\\\\number1\\\\src\\\\main\\\\java\\\\com\\\\nio\\\\t.txt");
FileInputStream fileInputStream = new FileInputStream(file);
// 通过输入流对象获取channel
FileChannel channel = fileInputStream.getChannel();
// 创建一个缓冲区
ByteBuffer allocate = ByteBuffer.allocate((int)file.length());
// 把数据放到缓冲区,对缓冲区来讲是读
channel.read(allocate);
// 将字节数据转换字符串数据,并且发送到控制台中
System.out.println(new String(allocate.array()));
// 关闭流操作
fileInputStream.close();
}
}
- 使用一个buffer完成对文件的读取,就是把上面的文件拷贝到另一个文件里面(这个文件可能没有创建)
buffer和channel的注意细节
- ByteBuffer 支持类型化的 put 和 get,put 放入的是什么数据类型,get 就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。
package com.atguigu.nio;
import java.nio.ByteBuffer;
public class NIOByteBufferPutGet {
public static void main(String[] args) {
//创建一个 Buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
//类型化方式放入数据
buffer.putInt(100);
buffer.putLong(9);
buffer.putChar('尚');
buffer.putShort((short) 4);
//取出
buffer.flip();
System.out.println(buffer.getInt());
System.out.println(buffer.getLong());
System.out.println(buffer.getChar());
System.out.println(buffer.getShort());
}
}
- 普通的buffer可以转换成只读的buffer
package com.atguigu.nio;
import java.nio.ByteBuffer;
public class ReadOnlyBuffer {
public static void main(String[] args) {
//创建一个 buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
for (int i = 0; i < 64; i++) {
buffer.put((byte) i);
}
//读取
buffer.flip();
//得到一个只读的 Buffer
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println(readOnlyBuffer.getClass());
//读取
while (readOnlyBuffer.hasRemaining()) {
System.out.println(readOnlyBuffer.get());
}
readOnlyBuffer.put((byte) 100); //ReadOnlyBufferException
}
}
selector 选择器
一个selector可以管理多个通道,如何管理?
- selector能够检测多个注册的通道上是否有事情发生,如果有事情发生,就选择事件然后针对事件进行处理,这样就可以实现一个线程去管理多个通道。
- 多个channel是以事件的方式注册到同一个selector中
selector的API
selector
是一个抽象类
- 当我们能够获得一个selectorkey的时候,我们就可以获得对应的channel,来对事件进行处理。
- select方法是阻塞的,如果我们调用这个方法,就直到我们能够获得至少一个事件的时候才会返回。
NIO 非阻塞网络编程原理分析图
NIO
非阻塞网络编程相关的(Selector
、SelectionKey
、ServerScoketChannel
和 SocketChannel
)关系梳理图
selector实例
可以注册很多通道。怎么注册?
- 当客户端连接的时候,会通过
serverSocketChannel
得到SocketChannel
- 将
SocketChannel
注册(register(Selector sel, int ops)
)到selector实例
中去- 注册之后,可以返回一个
selectorKey
,selectorKey
会被selector实例
管理起来(以集合的形式管理起来)selector实例
可以进行监听(select方法),这个方法会返回有事件的通道的个数- 进一步得到各个
selectorKey
- 再通过
selectorKey
反向获取SocketChannel
- 最后
SocketChannel
完成对事件的处理。
package com.atguigu.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws Exception{
//创建ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selecor对象
Selector selector = Selector.open();
//绑定一个端口6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("注册后的selectionkey 数量=" + selector.keys().size());
//循环等待客户端连接
while (true) {
//这里我们等待1秒,如果没有事件发生, 返回
if(selector.select(1000) == 0) { //没有事件发生
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的>0, 就获取到相关的 selectionKey集合
//1.如果返回的>0, 表示已经获取到关注的事件
//2. selector.selectedKeys() 返回关注事件的集合
// 通过 selectionKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectionKeys 数量 = " + selectionKeys.size());
//遍历 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 " + socketChannel.hashCode());
//将 SocketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
//关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size());
}
if(key.isReadable()) { //发生 OP_READ
//通过key 反向获取到对应channel
SocketChannel channel = (SocketChannel)key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("form 客户端 " + new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey, 防止重复操作
keyIterator.remove();
}
}
}
}
步骤:(服务器端)
- 创建ServerSocketChannel
- ServerSocketChannel绑定一个端口
- 得到一个上层的Selecor对象
- 将ServerSocketChannel绑定到上层的Selecor对象中,并且注明关心的事件
- 持续监听事件
- 对事件进行处理
package com.atguigu.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws Exception{
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞
socketChannel.configureBlocking(false);
//提供服务器端的ip 和 端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
}
}
//...如果连接成功,就发送数据
String str = "hello, 尚硅谷~";
//Wraps a byte array into a buffer
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//发送数据,将 buffer 数据写入 channel
socketChannel.write(buffer);
System.in.read();
}
}