引言
什么是Netty
Netty是一个异步事件驱动的网络应用框架。 用于快速开发可维护的高性能协议服务器和客户端。 Netty是一个NIO客户服务器框架,它能够快速和容易地开发网络应用,如协议服务器和客户端。它大大简化和精简了网络编程,如TCP和UDP套接字服务器。 快速和简单 "并不意味着开发出来的应用程序会出现可维护性或性能问题。Netty的设计是经过精心设计的,其经验来自于许多协议的实施,如FTP、SMTP、HTTP以及各种基于二进制和文本的遗留协议。因此,Netty成功地找到了一种方法来实现开发的简易性、性能、稳定性和灵活性,而没有任何妥协。
为什么要学习Netty
- Netty是行业网络通信编程的标准,广泛应用于通信领域和很多其他中间件技术的底层
- 应用非常广泛
- 互联网行业:
- 在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用
- 典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框 架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。
- 游戏行业
- Netty 作为高性能的基础通信组件,提供了 TCP/UDP 和 HTTP 协议栈,方便定制和开发私有协议栈,账号登录服务器。
- 地图服务器之间可以方便的通过 Netty 进行高性能的通信。
- 大数据行业:
- 经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信
- 它的 NettyService 基于 Netty 框架二次封装实现。
Java BIO编程
I/O模型
- I/O模型就是:用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
- Java 支持 3 种网络编程模型 I/O模式 : BIO,NIO,AIO。
- Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。 如图所示
- Java NIO:同步非阻塞,服务器实现式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。【简单示意图】
- Java AIO(NIO.2) :异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
BIO,NIO,AIO使用场景
- BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序简单易理解。
- NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持。
- AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。
Java BIO介绍
- Java BIO 就是传统的 Java I/O 编程,其相关的类和接口在 java.io。
- BIO(BlockingI/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。【后有应用实例】
- BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,程序简单易理解。
Java BIO工作机制
对 BIO 编程流程的梳理
- 服务器端启动一个 ServerSocket。
- 客户端启动 Socket 对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯。
- 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
- 如果有响应,客户端线程会等待请求结束后,在继续执行。
Java BIO应用实例
- 使用 BIO 模型编写一个服务器端,监听 8080 端口,当有客户端连接时,就启动一个线程与之通讯。
- 要求使用线程池机制改善,可以连接多个客户端。
- 服务器端可以接收客户端发送的数据(telnet 方式即可)。
package com.huy.bio;
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 {
//线程池机制
//思路
//1. 创建一个线程池
//2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器启动了");
while (true) {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接....");
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//就创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() {//我们重写
//可以和客户端通讯
handler(socket);
}
});
}
}
//编写一个handler方法,和客户端通讯
public static void handler(Socket socket) {
try {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
//通过socket获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true) {
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("关闭和client的连接");
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Java BIO问题分析
- 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write。
- 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。
多线程网络编程
线程池版网络编程
NIO编程
NIO全程:None Blocking IO (非阻塞IO) 非阻塞:主要应用在网络通信中,能够合理利用资源,提高系统的并发效率。支持高并发的系统访问 NIO 有三大核心部分: Channel(通道)、Buffer(缓冲区)、Selector(选择器) 。
Channel简介
- IO通信的通道,类似于InputStream、OutputStream
- Channel没有方向性
- ** 常见的Channel:**
- 文件操作:FileChannel,读写文件中的数据。
- 网络操作:
- SocketChannel,通过TCP读写网络中的数据。
- ServerSockectChannel,监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
- DatagramChannel,通过UDP读写网络中的数据。
- 获取Channel的方式
- FileInputStreanm/FileOutputStream
FileChannel channel = new FileInputStream("data.txt").getChannel();
FileChannel channel = new FileOutputStream("data2.txt").getChannel();
- RandomAccessFile
- Socket
SocketChannel socketChannel=SocketChannel.open();
- ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
- DatagramSocket
应用案例1-本地文件写操作
实例要求:
- 使用前面学习后的 ByteBuffer(缓冲)和 FileChannel(通道),将 "hello,尚硅谷" 写入到 file01.txt 中
- 文件不存在就创建
- 代码演示
public class NIOFileChannel01 {
public static void main(String[] args) throws IOException {
String str="hello NIOFileChannel01";
FileOutputStream fileOutputStream=new FileOutputStream("text.txt");
FileChannel channel = fileOutputStream.getChannel();
ByteBuffer byteBuffer= Charset.defaultCharset().encode(str);
channel.write(byteBuffer);
channel.close();
fileOutputStream.close();
}
}
应用案例2-本地文件读操作
实例要求:
- 使用前面学习后的 ByteBuffer(缓冲)和 FileChannel(通道),将 file01.txt 中的数据读入到程序,并显示在控制台屏幕
- 假定文件已经存在
- 代码演示
public class NIOFileChannel02 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream=new FileInputStream("text.txt");
FileChannel channel = fileInputStream.getChannel();
ByteBuffer buffer=ByteBuffer.allocate(64);
channel.read(buffer);
buffer.flip();
System.out.println(Charset.defaultCharset().decode(buffer));
channel.close();
fileInputStream.close();
}
}
应用案例3-使用一个Buffer完成文件读取,写入
- 使用 FileChannel(通道)和方法 read、write,完成文件的拷贝
- 拷贝一个文本文件 1.txt,放在项目下即可
- 代码演示
public class NIOFileChannel03 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream=new FileInputStream("text.txt");
FileOutputStream fileOutputStream=new FileOutputStream("text_copy.txt");
FileChannel channel1 = fileInputStream.getChannel();
FileChannel channel2 = fileOutputStream.getChannel();
ByteBuffer buffer=ByteBuffer.allocate(64);
while (true){
buffer.clear();
int read = channel1.read(buffer);
if(read==-1){
break;
}
buffer.flip();
channel2.write(buffer);
}
channel2.close();
channel1.close();
fileOutputStream.close();
fileInputStream.close();
}
}
应用案例3-拷贝文件transferFrom 方法
- 实例要求:
- 使用 FileChannel(通道)和方法 transferFrom,完成文件的拷贝
- 拷贝一张图片
- 代码演示
public class NIOFileChannel04 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream=new FileInputStream("myp.png");
FileOutputStream fileOutputStream=new FileOutputStream("myp_copy.png");
FileChannel channel1 = fileInputStream.getChannel();
FileChannel channel2 = fileOutputStream.getChannel();
//从 0 开始 copy channel1.size()个
channel2.transferFrom(channel1,0,channel1.size());
channel2.close();
channel1.close();
fileOutputStream.close();
fileInputStream.close();
}
}
Buffer 和 Channel的 注意事项和细节
- ByteBuffer 支持类型化的 put 和 get,put 放入的是什么数据类型,get 就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。【举例说明】
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();
System.out.println(buffer.getInt());
System.out.println(buffer.getLong());
System.out.println(buffer.getChar());
System.out.println(buffer.getShort());
}
}
- 可以将一个普通 Buffer 转成只读 Buffer【举例说明】
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
}
}
- NIO 还提供了 MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由 NIO 来完成。【举例说明】
/**
* 说明 1.MappedByteBuffer 可让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次
*/
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile("data.txt", "rw");
//获取对应的通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数 1:FileChannel.MapMode.READ_WRITE 使用的读写模式
* 参数 2:0:可以直接修改的起始位置
* 参数 3:5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存
* 可以直接修改的范围就是 0-5
* 实际类型 DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'I');
mappedByteBuffer.put(1, (byte) 'K');
mappedByteBuffer.put(2, (byte) 'U');
mappedByteBuffer.put(3, (byte) 'N');//IndexOutOfBoundsException
randomAccessFile.close();
System.out.println("修改成功~~");
}
}
Buffer
基本介绍
:::info
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer,如图:【后面举例说明】
:::
Buffer 类以及子类
- 在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类,类的层级关系图:
ByteBuffer详解
:::info ByteBuffer时抽象类,他的主要实现类为
- HeapByteBuffer 堆ByteBuffer JVM内的堆内存 ---> 读写操作 效率低 会收到GC影响
- MappedByteBuffer(DirectByteBuffer) OS内存 ---> 读写操作 效率高 不会收到GC影响 。 不主动析构,会造成内存的泄露
获取方法:
- ByteBuffer.allocate(10);//一旦分配空间,不可以动态调整
- encode()
核心结构: ByteBuffer是一个类似数组的结构,整个结构中包含三个主要的状态
-
Capacity buffer的容量,类似于数组的size
-
Position buffer当前缓存的下标,在读取操作时记录读到了那个位置,在写操作时记录写到了那个位置。从0开始,每读取一次,下标+1
-
Limit 读写限制,在读操作时,设置了你能读多少字节的数据,在写操作时,设置了你还能写多少字节的数据
所谓的读写模式,本质上就是这几个状态的变化。主要有Position和Limit联合决定了Buffer的读写数据区域。
:::
:::info
总结:
写入Buffer数据之前要设置写模式
- 写模式
- 新创建的Buffer自动是写模式
- 调用了clear,compact方法
读取Buffer数据之前要设置读模式 2. 读模式
- 调用flip方法 :::
核心API
- buffer中写入数据[写模式 创建一个bytebuffer ,clear(),compact()] :::info
- channel的read方法 channel.read(buffer)
- buffer的put方法 buffer.put(byte) buffer.put((byte)'a').. buffer.put(byte[]) :::
- 从buffer中读出数据 :::info
-
channel的write方法
-
buffer的get方法 //每调用一次get方法会影响,position的位置。
-
rewind方法(手风琴),可以将postion重置成0 ,用于复读数据。
-
mark&reset方法,通过mark方法进行标记(position),通过reset方法跳回标记,从新执行.
-
get(i) 方法,获取特定position上的数据,但是不会对position的位置产生影响。 :::
字符串操作
字符串存储到Buffer中
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("sunshuai".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println("buffer.get() = " + (char)buffer.get());
}
buffer.clear();
ByteBuffer buffer = Charset.forName("UTF-8").encode("sunshuai");
1、encode方法自动 把字符串按照字符集编码后,存储在ByteBuffer.
2、自动把ByteBuffer设置成读模式,且不能手工调用flip方法。
ByteBuffer buffer = StandardCharsets.UTF_8.encode("sunshuai");
while (buffer.hasRemaining()) {
System.out.println("buffer.get() = " + (char) buffer.get());
}
buffer.clear();
1、encode方法自动 把字符串按照字符集编码后,存储在ByteBuffer.
2、自动把ByteBuffer设置成读模式,且不能手工调用flip方法。
ByteBuffer buffer = ByteBuffer.wrap("sunshuai".getBytes());
while (buffer.hasRemaining()) {
System.out.println("buffer.get() = " + (char) buffer.get());
}
buffer.clear();
Buffer中数据转换成字符串
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("孙".getBytes());
buffer.flip();
CharBuffer result = StandardCharsets.UTF_8.decode(buffer);
System.out.println("result.toString() = " + result.toString());
半包粘包
:::info 处理方法:
- \n 作为分割符,进行行的区分。
- compact进行处理,把第一次没有读取完的数据,向前移动和后面的内容进行整合。
:::
//1. 半包 粘包
public class TestNIO10 {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(50);
buffer.put("Hi sunshuai\nl love y".getBytes());
doLineSplit(buffer);
buffer.put("ou\nDo you like me?\n".getBytes());
doLineSplit(buffer);
}
// ByteBuffer接受的数据 \n
private static void doLineSplit(ByteBuffer buffer) {
buffer.flip();
for (int i = 0; i < buffer.limit(); i++) {
if (buffer.get(i) == '\n') {
int length = i + 1 - buffer.position();
ByteBuffer target = ByteBuffer.allocate(length);
for (int j = 0; j < length; j++) {
target.put(buffer.get());
}
//截取工作完成
target.flip();
System.out.println("StandardCharsets.UTF_8.decode(target).toString() = " + StandardCharsets.UTF_8.decode(target).toString());
}
}
buffer.compact();
}
}
Selector(选择器)
基本介绍
- Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)。
- Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。【示意图】
- 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
- 避免了多线程之间的上下文切换导致的开销。
Selector 示意图和特点
:::info
特点:
- Netty的IO线程NioEventLoop 聚合了Selector(选择器,也就多路复用器),可以同时并发处理成百上千个客户端连接
- 当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务
- 线程通常将非阻塞IO 的空闲时间用于其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道
- 由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
- 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。 :::
Selector类相关方法
其中
:::info
keys(选择键):
在 Selector 中,keys 是一个 Set 集合,包含了所有已经注册到 Selector 上的通道和与之相关的信息。每个 key 对应一个通道,它包含了关于通道和通道所关联事件的信息。
通过 keys 集合,你可以遍历所有已注册的通道,检查它们的状态以及关联的事件。
selectorKeys(选择器键):
selectorKeys 是一个在 Selector 上注册的通道与其关联事件的表示。每个 selectorKey 包含了与特定通道关联的信息,包括所关注的事件和相关的通道。
当 Selector 检测到某个通道准备好执行某种 I/O 操作时,它会返回一组 selectorKeys,以便应用程序可以确定哪些通道已经准备好处理 I/O。
:::
注意事项
:::info
- NIO 中的 ServerSocketChannel 功能类似 ServerSocket、SocketChannel 功能类似 Socket。
- Selector 相关方法说明
- selector.select(); //阻塞
- selector.select(1000); //阻塞 1000 毫秒,在 1000 毫秒后返回
- selector.wakeup(); //唤醒 selector
- selector.selectNow(); //不阻塞,立马返还 :::
NIO网络编程
:::info
-
服务端 接受请求 ---》 ServerScoketChannel
-
进行实际通信 ScoketChannel :::
通过这版代码 证明了 服务器端 存在2中阻塞
1. 连接阻塞 ----> accept方法存在阻塞---> ServerSocketChannel阻塞。
2. IO阻塞 ----> channel的read方法存在阻塞---> SocketChannel阻塞。
上述分析 对应着的2个问题。
public class MyServer {
public static void main(String[] args) throws IOException {
//1. 创建ServerScoketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2. 设置服务端的监听端口:---》client通过网络进行访问 ip:port http://localhost:8989
serverSocketChannel.bind(new InetSocketAddress(8000));
List<SocketChannel> channelList = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.allocate(20);
//3. 接受client的连接
while (true) {
//4. ScoketChannle 代表 服务端与Client链接的一个通道
System.out.println("等待连接服务器...");
SocketChannel socketChannel = serverSocketChannel.accept();//阻塞 程序等待client
System.out.println("服务器已经连接..."+socketChannel);
channelList.add(socketChannel);
//5. client与服务端 通信过程 NIO代码
for (SocketChannel channel : channelList) {
System.out.println("开始实际的数据通信....");
channel.read(buffer);//阻塞 对应的IO通信的阻塞
buffer.flip();
CharBuffer decode = Charset.forName("UTF-8").decode(buffer);
System.out.println("decode.toString() = " + decode.toString());
buffer.clear();
System.out.println("通信已经结束....");
}
}
}
}
Seletor循环监听事件的方式 解决死循环空转的问题。
public class MyServer2 {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8000));
serverSocketChannel.configureBlocking(false);//Selector 只有在非阻塞的情况下 才可以使用。
//引入监管者
Selector selector = Selector.open();//1. 工厂,2. 单例
//监管者 管理谁? selector.xxxx(ssc); //管理者 ssc ---> Accept
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
// selector监控 SSC ACCEPT
// selector
// keys --> HashSet
// register注册 ssc
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
System.out.println("MyServler2.main");
//监控
while (true) {
selector.select();//等待.只有监控到了 有实际的连接 或者 读写操作 ,才会处理。
//对应的 有ACCEPT状态的SSC 和 READ WRITE状态的 SC 存起来
// SelectionsKeys HashSet
System.out.println("-------------------------");
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {//ServerSocketChannel ScoketChannel
SelectionKey key = iterator.next();
//用完之后 就要把他从SelectedKeys集合中删除掉。问题? ServerScoketChannel---SelectedKeys删除 ,后续 SSC建立新的连接?
iterator.remove();
if (key.isAcceptable()) {
//serverSocketChannel.accept();
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
//监控sc状态 ---> keys
SelectionKey sckey = sc.register(selector, 0, null);
sckey.interestOps(SelectionKey.OP_READ);
} else if (key.isReadable()) {
try {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(5);
int read = sc.read(buffer);
if (read == -1) {
key.cancel();
} else {
buffer.flip();
System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer).toString());
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();
}
}
}
}
}
}
:::info iterator.remove() 把用过的SelectionKey从SeletionKeys集合中剔除
selectionKey.cancle(); 把一些无法实际解决的内容,通过cancle()来取消。避免每一次都被select()获取到从新进行循环过程。
读取数据
- 通过附件的形式,把byteBuffer和channel进行了绑定,从而可以多次处理数据。
- ByteBuffer的扩容。
数据的写出
- 第一个问题 写一次数据,当发现数据没有写完,设置WRITE监听状态。
- 每一次发生Write的状态,都把剩下的数据写出去。 :::
- 主从Reactor模式
主从reactor模式
public class ReactorBossServer {
private static final Logger log = LoggerFactory.getLogger(ReactorBossServer.class);
public static void main(String[] args) throws IOException, InterruptedException {
log.debug("boss thread start ....");
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8000));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
//模拟多线程的环境,在实际开发中,还是要使用线程池
/*
Worker worker = new Worker("worker1");
*/
Worker[] workers = new Worker[2];
for (int i = 0; i < workers.length; i++) {
workers[i] = new Worker("worker - " + i);//worker-0 worker-1
}
AtomicInteger index = new AtomicInteger();
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sscSelectionKey = iterator.next();
iterator.remove();
if (sscSelectionKey.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//sc.register(selector, SelectionKey.OP_READ);
log.debug("boss invoke worker register ...");
//worker-0 worker-1 worker-0 worker-1
//hash取摸 x%2= 0 1 [0,1,0,1]
workers[index.getAndIncrement()% workers.length].register(sc);
log.debug("boss invoked worker register");
}
}
}
}
}
public class Worker implements Runnable {
private static final Logger log = LoggerFactory.getLogger(Worker.class);
private Selector selector;
private Thread thread;
private String name;
private volatile boolean isCreated;//false
private ConcurrentLinkedQueue<Runnable> runnables = new ConcurrentLinkedQueue<>();
//构造方法
//为什么不好?
//Select Thread
public Worker(String name) throws IOException {
this.name = name;
/* thread = new Thread(this, name);
thread.start();
selector = Selector.open();*/
}
//线程的任务
public void register(SocketChannel sc) throws IOException, InterruptedException {
log.debug("worker register invoke....");
if (!isCreated) {
thread = new Thread(this, name);
thread.start();
selector = Selector.open();
isCreated = true;
}
runnables.add(() -> {
try {
sc.register(selector, SelectionKey.OP_READ);//reigster select方法之前运行 。。
} catch (ClosedChannelException e) {
throw new RuntimeException(e);
}
});
selector.wakeup();//select
}
@Override
public void run() {
while (true) {
log.debug("worker run method invoke....");
try {
selector.select();
Runnable poll = runnables.poll();
if (poll != null) {
poll.run();
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sckey = iterator.next();
iterator.remove();
if (sckey.isReadable()) {
SocketChannel sc = (SocketChannel) sckey.channel();
ByteBuffer buffer = ByteBuffer.allocate(30);
sc.read(buffer);
buffer.flip();
String result = Charset.defaultCharset().decode(buffer).toString();
System.out.println("result = " + result);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}