NIO 原理&实现

相关博文:
https://www.cnblogs.com/YjfDIY/p/9835022.html
Linux 5种IO:
https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41&scene=21#wechat_redirect

NIO

NIO原理对比 Socket

在这里插入图片描述

NIO过程总结

  1. 创建选择器 Selector, 创建通道 Channel
  2. 通道开启,并设置为非阻塞
  3. Server(通道绑定 Port );Client(通道绑定 IP + Port )
  4. 选择器和多路通道绑定
  5. *多路选择器 轮询监听(接收数据)
  6. *向通道(写数据)
    (每个通道 对应一个key)==(selectionKey.channel()----SelectionKey)

NIO中的一些标志位表示

Buffer在与Channel交互时,需要一些标志:

  • buffer的大小/容量 - Capacity

作为一个内存块,Buffer有一个固定的大小值,用参数capacity表示。

  • 当前读/写的位置 - Position​

当写数据到缓冲时,position表示当前待写入的位置,position最大可为capacity – 1;当从缓冲读取数据时,position表示从当前位置读取。

  • 信息末尾的位置 - limit

在写模式下,缓冲区的limit表示你最多能往Buffer里写多少数据; 写模式下,limit等于Buffer的capacity,意味着你还能从缓冲区获取多少数据。

在这里插入图片描述

  • 从写到读的标志位变化 : limit --> positon, position–> 0(起点位置)
    级 从 写–> 读 需要 调用 Buffer.flip(); 方法

向缓冲区写数据:

  1. 从Channel写到Buffer;

  2. 通过Buffer的put方法写到Buffer中;

从缓冲区读取数据:

  1. 从Buffer中读取数据到Channel;

  2. 通过Buffer的get方法从Buffer中读取数据;

flip方法:

   将Buffer从写模式切换到读模式,将position值重置为0,limit的值设置为之前position的值;

clear方法 vs compact方法:

   clear方法清空缓冲区;compact方法只会清空已读取的数据,而还未读取的数据继续保存在Buffer中;

写数据

String message = "data";
byte[] str = message.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(str.length);
writeBuffer.put(str);
writeBuffer.flip();
socketChannel.write(writeBuffer); // 向通道发送数据

读数据

if (key.isReadable()) {
		ByteBuffer readBuffer = ByteBuffer.allocate(1024);
		int readBytes = socketChannel.read(readBuffer);
		if (readBytes > 0) {
			readBuffer.flip();
			byte[] bytes = new byte[readBuffer.remaining()];
			readBuffer.get(bytes);
			String result= new String(bytes, "UTF-8");
		} else if (readBytes < 0) {
			// 对端链路关闭
			key.cancel();
			socketChannel.close();
		} else
			; // 读到0字节,忽略
}

参考https://www.jianshu.com/p/362b365e1bcc

问题

  1. NIO 和 BIO 通信时发现, NIO 不能一次性读取 BIO发送的消息

服务端和客户端 通信的代码

  • 可以实现 服务端NIO 和 客户端NIO 的双向通信

实现 基于 FileChannel 的 文件传输

服务端(发送)文件
if(key.isWritable()){
         SocketChannel socketChannel = (SocketChannel)key.channel();
         FileInputStream file = new FileInputStream("D:\\test.txt");

         FileChannel fileChannel = file.getChannel();
         //500M  堆外内存
         ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
         while(fileChannel.position() < fileChannel.size()){
             System.out.println("position:"+ fileChannel.position() +" size:"+ fileChannel.size());
             fileChannel.read(byteBuffer);//从文件通道读取到byteBuffer
             byteBuffer.flip();
             while(byteBuffer.hasRemaining()){ // 读到数据了吗
                 socketChannel.write(byteBuffer);//写入通道
             }
             byteBuffer.clear();//清理byteBuffer
         }
         System.out.println("结束写操作");
         socketChannel.close();
}
客户端(接收)文件
if(key.isReadable()){ //有可读数据事件。
       SocketChannel channel = (SocketChannel)key.channel();
       ByteBuffer byteBuffer = ByteBuffer.allocate(10);
       
       File file = new File("D:\\test3.txt");
       if(!file.exists()) file.createNewFile(); 
       FileOutputStream fe =new FileOutputStream(file,true);//可追加写
       FileChannel outFileChannel = fe.getChannel();
       while(channel.read(byteBuffer) > 0){ //分多次读取
           System.out.println(new String(byteBuffer.array(), "UTF-8"));
           byteBuffer.flip(); // 读 模式 --转换为-- 写模式
           outFileChannel.write(byteBuffer);//byteBuffer转换为数据写到FileChannel
           fe.flush();  // 写入文件
           byteBuffer.clear();
       }
       outFileChannel.close();

       fe.close();
       System.out.println("读取结束");
       channel.close();
}

