《Debug NIO服务端运行 代码分析》

《Debug NIO服务端运行 代码分析》

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.Iterator;
public class NIOServer {
	// 通道管理器
	private Selector selector;
	/**
	 * 获得一个ServerSocket通道,并对该通道做一些初始化的工作
	 * 
	 * @param port
	 *            绑定的端口号
	 * @throws IOException
	 */
	public void initServer(int port) throws IOException {
		// 获得一个ServerSocket通道
		ServerSocketChannel serverChannel = ServerSocketChannel.open();
		// 设置通道为非阻塞
		serverChannel.configureBlocking(false);
		// 将该通道对应的ServerSocket绑定到port端口
		serverChannel.socket().bind(new InetSocketAddress(port));
		// 获得一个通道管理器
		this.selector = Selector.open();
		// 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
		// 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
		serverChannel.register(selector, SelectionKey.OP_ACCEPT);
	}
	/**
	 * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
	 * 
	 * @throws IOException
	 */
	public void listen() throws IOException {
		System.out.println("服务端启动成功!");
		// 轮询访问selector
		while (true) {
			// 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
			selector.select();
			// 获得selector中选中的项的迭代器,选中的项为注册的事件
			Iterator<?> ite = this.selector.selectedKeys().iterator();
			while (ite.hasNext()) {
				SelectionKey key = (SelectionKey) ite.next();
				// 删除已选的key,以防重复处理
				ite.remove();

				handler(key);
			}
		}
	}

