一、Java的IO演变历程
先介绍几个前置知识
同步和异步:
- 同步(Synchronous):指的是在进行某个操作时,发起方发出请求后需要等待操作完成才能继续后续的工作。调用方会一直等待,直到服务器操作完成并返回结果。
- 异步(Asynchronous):指的是在发起某个操作后,不会等待操作完成,而是可以继续执行其他任务。异步操作通常通过回调、事件通知等方式来处理操作完成后的结果。
阻塞和非阻塞:
- 阻塞(Blocking):在进行某个操作时,如果发起方需要等待操作完成,那么该操作是阻塞的。在阻塞状态下,调用方无法执行其他任务,一直等待操作完成。
- 非阻塞(Non-blocking):在进行某个操作时,如果发起方无需等待操作完成,而是可以继续执行其他任务,那么该操作是非阻塞的。在非阻塞状态下,调用方可以继续执行其他任务,不必等待操作完成。
在这两个维度上,可以将同步与异步和阻塞与非阻塞组合起来理解:
- 同步阻塞:调用方发起请求后,会阻塞等待操作完成。
- 同步非阻塞:调用方发起请求后,等待操作完成的同时可以执行其他任务。
- 异步阻塞:发起异步操作后,等待操作完成,但在等待的期间可以执行其他任务。
- 异步非阻塞:发起异步操作后,不等待操作完成,可以立即执行其他任务,通过回调或事件等方式获取操作完成的通知。
1.1 IO模型的基本说明
I/O模型:就是用什么样的通道或者说是通信模式和架构进行数据的传输和接收,很大程度上决定了程序通信的性能,Java共支持3种网络编程的/IO模型:BIO、NIO、AIO
实际通信需求下,要根据不同的业务场景和性能需求决定选择不同的I/O模型
1.2 IO模型
Java BIO
同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
代码:
package com.syc.one.test;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SocketServer {
static ExecutorService service = Executors.newFixedThreadPool(20);
public static void main(String[] args) throws IOException {
// 创建服务器,监听9999端口
ServerSocket serverSocket = new ServerSocket(9999);
// 在程序即将关闭时,关闭一个 serverSocket 实例
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}));
System.out.println("服务器启动成功");
while (true) {
// 监听与这个套接字的连接并接受它
Socket request = serverSocket.accept();
System.out.println(request.toString());
service.execute(() -> {//创建线程处理请求
try {
// 获取字节输入流
InputStream inputStream = request.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String msg, rsp = "";
while ((msg = bufferedReader.readLine()) != null) {
if (msg.length() == 0) break;
rsp += msg;
System.out.println(msg);
}
System.out.println("服务器收到数据" + rsp);
OutputStream outputStream = request.getOutputStream();
outputStream.write("HTTP/1.1 200 OK\r\n".getBytes());
outputStream.write("Content-Length: 10\r\n\r\n".getBytes());
outputStream.write("Hello world".getBytes());
outputStream.flush();
// 关闭流和套接字
bufferedReader.close();
inputStream.close();
outputStream.close();
request.close();
} catch (Exception e) {
System.out.println(e);
}
});
}
}
}
Java NIO
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理
【简单示意图】
Java AIO
异步 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由Os先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用
1.3 适用场景分析
1、BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解。
2、NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。
3、AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
二、BIO
1.基本介绍
Java BIO就是传统的java io 编程,其相关的类和接口在java.io
BIO(blocking l/0):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器).
2.工作机制
监听端口: 服务器创建一个 ServerSocket 并绑定到特定的端口上,开始监听客户端的连接请求。
等待连接: 服务器在一个循环中不断地等待客户端的连接请求到达。当一个客户端请求连接时,服务器会接受这个连接并为这个连接分配一个专用的套接字(Socket)。
数据传输: 一旦连接建立,服务器和客户端之间就可以通过 Socket 进行数据的读取和写入。服务器端会创建一个线程来处理这个连接,通过输入流(InputStream)读取客户端发送的数据,并通过输出流(OutputStream)向客户端发送响应数据。
阻塞等待: 在进行数据读写的过程中,如果没有数据可读或写,线程会被阻塞在读取或写入操作上,直到有数据可读或写入。这是阻塞的原因之一,阻塞意味着线程会停止执行并等待。
关闭连接: 当服务器或客户端需要关闭连接时,它们会关闭相应的 Socket。服务器端的线程也会被终止,资源得到释放。
需要注意的是,每个连接都需要一个单独的线程来处理,这就是 BIO 模型的阻塞性质所体现的地方。当有大量连接时,会导致大量线程的创建和销毁,从而消耗大量系统资源,并且容易引发性能问题和线程管理的复杂性。
案例:
package com.syc.test.BIO;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
//客户端发送消息,服务器接受消息
public class Server {
public static void main(String[] args) throws IOException {
//服务器端口注册
ServerSocket ss = new ServerSocket(9999);
//监听客户端的socket连接请求
Socket socket = ss.accept();
//从Socket管道中得到一个字节输入流
InputStream inputStream = socket.getInputStream();
//把字节输入流包装成字符缓冲输入流 //把字节转为字符
BufferedReader bufferedInputStream = new BufferedReader(new InputStreamReader(inputStream));
String msg;
while ((msg=bufferedInputStream.readLine())!=null){
System.out.println("服务端接收:"+msg);
}
}
}
------
package com.syc.test.BIO;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
//客户端发送消息,服务器接受消息
public class Server {
public static void main(String[] args) throws IOException {
//服务器端口注册
ServerSocket ss = new ServerSocket(9999);
//监听客户端的socket连接请求
Socket socket = ss.accept();
//从Socket管道中得到一个字节输入流
InputStream inputStream = socket.getInputStream();
//把字节输入流包装成字符缓冲输入流 //把字节转为字符
BufferedReader bufferedInputStream = new BufferedReader(new InputStreamReader(inputStream));
String msg;
while ((msg=bufferedInputStream.readLine())!=null){
System.out.println("服务端接收:"+msg);
}
}
}
案例总结:
我们先运行server端,当程序启动后,他会一直等待,在while处,好,然后我们启动client端
,他会很快就执行完,此时server端接收到消息;
并会报错,因为服务端会一直等待客户端的信息,如果客户端没有进行消息发送,会一直处于等待状态(阻塞),当客户端一行消息发送完成之后,就结束了 服务器发下客户端挂机了,就会服务重置
实现服务器接受多个客户端请求
3.伪异步编程
在上面一个案例中,我们可以看出,当有一个请求来到服务器的时候,服务器就要创建一个线程对象来处理请求,当访问量越来越大,最终会导致服务器宕机。
接下来我们采用一个伪异步I/o的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机
代码:
package com.syc.test.BIO;
import com.sun.corba.se.spi.orbutil.threadpool.ThreadPool;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.temporal.ValueRange;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
//服务器
public class Server {
//自定义线程池
public static ExecutorService service= new ThreadPoolExecutor(0,Integer.MAX_VALUE,
60L,TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
public static void main(String[] args) throws IOException {
//服务器端口注册
ServerSocket ss = new ServerSocket(9999);
//监听客户端的socket连接请求
while (true){
Socket socket = ss.accept();
SockedThread sockedThread = new SockedThread(socket);
service.submit(sockedThread);
}
}
}
//线程对象
class SockedThread implements Runnable{
Socket socket;
public SockedThread(Socket socket){
this.socket=socket;
}
@Override
public void run() {
//从Socket管道中得到一个字节输入流
try {
InputStream inputStream = socket.getInputStream();
//把字节输入流包装成字符缓冲输入流 //把字节转为字符
BufferedReader bufferedInputStream = new BufferedReader(new InputStreamReader(inputStream));
String msg;
while ((msg=bufferedInputStream.readLine())!=null){
long id = Thread.currentThread().getId();
System.out.println("服务端的线程"+id+"接收:"+msg);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
----------------
客户端
public class Client2 {
public static void main(String[] args) throws IOException {
//创建Socket对象请求服务器
Socket socket = new Socket("127.0.0.1",9999);
// 获取字节输出流
OutputStream outputStream = socket.getOutputStream();
// 包装成打印流
PrintStream printStream = new PrintStream(outputStream);
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("请输入:");
String next= scanner.next();
printStream.println(next);
printStream.flush();
}
}
}多开几个模拟多个客户端
实现效果:
小结
·伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型因此无法从根本上解决问题。
·如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的il/o消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。
4.服务器实现保存任意类型数据
客户端
package com.syc.test.BIO2;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
//实现客户端上传任意类型文件给服务器保存
public class Client {
public static void main(String[] args) throws IOException, InterruptedException {
//创建Socket对象请求服务器
Socket socket = new Socket("127.0.0.1",9999);
// 把字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
//先发送文件名
dos.writeUTF(".png");
InputStream is = new FileInputStream("C:\\Users\\shenhong\\Desktop\\测试图片.png");
byte[] bytes = new byte[1024];
int len;
while ((len=is.read(bytes))>0){
dos.write(bytes);
}
dos.flush();
Thread.sleep(1000000);
}
}
----------------
服务端
package com.syc.test.BIO2;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
//服务端,接受任意类型的文件保存起来
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(9999);
while (true){
Socket accept = ss.accept();
//创建新线程处理请求
SockedThread sockedThread = new SockedThread(accept);
new Thread(sockedThread).start();
}
}
}
class SockedThread implements Runnable{
Socket socket;
public SockedThread(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try {
DataInputStream dis = new DataInputStream(socket.getInputStream());
//获取客户端发送的文件类形
String s = dis.readUTF();
System.out.println("f服务端接收到了文件:"+s);
//定义一个路径,将文件写入到这里
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\shenhong\\Desktop\\test\\"+ UUID.randomUUID()+s);
//读取文件数据,写到字节输出流中
byte[] bytes = new byte[1024];
int len;
System.out.println("正在保存。。");
while ((len=dis.read(bytes))>0){
fileOutputStream.write(bytes);
}
fileOutputStream.close();
System.out.println("保存成功。。");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
代码运行现象:
并不会打印:保存成功这句话,也就是说,服务端会一直阻塞在
while ((len=dis.read(bytes))>0){这一行代码处进行等待客户端传数据(阻塞式)
三、NIO
NIO,即非阻塞I/O(Non-blocking I/O),是Java平台提供的一种用于处理I/O操作的模型,相对于传统的BIO(Blocking I/O)模型,它具有更高的并发性能和更灵活的I/O操作方式。NIO在Java 1.4中引入,提供了一系列新的类和接口,允许开发人员以非阻塞的方式进行I/O操作,从而在高并发情况下提高程序的性能。
NIO是面向缓冲区、或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
6)通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。
以下是NIO的一些关键概念和组件:
-
Buffer(缓冲区):
ByteBuffer
、CharBuffer
、ShortBuffer、
IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer等缓冲区类用于在内存中存储数据,用于读取和写入操作。缓冲区可以直接与通道(Channel)进行交互,减少了数据拷贝的开销。本质上就是一块可读写数据的内存块
-
Channel(通道):
Channel
是NIO中的一个重要概念,代表着与I/O源(如文件、网络套接字等)的连接。通过通道,可以同时进行数据的读取和写入操作。不同类型的通道支持不同的I/O源,例如FileChannel
、SocketChannel
、ServerSocketChannel、DatagramChannel
等。FileChannel用于文件读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写
-
Selector(选择器):
Selector
是NIO中的关键组件,用于实现非阻塞I/O。一个选择器可以同时监视多个通道的事件,如读、写、连接和接收。只有在通道真正有读写事件发生时,才会进行读写,就大大减少了系统开销,并且不必为每个连接都创建一个线程,避免了资源浪费和多线程的上下文切换导致的开销。 -
SelectionKey(选择键):
SelectionKey
代表了一个通道在选择器上注册的关联信息,包括感兴趣的事件(读、写等)、通道状态等。当通道上发生感兴趣的事件时,选择器会通知程序。 -
非阻塞操作:NIO允许在没有数据可读写的情况下继续进行其他任务,而不必等待I/O操作完成。这使得一个线程能够同时处理多个通道的事件,提高了程序的并发性。
- NIO相关类都被放在
java.nio
包及子包下,并且对原java.io
包中的很多类进行改写
使用NIO进行编程需要更多的思考和设计,因为与BIO相比,NIO需要手动处理缓冲区、通道注册、选择器等。然而,它在处理高并发的网络应用、实时数据传输等场景下表现出色。
Selector对应一个线程,一个线程对应多个Channel(连接),每个channel都有自己的Buffer,程序切换到哪个channel是由是否有事件发生决定的,Selector会根据不同的事件在不同的通道之间切换,
需要注意的是,虽然NIO提供了非阻塞的I/O操作,但在Java 7中引入的NIO.2(也称为NIO2或Java NIO.2)中,进一步扩展了NIO的功能,引入了更多便利的I/O操作和异步I/O支持。
1.缓冲区的基本属性
缓冲区本质上是一个可以写入数据的内存块(类似数组),然后可以再次读取。此内存块包含在NIOBuffer对象中,该对象提供了一组方法,可以更轻松地使用内存块。相比较直接对数组的操作,Buffer API更加容易操作和管理。
容量(capacity):作为一个内存块,Buffer具有一定的固定大小,也称为"容量",缓冲区容量不能为负,并且创建后不能更改。
限制(limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量。
位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制标记(mark)与重置(reset):标记是一个索引,通过Buffer 中的mark)方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position.
限制模式(Limit Mode):限制模式用于指示缓冲区当前是否处于限制模式。限制模式可以帮助您控制数据的读取和写入,以防止越界操作。
标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
2.Bytebuffer的内存类型
ByteBuffer为性能关键型代码提供了直接内存(direct堆外)和非直接内存(heap堆)两种实现。堆外内存获取的方式: ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(noBytes);
好处:
1、进行网络io或者文件io时比heapBuffer少一次拷贝。(file/socket---- OSmemory ---- jvm heap)GC会移动对象内存,在写file或socket的过程中,JVM的实现中,会先把数据复制到堆外,再进行写入。
2、GC范围之外,降低Gc压力,但实现了自动管理。DirectByteBuffer中有一个Cleaner对象(PhantomReference),Cleaner被GC前会执行clean方法,触发DirectByteBuffer中定义的Deallocator
建议:
1、性能确实可观的时候才去使用;分配给大型、长寿命;(网络传输、文件读写场景)
2
2、通过虚拟机参数MaxDirectMemorySize限制大小,防止耗尽整个机器的内存;
3.buff的方法介绍
allocate(int capacity)
:静态方法,用于分配一个新的ByteBuffer
实例,设置初始容量为指定的大小。
put(byte b)
:将一个字节写入缓冲区的当前位置,然后将位置增加1。
put(byte[] src)
:将整个字节数组写入缓冲区的当前位置,然后将位置增加写入字节数。
put(ByteBuffer src)
:将另一个ByteBuffer
的内容写入当前缓冲区,然后将位置增加写入字节数。
get()
:从缓冲区的当前位置读取一个字节,并将位置增加1。
get(byte[] dst)
:从缓冲区的当前位置读取数据到指定的字节数组,然后将位置增加读取字节数。
get(ByteBuffer dst)
:从当前缓冲区读取数据到另一个ByteBuffer
,然后将位置增加读取字节数。
flip()
:设置缓冲区的上界为当前位置,然后将位置重置为0,准备读取缓冲区中的数据。
rewind()
:将位置重置为0,保持上界不变,可以重新读取缓冲区中的数据。
clear()
:清除缓冲区,将位置设置为0,上界设置为容量,可以重新写入数据。
remaining()
:返回当前位置到上界之间的剩余可读取字节数。
hasRemaining()
:检查是否还有剩余可读取的数据。
mark()
:设置标记在当前位置,可以使用reset()
方法回到标记的位置。
reset()
:将位置回退到之前标记的位置。
compact()
:清除已读数据,将未读数据复制到缓冲区的开始位置,然后将位置设置为未读数据的末尾。int position() //返回缓冲区的当前位置position
Buffer position(int n) //将设置缓冲区的当前位置为n,并返回修改后的Buffer对象
int limit() //返回Buffer的界限(limit)的位置
Buffer limit(int n) //将设置缓冲区界限为n,并返回一个具有新limit的缓冲区对象
int capacity() //返回Buffer的capacity大小
//这个方法最终还是调用的ByteBuffer的构造方法,传入的值是:
// ByteBuffer(-1, 0,10,10,byte[10] ,0)
ByteBuffer allocate = ByteBuffer.allocate(10);
//打印缓冲区的当前位置和界限位置
System.out.println(allocate.position());//0
System.out.println(allocate.limit());//10
byte[] bytes = "hello".getBytes();
//将 hello写入缓冲区的当前位置
allocate.put(bytes);
//打印写入数据后的缓冲区的当前位置和界限位置
System.out.println(allocate.position());//5
System.out.println(allocate.limit());//10
System.out.println(allocate.capacity());//10,返回的是capacity的大小
//设置缓冲区的上界为当前位置,然后将位置重置为0,准备读取缓冲区中的数据。
allocate.flip();
System.out.println(allocate.position());//0
System.out.println(allocate.limit());//5
char c = (char) allocate.get();//从缓冲区的当前位置读取一个字节,并将位置增加1。
System.out.println(c);//h
System.out.println(allocate.position());//1
System.out.println(allocate.limit());//5
allocate.rewind();
char c1 = (char) allocate.get();//从缓冲区的当前位置读取一个字节,并将位置增加1。
System.out.println(c1);//h
System.out.println(allocate.position());//1
System.out.println(allocate.limit());//5
//清除已读数据,将未读数据复制到缓冲区的开始位置,然后将位置设置为未读数据的末尾。
allocate.compact();
char c2 = (char) allocate.get();//从缓冲区的当前位置读取一个字节,并将位置增加1。
System.out.println(c2);//o,读到了末尾字符
System.out.println(allocate.position());//5
System.out.println(allocate.limit());//10
System.out.println(allocate.hasRemaining());//true,返回是否还有可读数据
System.out.println(allocate.remaining());//5,返回当前位置到上界之间的剩余可读取字节数。
char c3 = (char) allocate.get();//从缓冲区的当前位置读取一个字节,并将位置增加1。
System.out.println(c3);//空
System.out.println(allocate.position());//6
System.out.println(allocate.limit());//10
allocate.flip();
char c4 = (char) allocate.get();//从缓冲区的当前位置读取一个字节,并将位置增加1。
System.out.println(c4);//e
System.out.println(allocate.position());//1
System.out.println(allocate.limit());//6
System.out.println("-------------分割线--------------");
allocate.clear();//清除缓冲区(实际上并没有清除),将位置设置为0,上界设置为容量,可以重新写入数据。这个方法将position设置为0,limit = capacity;mark = -1;
System.out.println(allocate.position());//0
System.out.println(allocate.limit());//10
System.out.println(allocate.capacity());//10,返回的是capacity的大小
System.out.println(allocate.hasRemaining());//true,返回是否还有可读数据
System.out.println((char) allocate.get());//e
4.Channel通道
服务器
package com.syc.one.test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerSocketChannelDemo {
public static void main(String[] args) throws IOException {
//打开一个通道,默认是阻塞的
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);//配置false表示不阻塞
//绑定端口
channel.bind(new InetSocketAddress(8088));
System.out.println("服务器启动成功");
while (true){
/*accept 的语义是非阻塞的尝试接受连接。如果没有连接准备好,该方法会返回 null,
并不会等待直到有连接进来。需要通过轮询或者结合选择器(Selector)等机制来实现非阻塞地等待连接。*/
SocketChannel accept = channel.accept();//返回的是与客户端通信的通道
if(accept!=null){//表示客户端的连接请求就绪
System.out.println("客户端连接服务器就绪");
try {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//accept.read(byteBuffer)将客户端数据读取到byteBuffer中,,返回的是读取到的字节数,
// 返回-1表示没有数据可读了,客户端关闭了连接
if(accept.isOpen()&&accept.read(byteBuffer)>-1){//判断通道是否打开并且是否还有数据读
//返回这个缓冲区的当前位置,可以理解为求最后有值的索引是多少
// if(byteBuffer.position()>0) break; 用break写是有误的,会提前退出while循环,
// 从而导致客户端报错服务器终止连接,姐姐办法可以在外面声明一个标志,读取成功之后就将标志位改为true
}
System.out.println("数据读取完成");
//转换为读模式
byteBuffer.flip();
//array返回字节数组, remaining():返回当前位置到limit之间的数量
System.out.println("读到数据"+new String(byteBuffer.array(),0,byteBuffer.remaining()));
//给客户端写会数据
byteBuffer.clear();
byteBuffer.put("我收到了你的消息啦".getBytes());
byteBuffer.flip();
accept.write(byteBuffer);
} catch (IOException e) {
System.out.println(e);
throw new RuntimeException(e);
}finally {
accept.close(); // 关闭连接
}
}else{
//没有连接上,有这样一个思路,创建一个集合,假设连接上就添加到集合之中,为空就在这里处理连接上的
}
}
}
}
客户端
package com.syc.one.test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class SocketChannelClient {
public static void main(String[] args) throws IOException {
// 打开客户端通道
SocketChannel socketChannel = SocketChannel.open();
// 配置为非阻塞模式
socketChannel.configureBlocking(false);
// 连接到服务器
socketChannel.connect(new InetSocketAddress("localhost", 8088));
while (!socketChannel.finishConnect()) {
// 非阻塞模式下,需要轮询等待连接完成
Thread.yield();
}
// 向服务器发送消息
Scanner scanner=new Scanner(System.in);
System.out.println("请输入:");
//wrap:将字节数组包装到缓冲区
ByteBuffer buffer = ByteBuffer.wrap(scanner.nextLine().getBytes());
//判断缓冲区是否有数据,有的话通过通道发送给服务器
while (buffer.hasRemaining()){
socketChannel.write(buffer);
}
// 读取服务器的响应
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
while (socketChannel.isOpen()&&socketChannel.read(responseBuffer) != -1){
//长连接情况下需要判断数据读取是否结束了,这里只做简单判断
if(responseBuffer.position()>0) break;
}
responseBuffer.flip();//切换读模式
System.out.println("收到服务器发送数据:"+new String(responseBuffer.array(),0,responseBuffer.remaining()));
// 关闭客户端通道
socketChannel.close();
}
}
5.Selector选择器
引出:先看一段代码:
package com.syc.one.test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ServerSocketChannelDemo {
public static List<SocketChannel> list = new ArrayList<>();
public static void main(String[] args) throws IOException {
//打开一个通道,默认是阻塞的
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);//配置false表示不阻塞
//绑定端口
channel.bind(new InetSocketAddress(8088));
System.out.println("服务器启动成功");
while (true){
/*accept 的语义是非阻塞的尝试接受连接。如果没有连接准备好,该方法会返回 null,
并不会等待直到有连接进来。需要通过轮询或者结合选择器(Selector)等机制来实现非阻塞地等待连接。*/
SocketChannel accept = channel.accept();//返回的是与客户端通信的通道
if(accept!=null){//表示客户端的连接请求就绪
list.add(accept);
}else{
Iterator<SocketChannel> iterator = list.iterator();
while (iterator.hasNext()){
SocketChannel next = iterator.next();
try {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//accept.read(byteBuffer)将客户端数据读取到byteBuffer中,,返回的是读取到的字节数,
// 返回-1表示没有数据可读了,客户端关闭了连接
if(next.isOpen()&&next.read(byteBuffer)>-1){//判断通道是否打开并且是否还有数据读
//返回这个缓冲区的当前位置,可以理解为求最后有值的索引是多少
// if(byteBuffer.position()>0) break; 用break写是有误的,会提前退出while循环,
// 从而导致客户端报错服务器终止连接,姐姐办法可以在外面声明一个标志,读取成功之后就将标志位改为true
}
System.out.println("数据读取完成");
//转换为读模式
byteBuffer.flip();
//array返回字节数组, remaining():返回当前位置到limit之间的数量
System.out.println("读到数据"+new String(byteBuffer.array(),0,byteBuffer.remaining()));
//给客户端写会数据
byteBuffer.clear();
byteBuffer.put("我收到了你的消息啦".getBytes());
byteBuffer.flip();
next.write(byteBuffer);
iterator.remove();
} catch (IOException e) {
System.out.println(e);
iterator.remove();
throw new RuntimeException(e);
}finally {
next.close(); // 关闭连接
}
}
}
}
}
}
上述代码,假设在并发过高的情况下,是否有可能出现情况:
if(accept!=null){//表示客户端的连接请求就绪 list.add(accept);//是否会出现一直在这里添加而没有去执行下面的处理请求呢? }
答案是肯定的。
Selector
是NIO中的关键组件,用于实现非阻塞I/O。一个选择器可以同时监视多个通道的事件,如读、写、连接和接收。通过选择器,程序可以在一个线程中处理多个通道的I/O事件,实现了单个线程可以管理多个通道,从而提高并发性能。
实现一个线程处理多个通道的核心概念理解:事件驱动机制 。
非阻塞的网络通道下,开发者通过Selector注册对于通道感兴趣的事件类型,线程通过监听事件来触发相应的代码执行。(拓展:更底层是操作系统的多路复用机制)
代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class SelectDemo {
public static void main(String[] args) throws IOException {
//打开服务器通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到哟个selector对象
Selector selector = Selector.open();
//配置为非阻塞模式,这意味着它可以在没有新连接时立即返回而不会阻塞等待。
serverSocketChannel.configureBlocking(false);
//将服务器通道注册到selector上,设置关心的事件为连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
serverSocketChannel.bind(new InetSocketAddress(8088));
System.out.println("服务器启动成功");
while (true) {
int readyChannels = selector.select();
if (readyChannels == 0) continue;
System.out.println("触发的事件数:" + readyChannels);
//获取事件集合,并获取他的迭代器
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
System.out.println("新的客户端发起连接事件");
handleAccept(key, selector);
} else if (key.isConnectable()) {
System.out.println("客户端连接到了服务器");
} else if (key.isReadable()) {
System.out.println("有数据可读");
handleRead(key, selector);
} else if (key.isWritable()) {
System.out.println("可以写数据了");
}
iterator.remove();
}
}
}
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
//从触发事件中获取到与客户端通信的通道
SocketChannel socketChannel = serverSocketChannel.accept();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
//将新创建的 SocketChannel 注册到 Selector 上,关注 OP_READ 事件,表示该通道已准备好进行读取操作。
socketChannel.register(selector, SelectionKey.OP_READ);
}
private static void handleRead(SelectionKey key, Selector selector) throws IOException {
//从触发事件中获取到与客户端通信的通道
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
socketChannel.close();
//取消对应的选择键,即从 Selector 的键集合中移除该键。
key.cancel();
} else if (bytesRead > 0) {
buffer.flip();//切换读模式
System.out.println("读到数据:" + new String(buffer.array(), 0, bytesRead));
// 处理完数据后,清除OP_READ事件,避免重复触发
key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
}
}
}
6.与BIO的对比
特点 | 阻塞I/O (BIO) | 非阻塞I/O (NIO) |
---|---|---|
阻塞方式 | 阻塞等待I/O操作完成 | 非阻塞,可同时处理多个通道 |
线程使用 | 通常使用多线程来处理并发连接 | 使用较少线程处理多个通道 |
性能 | 在并发连接数增加时,性能可能下降 | 并发连接数增加时,性能相对较好 |
缓冲区 | 通常使用输入/输出流,需要手动处理缓冲区 | 使用缓冲区类如ByteBuffer来处理数据 |
通道 | 通常使用普通的输入/输出流(InputStream/OutputStream) | 使用通道(Channel)来读写数据 |
选择器 | 不涉及选择器的概念 | 使用选择器(Selector)监视多个通道的事件 |
I/O等待 | I/O操作阻塞线程,需要等待操作完成 | I/O操作不会阻塞线程,可以继续执行其他任务 |
编程复杂度 | 相对简单,适用于少量并发连接 | 相对复杂,需要处理缓冲区、通道注册等 |
适用场景 | 适用于少量连接,例如简单的文件I/O或低并发的网络通信 | 适用于高并发网络应用、实时数据传输等 |
处理方式 | 流的方式处理数据 | 块的方式处理数据,块IO比流IO要快 |
7.群聊系统实现
需求:服务器端可以监测用户上线、下线、转发消息
客户端可以收发消息
代码:
服务器:
package com.syc.one.Test3;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class GroupChatServer {
//选择器和服务器通道
Selector selector;
ServerSocketChannel listenChannel;
static final int port=8899;
//构造器进行初始化
public GroupChatServer(){
try {
selector=Selector.open();
listenChannel=ServerSocketChannel.open();
//设置为非阻塞
listenChannel.configureBlocking(false);
//绑定端口
listenChannel.bind(new InetSocketAddress(port));
//服务器通道注册到选择器,并关注连接时间
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动成功");
} catch (IOException e) {
e.printStackTrace();
}
}
public void listen(){
try {
while (true){
int num = selector.select();
if(num>0){//有事件处理
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isAcceptable()){//连接事件
// SocketChannel channel =(SocketChannel) key.channel();
SocketChannel sc = listenChannel.accept();
sc.configureBlocking(false);
//将这个与客户端之间的通道注册到select
sc.register(selector,SelectionKey.OP_READ);
System.out.println(sc.getLocalAddress()+"上线");
}
if(key.isReadable()){
readData(key);
}
}
}else{
System.out.println("等待...");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
private void readData(SelectionKey key) {
SocketChannel socketChannel=null;
try {
socketChannel =(SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(buffer);
if(read>0){
String s = new String(buffer.array());
System.out.println("收到客户端消息:"+s);
//向其他客户端发送这个消息
sendMessageToOtherClient(s,socketChannel);
}
}catch (Exception e){
try {
System.out.println(socketChannel.getLocalAddress()+"离线了");
key.cancel();//取消这个键对应的通道的注册
socketChannel.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
e.printStackTrace();
}
}
private void sendMessageToOtherClient(String s, SocketChannel socketChannel) {
System.out.println("给客户端转发消息");
ByteBuffer buffer = ByteBuffer.wrap(s.getBytes());
for (SelectionKey key : selector.keys()) {
Channel clientChannel= key.channel();
if(clientChannel instanceof SocketChannel && clientChannel != socketChannel){
SocketChannel channel=(SocketChannel)clientChannel;
try {
channel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
GroupChatServer server = new GroupChatServer();
server.listen();
}
}
客户端:
package com.syc.one.Test3;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
public class GroupChatClient {
final String HOST="localhost";
static final int port=8899;
Selector selector;
SocketChannel socketChannel;
String userName;
public GroupChatClient() throws IOException {
selector = Selector.open();
socketChannel=SocketChannel.open(new InetSocketAddress(HOST,port));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
userName=socketChannel.getLocalAddress().toString().substring(1);
System.out.println(userName+"准备就绪");
}
void sendMessageToServer(String message) throws IOException {
message=userName+"说"+message;
socketChannel.write(ByteBuffer.wrap(message.getBytes()));
}
void readMessageFromServer() throws IOException {
int num = selector.select();
System.out.println("读数据num"+num);
if (num>0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isReadable()){
SocketChannel channel=(SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
System.out.println("接收到消息"+channel.getLocalAddress().toString().substring(1)+"说:"+new String(buffer.array()));
}
}
}else{
System.out.println("暂无可用通道");
}
}
public static void main(String[] args) throws IOException {
GroupChatClient client = new GroupChatClient();
new Thread(()->{
System.out.println("线程启动");
while (true){
try {
client.readMessageFromServer();
Thread.sleep(1000);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
//发送数据给服务器
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String next = scanner.nextLine();
client.sendMessageToServer(next);
}
}
}
五、Reactor线程模型
Reactor模式(反应器模式)是一种处理多个客户端并发交付服务请求的事件设计模式。当请求抵达后,服务处理程序使用I/0多路复用策略,然后同步地派发这些请求至相关的请求处理程序。
1.单线程版Reactor
代码:
服务器
package com.syc.one.test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class ReactorSignDemo {
public static void main(String[] args) throws IOException {
ReactorSignDemo reactorSignDemo = new ReactorSignDemo();
Reactor reactor = reactorSignDemo.new Reactor();
reactor.run();
}
class Reactor implements Runnable {
final Selector selector;
public Reactor() throws IOException {
selector=Selector.open();
//创建服务端channel对象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
//channel注册到selector上,并注册accept时间
SelectionKey selectionKey = serverSocketChannel.register(selector, 0);//0表示该通道不关注任何特定的事件类型
//表示当有新的客户端连接时,Selector 将通知该 SelectionKey。
selectionKey.interestOps(SelectionKey.OP_ACCEPT);// 设置关注 OP_ACCEPT 事件。
//将一个附加对象(Acceptor 的实例)关联到 SelectionKey。Acceptor 实例,它是一个实现了 Runnable 接口的类,用于处理新连接的接受操作。
selectionKey.attach(new Acceptor(selectionKey));
//绑定端口启动服务
serverSocketChannel.bind(new InetSocketAddress(8088));
System.out.println("服务器启动成功");
}
@Override
public void run() {
while (true){//在run方法中遍历事件
try {
//阻塞等待事件的发生,然后通过迭代器处理已选择的键集合,最终调用 dispatch 方法派发事件。
int num = selector.select();
if (num==0) continue;
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
dispatch(key);//通过dispatch方法进行事件的派发
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
// 从 SelectionKey 中获取附加的对象(在这里是 Acceptor 实例),然后调用其 run 方法。用于派发处理器的运行。
private void dispatch(SelectionKey key){
//取出上面绑定的附加对象,也就是Acceptor,调用他的run方法
Runnable attachment =(Runnable) key.attachment();
if(attachment!=null){
attachment.run();
}
}
}
class Acceptor implements Runnable{
final SelectionKey key;
final ServerSocketChannel serverSocketChannel;
public Acceptor(SelectionKey key ){
this.key=key;
this.serverSocketChannel= (ServerSocketChannel) key.channel();
}
//处理新的客户端连接,设置连接的通信通道为非阻塞模式,并在 Selector 上注册关注 OP_READ 事件的 SocketChannel,
// 最后创建一个新的 Handler 处理器。
@Override
public void run() {
try {
//接收到新的客户端连接,绑定到select上
System.out.println("收到客户端新连接");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); //设置为非阻塞模式,这个是与连接上的客户端的通信管道
//将新创建的 SocketChannel 注册到 Selector 上,关注 OP_READ 事件,表示该通道已准备好进行读取操作。然后获取到他的SelectionKey对象
SelectionKey register = socketChannel.register(key.selector(), SelectionKey.OP_READ);
//接收完客户端连接之后,处理读写事件
new Handler(register);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
//处理读写事件处理器
class Handler implements Runnable{
final SocketChannel socketChannel;
final SelectionKey key;
int readIng=0,writeIng=1;
int state=readIng;
public Handler(SelectionKey key ){
this.key=key;
this.socketChannel =(SocketChannel) key.channel();
key.attach(this);
}
//处理读写事件,根据状态进行读或写操作,包括读取客户端数据和向客户端发送数据
@Override
public void run() {
//处理读写
try {
if(state==readIng){
System.out.println("开始读数据。。");
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
socketChannel.close();
//取消对应的选择键,即从 Selector 的键集合中移除该键。
key.cancel();
} else if (bytesRead > 0) {
buffer.flip();//切换读模式
System.out.println("读到数据:" + new String(buffer.array(), 0, bytesRead));
// 处理完数据后,清除OP_READ事件,避免重复触发
state=writeIng;
//将 OP_WRITE 事件添加到 SelectionKey 的兴趣操作集合中
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
}
}else if(state==writeIng){
System.out.println("给客户端返回数据");
ByteBuffer byteBuffer = ByteBuffer.wrap("我是服务器发给客户端的信息".getBytes());
socketChannel.write(byteBuffer);
//写操作之后吗,应该清除op——write
state = readIng;
//将 OP_WRITE 事件从 SelectionKey 的兴趣操作集合中移除
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
客户端
package com.syc.one.test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class SocketChannelClient {
public static void main(String[] args) throws IOException {
// 打开客户端通道
SocketChannel socketChannel = SocketChannel.open();
// 配置为非阻塞模式
socketChannel.configureBlocking(false);
// 连接到服务器
socketChannel.connect(new InetSocketAddress("localhost", 8088));
while (!socketChannel.finishConnect()) {
// 非阻塞模式下,需要轮询等待连接完成
Thread.yield();
}
// 向服务器发送消息
Scanner scanner=new Scanner(System.in);
System.out.println("请输入:");
//wrap:将字节数组包装到缓冲区
ByteBuffer buffer = ByteBuffer.wrap(scanner.nextLine().getBytes());
//判断缓冲区是否有数据,有的话通过通道发送给服务器
while (buffer.hasRemaining()){
socketChannel.write(buffer);
}
// 读取服务器的响应
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
while (socketChannel.isOpen()&&socketChannel.read(responseBuffer) != -1){
//长连接情况下需要判断数据读取是否结束了,这里只做简单判断
if(responseBuffer.position()>0) break;
}
responseBuffer.flip();//切换读模式
System.out.println("收到服务器发送数据:"+new String(responseBuffer.array(),0,responseBuffer.remaining()));
// 关闭客户端通道
socketChannel.close();
}
}
2.线程池版Reator
线程池版Reator把业务从之前的单一线程脱离出来,换成线程池处理,也就是Reactor线程只处理连接事件和读写事件,业务处理(读出来的数据怎么处理?)交给线程池处理,充分利用多核机器的资源、提高性能并且增加可靠性
package com.syc.one.test;
import com.sun.org.apache.bcel.internal.generic.NEW;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Date;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPoolReactorSignDemo {
public static void main(String[] args) throws IOException {
Reactor reactor = new Reactor(8088);
reactor.run();
}
static
class Reactor implements Runnable {
final Selector selector;
public Reactor(int port) throws IOException {
selector=Selector.open();
//创建服务端channel对象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
//channel注册到selector上,并注册accept时间
SelectionKey selectionKey = serverSocketChannel.register(selector, 0);//0表示该通道不关注任何特定的事件类型
//表示当有新的客户端连接时,Selector 将通知该 SelectionKey。
selectionKey.interestOps(SelectionKey.OP_ACCEPT);// 设置关注 OP_ACCEPT 事件。
//将一个附加对象(Acceptor 的实例)关联到 SelectionKey。Acceptor 实例,它是一个实现了 Runnable 接口的类,用于处理新连接的接受操作。
selectionKey.attach(new Acceptor(selectionKey));
//绑定端口启动服务
serverSocketChannel.bind(new InetSocketAddress(port));
System.out.println("服务器启动成功");
}
@Override
public void run() {
while (true){//在run方法中遍历事件
try {
//阻塞等待事件的发生,然后通过迭代器处理已选择的键集合,最终调用 dispatch 方法派发事件。
int num = selector.select();
if (num==0) continue;
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
dispatch(key);//通过dispatch方法进行事件的派发
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
// 从 SelectionKey 中获取附加的对象(在这里是 Acceptor 实例),然后调用其 run 方法。用于派发处理器的运行。
private void dispatch(SelectionKey key){
//取出上面绑定的附加对象,也就是Acceptor,调用他的run方法
Runnable attachment =(Runnable) key.attachment();
if(attachment!=null){
attachment.run();
}
}
}
static class Acceptor implements Runnable{
final SelectionKey key;
final ServerSocketChannel serverSocketChannel;
public Acceptor(SelectionKey key ){
this.key=key;
this.serverSocketChannel= (ServerSocketChannel) key.channel();
}
//处理新的客户端连接,设置连接的通信通道为非阻塞模式,并在 Selector 上注册关注 OP_READ 事件的 SocketChannel,
// 最后创建一个新的 Handler 处理器。
@Override
public void run() {
try {
//接收到新的客户端连接,绑定到select上
System.out.println("收到客户端新连接");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); //设置为非阻塞模式,这个是与连接上的客户端的通信管道
//将新创建的 SocketChannel 注册到 Selector 上,关注 OP_READ 事件,表示该通道已准备好进行读取操作。然后获取到他的SelectionKey对象
SelectionKey register = socketChannel.register(key.selector(), SelectionKey.OP_READ);
//接收完客户端连接之后,处理读写事件
new Handler(register);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
//处理读写事件处理器
static class Handler implements Runnable{
final SocketChannel socketChannel;//与客户端的连接通道
final SelectionKey key;
final int MAXSIZE=1024;
ByteBuffer input=ByteBuffer.allocate(MAXSIZE);
//线程池
static final ExecutorService service=MyThreadPool.service;
public Handler(SelectionKey key ){
this.key=key;
this.socketChannel =(SocketChannel) key.channel();
key.attach(this);
}
//处理读写事件,根据状态进行读或写操作,包括读取客户端数据和向客户端发送数据
@Override
public void run() {
//处理读写
try {
if(key.isReadable()){
read();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void read() throws IOException {
System.out.println("开始读数据。。");
int read = socketChannel.read(input);
if(inputIsComplete()){//读取数据完成
input.flip();//转换为写模式
service.execute(new Processer(input,key,read));
input.clear();
}
}
boolean inputIsComplete(){
return input.position()>0;
}
}
static class Processer implements Runnable {
ByteBuffer byteBuffer;
SelectionKey key;
int size;
public Processer(ByteBuffer byteBuffer,SelectionKey key,int size){
this.byteBuffer=byteBuffer;
this.key=key;
this.size=size;
}
@Override
public void run() {
//在这里处理业务,如保存到数据库
System.out.println("读到客户端发来的信息:"+new String(byteBuffer.array(),0,size));
//给客户端发送数据
ByteBuffer allocate = ByteBuffer.allocate(1024);
allocate.put("我是服务器AA给客户端发送数据".getBytes());
try {
allocate.flip();
System.out.println("创建对象Sender");
new Sender(allocate,key).run();
}catch (Exception e){
System.out.println(e);
}
}
}
static class Sender implements Runnable{
final SocketChannel socketChannel;
final Object oldObject;
SelectionKey key;
ByteBuffer byteBuffer;
public Sender(ByteBuffer byteBuffer,SelectionKey key){
this.byteBuffer=ByteBuffer.wrap(byteBuffer.array(),0,byteBuffer.limit());
this.key=key;
socketChannel=(SocketChannel)key.channel();
//注册写事件
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
oldObject=key.attach(this);//返回上一个附加对象
}
@Override
public void run() {
System.out.println(key.isReadable());
System.out.println(key.isWritable());
try {
System.out.println("给客户端返回数据。。。");
socketChannel.write(byteBuffer);
if(outputIsComplete()){
byteBuffer.clear();
key.attach(oldObject);
key.interestOps(key.interestOps() & ~ SelectionKey.OP_WRITE);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public boolean outputIsComplete(){
return byteBuffer.position()==byteBuffer.limit();
}
}
}
//代码似乎存在一点问题,写操作是通过 new Sender(allocate,key).run();此处调用的run方法调用的,我决定应该可以dispatch方法进行派发,但是不知道怎么改了
3.多Reactor模型
这种模型下和第二种模型相比是把Reactor线程拆分了mainReactor和subReactor两个部分,mainReactor只处理连接事件,读写事件交给subReactor来处理。业务逻辑还是由线程池来处理。
代码:
package com.syc.one.Test2;
public class Main {
public static void main(String[] args) {
Reactor reactor = new Reactor(9090);
reactor.run();
}
}
package com.syc.one.Test2;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
import java.util.Set;
public class Reactor implements Runnable {
private ServerSocketChannel serverSocketChannel;
private Selector selector;
public Reactor(int port) {
try {
serverSocketChannel = ServerSocketChannel.open();
selector = Selector.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port));
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
selectionKey.attach(new Acceptor(serverSocketChannel));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
dispatcher(selectionKey);
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void dispatcher(SelectionKey selectionKey) {
Runnable runnable = (Runnable) selectionKey.attachment();
runnable.run();
}
}
package com.syc.one.Test2;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class Acceptor implements Runnable {
private ServerSocketChannel serverSocketChannel;
private final int CORE = 8;
private int index;
//从Reactor
private SubReactor[] subReactors = new SubReactor[CORE];
private Thread[] threads = new Thread[CORE];
private final Selector[] selectors = new Selector[CORE];
public Acceptor(ServerSocketChannel serverSocketChannel) {
//服务器通道对象
this.serverSocketChannel = serverSocketChannel;
for (int i = 0; i < CORE; i++) {
try {
//创建选择器
selectors[i] = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
//创建从Reactor,并将对应的选择器传给他
subReactors[i] = new SubReactor(selectors[i]);
//将从Reactor启动,就开始监听客户端和服务端的读事件了
threads[i] = new Thread(subReactors[i]);
threads[i].start();
}
}
@Override
public void run() {
try {
//接受客户端的连接。
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("有客户端连接上来了," + socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);//非阻塞
//会使当前阻塞在 select() 上的线程立即返回,即使此时没有任何感兴趣的事件发生
selectors[index].wakeup();
SelectionKey selectionKey = socketChannel.register(selectors[index], SelectionKey.OP_READ);
selectionKey.attach(new WorkHandler(socketChannel,selectionKey));
if (++index == threads.length) {
index = 0;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.syc.one.Test2;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class WorkHandler implements Runnable {
private SocketChannel socketChannel;
SelectionKey selectionKey;
public WorkHandler(SocketChannel socketChannel,SelectionKey selectionKey) {
//与客户端的通信通道
this.socketChannel = socketChannel;
this.selectionKey=selectionKey;
}
@Override
public void run() {
try {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(byteBuffer);
String message = new String(byteBuffer.array(), StandardCharsets.UTF_8);
System.out.println(socketChannel.getRemoteAddress() + "发来的消息是:" + message);
socketChannel.write(ByteBuffer.wrap("你的消息我收到了".getBytes(StandardCharsets.UTF_8)));
selectionKey.interestOps(selectionKey.interestOps() & ~SelectionKey.OP_READ);
} catch (IOException e) {
try {
socketChannel.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
e.printStackTrace();
}
}
}
package com.syc.one.Test2;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Iterator;
import java.util.Set;
public class SubReactor implements Runnable {
private Selector selector;
public SubReactor(Selector selector) {
this.selector = selector;
}
@Override
public void run() {
while (true) {
try {
selector.select();
System.out.println("selector:" + selector.toString() + "thread:" + Thread.currentThread().getName());
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
dispatcher(selectionKey);
iterator.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void dispatcher(SelectionKey selectionKey) {
Runnable runnable = (Runnable) selectionKey.attachment();
runnable.run();
}
}