实现 基于 SocketChannel 的 字符串传输

服务端 NIO
package com.net.nio;

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.*;

public class NioServer implements Runnable{

	public static Scanner cin = new Scanner(System.in);

	public static Map<Integer, SelectionKey> ClientMap = new HashMap<>();

    public static void main(String[] args) throws IOException {
    	int port = 8001;
    	Selector selector = null;
    	ServerSocketChannel serverSocketChannel = null;

    	new Thread(new NioServer()).start();

    	/** 通道初始化 */
    	try {
			selector = Selector.open(); // 产生多路选择器
			serverSocketChannel = ServerSocketChannel.open(); // 开启 socket通道(Channel)
			serverSocketChannel.configureBlocking(false); // 开启非阻塞模式
			serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024); // 驻守在 port端口
			System.out.println("accept 接受");
			serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 绑定多路选择器+通道
			System.out.printf("服务器在%d端口守候\n", port);
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}
    	
    	while(true) {
    		try {
				selector.select(2000); // 开始轮询 通道,每隔2000ms轮询一次

    			Set<SelectionKey> selectedKeys = selector.selectedKeys();
    			Iterator<SelectionKey> it = selectedKeys.iterator();
    			SelectionKey key = null;
    			while (it.hasNext()) {
    				key = it.next();
    				it.remove();
    				try {
    					/** 开始处理通道 */
    					handleInput(selector, key);
    				} catch (Exception e) {
    					if (key != null) {
    						key.cancel();
    						if (key.channel() != null)
    							key.channel().close();
    					}
    				}
    			}
    		} catch(Exception ex) {
    			ex.printStackTrace();    			
    		}
    		
    		try {
    			Thread.sleep(500);
    		} catch(Exception ex) {
    			ex.printStackTrace();    			
    		}
    	}
    }
    
    public static void handleInput(Selector selector, SelectionKey key) throws IOException {

		if (key.isValid()) {
			// 处理新接入的请求消息
			if (key.isAcceptable()) { // 连接刚刚建立
				// Accept the new connection
				ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
				SocketChannel sc = ssc.accept();
				sc.configureBlocking(false);
				// Add the new connection to the selector
				System.out.println("reda 读取");
				sc.register(selector, SelectionKey.OP_READ);


				System.out.println("有一个新的连接");
			}
			if (key.isReadable()) { // 可以读数据
				// Read the data
				SocketChannel sc = (SocketChannel) key.channel();
				ByteBuffer readBuffer = ByteBuffer.allocate(1024);
				int readBytes = sc.read(readBuffer);
				if (readBytes > 0) {
					readBuffer.flip();
					byte[] bytes = new byte[readBuffer.remaining()];
					readBuffer.get(bytes);
					String request = new String(bytes, "UTF-8"); //接收到的输入

					String[] requests = request.split(":");
					int id = Integer.parseInt(requests[0]);
					System.out.printf("client (id = %d)said: %s\n" , id, requests[1]);

					/** 记录客户 */
					if(ClientMap.containsValue(key) == false) {
						ClientMap.put(id, key);
						System.out.printf("客户 %d 初次注册", id);
					}else {
						System.out.printf("客户 %d 已经注册", id);
					}

					String response = requests[0] + ":I know";
					doWrite(sc, response);
				} else if (readBytes < 0) {
					// 对端链路关闭
					key.cancel();
					sc.close();
				} else
					; // 读到0字节,忽略
			}
		}
	}

	public static void doWrite(SocketChannel socketChannel, String response) throws IOException {
		if (response != null && response.trim().length() > 0) {
			byte[] bytes = response.getBytes();
			ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);

			writeBuffer.put(bytes);
			writeBuffer.flip();

			socketChannel.write(writeBuffer);
		}
	}

	@Override
	public void run() {
    	while (true) {

    		System.out.print("输入客户端id:");
			int a = Integer.parseInt(cin.nextLine());
			if (ClientMap.containsKey(a) == true) {
				System.out.println("发送消息给:" + a);

				try {
					String temp = cin.nextLine();
					SocketChannel sc = (SocketChannel) ClientMap.get(a).channel();
					doWrite(sc, temp);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}else{
				System.out.println("没有此用户");
			}
		}

	}
}

客户端 NIO
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.Scanner;
import java.util.Set;
import java.util.UUID;

public class NioClient implements Runnable{

	public static Scanner cin = new Scanner(System.in);

	public static int ID = 1;
	
	public static SelectionKey key = null;
	public static Selector selector = null;

