NIO网络通信编程

前面学习到了伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。

伪异步I/O弊端分析

Socket和ServerSocket都是字节流传输数据,需要使用到输入输出流,而输入输出流的read和write方法都是阻塞式的。

从JDK中可以看到,在输入数据可用、检测到流末尾或者抛出异常前,read方法一直阻塞。

在这里插入图片描述

当调用OutputStream的write方法写输出流的时候,它也会被阻塞,直到所有要发送的字节全部写入完毕,或者发送异常。
在这里插入图片描述

还有一个关键的问题是,当消息的接收方处理缓慢的时候,将不能及时地从TCP缓冲区读取数据,这将会导致发送方的TCP window size 不断减小,直到为0,双方处于Keep-Alive状态,消息发送方将不能再向TCP缓冲区写入消息,这时如果采用的是同步阻塞I/O,write操作将会被无限期阻塞,直到TCP window size 大于0或者发生I/O异常。

总的来说,输入输出流的读和写操作都是同步阻塞的,阻塞的时间取决于对方I/O线程的处理速度和网络I/O的传输速度。

伪异步I/O实际上仅仅只是对之前I/O线程模型的一个简单优化(采用线程池),他无法从根本上解决同步I/O导致的通信线程阻塞问题:

(1) 假如所有的可用线程都被阻塞,那后续所有的I/O消息都将在队列中排队;
(2) 当队列满之后,线程池也将拒绝服务;
(3) 又由于只有一个Accptor线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时。

NIO简介

NIO库是在JDK 1.4中引入的,NIO弥补了原来同步阻塞I/O的不足,它在标准Java代码中提供了高速的、面向块的I/O。

NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

NIO核心组件

在Socket网络通信中主要用到的NIO核心组件是:缓冲区Buffer、通道Channel和多路复用器Selector。

缓冲区Buffer

Buffer是一个缓冲区对象,实质上是一个数组。它包含一些要写入或者要读出的数据。最常用的缓冲区是ByteBuffer,每一种Java基本类型都对应有一种缓冲区。

在这里插入图片描述

Buffer对象是NIO库与IO库的一个重要区别。在原面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中;在NIO库中,所有的数据都是用缓冲区处理的,在读取数据的时,它是直接读到缓冲区中的,在写数据时也是写入到缓冲区中。

通道Channel

Channel是一个通道,可以通过它读取和写入数据,它就像自来水管道一样,网络数据通过Channel读取和写入。

通道与流的不同之处在于通道是双向的,而流是单向的(一个流必须是InputStream或者OutputStream的子类)。

实际上Channel可以分为两大类:分别是用于网络读写的SelectableChannel和用于文件操作的FileChannel。Socket网络通信中要用到的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。

多路复用器 Selector

多路复用器Selector提供选择已经就绪的任务的能力。Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的IO操作。

一个Selector可以轮询多个Channel。

示例代码

服务端:
在这里插入图片描述

package com.niotest;

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.Date;
import java.util.Iterator;
import java.util.Set;

public class MutiplexerServerTest implements Runnable{

	private Selector selector;
	private ServerSocketChannel servChannel;
	private volatile boolean stop;
	
