IO网络编程(二)

NIO

简介

有人称之为 New I/O,因为它相对于之前的 I/O 类库是新增的,所以被称为 NewI/O,这是它的官方叫法。但是,由于之前老的 I/O 类库是阻塞 I/O,New I/O 类库的目标就是要让 Java 支持非阻塞 I/O,所以,更多的人喜欢称之为非阻塞 I/O(Non-block I/O),由于非阻塞 I/O 更能够体现 NIO 的特点,所以本文使用的NIO 都指的是非阻塞 I/O。IO是面向流的,NIO是面向缓冲区,面向块的。NIO相关的包都在java.nio包下,并且对原来java.io包中很多类进行了改写。
NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
Channel
首先说一下Channel,国内大多翻译成“通道”。通道可以实现异步读写数据,可以从缓冲区读也可以写到缓冲区。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。
NIO中的Channel的主要实现有:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
这里看名字就可以猜出个所以然来:分别可以对应文件IO、UDP和TCP(Server和Client)。下面演示的案例基本上就是围绕这4个类型的Channel进行陈述的。
简单使用(以FileChannel为例):

//写入本地文件。
//str--->ByteBuffer--->NIOFileChannel(其实还是FileOutputStream )--->本地文件
public class NIOFileChannel {
    public static void main(String[] args) throws IOException {
        String str = "hello feng";
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file.txt");
        //通过fileOutputStream获取对应的FileChanne
        FileChannel fileChannel = fileOutputStream.getChannel();
        //创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //将str放入到byteBuffer
        byteBuffer.put(str.getBytes());
        //flip 切换
        byteBuffer.flip();
        //将byteBuffer写入到fileChannel
        fileChannel.write(byteBuffer);
        fileOutputStream.close();
    }
}

Buffer
NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。当然NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等这里先不进行陈述。
三个重要属性:
1.capacity: 容量,Buffer中元素的个数,永远不能为负数,永远不会变化
2.limit: 限制,实际上它是Buffer所维护的那个数组中的一个下标,limit是第一个不能被读,或者第一个不能被写的元素的index,limit永远不会是负数,永远不会超过capacity
3.Position: 定位,指数组中下一个将要被读或者将要被写的元素的索引。

简单使用(以IntBuffer为例):

public class BasicBuffer {

    public static void main(String[] args) {

        IntBuffer allocate = IntBuffer.allocate(5);
        //向buffer中传数据
        for (int i = 0; i < allocate.capacity(); i++) {
            allocate.put(i * 2);
        }
        //从buffer取数据,读写切换
        allocate.flip();
        while (allocate.hasRemaining()){
            System.out.println(allocate.get());
        }
    }
}

Selector
Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。例如在一个聊天服务器中。要使用Selector, 得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。
channel,buffer,selector对应图
上图反应出Selector,Channel,Buffer之间的关系:
1.每个channel对应一个buffer
2.selector对应一个线程,一个线程对应多个channel(连接)
3.上图有三个channel注册到了selector
4.程序切换到哪个channel是由事件决定的,event是个重要概念
5.selector会根据不同事件,在各个通道上切换
6.buffer就是一个内存块,底层是一个数组
7.数据的读取写入是通过buffer,BIO中要么是输入流要么是输出流,不能双向,NIO中buffer既可以读又可以写,只不过需要flip切换
8.channel是双向的,可以返回底层操作系统情况。(linux底层通道就是双向的)

NIO Server端创建

服务端的主要步骤如下:

  1. 打开ServerSocketChannel,监听客户端连接
  2. 绑定监听端口,设置连接为非阻塞模式
  3. 创建Reactor线程,创建多路复用器并启动线程
  4. 将ServerSocketChannel注册到Reactor线程中的Selector上,监听ACCEPT事件
  5. Selector轮询准备就绪的key
  6. Selector监听到新的客户端接入,处理新的接入请求,完成TCP三次握手,简历物理链路
  7. 设置客户端链路为非阻塞模式
  8. 将新接入的客户端连接注册到Reactor线程的Selector上,监听读操作,读取客户端发送的网络消息
  9. 异步读取客户端消息到缓冲区
  10. 对Buffer编解码,处理半包消息,将解码成功的消息封装成Task
  11. 将应答消息编码为Buffer,调用SocketChannel的write将消息异步发送给客户端

NIO创建的Server:

public class Server {
	private static int DEFAULT_PORT = 8888;
	private static ServerHandle serverHandle;
	public static void start(){
		start(DEFAULT_PORT);
	}
	public static synchronized void start(int port){
		if(serverHandle!=null)
			serverHandle.stop();
		serverHandle = new ServerHandle(port);
		new Thread(serverHandle,"Server").start();
	}
	public static void main(String[] args){
		start();
	}
}

ServerHandle:


public class ServerHandle implements Runnable{
	private Selector selector;
	private ServerSocketChannel serverChannel;
	private volatile boolean started;
	/**
	 * 构造方法
	 * @param port 指定要监听的端口号
	 */
	public ServerHandle(int port) {
		try{
			//创建选择器
			selector = Selector.open();
			//打开监听通道
			serverChannel = ServerSocketChannel.open();
			//如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
			serverChannel.configureBlocking(false);//开启非阻塞模式
			//绑定端口 backlog设为1024
			serverChannel.socket().bind(new InetSocketAddress(port),1024);
			//监听客户端连接请求
			serverChannel.register(selector, SelectionKey.OP_ACCEPT);
			//标记服务器已开启
			started = true;
			System.out.println("服务器已启动,端口号:" + port);
		}catch(IOException e){
			e.printStackTrace();
			System.exit(1);
		}
	}
	public void stop(){
		started = false;
	}
	@Override
	public void run() {
		//循环遍历selector
		while(started){
			try{
				//无论是否有读写事件发生,selector每隔1s被唤醒一次
				selector.select(1000);
				//阻塞,只有当至少一个注册的事件发生的时候才会继续.
//				selector.select();
				Set<SelectionKey> keys = selector.selectedKeys();
				Iterator<SelectionKey> it = keys.iterator();
				SelectionKey key = null;
				while(it.hasNext()){
					key = it.next();
					it.remove();
					try{
						handleInput(key);
					}catch(Exception e){
						if(key != null){
							key.cancel();
							if(key.channel() != null){
								key.channel().close();
							}
						}
					}
				}
			}catch(Throwable t){
				t.printStackTrace();
			}
		}
		//selector关闭后会自动释放里面管理的资源
		if(selector != null)
			try{
				selector.close();
			}catch (Exception e) {
				e.printStackTrace();
			}
	}
	private void handleInput(SelectionKey key) throws IOException{
		if(key.isValid()){
			//处理新接入的请求消息
			if(key.isAcceptable()){
				ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
				//通过ServerSocketChannel的accept创建SocketChannel实例
				//完成该操作意味着完成TCP三次握手,TCP物理链路正式建立
				SocketChannel sc = ssc.accept();
				//设置为非阻塞的
				sc.configureBlocking(false);
				//注册为读
				sc.register(selector, SelectionKey.OP_READ);
			}
			//读消息
			if(key.isReadable()){
				SocketChannel sc = (SocketChannel) key.channel();
				//创建ByteBuffer,并开辟一个1M的缓冲区
				ByteBuffer buffer = ByteBuffer.allocate(1024);
				//读取请求码流,返回读取到的字节数
				int readBytes = sc.read(buffer);
				//读取到字节,对字节进行编解码
				if(readBytes>0){
					//将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
					buffer.flip();
					//根据缓冲区可读字节数创建字节数组
					byte[] bytes = new byte[buffer.remaining()];
					//将缓冲区可读字节数组复制到新建的数组中
					buffer.get(bytes);
					String expression = new String(bytes,"UTF-8");
					System.out.println("服务器收到消息:" + expression);
					//处理数据
					String result = null;
					try{
						result = Calculator.cal(expression).toString();
					}catch(Exception e){
						result = "计算错误:" + e.getMessage();
					}
					//发送应答消息
					doWrite(sc,result);
				}
				//没有读取到字节 忽略
//				else if(readBytes==0);
				//链路已经关闭,释放资源
				else if(readBytes<0){
					key.cancel();
					sc.close();
				}
			}
		}
	}
	//异步发送应答消息
	private void doWrite(SocketChannel channel,String response) throws IOException{
		//将消息编码为字节数组
		byte[] bytes = response.getBytes();
		//根据数组容量创建ByteBuffer
		ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
		//将字节数组复制到缓冲区
		writeBuffer.put(bytes);
		//flip操作
		writeBuffer.flip();
		//发送缓冲区的字节数组
		channel.write(writeBuffer);
		//****此处不含处理“写半包”的代码
	}
}

NIO Client端创建

NIO创建的Client :