	public static void main(String[] args) {

		//String host = "192.168.179.182";
		String host = "127.0.0.1";
		int port = 8001;
		
		System.out.print("输入客户端编号:" );
		ID = Integer.parseInt(cin.nextLine());
		
		new Thread(new NioClient()).start();

		
		System.out.println(ID);

		selector = null;
		SocketChannel socketChannel = null;

		try {
			selector = Selector.open(); // 开选择器
			socketChannel = SocketChannel.open(); // 开通道
			socketChannel.configureBlocking(false); // 设置非阻塞

			// 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
			if (socketChannel.connect(new InetSocketAddress(host, port))) {
				System.out.println("服务器连接成功");
				System.out.println("reda 读取");
				System.out.println(socketChannel.register(selector, SelectionKey.OP_READ));
				/** 开始写数据 */
				doWrite(socketChannel);
			} else {
				
				socketChannel.register(selector, SelectionKey.OP_CONNECT); // 选择器和通道 注册绑定
				System.out.println("选择器注册成功, connect 连接");
			}
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}

		while (true) {
			try {
				selector.select(1000);  // 遍历选择器(中的通道)
				Set<SelectionKey> selectedKeys = selector.selectedKeys(); //所有连接上的选择器 key
				Iterator<SelectionKey> it = selectedKeys.iterator();
				key = null;
				while (it.hasNext()) { // 顺序处理 选择器中 有反应的通道
					key = it.next();
					it.remove();
					try {
						// 处理每一个channel
						handleInput(selector, key);
					}
					catch (Exception e) {
						if (key != null) {
							key.cancel();
							if (key.channel() != null)
								key.channel().close();
						}
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}


		// 多路复用器关闭后,所有注册在上面的Channel资源都会被自动去注册并关闭
//		if (selector != null)
//			try {
//				selector.close();
//			} catch (IOException e) {
//				e.printStackTrace();
//			}
//
//		}
	}

	/**
	 * 写数据
	 * @param sc
	 * @throws IOException
	 */
	public static void doWrite(SocketChannel sc) throws IOException {

		
		String temp = cin.nextLine();
		temp = Integer.toString(ID) + ":" + temp;

		byte[] str = temp.getBytes();
//		byte[] str = UUID.randomUUID().toString().getBytes();
		ByteBuffer writeBuffer = ByteBuffer.allocate(str.length);
		writeBuffer.put(str);
		writeBuffer.flip();

		sc.write(writeBuffer); // 向通道发送数据
	}

	/**
	 * 读数据:处理通道
	 * @param selector
	 * @param key
	 * @throws Exception
	 */
	public static void handleInput(Selector selector, SelectionKey key) throws Exception {
		/**  判断是否连接成功*/
		if (key.isValid()) {
			// 获取key 相对应 的 通道Channel
			SocketChannel sc = (SocketChannel) key.channel();
			if (key.isConnectable()) { // 判断 key 和 selector 是否已经注册
				if (sc.finishConnect()) {
					sc.register(selector, SelectionKey.OP_READ);
				}
			}

			// 可以读数据
			if (key.isReadable()) {
				System.out.println("可以读数据");
				ByteBuffer readBuffer = ByteBuffer.allocate(1024);
				int readBytes = sc.read(readBuffer);
				if (readBytes > 0) {
					readBuffer.flip();
					byte[] bytes = new byte[readBuffer.remaining()];
					readBuffer.get(bytes);
					String body = new String(bytes, "UTF-8");
					System.out.println("Server said : " + body);
				} else if (readBytes < 0) {
					// 对端链路关闭
					key.cancel();
					sc.close();
				} else
					; // 读到0字节,忽略
			}
			//Thread.sleep(3000);

			/** 发送数据 */
			doWrite(sc);
		}
	}

	@Override
	public void run() {
		while (true) {
			try {
				
			SocketChannel sc = (SocketChannel) key.channel();
			if (key.isConnectable()) { // 判断 key 和 selector 是否已经注册
				if (sc.finishConnect()) {
					sc.register(selector, SelectionKey.OP_READ);
				}
				// System.out.printf("已经注册");
			}

			if (key.isReadable()) {
				System.out.print("准备接收消息");
				ByteBuffer readBuffer = ByteBuffer.allocate(1024);
				int readBytes = sc.read(readBuffer);
				if (readBytes > 0) {
					readBuffer.flip();
					byte[] bytes = new byte[readBuffer.remaining()];
					readBuffer.get(bytes);
					String body = new String(bytes, "UTF-8");
					
					System.out.println("Server said : " + body);
				} else if (readBytes < 0) {
					// 对端链路关闭
					key.cancel();
					sc.close();
				} else
					; // 读到0字节,忽略
			}
			
				Thread.sleep(500);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (Exception e) {
				// TODO: handle exception
			}
		}
		
	}
}

待续··· ···

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值