	/**
	 * 初始化多路复用器、绑定监听端口
	 * 
	 * @param port
	 */
	public MutiplexerServerTest(int port) {
		// TODO Auto-generated constructor stub
		try {
			//1、打开ServerSocketChannel和Selector
			selector = Selector.open();
			servChannel = ServerSocketChannel.open();
			//2、设置为非阻塞方式
			servChannel.configureBlocking(false);
			//3、绑定监听端口
			servChannel.bind(new InetSocketAddress(port), 1024);
			//4、将ServerSocketChannel注册到多路复用器Selector上
			servChannel.register(selector, SelectionKey.OP_ACCEPT);
			System.out.println("The time server is start in port : " + port);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void stop() {
		this.stop = stop;
	}
	
	@Override
	public void run() {
		//5、轮询就绪准备的key
		while (!stop) {
			try {
				//每隔一秒轮询一次
				selector.select(1000);
				Set<SelectionKey> selectedKeys = selector.selectedKeys();
				Iterator<SelectionKey> it = selectedKeys.iterator();
				SelectionKey key = null;
				while (it.hasNext()) {
					key = it.next();
					it.remove();
					try {
						//7、处理新接入的请求
						handleInput(key);
					}catch (Exception e) {
						// TODO: handle exception
						if (key != null) {
							key.cancel();
							if (key.channel() != null) {
								key.channel().close();
							}
						}
					}
				}
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		//多路复用器关闭后,所有注册在上面的Channel和 Pipe等资源都会被自动去注册并关闭,
		//所以不需要重复释放资源
		if (selector != null) {
			try {
				selector.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	private void handleInput(SelectionKey key) throws IOException {
		if (key.isValid()) {
			//处理新接入的请求消息
			if (key.isAcceptable()) {
				ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
				//监听客户端连接
				SocketChannel sc = ssc.accept();
				//设置客户端链路为非阻塞模式
				sc.configureBlocking(false);
				//将新接入的客户端连接注册到多路复用器Selector,监听读操作
				sc.register(selector, SelectionKey.OP_READ);
			}
			if (key.isReadable()) {
				//读取数据
				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 body = new String(bytes, "UTF-8");
					System.out.println("The time server receive order : " + body);
					String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) 
							? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
					//8、将消息异步发送到客户端
					doWrite(sc, currentTime);
				}else if (readBytes < 0) {
					//对端链路关闭
					key.cancel();
					sc.close();
				}
			}
		}
	}
	
	private void doWrite(SocketChannel channel, 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();
			channel.write(writeBuffer);
		}
	}
	
}

服务端测试主代码:

package com.niotest;

public class NIOServerTest {
	public static void main(String[] args) {
		int port = 3333;
		MutiplexerServerTest mutiplexerServer = new MutiplexerServerTest(port);
		new Thread(mutiplexerServer,"NIOServerTest-001").start();
	}
}

客户端:
在这里插入图片描述

package com.niotest;

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

public class ClientHandle implements Runnable{
	private String host;
	private int port;
	private Selector selector;
	private SocketChannel socketChannel;
	private volatile boolean stop;
	
	/**
	 * 构造方法
	 * @param host
	 * @param port
	 */
	public ClientHandle(String host, int port) {
		// TODO Auto-generated constructor stub
		this.host = host;
		this.port = port;
		try {
			//1、打开ServerSocketChannel和Selector
			selector = Selector.open();
			socketChannel = SocketChannel.open();
			//2、设置为非阻塞方式
			socketChannel.configureBlocking(false);
		} catch (IOException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		try {
			//3、异步连接服务端
			doConnect();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		while(!stop) {
			//轮询准备就绪的Key
			try {
				selector.select(1000);
				Set<SelectionKey> selectedKeys = selector.selectedKeys();
				Iterator<SelectionKey> it = selectedKeys.iterator();
				SelectionKey key = null;
				while (it.hasNext()) {
					key = it.next();
					it.remove();
					try {
						handleInput(key);
					} catch (Exception e) {
						// TODO: handle exception
						if (key != null) {
							key.cancel();
							if (key.channel() != null) {
								key.channel().close();
							}
						}
					}
				}
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}
		//多路复用器关闭后,所有注册在上面的Channel和 Pipe等资源都会被自动去注册并关闭,
		//所以不需要重复释放资源
		if (selector != null) {
			try {
				selector.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	private void handleInput(SelectionKey key) throws IOException {
		if (key.isValid()) {
			//判断是否连接成功
			SocketChannel sc = (SocketChannel) key.channel();
			if (key.isConnectable()) {
				if (sc.finishConnect()) {
					//注册读事件到多路复用器
					sc.register(selector, SelectionKey.OP_READ);
					doWrite(sc);
				}
			}
			if (key.isReadable()) {
				//异步读客户端请求消息到缓冲区
				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("Now is : " + body);
					this.stop = true;
				}else if (readBytes < 0) {
					//对端链路关闭
					key.cancel();
					sc.close();
				}
			}
		}
	}
	
	private void doConnect() throws IOException {
		//判断是否连接成功,如果连接成功,则直接注册状态位到多路复用器中
		if (socketChannel.connect(new InetSocketAddress(host,port))) {
			socketChannel.register(selector, SelectionKey.OP_READ);
			doWrite(socketChannel);
		}else {
			//向Reactor线程的多路复用器注册OP_CONNECT状态位,监听服务端的TCP ACK应答
			socketChannel.register(selector, SelectionKey.OP_CONNECT);
		}
	}
	
	private void doWrite(SocketChannel sc) throws IOException {
		byte[] req = "QUERY TIME ORDER".getBytes();
		ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
		writeBuffer.put(req);
		writeBuffer.flip();
		sc.write(writeBuffer);
		if (!writeBuffer.hasRemaining()) {
			System.out.println("Send order 2 server succeed.");
		}
	}
	
}

客户端测试主代码:

package com.niotest;

public class NIOClientTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int port = 3333;
		new Thread(new ClientHandle("127.0.0.1", port), "Client-001").start();
	}

}

运行结果:
在这里插入图片描述

左边位服务端,右边位客户端。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
NIO(New I/O)是一种基于通道和缓冲区的IO模型,与传统的基于流的IO模型相比,NIO 提供了更高效、更灵活的IO操作方式。Socket编程是一种网络编程技术,用于实现计算机之间的通信。 在Java中,NIO是通过java.nio包来实现的,主要涉及到以下几个重要的类和接口: 1. Channel(通道):负责连接和传输数据。 2. Buffer(缓冲区):用于存储数据。 3. Selector(选择器):用于管理多个通道的事件,实现非阻塞IO。 NIO的主要特点包括: 1. 非阻塞IO:可以通过Selector实现非阻塞IO操作,一个线程可以管理多个通道。 2. 缓冲区操作:数据的读写通过缓冲区进行,提高了效率。 3. 通道之间的数据传输:可以直接从一个通道将数据传输到另一个通道,避免了数据在应用程序和内核空间之间的拷贝。 而Socket编程则是基于TCP/IP协议的一种网络编程技术,通过Socket可以实现不同计算机之间的通信。在Java中,可以使用java.net包中的Socket和ServerSocket类来实现Socket编程Socket编程主要涉及到客户端和服务器端两个角色,其中客户端使用Socket与服务器端建立连接,进行数据的发送和接收;服务器端使用ServerSocket监听指定的端口,接收客户端的连接请求,并进行数据处理。 Socket编程常用的一些方法包括: 1. Socket类的构造方法:用于创建一个套接字对象。 2. connect()方法:用于与服务器建立连接。 3. getInputStream()和getOutputStream()方法:用于获取套接字的输入流和输出流,进行数据的读写。 4. close()方法:用于关闭套接字。 总而言之,NIO是一种高效的IO模型,而Socket编程则是一种实现网络通信的技术,它们在Java中可以相互结合使用,实现高性能的网络应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值