目录
IO的阻塞与非阻塞
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
Java 中新建一个线程,new Thread( Runnable …) 之后调用 start() 方法时,最终是调pthread_create 系统方法来创建的线程,这里会从用户态切换到内核态完成系统资源的分配,线程的创建。(new Thread 创建的线程运行在内核地址空间中,运行的代码不受任何的限制,所以在线程中调用read()或write()方法时,该线程被阻塞)
例如下面客户端网络数据的收发:
public class Server {//客户端的收发数据
public static void main(String[] args) {
ServerSocket serverforclient ;
DataInputStream datainput ;
DataOutputStream dataoutput ;
try {
serverforclient = new ServerSocket( 9999);//服务器端口
Socket Servsocket = serverforclient.accept();//阻塞 监听连接
datainput = new DataInputStream(Servsocket.getInputStream());
dataoutput = new DataOutputStream(Servsocket.getOutputStream());
new Thread(new IOThread(dataoutput,datainput)).start();//一个线程对应于一个IO通信
/*当服务器端需要处理大量客户端时,性能急剧下降
new Thread()... ...*/
} catch (IOException e) {
e.printStackTrace();
}
}
}
class IOThread implements Runnable {
DataOutputStream outputStream;
DataInputStream inputStream;
public IOThread(DataOutputStream outputStream, DataInputStream inputStream) {
this.inputStream = inputStream;
this.outputStream = outputStream;
}
@Override
public void run() {
//发送数据
String message = "你好";
byte[] sendbytes = message.getBytes();
try {
outputStream.write(sendbytes);//该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务
} catch (IOException e) {
e.printStackTrace();
}
//读取发来的数据
byte[] getbytes = new byte[1024];
try {
inputStream.readFully(getbytes);//同上
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("对方发来消息:"+new String(getbytes));
}
}
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
通过Selector选择器:
把每一个通道都注册到该选择器上,选择器用来监控这些通道的IO状况:当某一个通道上的某个请求事件的数据已经完全准备就绪时,选择器才会把这个任务分配到服务器端一个或多个线程上运行,否则服务器端的这些线程可以执行其他任务。(Selector 可使一个单独的线程管理多个 Channel,是非阻塞 IO 的核心。)
NIO网络通信
没有使用Selector的阻塞NIO通信:
(双方单纯依靠的各自的线程进行收发数据。并且为阻塞式的收发)
public class TestBlockingNIO2 {
//客户端
@Test
public void client() throws IOException{
//1. 获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
//2. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//3. 读取本地文件,并发送到服务端
while(inChannel.read(buf) != -1){
buf.flip();
sChannel.write(buf);
buf.clear();
}
//sChannel.shutdownOutput(); 不加这一步,服务器端无法知道客户端是否发送完数据,所以就不会发送反馈报文,进而发生阻塞。
//接收服务端的反馈
int len = 0;
while((len = sChannel.read(buf)) != -1){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
//4. 关闭通道
inChannel.close();
sChannel.close();
}
//服务端
@Test
public void server() throws IOException{
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//2. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//3. 获取客户端连接的通道
SocketChannel sChannel = ssChannel.accept();
//4. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//5. 接收客户端的数据,并保存到本地
while(sChannel.read(buf) != -1){
buf.flip();
outChannel.write(buf);
buf.clear();
}
//发送反馈给客户端
buf.put("服务端接收数据成功".getBytes());
buf.flip();
sChannel.write(buf);
//6. 关闭通道
sChannel.close();
outChannel.close();
ssChannel.close();
}
}
这里服务器和客户端都相当于用一个线程(new Thread)用于收发数据。线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,线程会阻塞。
非阻塞NIO通信(重点)
Selector
Selector 一般称为选择器 ,也可以翻译为 多路复用器 。它是 Java NIO 核心组件中 的一个,用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。如 此可以实现单线程管理多个 channels,也就是可以管理多个网络链接。
使用 Selector 的好处在于:使用更少的线程来就可以来处理通道了, 相比使用多个 线程,避免了线程上下文切换带来的开销。
不是所有的 Channel 都可以被 Selector 复用的:判断一个 Channel 能被 Selector 复用,有一个前提:判断他是否继承了一个抽象类 SelectableChannel。如果继承了 SelectableChannel,则可以被复用,否则不能。
Channel 注册到 Selector
(1)使用 Channel.register(Selector sel,int ops)方法,将一个通道注册到一个选择器时。第一个参数,指定通道要注册的选择器。第二个参数指定选择器需要查询的通道操作。
选择器查询的不是通道的操作,而是通道的某个操作的一种就绪状态。
什么是操作的就绪状态?
一旦通道具备完成某个操作的条件,表示该通道的某个操作已经就绪, 就可以被 Selector 查询到,程序可以对通道进行对应的操作。比方说,某个 SocketChannel 通道可以连接到一个服务器,则处于“连接就绪”(OP_CONNECT)。 再比方说,一个ServerSocketChannel 服务器通道准备好接收新进入的连接,则处于 “接收就绪”(OP_ACCEPT)状态。还比方说,一个有数据可读的通道,可以说是 “读就绪”(OP_READ)。一个等待写数据的通道可以说是“写就绪”(OP_WRITE)。
(2)可以供选择器查询的通道操作,从类型来分,包括以下四种:
可读 : SelectionKey.OP_READ (某通道准备被读)
可写 : SelectionKey.OP_WRITE (某通道准备被写)
连接 : SelectionKey.OP_CONNECT (某通道准备去连接,用于客户端)
接收 : SelectionKey.OP_ACCEPT (某通道准备被连接,用于服务器)
如果 Selector 对通道的多操作类型感兴趣,可以用“位或”操作符来实现:
int key = SelectionKey.OP_READ|SelectionKey.OP_WRITE ;
选择键(SelectionKey)
(1)Channel 注册到后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到。 这个工作,使用选择器 Selector的 select()方法完成。select方法的作用,对感兴趣的通道操作,进行就绪状态的查询。
(2)并且Selector 可以不断的查询 Channel 中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是 Selector 感兴趣的操作, 就会被 Selector选中,放入选择键集合中。
Selector的使用方法
1、 Selector 的创建
通过调用 Selector.open()方法创建一个 Selector 对象
// 1、获取 Selector 选择器
Selector selector = Selector.open();
2、 注册 Channel 到 Selector
// 1、获取 Selector 选择器
Selector selector = Selector.open();
// 2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4、绑定连接
serverSocketChannel.bind(new InetSocketAddress(9999));
// 5、将通道注册到选择器上,并制定监听事件为:“接收”事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
注意:
(1)与 Selector 一起使用时,Channel 必须处于非阻塞模式下,否则将抛出异常 IllegalBlockingModeException。这意味着,FileChannel 不能与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式,而套接字相关的所有的通道都可以。
(2)一个通道,并非一定要支持所有的四种操作。
比如服务器通道 ServerSocketChannel 支持 Accept 接受操作,而 SocketChannel 客户端通道则不支持。 可以通过通道上的 validOps()方法,来获取特定通道下所有支持的操作集合。
3、轮询查询就绪操作
通过 Selector 的 select()方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,包存在一个元素是 SelectionKey 对象的 Set 集合中。
select()方法返回的 int 值,表示有多少通道已经就绪, 更准确的说,是自前一次 select 方法以来到这一次 select 方法之间的时间段上,有多少通道变成就绪状态。(不断更新)
一旦调用 select()方法,并且返回值不为 0 时,可以用Selector 中的 selectedKeys()方法用来访问已选择键集合,然后再迭代集合的每一个选择键元素,根据就绪操作的类型, 完成对应的操作:
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext())
{
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
}
else if (key.isReadable()) {
// a channel is ready for reading
}
else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();//完成此事件之后就将它移除
}