NIO 同步非阻塞的编程方式
主要是解决BIO的大并发问题,NIO最重要的地方是当一个连接创建后,对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以完成,当这个线程中的多路复用器进行轮询的 时候,发现连接上有请求的时候,才开启线程进行处理,也就是一个请求一个线程模式。
NIO的处理方式中,当一个请求来的时候,开启线程进行处理,可能会等待后端的资源连接等等,其实该线程就被阻塞了,当并发上来的时候,还是会出现BIO一样的阻塞问题。
首先创建一个服务端的程序,客户端发起请求的时候,服务端会创建一个SocketChannel通道,客户端会和通道一一对应,然后服务端会把通道注册到多路复用器Selector上面。当多路复用器对通道进行轮询的时候,当通道需要进行操作的时候,多路复用器会通知通道去工作线程(work thread)去操作,把所有的读写数据写入Buffer缓存区中。即所有的操作都是面向Buffer实现的,而工作线程不是和客户端线程一一对应的,而是一个工作线程为多个客户端提供服务,Selector通过SocketChannel可以处理多个客户端请求。
同步非阻塞,服务器实现模式为一个请求一个通道,即客户端发送的连接请求都会注册到多路复用器上面,多路复用器轮询到有IO请求时才会启动一个线程处理。
NIO方式适用于连接数目多,且连接比较短(轻操作)的架构,比如聊天服务器。
Buffer:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer
Channel:SocketChannel、serverSocketChannel
Selector:Selector、AbstractSelector
SelectionKey: OP_ACCEPT :连接成功的标记
OP_READ :可以读取数据的标记
OP_WRITE :可以写入数据的标记
OP_CONNECT :建立连接后的标记
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class NIOClient {
public static void main(String[] args){
// 创建远程连接地址
InetSocketAddress remote = new InetSocketAddress("localhost",9999);
SocketChannel channel = null;
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
channel = SocketChannel.open();
channel.connect(remote);
Scanner reader = new Scanner(System.in);
while (true){
System.out.println("put message for send to Server >");
String line = reader.nextLine();
if(line.equals("exit")){
break;
}
buffer.put(line.getBytes("UTF-8"));
buffer.flip();
channel.write(buffer);
buffer.clear();
int readLength = channel.read(buffer);
if(readLength == -1){
break;
}
// 重置缓存游标
buffer.flip();
byte[] datas = new byte[buffer.remaining()];
// 读取数据到数组
buffer.get(datas);
System.out.println("from server : " + new String(datas, "UTF-8"));
// 清空缓存
buffer.clear();
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(channel != null){
try {
channel.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
}
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.*;
import java.nio.channels.*;
import java.nio.channels.spi.AbstractSelector;
import java.util.Iterator;
import java.util.Scanner;
public class NIOServer implements Runnable {
// 多路复用器,选择器。用于注册通道
private Selector selector;
// 定义两个缓存 分别用于读和写;初始化空间大小为1024字节
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
public static void main(String[] args){
new Thread(new NIOServer(9999)).start();
}
public NIOServer (int port){
init(port);
}
private void init(int port) {
try {
System.out.println("server starting at port " + port + "...");
this.selector = Selector.open();
// 开启服务通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 非阻塞,传递参数为true为阻塞模式
serverSocketChannel.configureBlocking(false);
// 绑定端口
serverSocketChannel.bind(new InetSocketAddress(port));
// 注册,并标记当前服务通道状态
/**
* register(Selector,int)
* int - 状态编码
* OP_ACCEPT :连接成功的标记
* OP_READ :可以读取数据的标记
* OP_WRITE :可以写入数据的标记
* OP_CONNECT :建立连接后的标记
*/
serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
System.out.println("server start");
}catch (IOException e){
e.printStackTrace();
}
}
@Override
public void run() {
while (true){
try {
// 阻塞方法,当至少一个通道被选中,此方法返回。
this.selector.select();
// 返回以选中的通道标记集合,集合保存的是通道的标记,相当于是通道的ID
Iterator keys = this.selector.selectedKeys().iterator();
while (keys.hasNext()){
SelectionKey key = keys.next();
// 将本次要处理的通道冲集合中删除,下次删除根据新的通道列表再次执行必要的业务逻辑
keys.remove();
// 通道是否有效
if(key.isValid()){
try {
if (key.isAcceptable()){
accept(key);
}
}catch (CancelledKeyException e){
key.cancel();
}
try{
if(key.isReadable()){
read(key);
}
}catch (CancelledKeyException cke){
key.cancel();
}
try{
if(key.isWritable()){
write(key);
}
}catch (CancelledKeyException cke){
key.cancel();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void write(SelectionKey key){
this.writeBuffer.clear();
SocketChannel channel = (SocketChannel)key.channel();
Scanner reader = new Scanner(System.in);
try {
System.out.println("put message for send to client > ");
String line = reader.nextLine();
writeBuffer.put(line.getBytes("UTF-8"));
writeBuffer.flip();
channel.write(writeBuffer);
channel.register(this.selector,SelectionKey.OP_READ);
}catch (IOException e){
e.printStackTrace();
}
}
private void read(SelectionKey key){
try {
// 清空读缓存
this.readBuffer.clear();
// 获取通道
SocketChannel channel = (SocketChannel)key.channel();
// 将通道中的数据读到缓存中。通道中的数据,就是客户端发送给服务器的数据。
int readLength = channel.read(readBuffer);
// 检查客户端是否写入数据
if(readLength == -1){
// 通道关闭
key.channel().close();
// 关闭连接
key.cancel();
return;
}
// flip,NIO中最复杂的操作就是Buffer的控制
/** Buffer中有一个游标。游标的信息在操作后不会归零,如果直接访问Buffer的话,数据有可能不一致。
* flip是重置游标的方法.NIO编程中,flip方法是常用的方法
*
*/
this.readBuffer.flip();
// 字节数据,保存具体数据。Buffer.remaining() ->获取Buffer中有效数据长度的方法。
byte[] datas = new byte[readBuffer.remaining()];
// 是将Buffer中的有效数据保存到有效数组中。
readBuffer.get(datas);
System.out.println("from" + channel.getRemoteAddress() + " client : " + new String(datas,"UTF-8"));
channel.register(this.selector,SelectionKey.OP_WRITE);
}catch (IOException e){
e.printStackTrace();
}
}
private void accept(SelectionKey key){
try {
// 此通道为init方法中注册到Seleor上的ServerSocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
// 阻塞方法,当客户端发起请求后返回.此通道和客户端一一对应
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
// 设置对用客户端的通道标记状态,此通道为读取数据使用的。
channel.register(this.selector,SelectionKey.OP_READ);
}catch (IOException e){
e.printStackTrace();
}
}
}
Buffer的应用固定逻辑