网络编程之每天学习一点点[day4]-----nio实现单向通信

Posted talk.push

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络编程之每天学习一点点[day4]-----nio实现单向通信相关的知识,希望对你有一定的参考价值。

重要观点

客户端的通道注册和事件监听是在服务器端实现的!NIO的本质是避免原始的TCP连接建立时使用的三次握手操作,减少连接的开销!因为Channel非直连,而是注册到了多路复用器上。一个TCP连接对应多个channel。

NIO概念

我们使用nio来实现客户端发送数据,服务端接收数据这样一个例子。

先来看下NIO的几个概念:

Channel

通道,它就像自来水管道一样,网络数据通过Channel读取和写入,通道与流的不同之处在于通道是双向的,流是单向的,且是OutPutStream和InputStream的子类。通道可以用于读、写或者二者同时进行。

而最关键的是通道可以与多路复用器selector结合起来,有多种的状态位,方便多路复用器去识别。

事实上通道分为两大类:一类是网络读写的SelectableChannel;一类是用于文件操作的FileChannel。

我们使用的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。

它们是对Socket和ServerScoket的向上的抽象,Channel是一个TCP连接之间的抽象,一个Tcp连接可以对应多个Channel,而不是以前的方式只有一个通信信道,这个时候就减少TCP了连接的次数。,然后将SocketChannel的相应事件注册到selector多路复用器上,监听对应的事件。

    private void accept(SelectionKey key) 
		try 
			//1 获取服务通道
			ServerSocketChannel ssc =  (ServerSocketChannel) key.channel();
			//2 执行阻塞方法
			SocketChannel sc = ssc.accept();
			//3 设置阻塞模式
			sc.configureBlocking(false);
			//4 注册到多路复用器上,并设置读取标识
			sc.register(this.seletor, SelectionKey.OP_READ);
		 catch (IOException e) 
			e.printStackTrace();
		
	

 

Selector

多路复用器,它是NIO编程的基础,非常重要,多路复用器选择已经就绪的任务的能力。

简单说,就是Selector会不断轮询注册在其上的通道Channel,如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的channel集合,从而进行后续的IO操作。

一个多路复用器Selector可以负责成千上万的Channel通道,没有上限,这也是JDK采用了epoll替代了传统的select实现,获得连接句柄没有限制,这也就意味着我们只需要一个线程thread负责selector的轮询,就可以接入成千上万的客户端,这是JDK NIO库的巨大进步。Selector线程类似于一个管理者master,管理成千上万的通道,然后轮询哪个通道数据已经准备好,通知CPU执行IO的读写操作。

 

selector模式

当网络IO事件(即Channel)注册到选择器以后,selector会分配给每个管道一个key值,相当于标签。selector选择器是以轮询的方式进行查找在其上注册的所有网络IO事件(channel)。当我们的网络IO事件(Channel)准备就绪,selector就会识别,会通过key来找到相应的管道channel,进行数据的相关操作,从这个 角度看,其主动轮询的方式 仍然是同步模式

事件状态

每个管道都会对selector选择器注册不同的事件状态,以便选择器查找。

SelectionKey.OP_CONNECT

SelectionKey.OP_ACCEPT

SelectionKey.OP_READ

SelectionKey.OP_WRITE

 

我们来看一幅图:

图示分析:

 public Server(int port)
		try 
			//1 打开路复用器
			this.seletor = Selector.open();
			//2 打开服务器通道
			ServerSocketChannel ssc = ServerSocketChannel.open();
			//3 设置服务器通道为非阻塞模式
			ssc.configureBlocking(false);
			//4 绑定地址
			ssc.bind(new InetSocketAddress(port));
			//5 把服务器通道注册到多路复用器上,并且监听阻塞事件
			ssc.register(this.seletor, SelectionKey.OP_ACCEPT);
			
			System.out.println("Server start, port :" + port);
			
		 catch (IOException e) 
			e.printStackTrace();
		
	

NIO服务端实例化流程

构造方法中的步骤描述了服务器端的实例化流程:

1 打开多路复用器selector。

2 打开ServerSocket服务器通道 

3设置非阻塞模式,才能使得channel和selector结合使用

4 绑定了监听端口

5将serverSocketChannel注册到selector上,监听阻塞事件。

看下sever.java的run方法:

@Override
	public void run() 
		while(true)
			try 
				//1 必须要让多路复用器开始监听 select方法阻塞 直到有新的key进来
				this.seletor.select();
				//2 返回多路复用器已经选择的结果集
				Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
				//3 进行遍历
				while(keys.hasNext())
					//4 获取一个选择的元素
					SelectionKey key = keys.next();
					//5 直接从容器中移除就可以了
					keys.remove();
					//6 如果是有效的
					if(key.isValid())
						//7 如果为阻塞状态
						if(key.isAcceptable())
							this.accept(key);
						
						//8 如果为可读状态
						if(key.isReadable())
							this.read(key);
						
						//9 写数据
						if(key.isWritable())
							//this.write(key); //ssc
						
					
					
				
			 catch (IOException e) 
				e.printStackTrace();
			
		
	

server.java启动时,进入run方法执行到this.selector.select()阻塞,直到有新的SocketChannel注册key到selector。

注意:实例化server'时注册的通道不会向下执行,this.selector.select()直接阻塞,监听客户端的通道注册

 

run方法分析:

1 在run方法中服务器端迭代selectionKey,每次迭代拿出来后执行keys.remove();删除这个key。