public class Client {
	private static String DEFAULT_HOST = "127.0.0.1";
	private static int DEFAULT_PORT = 8888;
	private static ClientHandle clientHandle;
	public static void start(){
		start(DEFAULT_HOST,DEFAULT_PORT);
	}
	public static synchronized void start(String ip,int port){
		if(clientHandle!=null)
			clientHandle.stop();
		clientHandle = new ClientHandle(ip,port);
		new Thread(clientHandle,"Server").start();
	}
	//向服务器发送消息
	public static boolean sendMsg(String msg) throws Exception{
		if(msg.equals("q")) return false;
		clientHandle.sendMsg(msg);
		return true;
	}
	public static void main(String[] args){
		start();
	}
}

ClientHandle :

public class ClientHandle implements Runnable{
	private String host;
	private int port;
	private Selector selector;
	private SocketChannel socketChannel;
	private volatile boolean started;
 
	public ClientHandle(String ip,int port) {
		this.host = ip;
		this.port = port;
		try{
			//创建选择器
			selector = Selector.open();
			//打开监听通道
			socketChannel = SocketChannel.open();
			//如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
			socketChannel.configureBlocking(false);//开启非阻塞模式
			started = true;
		}catch(IOException e){
			e.printStackTrace();
			System.exit(1);
		}
	}
	public void stop(){
		started = false;
	}
	@Override
	public void run() {
		try{
			doConnect();
		}catch(IOException e){
			e.printStackTrace();
			System.exit(1);
		}
		//循环遍历selector
		while(started){
			try{
				//无论是否有读写事件发生,selector每隔1s被唤醒一次
				selector.select(1000);
				//阻塞,只有当至少一个注册的事件发生的时候才会继续.
//				selector.select();
				Set<SelectionKey> keys = selector.selectedKeys();
				Iterator<SelectionKey> it = keys.iterator();
				SelectionKey key = null;
				while(it.hasNext()){
					key = it.next();
					it.remove();
					try{
						handleInput(key);
					}catch(Exception e){
						if(key != null){
							key.cancel();
							if(key.channel() != null){
								key.channel().close();
							}
						}
					}
				}
			}catch(Exception e){
				e.printStackTrace();
				System.exit(1);
			}
		}
		//selector关闭后会自动释放里面管理的资源
		if(selector != null)
			try{
				selector.close();
			}catch (Exception e) {
				e.printStackTrace();
			}
	}
	private void handleInput(SelectionKey key) throws IOException{
		if(key.isValid()){
			SocketChannel sc = (SocketChannel) key.channel();
			if(key.isConnectable()){
				if(sc.finishConnect());
				else System.exit(1);
			}
			//读消息
			if(key.isReadable()){
				//创建ByteBuffer,并开辟一个1M的缓冲区
				ByteBuffer buffer = ByteBuffer.allocate(1024);
				//读取请求码流,返回读取到的字节数
				int readBytes = sc.read(buffer);
				//读取到字节,对字节进行编解码
				if(readBytes>0){
					//将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
					buffer.flip();
					//根据缓冲区可读字节数创建字节数组
					byte[] bytes = new byte[buffer.remaining()];
					//将缓冲区可读字节数组复制到新建的数组中
					buffer.get(bytes);
					String result = new String(bytes,"UTF-8");
					System.out.println("客户端收到消息:" + result);
				}
				//没有读取到字节 忽略
//				else if(readBytes==0);
				//链路已经关闭,释放资源
				else if(readBytes<0){
					key.cancel();
					sc.close();
				}
			}
		}
	}
	//异步发送消息
	private void doWrite(SocketChannel channel,String request) throws IOException{
		//将消息编码为字节数组
		byte[] bytes = request.getBytes();
		//根据数组容量创建ByteBuffer
		ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
		//将字节数组复制到缓冲区
		writeBuffer.put(bytes);
		//flip操作
		writeBuffer.flip();
		//发送缓冲区的字节数组
		channel.write(writeBuffer);
		//****此处不含处理“写半包”的代码
	}
	private void doConnect() throws IOException{
		if(socketChannel.connect(new InetSocketAddress(host,port)));
		else socketChannel.register(selector, SelectionKey.OP_CONNECT);
	}
	public void sendMsg(String msg) throws Exception{
		socketChannel.register(selector, SelectionKey.OP_READ);
		doWrite(socketChannel, msg);
	}
}

参考资料:
Java 网络IO编程总结(BIO、NIO、AIO均含完整实例代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值