分布式专题(七)NIO

netty 和 mina是对NIO进行封装的框架,所以学习netty之前先了解NIO

具体参考文档

链接:https://pan.baidu.com/s/1sCjacdyC3fYN3SBubgCiuw 
提取码:osim

1)阻塞(Block)和非阻塞(Non-Block):
阻塞和非阻塞是进程在访问数据的时候, 数据是否准备就绪的一种处理方式,当数据没
有准备的时候 阻塞: 往往需要等待缓冲区中的数据准备好过后才处理其他的事情, 否则一
直等待在那里。
非阻塞:当我们的进程访问我们的数据缓冲区的时候, 如果数据没有准备好则直接返回,
不会等待。 如果数据已经准备好, 也直接返回。
2) 同步(Synchronization)和异步(Asynchronous)的方式:
同步和异步都是基于应用程序和操作系统处理 IO 事件所采用的方式。 比如同步: 是应
用程序要直接参与 IO 读写的操作。 异步: 所有的 IO 读写交给操作系统去处理, 应用程序只
需要等待通知。
同步方式在处理 IO 事件的时候, 必须阻塞在某个方法上面等待我们的 IO 事件完成(阻
塞 IO 事件或者通过轮询 IO 事件的方式),对于异步来说, 所有的 IO 读写都交给了操作系统。
这个时候, 我们可以去做其他的事情, 并不需要去完成真正的 IO 操作, 当操作完成 IO 后,
会给我们的应用程序一个通知。
同步:1)阻塞到 IO 事件, 阻塞到 read 或则 write。 这个时候我们就完全不能做自己的
事情。 让读写方法加入到线程里面, 然后阻塞线程来实现, 对线程的性能开销比较大。
 

nio:同步非阻塞

面向流和缓冲区

Java NIO 和 IO 之间第一个最大的区别是, IO 是面向流的, NIO 是面向缓冲区的

阻塞与非阻塞
Java IO 的各种流是阻塞的。 这意味着, 当一个线程调用 read() 或 write()时, 该线程被阻塞, 直到有一些数据被读取, 或数据完全写入。该线程在此期间不能再干任何事情了
JavaNIO 的非阻塞模式,一个线程请求写入一些数据到某通道, 但不需要等待它完全写入, 这个线程同时可以去做别的事情。线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作, 所以一个单独的线程现在可以管理多个输入和输出通道(channel)
 

选择器

Java NIO 的选择器允许一个单独的线程来监视多个输入通道, 你可以注册多个通道使用一个选择器, 然后使用一个单独的线程来“选择” 通道: 这些通道里已经有可以处理的输入,或者选择已准备写入的通道。 这种选择机制, 使得一个单独的线程很容易来管理多个通道。
 