	/**
	 * 处理请求
	 * 
	 * @param key
	 * @throws IOException
	 */
	public void handler(SelectionKey key) throws IOException {
		
		// 客户端请求连接事件
		if (key.isAcceptable()) {
			handlerAccept(key);
			// 获得了可读的事件
		} else if (key.isReadable()) {
			handelerRead(key);
		}
	}
	/**
	 * 处理连接请求
	 * 
	 * @param key
	 * @throws IOException
	 */
	public void handlerAccept(SelectionKey key) throws IOException {
		ServerSocketChannel server = (ServerSocketChannel) key.channel();
		// 获得和客户端连接的通道
		SocketChannel channel = server.accept();
		// 设置成非阻塞
		channel.configureBlocking(false);

		// 在这里可以给客户端发送信息哦
		System.out.println("新的客户端连接");
		// 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
		channel.register(this.selector, SelectionKey.OP_READ);
	}
	/**
	 * 处理读的事件
	 * 
	 * @param key
	 * @throws IOException
	 */
	public void handelerRead(SelectionKey key) throws IOException {
		// 服务器可读取消息:得到事件发生的Socket通道
		SocketChannel channel = (SocketChannel) key.channel();
		// 创建读取的缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		int read = channel.read(buffer);
		if(read > 0){
			byte[] data = buffer.array();
			String msg = new String(data).trim();
			System.out.println("服务端收到信息:" + msg);
			
			//回写数据
			ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
			channel.write(outBuffer);// 将消息回送给客户端
		}else{
			System.out.println("客户端关闭");
			key.cancel();
		}
	}
	/**
	 * 启动服务端测试
	 * 
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
		NIOServer server = new NIOServer();
		server.initServer(30000);
		server.listen();
	}
}

服务端:

public class NClient
{
	// 定义检测SocketChannel的Selector对象
	private Selector selector = null;
	static final int PORT = 8888;
	// 定义处理编码和解码的字符集
	private Charset charset = Charset.forName("UTF-8");
	// 客户端SocketChannel
	private SocketChannel sc = null;
	public void init()throws IOException
	{
		selector = Selector.open();
		InetSocketAddress isa = new InetSocketAddress("10.12.16.160", PORT);
		// 调用open静态方法创建连接到指定主机的SocketChannel
		sc = SocketChannel.open(isa);
		// 设置该sc以非阻塞方式工作
		sc.configureBlocking(false);
		// 将SocketChannel对象注册到指定Selector
		sc.register(selector, SelectionKey.OP_READ);
		// 启动读取服务器端数据的线程
		new ClientThread().start();
		// 创建键盘输入流
		Scanner scan = new Scanner(System.in);
		while (scan.hasNextLine())
		{
			// 读取键盘输入
			String line = scan.nextLine();
			// 将键盘输入的内容输出到SocketChannel中
			sc.write(charset.encode(line));
		}
	}
	// 定义读取服务器数据的线程
	private class ClientThread extends Thread
	{
		public void run()
		{
			try
			{
				while (selector.select() > 0)    //①
				{
					// 遍历每个有可用IO操作Channel对应的SelectionKey
					for (SelectionKey sk : selector.selectedKeys())
					{
						// 删除正在处理的SelectionKey
						selector.selectedKeys().remove(sk);
						// 如果该SelectionKey对应的Channel中有可读的数据
						if (sk.isReadable())
						{
							// 使用NIO读取Channel中的数据
							SocketChannel sc = (SocketChannel)sk.channel();
							ByteBuffer buff = ByteBuffer.allocate(1024);
							String content = "";
							while(sc.read(buff) > 0)
							{
								sc.read(buff); 
								buff.flip();
								content += charset.decode(buff);
							}
							// 打印输出读取的内容
							System.out.println("聊天信息:" + content);
							// 为下一次读取作准备
							sk.interestOps(SelectionKey.OP_READ);
						}
					}
				}
			}
			catch (IOException ex)
			{
				ex.printStackTrace();
			}
		}
	}
	public static void main(String[] args)
		throws IOException
	{
		new NClient().init();
	}
}

调试过程

一、Debug启动服务端
单步执行:你会发现服务器端会先通过一个open方法创建一个ServerSocketChannel,然后绑定该ServerSocketChannel的地址与端口,ServerSocketChannel相当于BIO中的ServerSocket,并设置工作方式是非阻塞式的,最后将该ServerSocketChannel注册到Selector对象中。
在这里插入图片描述
  如何大白话理解ServerSocketChannel、Selector?我们把ServerSocketChannel看做是一个餐厅的大门,把Selector当成服务员,当ServerSocketChannel这扇大门注册到某一个服务员Selector上后,这个门进来的所有客人都由该Selector服务员负责。
在这里插入图片描述
  你会发现最终停留在selector.select()方法中,该方法你看不到源代码,底层是C语言实现,若Selector上注册的channel没有事件发生,会一直停留在此方法中。
在这里插入图片描述
二、启动telnet模拟客户端
  确保打开了telnet程序,否则会提示’telnet’ 不是内部或外部命令,也不是可运行的程序或批处理文件。
在这里插入图片描述
cmd中输入如下命令:由于是本地启动服务端
在这里插入图片描述
就在你打开telnet同时,程序从selector()方法中返回,开始执行下一步。

在这里插入图片描述
  继续往下执行,由于是首次连接,因此selector监测到的时间selectiongKey中是isAcceptable,所以,会执行handlerAccept函数,输出新的客户端连接字样。
在这里插入图片描述
  连接后,程序仍然阻塞在select()方法中,除非输入数字,即会从该方法返回。当在telnet中输入hello时,继续debug下一步,可以发现,通过判断selector上就绪的SelectionKey sk,发现可读,然后接收并输出。若在telnet中的输入只看到下标闪烁,按ctrl+] 即可。
在这里插入图片描述
  当断开客户端连接后,再次向到channel中读数据时报异常。
在这里插入图片描述

NIO理解的误区

  从上面我们可以看出,如果Selector没有注册事件发生,会一直阻塞在select()方法中,那为什么说nio还是非阻塞的NIO?select()方法确实是阻塞的,但是你可以选择select(1000);方法,该方法超过一定时间后会返回。当然也可直接调用selector.selectNow()方法,立刻返回。
  在nio的读方法中,需要对channel中读到的长度进行判断,否则在客户端断开连接后,容易出现客户端关闭的时候会抛出异常,死循环。

public void handelerRead(SelectionKey key) throws IOException {
		// 服务器可读取消息:得到事件发生的Socket通道
		SocketChannel channel = (SocketChannel) key.channel();
		// 创建读取的缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		int read = channel.read(buffer);
		if(read > 0){
			byte[] data = buffer.array();
			String msg = new String(data).trim();
			System.out.println("服务端收到信息:" + msg);			
			//回写数据
			ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
			channel.write(outBuffer);// 将消息回送给客户端
		}else{
			System.out.println("客户端关闭");
			key.cancel();
		}
	}

  SelectionKey.OP_WRITE使用的情况很少,OP_WRITE表示底层缓冲区是否有空间,是则响应返还true。
  一个NIO不是只能有一个selector,一个selector不是只能注册一个ServerSocketChannel,可以多个。

猜你喜欢

转载自blog.csdn.net/weixin_41262453/article/details/89025085