IO模型与NIO基础--NIO网络传输选择器

news/2025/2/21 10:07:57

放进NIO体系进行网络编程的工作流程:

在这里插入图片描述

Selector的创建

通过调用Selector.open()方法创建一个Selector,如下:
Selector selector = Selector.open();

向Selector注册通道

通过Channel.register()方法来实现,
注意:Channel和Selector一起使用时,Channel必须处于非阻塞模式下。
channel.configureBlocking(false); //设置通道为非阻塞模式
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);

register()方法的第二个参数:是一个“兴趣(interest)集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。
可以监听四种不同类型的事件:

  1. Connect 链接就绪,某个channel成功连接到另一个服务器称为“连接就绪”。
  2. Accept 接收就绪,一个server socket channel准备好接收新进入的连接称为“接收就绪”。
  3. Read 读就绪,一个有数据可读的通道可以说是“读就绪”。
  4. Write 写就绪,等待写数据的通道可以说是“写就绪”。
    这四种事件用SelectionKey的四个常量来表示:
    1.SelectionKey.OP_CONNECT
    2.SelectionKey.OP_ACCEPT
    3.SelectionKey.OP_READ
    4.SelectionKey.OP_WRITE
    如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:
    int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

SelectionKey说明
      当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。
这个对象包含了一些有用的属性:
1.interest集合
2.ready集合
3.Channel
4.Selector
5.附加的对象(可选)
interest集合
interest集合是你所选择的感兴趣的事件集合。
可以通过SelectionKey读写interest集合,像这样:

SelectionKey selectionKey=channel.register(selector, SelectionKey.OP_xxxx);
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

      可以看到,用“和”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。
ready集合
      ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。
可以这样访问ready集合:
int readySet = selectionKey.readyOps();
      可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

另外
从SelectionKey访问Channel和Selector很简单。如下:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();  

附加的对象
      可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。
例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
还可以在用register()方法向Selector注册Channel的时候附加对象。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

选择器的select()
一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。
这些方法返回你所感兴趣的事件(如连接、接受、读或写)同时这些事件已经准备就绪的那些通道。
换句话说,如果你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。
下面是select()方法:
1.select()阻塞到至少有一个通道在你注册的事件上就绪了。
2.select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数),一般用这个,不能无限阻塞。
3.selectNow()不会阻塞,不管什么通道就绪都立刻返回,此方法执行非阻塞的选择操作。
如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。

select()方法有返回值,返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。
如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,
如果另一个通道就绪了,它会再次返回1。
如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,
但在每次select()方法调用之间,只有一个通道就绪了。
选择器的selectedKeys()
      一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法获取到:

Set selectedKeys = selector.selectedKeys();

   当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey对象。
这个对象代表了注册到该Selector的通道。
可以通过SelectionKey的selectedKeySet()方法访问这些对象。

可以遍历这个已选择的键集合来访问就绪的通道。如下:

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // 一个连接被ServerSocketChannel接受
    } else if (key.isConnectable()) {
        // 与远程服务器建立了连接
    } else if (key.isReadable()) {
        // 一个channel做好了读准备
    } else if (key.isWritable()) {
        // 一个channel做好了写准备
    }
    keyIterator.remove();
}

      这个循环遍历已选择键集中的每个键,并检测各个键所对应的通道的就绪事件。
注意每次迭代末尾的keyIterator.remove()一定要调用,Selector不会自己从已选择键集中移除SelectionKey实例。
必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。若不移除,下次还是读取原来的数据,这是要命的。
      SelectionKey.channel()方法返回的通道需要转型成你要处理的类型,如ServerSocketChannel或SocketChannel等。
选择器的wakeUp()
      某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。
只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。
阻塞在select()方法上的线程会立马返回。
如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,上一个调用select()方法的阻塞线程会立即醒来(wake up)。

选择器的close()
      用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。
通道本身并不会关闭。

客户端与服务端简单交互实例

下面的程序涉及到一些网络编程的知识:

  1. 服务器必须先建立ServerSocket或者ServerSocketChannel 来等待客户端的连接
  2. 客户端必须建立相对应的Socket或者SocketChannel来与服务器建立连接
  3. 服务器接受到客户端的连接受,再生成一个Socket或者SocketChannel与此客户端通信