1.BIO写法

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class BIOServer {

    ServerSocket server;
	//服务器
	public BIOServer(int port){
		try {
			//把Socket服务端启动
			server = new ServerSocket(port);
			System.out.println("BIO服务已启动,监听端口是:" + port);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 开始监听,并处理逻辑
	 * @throws IOException 
	 */
	public void listener() throws IOException{
		//死循环监听
		while(true){
			//虽然写了一个死循环,如果一直没有客户端连接的话,这里一直不会往下执行
			Socket client = server.accept();//等待客户端连接,阻塞方法
			
			//拿到输入流,也就是乡村公路
			InputStream is = client.getInputStream();
			
			//缓冲区,数组而已
			byte [] buff = new byte[1024];
			int len = is.read(buff);
			//只要一直有数据写入,len就会一直大于0
			if(len > 0){
				String msg = new String(buff,0,len);
				System.out.println("收到" + msg);
			}
		}
	}
	
	
	public static void main(String[] args) throws IOException {
		new BIOServer(8080).listener();
	}
	
}
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;

public class BIOClient2 {

	public static void main(String[] args) throws UnknownHostException, IOException {

		try{


            //开一条乡村公路
			Socket client = new Socket("localhost", 8080);
			//输出流通道打开
			OutputStream os = client.getOutputStream();
			//产生一个随机的字符串,UUID
			String name = UUID.randomUUID().toString();
			
			//发送给服务端
			os.write(name.getBytes());
			os.close();
			client.close();
			
		}catch(Exception e){
			
		}
	}
		
	
}

2.NIO写法

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * 网络多客户端聊天室
 * 功能1: 客户端通过Java NIO连接到服务端,支持多客户端的连接
 * 功能2:客户端初次连接时,服务端提示输入昵称,如果昵称已经有人使用,提示重新输入,如果昵称唯一,则登录成功,之后发送消息都需要按照规定格式带着昵称发送消息
 * 功能3:客户端登录后,发送已经设置好的欢迎信息和在线人数给客户端,并且通知其他客户端该客户端上线
 * 功能4:服务器收到已登录客户端输入内容,转发至其他登录客户端。
 * 
 * TODO 客户端下线检测
 */
public class NIOServer {

    private int port = 8080;
    private Charset charset = Charset.forName("UTF-8");
    //用来记录在线人数,以及昵称
    private static HashSet<String> users = new HashSet<String>();
    
    private static String USER_EXIST = "系统提示:该昵称已经存在,请换一个昵称";
    //相当于自定义协议格式,与客户端协商好
    private static String USER_CONTENT_SPILIT = "#@#";
    
    private Selector selector = null;
    
    
    public NIOServer(int port) throws IOException{

        this.port = port;
		//要想富,先修路
		//先把通道打开
		ServerSocketChannel server = ServerSocketChannel.open();
		
		//设置高速公路的关卡
		server.bind(new InetSocketAddress(this.port));
		server.configureBlocking(false);
		
		
		//开门迎客,排队叫号大厅开始工作
		selector = Selector.open();
		
		//告诉服务叫号大厅的工作人员,你可以接待了(事件)
		server.register(selector, SelectionKey.OP_ACCEPT);
		
		System.out.println("服务已启动,监听端口是:" + this.port);
	}
    
    
    public void listener() throws IOException{
    	
    	//死循环,这里不会阻塞
    	//CPU工作频率可控了,是可控的固定值
    	while(true) {
    		
    		//在轮询,我们服务大厅中,到底有多少个人正在排队
            int wait = selector.select();
            if(wait == 0) continue; //如果没有人排队,进入下一次轮询
            
            //取号,默认给他分配个号码(排队号码)
            Set<SelectionKey> keys = selector.selectedKeys();  //可以通过这个方法,知道可用通道的集合
            Iterator<SelectionKey> iterator = keys.iterator();
            while(iterator.hasNext()) {
				SelectionKey key = (SelectionKey) iterator.next();
				//处理一个,号码就要被消除,打发他走人(别在服务大厅占着茅坑不拉屎了)
				//过号不候
				iterator.remove();
				//处理逻辑
				process(key);
            }
        }
		
	}
    
    
    public void process(SelectionKey key) throws IOException {
    	//判断客户端确定已经进入服务大厅并且已经可以实现交互了
        if(key.isAcceptable()){
        	ServerSocketChannel server = (ServerSocketChannel)key.channel();
            SocketChannel client = server.accept();
            //非阻塞模式
            client.configureBlocking(false);
            //注册选择器,并设置为读取模式,收到一个连接请求,然后起一个SocketChannel,并注册到selector上,之后这个连接的数据,就由这个SocketChannel处理
            client.register(selector, SelectionKey.OP_READ);
            
            //将此对应的channel设置为准备接受其他客户端请求
            key.interestOps(SelectionKey.OP_ACCEPT);
//            System.out.println("有客户端连接,IP地址为 :" + sc.getRemoteAddress());
            client.write(charset.encode("请输入你的昵称"));
        }
        //处理来自客户端的数据读取请求
        if(key.isReadable()){
            //返回该SelectionKey对应的 Channel,其中有数据需要读取
            SocketChannel client = (SocketChannel)key.channel(); 
            
            //往缓冲区读数据
            ByteBuffer buff = ByteBuffer.allocate(1024);
            StringBuilder content = new StringBuilder();
            try{
                while(client.read(buff) > 0)
                {
                    buff.flip();
                    content.append(charset.decode(buff));
                    
                }
//                System.out.println("从IP地址为:" + sc.getRemoteAddress() + "的获取到消息: " + content);
                //将此对应的channel设置为准备下一次接受数据
                key.interestOps(SelectionKey.OP_READ);
            }catch (IOException io){
            	key.cancel();
                if(key.channel() != null)
                {
                	key.channel().close();
                }
            }
            if(content.length() > 0) {
                String[] arrayContent = content.toString().split(USER_CONTENT_SPILIT);
                //注册用户
                if(arrayContent != null && arrayContent.length == 1) {
                    String nickName = arrayContent[0];
                    if(users.contains(nickName)) {
                    	client.write(charset.encode(USER_EXIST));
                    } else {
                        users.add(nickName);
                        int onlineCount = onlineCount();
                        String message = "欢迎 " + nickName + " 进入聊天室! 当前在线人数:" + onlineCount;
                        broadCast(null, message);
                    }
                } 
                //注册完了,发送消息
                else if(arrayContent != null && arrayContent.length > 1) {
                    String nickName = arrayContent[0];
                    String message = content.substring(nickName.length() + USER_CONTENT_SPILIT.length());
                    message = nickName + " 说 " + message;
                    if(users.contains(nickName)) {
                        //不回发给发送此内容的客户端
                    	broadCast(client, message);
                    }
                }
            }
            
        }
    }
    
    //TODO 要是能检测下线,就不用这么统计了
    public int onlineCount() {
        int res = 0;
        for(SelectionKey key : selector.keys()){
            Channel target = key.channel();
            
            if(target instanceof SocketChannel){
                res++;
            }
        }
        return res;
    }
    
    
    public void broadCast(SocketChannel client, String content) throws IOException {
        //广播数据到所有的SocketChannel中
        for(SelectionKey key : selector.keys()) {
            Channel targetchannel = key.channel();
            //如果client不为空,不回发给发送此内容的客户端
            if(targetchannel instanceof SocketChannel && targetchannel != client) {
                SocketChannel target = (SocketChannel)targetchannel;
                target.write(charset.encode(content));
            }
        }
    }
    
    
    public static void main(String[] args) throws IOException {
        new NIOServer(8080).listener();
    }
}
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.nio.charset.Charset;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;

public class NIOClient {

	private final InetSocketAddress serverAdrress = new InetSocketAddress("localhost", 8080);
    private Selector selector = null;
    private SocketChannel client = null;
    
    private String nickName = "";
    private Charset charset = Charset.forName("UTF-8");
    private static String USER_EXIST = "系统提示:该昵称已经存在,请换一个昵称";
    private static String USER_CONTENT_SPILIT = "#@#";
    
    
    public NIOClient() throws IOException{

        //不管三七二十一,先把路修好,把关卡开放
        //连接远程主机的IP和端口
        client = SocketChannel.open(serverAdrress);
        client.configureBlocking(false);
        
        //开门接客
        selector = Selector.open();
        client.register(selector, SelectionKey.OP_READ);
    }
    
    public void session(){
    	//开辟一个新线程从服务器端读数据
        new Reader().start();
        //开辟一个新线程往服务器端写数据
        new Writer().start();
	}
    
    private class Writer extends Thread{

		@Override
		public void run() {
			try{
				//在主线程中 从键盘读取数据输入到服务器端
		        Scanner scan = new Scanner(System.in);
		        while(scan.hasNextLine()){
		            String line = scan.nextLine();
		            if("".equals(line)) continue; //不允许发空消息
		            if("".equals(nickName)) {
		            	nickName = line;
		                line = nickName + USER_CONTENT_SPILIT;
		            } else {
		                line = nickName + USER_CONTENT_SPILIT + line;
		            }
//		            client.register(selector, SelectionKey.OP_WRITE);
		            client.write(charset.encode(line));//client既能写也能读,这边是写
		        }
		        scan.close();
			}catch(Exception e){
				
			}
		}
    	
    }
    
    
    private class Reader extends Thread {
        public void run() {
            try {
            	
            	//轮询
                while(true) {
                    int readyChannels = selector.select();
                    if(readyChannels == 0) continue;
                    Set<SelectionKey> selectedKeys = selector.selectedKeys();  //可以通过这个方法,知道可用通道的集合
                    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
                    while(keyIterator.hasNext()) {
                         SelectionKey key = (SelectionKey) keyIterator.next();
                         keyIterator.remove();
                         process(key);
                    }
                }
            }
            catch (IOException io){
            	
            }
        }

        private void process(SelectionKey key) throws IOException {
            if(key.isReadable()){
                //使用 NIO 读取 Channel中的数据,这个和全局变量client是一样的,因为只注册了一个SocketChannel
                //client既能写也能读,这边是读
                SocketChannel sc = (SocketChannel)key.channel();
                
                ByteBuffer buff = ByteBuffer.allocate(1024);
                String content = "";
                while(sc.read(buff) > 0)
                {
                    buff.flip();
                    content += charset.decode(buff);
                }
                //若系统发送通知名字已经存在,则需要换个昵称
                if(USER_EXIST.equals(content)) {
                	nickName = "";
                }
                System.out.println(content);
                key.interestOps(SelectionKey.OP_READ);
            }
        }
    }
    
    
    
    public static void main(String[] args) throws IOException
    {
        new NIOClient().session();
    }
}

缓冲区 Buffer
 

缓冲区实际上是一个容器对象, 更直接的说, 其实就是一个数组, 在 NIO 库中, 所有数据都
是用缓冲区处理的。 在读取数据时, 它是直接读到缓冲区中的; 在写入数据时, 它也是写
入到缓冲区中的; 任何时候访问 NIO 中的数据, 都是将它放到缓冲区中。 而在面向流 I/O
系统中, 所有数据都是直接写入或者直接将数据读取到 Stream 对象中。

在 NIO 中, 所有的缓冲区类型都继承于抽象类 Buffer, 最常用的就是 ByteBuffer, 对于 Java
中的基本类型, 基本都有一个具体 Buffer 类型与之相对应, 它们之间的继承关系如下图所
示:

下面是一个简单的使用 IntBuffer 的例子:
 

import java.nio.IntBuffer;
public class TestIntBuffer {
public static void main(String[] args) {
// 分配新的 int 缓冲区, 参数为缓冲区容量
// 新缓冲区的当前位置将为零, 其界限(限制位置)将为其容量。 它将具有一个底层实现数组,
//其数组偏移量将为零。
IntBuffer buffer = IntBuffer.allocate(8);
for (int i = 0; i < buffer.capacity(); ++i) {
int j = 2 * (i + 1);
// 将给定整数写入此缓冲区的当前位置, 当前位置递增
buffer.put(j);
}
// 重设此缓冲区, 将限制设置为当前位置, 然后将当前位置设置为 0
buffer.flip();
// 查看在当前位置和限制位置之间是否有元素
while (buffer.hasRemaining()) {
// 读取此缓冲区当前位置的整数, 然后当前位置递增
int j = buffer.get();
System.out.print(j + " ");
}
}
}

运行后可以看到:

猜你喜欢

转载自blog.csdn.net/q975583865/article/details/82743772