2 判断key的状态,当客户端启动以后(只要执行sc.connect(address);),客户端的SocketChannel就被连接到了selector上,这个时候:

Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();

开始执行,得到了多路复用器上的所有key,其中有一个是服务器端的,监听的阻塞事件,所以当客户端connect方法执行后(只是启动,不发送数据),相应服务器端拿到了所有key进行迭代和判断:

if(key.isAcceptable())
	this.accept(key);

服务器端的那个key必然是服务器端的阻塞状态的key,代码一定会执行到上边代码进入accept方法:

private void accept(SelectionKey key) 
		try 
			//1 获取服务通道
			ServerSocketChannel ssc =  (ServerSocketChannel) key.channel();
			//2 执行阻塞方法
			SocketChannel sc = ssc.accept();
			//3 设置阻塞模式
			sc.configureBlocking(false);
			//4 注册到多路复用器上,并设置读取标识
			sc.register(this.seletor, SelectionKey.OP_READ);
		 catch (IOException e) 
			e.printStackTrace();
		
	

通过这个key就得到了ServerSocketChannel,并从ssc.accept方法获得了SocketChannel,设置其为非阻塞模式,并注册到多路复用器上监听“读就绪事件”,由于这个客户端的SocketChannel已经被注册到了selctor多路复用器上,那么server的run方法一定会继续将this.selector.select()阻塞方法放开,毕竟有新的通道注册上去了。由于刚才的SocketChannel注册了读就绪事件,那么:

if(key.isReadable())
	this.read(key);

就会执行。

private void read(SelectionKey key) 
		try 
			//1 清空缓冲区旧的数据
			this.readBuf.clear();
			//2 获取之前注册的socket通道对象
			SocketChannel sc = (SocketChannel) key.channel();
			//3 读取数据
			int count = sc.read(this.readBuf);
			//4 如果没有数据
			if(count == -1)
				key.channel().close();
				key.cancel();
				return;
			
			//5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
			this.readBuf.flip();
			//6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
			byte[] bytes = new byte[this.readBuf.remaining()];
			//7 接收缓冲区数据
			this.readBuf.get(bytes);
			//8 打印结果
			String body = new String(bytes).trim();
			System.out.println("Server : " + body);
			
			// 9..可以写回给客户端数据 
			
		 catch (IOException e) 
			e.printStackTrace();
		
		
	

读取通道serverSocket的数据打印。在读取前清空缓冲区readBuffer。

接下来就可以进行客户端和服务器端的通信了!!!

需要注意的是,服务器端的accept方法只会在客户端启动的时候执行一次用来获得ServerSocket通道,并将其注册到多路复用器上,监听对应的事件。通道注册后,当前客户端就不需要再次注册通道了,除非通道断开!后续的客户端发送buffer和服务端读取buffer不需要注册通道,只是事件的监听而已。

 

看下客户端的代码,比较简单:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client 

	//需要一个Selector 
	public static void main(String[] args) 
		
		//创建连接的地址
		InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8765);
		
		//声明连接通道
		SocketChannel sc = null;
		
		//建立缓冲区
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		try 
			//打开通道
			sc = SocketChannel.open();
			//进行连接
			sc.connect(address);
			
			while(true)
				//定义一个字节数组,然后使用系统录入功能:
				byte[] bytes = new byte[1024];
				System.in.read(bytes);
				
				//把数据放到缓冲区中
				buf.put(bytes);
				//对缓冲区进行复位
				buf.flip();
				//写出数据
				sc.write(buf);
				//清空缓冲区数据
				buf.clear();
			
		 catch (IOException e) 
			e.printStackTrace();
		 finally 
			if(sc != null)
				try 
					sc.close();
				 catch (IOException e) 
					e.printStackTrace();
				
			
		
		
	
	

这个例子中我们实现的是客户端发送数据--->服务器端读取数据,对于双向通信可以自行研究实现!

 

通俗理解

        客户端(socketChannel)就像一群乞丐,服务端就像一个富有的大善人(serverSocketChannel
大善人给乞丐们提供食物救济,于是派了一个大管家(selector)去负责登记并查看(register)
这些乞丐们,只有登记的才能领到食物,每个乞丐只需要登记一次就可以一直领(跟selector建立长连接)。
每当新来一个乞丐(客户端),都会登记(register)到管家(selector)那里。
        当乞丐们饿了的时候就到管家那里领取(客户端发送buffer数据)食物,管家拿出登记册看到了乞丐已经登记的名字(selectionkey),且看到乞丐饿的张着嘴(读就绪),于是把食物给了乞丐(服务端发送数据)。食物每次管够(非阻塞),不会让乞丐半饱离开。乞丐只要饿了就来,不会不饿也来领事务(非阻塞)。即便来了一群乞丐,只要有人不太饿,这个乞丐也不会领食物(一个TCP连接对应多了通道),算是比较良心的,食物也不会被浪费(节省了TCP连接数)。大善人的善举传到了千里之外,大家都知道去他那里就可以得到事务,只要他活着(服务端阻塞等待乞丐到来),乞丐们就不会饿死。

 

 


 

 

 

以上是关于网络编程之每天学习一点点[day4]-----nio实现单向通信的主要内容,如果未能解决你的问题,请参考以下文章

学习windows编程 day4 之 自定义映射

学习windows编程 day4 之 多边矩形填充

软件测试自动化测试之——接口测试从入门到精通,每天学习一点点

每天学习一点点之 Hystrix 之 Request Cache

python 函数之day4

每天一点点之vue框架学习 - uni-app 修改上一页参数