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

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


我们使用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();
		}
	}

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

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();
				}
			}
		}
		
	}
	
}

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










猜你喜欢

转载自blog.csdn.net/shengqianfeng/article/details/80732851
今日推荐