服务器

/*
 * nio网络编程实例,这是服务端
 * 1.建立ServerSocketChannel通道,设置非阻塞 ,并绑定一个地址,客户链接到这个地址,等待客户端的链接
 * 2.获取选择器的实例,注册通道到里面去,设置感兴趣的事件,ServerSocketChannel这种通道只有一个事件--链接事件
 * 3.准备缓冲区:两个,一个读,一个写
 * 4.实现与客户端交互,首先一个死循环,不停的获取各种客户端链接请求通道,轮询作用似的
 *   a.选择器的select(1000)方法,会获取各通道对象个数(通道感兴趣事件的)1000是表示阻塞一秒,如果一秒之内没结果,重新轮询
     b.有通道就绪,则拿到通道对象键值集合Set,遍历集合,找到接入链接的通道,让其接入,返回一个能读写的通道,不是上面的接入通道
     c.读写通道也要注册到选择器里面,先读事件,同一个通道的多次注册,如是不同的事件改变,只是注删了一个通道
 */
public class ServerDemo {
	public static void main(String[] args) {
		try {
		    
			ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
			serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1",8000));
			serverSocketChannel.configureBlocking(false);
			
			Selector selector=Selector.open();
			serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);  //接入事件就绪
			
			ByteBuffer readBuffer= ByteBuffer.allocate(1024);
			ByteBuffer writeBuffer=ByteBuffer.allocate(1024);
			writeBuffer.put("this is server".getBytes());  //写缓冲区中写入这个字符串
			writeBuffer.flip();             //翻转非直接缓冲区,非直接缓冲区,postion指针不能自动指向开缓冲区开头处
			
			while(true) {
				int nReady=selector.select(1000);  //捕获所有通道
				if(nReady==0)  continue;    //一个通道都没有,一秒后继续查询
				Set<SelectionKey> keys =selector.selectedKeys();
				Iterator<SelectionKey> iterator = keys.iterator();
				while(iterator.hasNext()) {
					SelectionKey key=iterator.next();
					iterator.remove();  //手动移除,让下次轮询通道是全新的,而不是这次的
					if(key.isAcceptable()) {  //如果是准备链接的通道,就让客户端链接, 当然了,这个程序简单只有一个通道
						SocketChannel channel=serverSocketChannel.accept();
						channel.configureBlocking(false);
						channel.register(selector, SelectionKey.OP_READ);  //不用担心,下次读客户发送的信息,因为有死循在那不的轮询
					}else if(key.isReadable()) {  //如果通道对象key是有可读事件,就读到缓冲区中
						SocketChannel channel = (SocketChannel) key.channel();
						readBuffer.clear();
						channel.read(readBuffer);
						readBuffer.flip();
						/*while(readBuffer.hasRemaining()) {  //判断缓冲区还没有数据,有的话,从缓冲区读数据,打印到控制台
				    		System.out.print("读到的数据是:"+(char)readBuffer.get());  //读得是字节码,中文会乱码,一次读一个字节
				    	}*/
						System.out.println("服务器端收到的数据,直接打印:"+ new String(readBuffer.array()));
					    key.interestOps(SelectionKey.OP_WRITE);
					}else if(key.isWritable()){
						SocketChannel channel =(SocketChannel)key.channel();
						writeBuffer.rewind();
						channel.write(writeBuffer);
						key.interestOps(SelectionKey.OP_READ);  //以便下次再读
						
					}
				}
			}
			
	     }catch (Exception e) {
			e.printStackTrace();
		}
	}

}

客户端

/*
 * nio网络编程实例,这是客户端
 * 不用实例化,链接通道(类似洒店的迎宾)
 * 1.通道
 * 2.缓冲区,两个,一个读,一个写
 * 3.先向服务器写,再读数据
 * 
 */
