对于NIO的概念网上有好多文档式的解释,但作为读者不是很好理解,因此我想用个贴近生活的例子介绍下。
一、首先他的设计模型叫反应器模型(Reactor)
有这么个例子:
一个饭店刚开业,老板请了3个服务员负责等待顾客的点单下菜,因为客户人数不多,服务员可以胜任,但是后来饭店火了,顾客变得好多,这3个服务员忙不过来了,老板就想有没有什么方法,及不需要新招员工(成本控制)又可以不让顾客长时间等待(线程堵塞),灵光一现,老板就想到了 想给顾客看菜单,等到他们选好了,再让服务员去完成下单工作,这样就大大提高了顾客的接待效率。
这个先看菜单,在点菜的过程就是反应器模型。
二、NIO中最重要的3个组件及其概念
1.Channel 通道
2.Buffer 缓冲区
3.Selector 选择器
其中Channel对应咱们接触最多的流,Buffer(缓存)不是什么新东西,Selector是因为nio可以使用同步的非堵塞模式才加入的东西。
以前的流总是堵塞的,一个线程只要对它进行操作,其它操作就会被堵塞,也就相当于一个没有阀门的水管,你伸手接水的时候,不管水到了没有,你就都只能耗在接水(流)上(线程堵塞)。
nio的Channel的加入,相当于增加了水龙头(有阀门),虽然一个时刻也是只能接一个水管的水,但依赖轮换策略(循环),在水量不大的时候,各个水管里流出来的水,都可以被处理接纳,但是还有个关键之处就是增加了一个接水工,也就是Selector(选择器),他负责协调,也就是看哪根水管有水了(就绪状态),在当前水管的水接到一定程度的时候,就切换一下:临时关上当前水龙头,试着打开另一个水龙头(看看有没有水)。
当其他人需要用水的时候,不是直接去接水,而是事前提了一个水桶给接水工,这个水桶就是Buffer。也就是,其他人虽然也可能要等,但不会在现场等,而是回家等,可以做其它事去,水接满了,接水工会通知他们。
好了我想经过上面2个例子应该可以对NIO的概念有个比较清晰的理解了吧,下面我们来用具体的代码来实例开发(代码有详细注解说明)
服务端
package demo;
public class NIODemo_Server {
private static int server_port=9999;//服务器端口
private static ServerHandler serverhandler;//服务端处理类
public static void start(){
Start(server_port);
}
/**
* 创建一个同步的线程启动服务端
*
* **/
public static synchronized void Start(int server_port2) {
if (null!=serverhandler) {
serverhandler.shop();
}
serverhandler=new ServerHandler(server_port2);
new Thread(serverhandler, "server").start();;
}
public static void main(String[] args) {
start();
}
}
package demo;
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;
import java.util.Set;
public class ServerHandler implements Runnable {
private Selector selector;//NIO中的选择器
private ServerSocketChannel serverchannel;//通道
private volatile boolean started;//服务器状态并线程可见
public ServerHandler(int server_port2) {
try {
selector=Selector.open();//1.开启选择器,用于监听、轮查
serverchannel=ServerSocketChannel.open();//2.开启通道
serverchannel.configureBlocking(false);//3.调用configureBlocking(),开启非堵塞模式,true:堵塞;false:非堵塞
//4.通过ServerSocketChannel中的socket()获取一个ServerSocket,然后使用ServerSocket的bind方法为其绑定通讯地址,并设置连接队列长度
serverchannel.socket().bind(new InetSocketAddress(server_port2),1024);
//5.将信道注册到选择器上并配置事件类型
/*
SelectionKey.OP_ACCEPT —— 接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户与服务器的连接已经建立成功
SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)
*/
serverchannel.register(selector, SelectionKey.OP_ACCEPT);
started=true;//6.完成上述操作后,将服务器状态变为启动
System.out.println("服务器已启动,端口号"+server_port2);
} catch (IOException e) {
e.printStackTrace();
}
}
public void shop() {
started = false;
}
@Override
public void run() {
//通过started来判断服务器是否启动,并在启动的情况下遍历selector
while (started) {
try {
selector.select(1000);//设置选择器的工作周期1s
Set keys=selector.selectedKeys();//使用Set集合获取当前选择器中的主键集合
Iterator it=keys.iterator();//使用迭代器,遍历set集合
SelectionKey key = null; //定义key 用于接收遍历后的值
while (it.hasNext()) {
key=it.next();//赋值
//稍微提下 在多线程情况下 要删除集合里的元素 需要使用迭代器的remove方法 因为remove方法可以保障从源集合中安全删除对象
it.remove();
handleInput(key);
}
} catch (IOException e) {
e.printStackTrace();
}
}
//释放资源
if(selector != null)
try{
selector.close();
}catch (Exception e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key)throws IOException {
//判断key是否有效
if (key.isValid()) {
//通过isAcceptable方法判断是否有新的信息进入通道,有着进行下步处理
if (key.isAcceptable()) {
ServerSocketChannel ssc=(ServerSocketChannel) key.channel();//创建新的通道用于接收key代表的通道
SocketChannel sc = ssc.accept(); //从accept中获取连接通道
sc.configureBlocking(false);//设置非堵塞
sc.register(selector, SelectionKey.OP_READ);//注册到选择器中,并规定模式为读
}
//读取通道信息
if (key.isReadable()) {
SocketChannel sc=(SocketChannel) key.channel();//创建新的通道用于接收key代表的通道
ByteBuffer bb=ByteBuffer.allocate(1024);//使用静态方法allocate定义一个1M的byte类型的缓存区域,用于保存通道中的数据
int returnReadByte=sc.read(bb);//读取通道中的数据并返回其字节码
if (returnReadByte>0) {
bb.flip();//pos变为0,从buffer头开始读取数据
byte[] b=new byte[bb.remaining()];//使用remaining方法获取buffer中的元素大小,并创建对应大小的byte数组
bb.get(b);//将buffer中的数据赋给byte数组中
String CilentMsg=new String(b, "utf-8");//编译
System.out.println("服务端:接收到的信息是"+CilentMsg);
//业务处理...........
//...........
String result=CilentMsg+"(已处理)";
//发送应答消息
doWrite(sc,result);
}
//链路已经关闭,释放资源
else if(returnReadByte<0){
key.cancel();
sc.close();
}
}
}
}
private void doWrite(SocketChannel sc, String result) throws IOException{
byte[] bytes = result.getBytes(); //将返回信息变成字节数组
ByteBuffer rebb=ByteBuffer.allocate(bytes.length);//根据数组容量创建ByteBuffer
rebb.put(bytes); //将字节数组复制到缓冲区
rebb.flip(); //flip操作
sc.write(rebb); //想通道中发送缓冲区的字节数组
}
}
客户端
package demo;
public class NIODemo_Client {
private static String server_host="127.0.0.1";//服务器地址
private static int server_port=9999;//服务器端口
private static ClientHandle clientHandle ;
public static void start(){
start(server_host,server_port);
}
//启动一个线程用于连接server
private static synchronized void start(String server_host2, int server_port2) {
if(clientHandle!=null)clientHandle.shop();
clientHandle=new ClientHandle(server_host2,server_port2);
new Thread(clientHandle, "client").start();
}
//用于业务请求
public static boolean sendMsg(String msg)throws Exception{
if(null==msg||"".equals(msg)||" ".equals(msg))return false;
clientHandle.sendMsg(msg);
return true;
}
public static void main(String[] args) {
start();
}
}
package demo;
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.Set;
public class ClientHandle implements Runnable{
private String host;//地址
private int port;//端口
private SocketChannel clientSocketChannel;//通道
private Selector clientSelector;//选择器
private volatile boolean started;//线程可见的客户端状态
public ClientHandle(String server_host2, int server_port2) {
this.host = server_host2;
this.port = server_port2;
try {
clientSelector=Selector.open();//开启选择器
clientSocketChannel=clientSocketChannel.open();//开启通道
clientSocketChannel.configureBlocking(false);//设为非堵塞模式
started = true;
} catch (IOException e) {
e.printStackTrace();
}
}
public void shop() {
started = false;
}
@Override
public void run() {
try{
doConnect();
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
//循环遍历selector
while(started){
try{
//无论是否有读写事件发生,selector每隔1s被唤醒一次
clientSelector.select(1000);
Set keys = clientSelector.selectedKeys();
Iterator it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
try{
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}
}catch(Exception e){
e.printStackTrace();
System.exit(1);
}
}
//selector关闭后会自动释放里面管理的资源
if(clientSelector != null)
try{
clientSelector.close();
}catch (Exception e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
SocketChannel sc = (SocketChannel) key.channel();
if(key.isConnectable()){
if(sc.finishConnect());
else System.exit(1);
}
//读消息
if(key.isReadable()){
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if(readBytes>0){
//将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String result = new String(bytes,"UTF-8");
System.out.println("客户端收到消息:" + result);
}
//没有读取到字节 忽略
// else if(readBytes==0);
//链路已经关闭,释放资源
else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}
public void sendMsg(String msg)throws Exception {
clientSocketChannel.register(clientSelector, SelectionKey.OP_READ);
doWrite(clientSocketChannel, msg);
}
private void doConnect() throws IOException{
if(clientSocketChannel.connect(new InetSocketAddress(host,port)));
else clientSocketChannel.register(clientSelector, SelectionKey.OP_CONNECT);
}
//异步发送消息
private void doWrite(SocketChannel channel,String request) throws IOException{
//将消息编码为字节数组
byte[] bytes = request.getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
//发送缓冲区的字节数组
channel.write(writeBuffer);
//****此处不含处理“写半包”的代码
}
}
完成后我们写个测试类
package demo;
import java.util.Scanner;
public class NioTest {
public static void main(String[] args) throws Exception{
NIODemo_Server.Start(9999);//启动服务器
Thread.sleep(1000);//等待
NIODemo_Client.start();//启动客户端
while(NIODemo_Client.sendMsg(new Scanner(System.in).nextLine()));
}
}