结合代码聊聊BIO、NIO,代码都是可以运行成功的伪代码
BIO(Blocking IO)
最早的网络通信模型
accept 监听连接是阻塞的,监听已连接客户端的输入 read 也是阻塞的,解决方式就是有一个连接进来后,开启一个线程去处理该客户端的输入,不然该客户端一直不发送数据,服务器就会一直阻塞着等待它的数据发送过来,期间就不能监听到其他客户端发来请求,所以是每连接创建每线程
import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketBIO {
public static void main(String[] args) throws Exception {
ServerSocket server = null;
try {
server = new ServerSocket(9090);
// server.bind(new InetSocketAddress("127.0.0.1",9090)); //或者可以这样写
System.out.println("step1: new ServerSocket(9090)");
while (true) {
Socket client = server.accept();//阻塞1
System.out.println("step2: client port: " + client.getPort());
//每连接创建每线程,防止服务器阻塞
new Thread(new Runnable() {
@Override
public void run() {
InputStream inputStream = null;
try {
inputStream = client.getInputStream();//NIO对Socket的getInputStream()、getOutputStream()进行了封装,SocketChannel就直接可以可读可写
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
while (true) {
System.out.println("step3: readClient:");
String s = reader.readLine();//阻塞2
System.out.println("step3: read: " + s + "from client: " + client.getPort());
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
//阻塞式读取
//获取输入字符流
// BufferedReader reader = new BufferedReader(new InputStreamReader(
// client.getInputStream()
// ));
// //获取输出字符流
// BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
// client.getOutputStream()
// ));
//
// while (true) {
// String s = reader.readLine();//阻塞2
// System.out.println("client[" + client.getPort() + "]:" + s);
//
// writer.write(s + "\n");
// writer.flush();
//// writer.close();
//
// if (s.equals("quit")) {
// System.out.println("client close!");
// break;
// }
// }
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (server != null) {
server.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
因为BIO阻塞开始的每连接创建每线程,太浪费资源,可能有些连接一直都不发送数据,于是就有了NIO 非阻塞
NIO(Non-Blocking IO/New IO)
non-blocking mode 同步非阻塞的网络通信模型
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.LinkedList;
import java.util.concurrent.TimeUnit;
/**
* BIO到NIO的过渡,这个并不是真正意义上的NIO,
* 因为java中的NIO组成:SocketChannel、Selector、configureBlocking()、SelectionKey、Handler、ByteBuffer(Buffer的一个子类)缓冲区
* (缓冲区又分为:直接缓冲区、非直接缓冲区)
* (SelectionKey封装了一个SocketChannel通道和Selector的注册关系)
* 所以这个只是用到了部分NIO的组件,NIO的Selector多路复用器SelectionKey都没有用到,也就不是真正意义上的NIO,NIO是使用了多路复用器的,不过当客户端数量不多时BIO也可以,当然客户端不可能不多。
*/
public class SocketNIO {
public static void main(String[] args) {
LinkedList<SocketChannel> clients = new LinkedList<>();
try {
// 在BIO中,需要使用getInputStream()、getOutputStream()对传输的数据进行读取写入,
// 而在NIO中,将数据存入Buffer缓冲区中,读写操作的数据都使用Buffer,ServerSocketChannel、SocketChannel 这些Channel通道来传输数据,Buffer是打包数据,Channel是传输数据,
// Channel结合Buffer就完成了网络中数据的传输
// Channel 特点:
// 1. 可使用configureBlocking(false)设置为非阻塞,之后的read()、accept()、connect()就都不会阻塞了
// 2. 可以注册到Selector多路复用器上(ServerSocketChannel、SocketChannel都继承自SelectableChannel abstract class抽象类,所以有register(Selector sel, int ops)可以注册将自身到某个Selector上)
// ServerSocketChannel、SocketChannel是线程安全的
// ServerSocketChannel、SocketChannel是abstract class抽象类,调用open()创建SocketChannel对象,Selector同理
ServerSocketChannel ssc = ServerSocketChannel.open();//服务端绑定端口,开启监听
ssc.bind(new InetSocketAddress(9090));
//在NIO中ServerSocketChannel、SocketChannel默认为blocking mode阻塞模式
ssc.configureBlocking(false); //false 设为非阻塞模式,之后的accept()就不会阻塞
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
SocketChannel client = ssc.accept();//接收客户端连接,不会阻塞,没有客户端连接 返回值 null 在linux中 -1。
//如果有客户端的连接, accept返回的是这个客户端的fd,client object
// NONBLOCKING就是代码能往下走了,只不过有不同的情况
if (client == null) {
System.out.println("client is null");
} else {
client.configureBlocking(false);//non-blocking 读取客户端发送的数据非阻塞
int port = client.socket().getPort();//getPort() 返回此socket连接的远程端口
System.out.println("client port:" + port);
clients.add(client);
}
// 在BIO中,使用InputStream、OutputStream对存放数据的array进行操作,对数据的处理也就是使用array的api,
// 而在NIO中,网络中传输的数据都存入Buffer缓冲区中,对数据的读写操作都操作的是Buffer,也就是使用的Buffer的api,Buffer就是一个存放数据的容器,对数据的操作当然是操作容器了
// Buffer底层也是使用array,只不过提供了一些对array操作的api 比如:get()、put()
// Buffer缓冲区又分为:direct Buffer直接缓冲区、non-direct Buffer非直接缓冲区,
// 直接缓存区是在user space用户空间 即在当前运行的jvm中开辟了一个空间,而且也在kernel space内核空间中开辟了空间,
// 而非直接缓冲区是只在kernel space内核空间中开辟空间,jvm中没有空间,所以使用非直接缓存区对于数据的读写就少copy了一层(jvm) zero copy零拷贝(netty中也有此概念),性能也提高了
// Buffer是非线程安全的
// allocateDirect()/allocate() 创建读操作的Buffer,这里创建的是直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024); //创建Buffer有两种:1、调用allocateDirect()/allocate() 2、调用ByteBuffer.wrap(byte[] array)
//遍历已经连接进来的客户端能不能读写数据,NIO中是对Buffer进行数据的读写操作
for (SocketChannel cli : clients) {
//read():会使ByteBuffer的父类Buffer的position属性移动到下一个可读或写的位置上,所以需要在调用read()后再调用flip()将limit = position 且position重置为0,方便再次读取
int read = cli.read(buffer); //将读取到数据存入Buffer中,接下来就是对于Buffer的操作了。>0 有数据 -1 没有数据,read() 不会阻塞
if (read > 0) {
//1)rewind()方法的侧重点在“重新”,在重新读取、重新写入时可以使用;
//2)clear()方法的侧重点在“还原一切状态”;
//3)flip()方法的侧重点在substring截取。
buffer.flip();
byte[] aaa = new byte[buffer.limit()];
buffer.get(aaa);//get(byte[] dst) 将Buffer中的连续字节传输到byte[] dst目标数组中
String b = new String(aaa);
System.out.println(cli.socket().getPort() + ":" + b);
buffer.clear(); //重置Buffer,便于下次对同一个Buffer的操作
//因为调用allocateDirect()创建的是direct Buffer,所以jvm中没有array存在,所以不能调用buffer.array()返回jvm中的array
// System.out.println(cli.socket().getPort() + ":" + new String(buffer.array()));
// ByteBuffer bufferWrite = ByteBuffer.wrap("HelloClient".getBytes());
ByteBuffer bufferWrite = ByteBuffer.wrap(b.getBytes());
cli.write(bufferWrite);//写入Buffer
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
-
ServerSocketChannel、SocketChannel:在BIO中使用的是ServerSocket、Socket,在NIO中使用的SocketChannel都继承自SelectableChannel abstract class抽象类,所以都有
register(Selector sel, int ops),可以将自身加入到某个Selector 多路复用器。它们默认是blocking mode阻塞模式的,需要使用configureBlocking(false)设置成非阻塞,accept()、read()、connect()就都是非阻塞的了,在NIO中ServerSocketChannel、SocketChannel读写操作都是对于Buffer缓冲区操作的SocketChannel是线程安全的
-
ByteBuffer:继承自Buffer abstract class抽象类,缓冲区又分为:直接缓冲区、非直接缓冲区,
allocateDirect()创建直接缓冲区,底层实现DirectByteBuffer,allocate()创建非直接缓冲区,底层实现HeapByteBuffer。非直接缓冲区底层使用了array 数组实现,传统的IO中使用InputStream直接对array操作,array的api也少,在NIO中使用Buffer封装了array,有一些便于操作array的api可直接使用,所以读取/写入操作的是Buffer而不是传统IO中的arrayBuffer是非线程安全的
使用链表存储已与服务器建立连接的SocketChannel,以便于可以一直轮询的监听这些已连接的SocketChannel是否有数据发送过来,同时ServerSocketChannel也不断的监听新的客户端连接请求。但是这样又产生了一直轮询的资源消耗,所以在NIO中还有一个重要的提高效率、节省资源消耗的组件Selector 多路复用器
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* 以下的代码 服务器中只有一个Selector多路复用器
* 如果调用epoll_wait()返回了大量的socket的fd,那么就可以采用分治的思想,将建立连接的请求和已建立连接有数据发送来的请求分开处理,
* 可以在加入Selector的时候使用标记区分开,
* 如果需要建立连接的请求 或者 已建立连接有数据发送来的请求 很多,可以创建多个Selector多路复用器专门负责处理建立连接的请求 或者 有数据发送的请求,
* hint:netty中提供了线程池来处理数据的请求,在NIO中需要手动编写线程池
* <p>
* 多线程处理,一个线程处理需要建立连接的socket,多个线程来处理需要读写的socket。
*/
public class SocketMultiplexingSingleThreadv1 {
// 在BIO中,需要使用getInputStream()、getOutputStream()对传输的数据进行读取写入,
// 而在NIO中,将数据存入Buffer缓冲区中,读写操作的数据都使用Buffer,ServerSocketChannel、SocketChannel 这些Channel通道来传输数据,Buffer是打包数据,Channel是传输数据,
// Channel结合Buffer就完成了网络中数据的传输
// Channel 特点:
// 1. 可使用configureBlocking(false)设置为非阻塞,之后的read()、accept()、connect()就都不会阻塞了
// 2. 可以注册到Selector多路复用器上(ServerSocketChannel、SocketChannel都继承自SelectableChannel abstract class抽象类,所以有register(Selector sel, int ops)可以注册将自身到某个Selector上)
// ServerSocketChannel、SocketChannel是线程安全的
// ServerSocketChannel、SocketChannel是abstract class抽象类,调用open()创建SocketChannel对象,Selector同理
private ServerSocketChannel server = null;
private Selector selector = null; //多路复用器
int port = 9090;
// Selector:多路复用器,可以一次监听多个注册到其Selector上的Channel,注册时需要指定监听Channel的哪些动作 即event事件,当指定的event发生时就会返回监听的Channel。
// 使用BIO时只能轮询查看已连接的客户端有没有发送数据过来的,有了Selector只需要调用一次就可以监听多个Channel的状态
// 使用Selector的是同步IO模型,即Selector只是返回有状态的Channel,之后对于有状态的Channel的处理还是由服务器来处理
// 不同操作系统使用的Selector也不同,windows select、linux select、poll、epoll(因为epoll效率高会使用epoll)、mac kqueue,
// 因为使用的Selector不同,Selector返回的数据也不同,使用select会返回所有注册到Selector上的Channel,使用epoll只会返回有状态(即指定的event发生了)的注册到Selector上的Channel
// ServerSocketChannel注册到Selector上时只能指定accept event,SocketChannel可以指定connect、read、write event
// SelectionKey:一个Channel注册到Selector上时就会创建一个SelectionKey,有:channel、selector、interestOps、readyOps等属性
// Selector维护了3个集合:
// 1. HashSet keys 存储所有注册到该Selector上的SocketChannel对应的SelectionKey
// 2. HashSet selectedKeys 存储就绪(即有状态)的SocketChannel对应的SelectionKey
// 3. HashSet cancelledKeys 存储取消的SocketChannel对应的SelectionKey
public void initServer() {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
//如果是select、poll就只是在程序的jvm中开辟一个空间,将之后的客户端先存放到程序中,然后调用select()、poll()系统调用函数时 再将所有的客户端传给内核。
//如果在epoll模型下,open --> epoll_create --> fd3
selector = Selector.open();//select、poll、epoll 优先选择epoll 但是可以修改
//server 约等于 listen状态的 fd4
/*
register
如果:
select、poll:将fd4的socket存入上面在程序中开辟出来的空间中
epoll:epoll_ctrl(fd3,ADD,fd4,EPOLLIN)
原来使用NIO的时候,需要程序一直循环调用accept()、read(),而现在只需要循环调用select()、poll()、epoll() 将接收连接的socket、读取数据的socket传给内核,
内核通过信号驱动感知有客户端连接/客户端有数据发来,就会将这些有状态的socket返回给程序,只不过select会返回所有的客户端socket,而epoll只会返回有状态的socket
*/
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
initServer();
System.out.println("服务器启动了……");
try {
while (true) {
// Set<SelectionKey> keys = selector.keys();//keys()获取注册在Selector上的所有SocketChannel的SelectionKey 即keys集合
// System.out.println(keys.size() + " size");
/*
1、调用多路复用器(select、poll or epoll (epoll_wait))
select()是啥意思
1、select、poll:其实是 内核的select(fd4) poll(fd4),将java程序中缓存的所有socket作为参数传给select()、poll()
2、epoll:其实 内核的epoll_wait()
epoll_wait()约等于select() poll()
参数可以带时间 设置一个阻塞的超时时间、没有时间 阻塞等待 直到至少有一个有状态的(即有数据发送来的)socket被返回
*/
while (selector.select(500) > 0) { //返回Selector的selectedKeys 就绪队列 的更改数量
Set<SelectionKey> selectionKeys = selector.selectedKeys(); //返回注册在Selector上的有状态的SocketChannel的fd集合 即selectedKeys集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//管你啥Selector多路复用器,你只能给我返回状态,我还得一个个的去处理它们的R/W,同步IO模型
//不使用Selector多路复用器的时候,需要对每一个fd(即客户端socket)调用系统调用,浪费资源,使用多路复用器只需要调用一次就可以获取到多个有状态的fd信息
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
iterator.remove();//不从keySet中移除会重复处理
// 之前使用NIO的时候,还需要分别调用accept()、recv()判断是否有数据传输发来,现在只需要一并将所有socket传给多路复用器,再对多路复用器返回的sockets进行区分 分别操作就好了
if (next.isAcceptable()) { //是否已准备好新的客户端连接
//这里是重点,如果要去接收一个新的连接
//语义上,accept接收连接且返回新连接的FD
//那新的FD怎么办?
//select、poll:因为它们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起
//epoll:通过epoll_ctrl把新的客户端注册到内核空间
acceptHandler(next);
} else if (next.isReadable()) { //是否准备好进行读取了
readHandler(next);//R/W都处理了
//在当前线程中,这个方法可能会阻塞,所以可以使用线程池来异步处理,在netty中有提供线程池,也可以自定义线程池
//去掉:
// 提出了IO Threads
// redis用了epoll、IO Threads的概念,redisIO多线程、worker单线程
// tomcat8、9异步的处理方式 IO 和 处理上 解耦
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void readHandler(SelectionKey next) {
SocketChannel sc = null;
try {
sc = (SocketChannel) next.channel();
ByteBuffer buffer = ByteBuffer.allocate(512);
buffer.clear();
int read = sc.read(buffer); //将读取到数据存入Buffer中,接下来就是对于Buffer的操作了
if (read != -1) {
byte[] array = buffer.array();
System.out.println(sc.socket().getPort() + ":" + new String(array)); //因为调用allocate()创建的是non-direct Buffer,所以jvm中有array存在,所以调用buffer.array()会返回jvm中的array
ByteBuffer bufferWrite = ByteBuffer.wrap(array);
sc.write(bufferWrite);
}
} catch (IOException e) {
e.printStackTrace();
}
// finally {
// if (sc != null) {
// try {
// sc.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }
}
private void acceptHandler(SelectionKey next) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) next.channel();
SocketChannel client = ssc.accept();//调用accept接收客户端 fd7
client.configureBlocking(false);
//调用了register
/*
select、poll:jvm里开辟了一个数组 fd7放进去
epoll:epoll_ctrl(fd3,ADD,fd7,EPOLLIN)
*/
client.register(selector, SelectionKey.OP_READ);
System.out.println("-------------------------------");
System.out.println("新客户端:" + client.getRemoteAddress());
System.out.println("-------------------------------");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SocketMultiplexingSingleThreadv1 singleThreadv1 = new SocketMultiplexingSingleThreadv1();
singleThreadv1.start();
}
}
NIO除了以上已经列举出来的组件外,还包括:
-
Selector:多路复用器,可一次同时监听多个SocketChannel的请求,即不会像上面的那样需要一直for轮询的监听是否有请求发送过来,当注册到Selector上的某个SocketChannel的指定event 事件发生后,会返回SocketChannel
在之前的代码中,需要监听已经建立连接的SocketChannel是否有数据发送过来 并对其发送的请求进行读写操作,也需要监听是否有想要与ServerSocketChannel建立连接的SocketChannel 并对其进行建立连接的操作,现在想要使用Selector来完成一次调用 监听并返回多个SocketChannel,需要先将SocketChannel注册到Selector上,Selector也提供了在注册的时候指定想要监听的event类型,通过SocketChannel对象调用
public final SelectionKey register(Selector sel, int ops)来完成注册,当某个SocketChannel指定的event发生时就会返回注册到Selector上的SocketChannel,然后就可以在使用selector.selectedKeys()(返回监听的SocketChannel,注意:不同的OS 操作系统Selector的实现也不同,linux上有多个Selector,但是由于epoll较好会使用epoll,windows没有epoll,会使用select,mac使用kqueue,如果使用的是epoll,只返回有状态(即有数据发送来)的SocketChannel,如果是select,不论是否有状态都会把所有注册到Selector上的SocketChannel返回)之后通过event来区分新老SocketChannel了,也就可以对于不同的event做出相应的操作了event:事件,注册到Selector时可指定的event包括:READ、WRITE、CONNECT、ACCEPT,有event发生就会返回SocketChannel
-
SelectKey:SocketChannel注册到Selector上时拥有的key,同一个SocketChannel注册到不同Selector 拥有的key不同,不同SocketChannel注册到同一个Selector 拥有的key也不同
-
handler:event handler 事件处理器,对于不同的event做不同的处理
(以上只列举了主要的几个组件)
现在想来 由最初的阻塞等待一个客户端的请求,到非阻塞的轮询已建立连接的客户端的读写请求 再到使用Selector监听多个SocketChannel的读写/建立连接请求,将请求按照请求类型做了区分,区分出新老客户端,也对于不同请求类型进行相应的处理
以上所有监听到的SocketChannel都会注册到同一个Selector上,如果一个Selector上已经有很多个SocketChannel,这个Selector处理起来就会花费大量时间,而且想要建立连接的客户端也会等待很长时间,所以可以想到采用分而治之的思想来将所有的请求分开,可以还是只有一个Selector来监听所有请求,但是可以使用线程池来对于读写操作的SocketChannel做异步处理,提高处理请求的效率,也可以使用多个Selector,一个Selector专门负责处理建立连接的请求,多个Selector只负责处理有读写操作的请求,因为是多个Selector共同协作 服务器处理大量客户端请求也可以做出很快的响应。如果已经在有多个Selector负责处理有读写操作请求的场景下,还想提高服务器处理请求的效率,还可以在每个负责读写操作请求的Selector中使用线程池来异步处理,再次提高服务器响应速度(netty中有提供线程池,也可以自定义线程池)
这样就比如起初一个电话员要处理所有的包括新客户、老客户的请求,之后有了专门处理某件事的专员,新客户只需要在第一次来访的时候联系电话员 电话员推荐一个专员来处理,之后老客户的每次来访都直接联系专员,电话员只负责新客户的来访,一样的逻辑
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 如果程序采用单线程处理,连接的客户端又很多时,调用epoll_wait的返回值就会很大,单线程处理就会需要花费很长时间,下次调用epoll_wait就会很久,
* 而且将监听到的新客户端添加到维护监听队列也会因为前面的一大堆处理连接客户端发来的数据而等很久
* <p>
* 所以可以采用多线程,而且netty就是多线程处理,每个线程有一个Selector多路复用器
* <p>
* 接收新客户端的连接和处理已连接客户端的请求是可以并行(并行:多核CPU可以同时处理多个任务,并发:同一时刻多个客户端请求服务器)处理的,它们之间是没有强的上下依赖关系的,
* 所以可以为他俩各创建一个线程进行处理请求,而单线程处理所有的已连接客户端的请求和接收新的客户端连接是个很耗时的工作,就可以多线程来处理已连接客户端的请求,使用了分治
*/
public class SocketMultiplexingThreadsTest2 {
private static Selector listener;
private static Selector work1;
private static Selector work2;
//初始化服务器的ServerSocketChannel
private static void initServer() {
try {
// 在BIO中,需要使用getInputStream()、getOutputStream()对传输的数据进行读取写入,
// 而在NIO中,将数据存入Buffer缓冲区中,读写操作的数据都使用Buffer,ServerSocketChannel、SocketChannel 这些Channel通道来传输数据,Buffer是打包数据,Channel是传输数据,
// Channel结合Buffer就完成了网络中数据的传输
// Channel 特点:
// 1. 可使用configureBlocking(false)设置为非阻塞,之后的read()、accept()、connect()就都不会阻塞了
// 2. 可以注册到Selector多路复用器上(ServerSocketChannel、SocketChannel都继承自SelectableChannel abstract class抽象类,所以有register(Selector sel, int ops)可以注册将自身到某个Selector上)
// ServerSocketChannel、SocketChannel是线程安全的
// ServerSocketChannel、SocketChannel是abstract class抽象类,调用open()创建SocketChannel对象,Selector同理
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
int port = 9090;
ssc.bind(new InetSocketAddress(port));
listener = Selector.open();
work1 = Selector.open();
work2 = Selector.open();
// Selector:多路复用器,可以一次监听多个注册到其Selector上的Channel,注册时需要指定监听Channel的哪些动作 即event事件,当指定的event发生时就会返回监听的Channel。
// 使用BIO时只能轮询查看已连接的客户端有没有发送数据过来的,有了Selector只需要调用一次就可以监听多个Channel的状态
// 使用Selector的是同步IO模型,即Selector只是返回有状态的Channel,之后对于有状态的Channel的处理还是由服务器来处理
// 不同操作系统使用的Selector也不同,windows select、linux select、poll、epoll(因为epoll效率高会使用epoll)、mac kqueue,
// 因为使用的Selector不同,Selector返回的数据也不同,使用select会返回所有注册到Selector上的Channel,使用epoll只会返回有状态(即指定的event发生了)的注册到Selector上的Channel
// ServerSocketChannel注册到Selector上时只能指定accept event,SocketChannel可以指定connect、read、write event
// SelectionKey:一个Channel注册到Selector上时就会创建一个SelectionKey,有:channel、selector、interestOps、readyOps等属性
// Selector维护了3个集合:
// 1. HashSet keys 存储所有注册到该Selector上的SocketChannel对应的SelectionKey
// 2. HashSet selectedKeys 存储就绪(即有状态)的SocketChannel对应的SelectionKey
// 3. HashSet cancelledKeys 存储取消的SocketChannel对应的SelectionKey
//ServerSocketChannel注册到Selector上时只能指定ACCEPT event,而SocketChannel可以是除了ACCEPT之外的其余3个event:CONNECT、READ、WRITE
ssc.register(listener, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
initServer();
NewThread accept = new NewThread(listener, 2);
NewThread read1 = new NewThread(work1);
NewThread read2 = new NewThread(work2);
accept.setName("listener");
read1.setName("read1");
read2.setName("read2");
accept.start();
//先让listener线程运行
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
read1.start();
read2.start();
System.out.println("服务器启动了……");
try {
System.in.read(); //阻塞服务器,不让服务器关闭
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("server end!");
}
//服务器
public static class NewThread extends Thread {
private Selector selector;
private volatile static LinkedBlockingQueue<SocketChannel>[] queues;
private static int queLen;
private final AtomicInteger rand = new AtomicInteger(0); //随机存放到某个队列中
private static final AtomicInteger index = new AtomicInteger(0); //随机指定当前线程的队列数组的下标
private int id;
//NIO中Buffer 缓冲区是非线程安全的,这里每个线程都有一个Buffer,一个线程内对Buffer的读写操作就是同步的了
private ByteBuffer buffer;
public NewThread() {
}
//处理ACCEPT请求的线程
public NewThread(Selector selector, int queNum) {
this.selector = selector;
queues = new LinkedBlockingQueue[queNum];
queLen = queNum;
for (int i = 0; i < queues.length; i++) {
queues[i] = new LinkedBlockingQueue<>();
}
id = -1; //因为是基本类型,所以listener线程的id初始值是0,所以为了避免listener线程可以操作queues[0]的queue队列,所以这里id = -1
System.out.println("Boss 启动");
}
//处理READ请求的线程
public NewThread(Selector selector) {
this.selector = selector;
id = index.getAndIncrement() % queLen;
this.buffer = ByteBuffer.allocate(512); //因为Buffer是非线程安全的,所以这里在每个线程中都只有一个Buffer,这样在每个线程内部对于Buffer的处理就是同步的了
System.out.println("worker: " + id + " 启动");
}
@Override
public void run() {
try {
while (true) {
// System.out.println(Thread.currentThread().getName() + " id: " + id + ", Selector's keys: ");
// Set<SelectionKey> keys = selector.keys();
// for (SelectionKey key : keys) {
// int localPort = 0;
// if (key.channel() instanceof ServerSocketChannel) {
// ServerSocketChannel channel = (ServerSocketChannel) key.channel();
// localPort = channel.socket().getLocalPort(); //返回此socket连接的本地端口
// }
// if (key.channel() instanceof SocketChannel) {
// SocketChannel channel = (SocketChannel) key.channel();
// localPort = channel.socket().getPort(); //返回此socket连接的远程端口
// }
// System.out.print(localPort + " ");
// }
// System.out.println();
//NIO是同步非阻塞的通信模型,因为只是调用Selector获得有状态的客户端,最终处理请求还是需要自己做处理
//先监听有没有有状态的客户端,select()返回就绪的客户端(即有状态)的数量
if (selector.select(500) > 0) { //返回Selector的selectedKeys 就绪队列 的更改数量
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
// 从Selector的selectedKeys 就绪队列中移除,如果不移除,Selector的selectedKeys 就绪队列中就会一直存在该值,会导致下次客户端发送数据过来后,
// 服务器的Selector的selectedKeys 就绪队列中还一直存在着那个客户端,调用selector.select(500)的返回值只会=0,就无法对于客户端的请求作出响应,所以也就会出现服务器只会响应客户端的第一次请求,之后客户端再怎么发出请求,服务器都没有响应
iterator.remove(); //不添加这句服务器只会接收并处理一条客户端发送来的数据
if (next.isReadable()) { //判断SelectionKey的SocketChannel是否可以读取,处理READ请求的线程
//如果处理效率还想提高,可以使用线程池异步处理
readHandler(next);
} else if (next.isAcceptable()) { //处理ACCEPT请求的线程
acceptHandler(next);
}
}
}
//将新的连接注册到本Selector多路复用器中,get your eye on here:listener线程不可以执行这里,只有read线程才需要读取queue队列中的客户端SocketChannel
if (id >= 0 && queues[id].size() > 0) {
SocketChannel poll = queues[id].poll(); //TODO 这里使用take()欠妥,因为当前线程不只有一个添加到当前线程的Selector中的动作,还要监听当前线程的Selector上注册的客户端状态,所以不应该使用take()阻塞
poll.register(selector, SelectionKey.OP_READ);
System.out.println("=================================");
System.out.println("新客户端:" + poll.socket().getPort() + " 分配到:" + id);
System.out.println("=================================");
}
//可以做一些除了处理客户端请求之外的操作,比如redis中除了处理客户端请求外还会操作一些定时任务 比如:回收内存……,
// 所以上面的就应该是if而不是while
}
} catch (IOException e) {
e.printStackTrace();
}
}
//服务器处理ACCEPT请求的ServerSocketChannel 将监听到的SocketChannel添加到队列中,等待其他线程将其添加到它自己的Selector中
public void acceptHandler(SelectionKey sk) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) sk.channel();
System.out.println(ssc.socket().getLocalPort() + " server is accept");
SocketChannel client = ssc.accept();
client.configureBlocking(false);
int i = rand.getAndIncrement() % queLen;
queues[i].offer(client);
} catch (IOException e) {
e.printStackTrace();
}
}
//如果处理效率还想提高,可以使用线程池异步处理
public void readHandler(SelectionKey sk) {
try {
SocketChannel client = (SocketChannel) sk.channel();
System.out.println(client.socket().getPort() + " client is read");
//ByteBuffer是非线程安全的
// ByteBuffer buffer=ByteBuffer.allocate();
buffer.clear(); //因为这里一个线程内的多次操作的是同一个ByteBuffer缓冲区,所以会有服务器响应客户端数据出现上一次的重复数据,要么在当前线程中每次读取数据时创建新的Buffer,然后使用buffer.array(),要么手动创建一个array,然后使用buffer.get(array)
int read = client.read(buffer); //对Buffer的读写操作会移动position属性
buffer.flip();
if (read > 0) {
//array()返回非直接Buffer缓冲区在jvm中的数组,在传输数据时,除了在kernel space内核空间中有copy外,还会在user space用户空间copy一份,使用non-direct Buffer非直接缓冲区就会在jvm中创建一个数组来作为Buffer缓冲区,direct Buffer直接缓冲区不会在jvm在开辟空间
//如果多次请求使用的同一个non-direct Buffer,就会出现Buffer的arr数据重复的现象,为了避免就不能直接返回jvm中的arr,可以和处理direct buffer一样,创建一个arr来存放non-direct buffer中的数据
// String s = new String(buffer.array());
// System.out.println(client.socket().getPort() + ": " + s);
// buffer.clear();
//优化:
byte[] copyArr = new byte[buffer.limit()];
buffer.get(copyArr); //调用get()将buffer中的数据存入arr中
String s = new String(copyArr);
System.out.println(client.socket().getPort() + ": " + s);
client.write(ByteBuffer.wrap(s.getBytes()));
if (s.equals("quit")) {
sk.cancel(); //如果客户端发来"quit",就从服务器中注销此客户端的连接
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1个listen线程负责accept类型的请求,2个read线程负责read类型的请求,每个线程都有一个Selector,listen线程将监听到的新的SocketChannel随机给到其中一个read线程 让其注册到线程中的Selector上,这里使用了queue array 队列数组来作为线程之间通信的容器,get your eye on here:要使用线程安全的容器来存储SocketChannel,避免同一个SocketChannel在多个线程的Selector上注册。这样就实现了分而治之。之后就是对于不同的请求类型做出不同的处理
Reactor
网络通信模型有:Reactor, Proactor, Asynchronous, Completion Token, and Acceptor-Connector
The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers
reactor是用于处理多个请求并发访问服务器时的一个基于事件驱动的通信模式,服务器会对请求进行解析 解析出它们属于什么类型的请求,并将它们分发给相应的event handler进行处理
这不就是上面NIO中使用Selector基于事件驱动的方式嘛