public class ClientDemo {
	public static void main(String[] args) {
		try {
			SocketChannel channel = SocketChannel.open();
			channel.connect(new InetSocketAddress("127.0.0.1",8000));  //链接到服务器
			
			ByteBuffer readBuffer= ByteBuffer.allocate(1024);
			ByteBuffer writeBuffer=ByteBuffer.allocate(1024);
			writeBuffer.put("this is client".getBytes());  //写缓冲区中写入这个字符串
			writeBuffer.flip(); 
			
			while(true) {
				writeBuffer.rewind();    //重置缓冲区,写入
				channel.write(writeBuffer);
				
				readBuffer.clear();
				channel.read(readBuffer);
				readBuffer.flip();
				System.out.println("客户端收到的数据:"+new String(readBuffer.array()));
				
				
			}
		}catch (IOException e) {
			e.printStackTrace();
		}
	}

}


http://www.niftyadmin.cn/n/5860665.html

相关文章

微服务入门-笔记

微服务入门-笔记 介绍 这个mvn工程里边包含了微服务里最基础的服务拆分、基于nacos的服务注册和发现、配置中心以及OpenFeign版的远程调用。 初始化这个工程是来源于阿里云的脚手架&#xff0c;https://start.aliyun.com/&#xff0c;从这上边创建一个包含各种依赖的工程&am…

基于MFC实现的键盘电子乐器演奏程序

基于MFC实现的键盘电子乐器演奏程序设计 1.项目简介 需要连接西电微机原理实验室提供的 QTH9054 微机试验箱&#xff0c;使用其蜂鸣器发声&#xff0c;若不连接&#xff0c;程序会直接播放 mp3 文件模拟钢琴声。 请在 release 处下载编译好的 exe 文件运行&#xff0c;如需计…

LangChain大模型应用开发:多模态输入与自定义输出

介绍 大家好&#xff0c;博主又来给大家分享知识了。今天给大家分享的内容是使用LangChain进行大模型应用开发中的多模态输入与自定义输出。 LangChain中的多模态数据输入是指将多种不同形式的数据作为输入提供给基于语言模型的应用程序或系统&#xff0c;以丰富交互内容和提…

Qt常用控件之标签QLabel

标签QLabel QLabel 标签用来显示文本和图片&#xff0c;在 Qt 中使用频率很高。 1. Label属性 属性说明textQLabel 中的文本。textFormat文本的格式。其中 Qt::PlainText 为纯文本&#xff1b;Qt::RichText 为富文本&#xff08;支持 html 格式&#xff09;&#xff1b; Qt:…

sass中@import升级@use的使用区别与案例

在 Sass 中&#xff0c;import 和 use 都用于模块化代码&#xff0c;但二者有显著区别。以下是主要差异和具体案例说明&#xff1a; 核心区别对比 特性 import (旧版) use (新版) 作用域 全局作用域&#xff08;变量/混合易冲突&#xff09; 局部作用域&#xff08;需通过…

敏捷开发07:敏捷项目可视化管理-ScrumBoard(Scrum板)使用介绍

ScrumBoard(Scrum板)介绍 ScrumBoard&#xff08;Scrum板&#xff09;是敏捷项目管理中使用的可视化工具&#xff0c;用于跟踪和监控冲刺阶段的任务进度。 主要通过可视化的看板来管理工作&#xff0c;它可视化了敏捷开发中的工作流程、任务状态、团队角色。 Scrum 团队在各…

基于Spark和Hive的酒店数据分析与推荐系统

技术介绍前端&#xff1a;html&#xff0c;css&#xff0c;js&#xff0c;Echats 后端&#xff1a;Django 数据库&#xff1a;MySQL, Hive 推荐算法&#xff1a;基于用户的协同过滤&#xff08;UserCF&#xff09; 爬虫&#xff1a;Selenium大数据框架&#xff1a;Spark#数据分…

微信小程序实现拉卡拉支付

功能需求&#xff1a;拉卡拉支付&#xff08;通过跳转拉卡拉平台进行支付&#xff09;&#xff0c;他人支付&#xff08;通过链接进行平台跳转支付&#xff09; 1.支付操作 //支付 const onCanStartPay async (obj) > {uni.showLoading({mask: true})// 支付接口获取需要传…