Socket编程(二)(NIO)

版权声明:版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_37598011/article/details/83213425

    与Socket和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。

1.缓冲区Buffer

    Buffer是一个对象,它包含一些要写入或读出的数据。在NIO类库加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中。

    在NIO库中所有数据都用缓冲区处理的。在读取数据时,它是直接读到缓冲区中。在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

    缓冲区实质上是一个数组。通常它是一个字节数组(ByteBuffer),也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置等信息。

    最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能用于操作byte数组。除了ByteBuffer还有其他缓冲区,除了Boolean类型每一种Java基本类型都对应一种缓冲区。

2.通道Channel

    Channel是一个通道,网络数据通过Channel读取和写入。通道与流不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或OutputStream的子类),而通道可以用于读、写或者两者同时进行。因为Channel是双全工的,所以它可以比流更好的映射底层操作系统的API。

    从类图中可以看出Channel可以分为两大类:用于网络读写的SelectableChannel和用于文件操作的FileChannel。

3.多路复用器Selector

    多路复用器提供选择已经就绪的任务的能力。简单来说Selector会不断地轮询注册其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

    一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询就可以接入成千上万的客户端。

服务端代码:

//nio 时间服务器 
public class TimeServer {
	public static void main(String[] args) {
		int port = 9999;
		MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
		new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();
	}
}

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 MultiplexerTimeServer implements Runnable {
	private Selector selector;
	private ServerSocketChannel servChannel;
	private volatile boolean stop;

	public MultiplexerTimeServer(int port) {
		try {
			selector = Selector.open();
			servChannel = ServerSocketChannel.open();
			servChannel.configureBlocking(false);
			servChannel.socket().bind(new InetSocketAddress(port));
			servChannel.register(selector, SelectionKey.OP_ACCEPT);
			System.out.println("时间服务器在端口启动:" + port);
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}
	}

	public void stop() {
		this.stop = true;
	}

	@Override
	public void run() {
		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 {
						handleInput(key);
					} catch (Exception e) {
						if (key != null) {
							key.cancel();
							if (key.channel() != null) {
								key.channel().close();
							}
						}
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if (selector != null) {
			try {
				System.out.println("选择器不为空");
				selector.close();
			} catch (IOException e) {
				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);// 为ByteBuffer分配空间大小(位于 jvm) 
				//ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);// 为ByteBuffer分配空间大小(基于操作系统)
				int readBytes = sc.read(readBuffer);
				if (readBytes > 0) {
					readBuffer.flip();
					// 将缓存字节数组的指针设置为数组的开始序列即数组下标0
					byte[] bytes = new byte[readBuffer.remaining()];
					// 返回剩余的可用长度,此长度为实际读取的数据长度,最大自然是底层数组的长度
					readBuffer.get(bytes);
					String body = new String(bytes, "UTF-8");
					System.out.println("时间服务器接收数据 : " + body);
					String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)
							? new Date(System.currentTimeMillis()).toString()
							: "BAD ORDER";
					doWrite(sc, currentTime);
				} else if (readBytes < 0) {
					// 对端链路关闭
					key.cancel();
					sc.close();
				} else { 
					// 读取到 0 字节,忽略
				}
			}
		}
	}

	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();
			// 将position 设置为0
			channel.write(writeBuffer);
			writeBuffer.clear();
		}
	}
}

代码分析:

1.首先会在构造方法中进行资源初始化,创建多路复用器Selector、ServerSocketChannel,对Channel和TCP参数进行配置。如,将ServerSocketChannel设置为异步非阻塞模式,它的backlog设为1024,系统资源初始化成功后,将ServerSocketChannel注册到Selector,监听SelectionKey.OP_ACCEPT操作位。如果资源初始化失败,则退出。

2.在run方法的while循环体中循环遍历selector,设置休眠时间为1s。无论是否有读写事件发生,selector每隔1s都被唤醒一次。selector也有一个无参的select方法:当有处于就绪状态的Channel时,selector将返回该Channel的SelectionKey集合。通过对就绪状态的Channel集合进行迭代,可以进行网络的异步读写操作。

3.当有客户端接入时,根据SelectionKey的操作位进行判断获知网络事件的类型,通过ServerSocketChannel的accept接受客户端的连接请求并创建SocketChannel实例。完成上述操作后,相当于完成了TCP的三次握手,TCP物理链路正式建立。然后将新创建的SocketChannel设置成异步非阻塞。

4.然后就是读取客户端请求,首先创建一个ByteBuffer,然后调用SocketChannel的read方法读取请求码流。因为此时已经把SocketChannel设置为异步非阻塞模式,因此它的read是非阻塞的。所以要将返回值进行判断:

1.返回值大于0:读取到字节,对字节进行编解码。

2.返回值等于0:没有读取到字节,忽略;

3.返回值小于0:链路已经关闭,需要关闭SocketChannel释放资源。

当读取到码流后,进行解码。首先对readBuffer进行flip操作,它的作用是将缓冲区当前的limit设置为position,position设置为0,用于后续对缓冲区进行读取操作。然后根据缓冲区可读字节个数创建字节数组,调用ByteBuffer的get操作将缓冲区可读的字节数组复制到新创建的字节数组中,最后调用字符串构造将其打印出来。如果请求的字符串是“QUERY TIME ORDER”则把服务器的当前时间编码后返回给客户端。

5.在读取完数据后会调用doWrite方法,首先他会将字符串编码成字节数组,根据字节数组的容量创建ByteBuffer,调用ByteBuffer的put操作将字节数组复制到缓冲区中,然后对缓冲区进行flip操作,最后调用SocketChannel的write方法将缓冲区中的字节数组发送出去。由于SocketChannel是异步非阻塞的,他并不保证一次能够把需要发送的字节数组发送完,此时会出现“写半包”问题。我们需要注册写操作,不断轮询Selector将没有发送完的ByteBuffer发送完毕,然后通过ByteBuffer的hasRemain()方法判断消息是否发送完成。

客户端代码:,

public class TimeClient {
	public static void main(String[] args) {
		int port = 9999;
		new Thread(new TimeClientHandler("127.0.0.1", port)).start();
	}
}
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 TimeClientHandler implements Runnable {
	private int port;
	private String host;
	private Selector selector;
	private SocketChannel channel;
	private volatile boolean stop;

	public TimeClientHandler(String host, int port) {
		this.host = host == null ? "127.0.0.1" : host;
		this.port = port;
		try {
			selector = Selector.open();
			channel = SocketChannel.open();
			channel.configureBlocking(false);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void run() {
		try {
			doConnect();
		} catch (IOException e1) {
			e1.printStackTrace();
			System.exit(1);
		}
		while (!stop) {
			try {
				// selector每一秒被唤醒一次
				selector.select(1000);
				// 还回就绪状态的chanel的 selectedKeys
				Set<SelectionKey> selectedKeys = selector.selectedKeys();
				Iterator<SelectionKey> iterator = selectedKeys.iterator();
				SelectionKey key = null;
				while (iterator.hasNext()) {
					key = iterator.next();
					iterator.remove();
					try {
						handleInput(key);
					} catch (Exception e) {
						if (key != null) {
							key.cancel();
							if (key.channel() != null)
								key.channel().close();
						}
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		if (selector != null) {
			try {
				selector.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
			selector = null;
		}
	}

	public 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);
				} else {
					System.exit(1);// 连接失败,进程退出
				}
			}
			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("当前时间 : " + body);
					this.stop = true;
				} else if (readBytes < 0) { // 对端链路关闭
					key.cancel();
					sc.close();
				} else
					; // 读到0字节,忽略
			}
		}
	}

	private void doConnect() throws IOException {
		if (channel.connect(new InetSocketAddress(host, port))) {
			channel.register(selector, SelectionKey.OP_READ);
			doWrite(channel);
		} else {
			channel.register(selector, SelectionKey.OP_CONNECT);
		}
	}

	private void doWrite(SocketChannel schannel) throws IOException {
		byte[] bytes = "QUERY TIME ORDER".getBytes();
		ByteBuffer buff = ByteBuffer.allocate(bytes.length);
		buff.put(bytes);
		buff.flip();
		schannel.write(buff); // 判断是否发送完毕
		if (!buff.hasRemaining()) {
			System.out.println("发送成功!");
		}
	}
}

1.首先初始化NIO的多路复用器和SocketChannel对象。需要注意的是创建SocketChannel之后,需要将其设置为异步非阻塞模式。

2.doConnect()函数用于发送连接请求,首先对SocketChannel的connect()操作进行判断,如果成功则将SocketChannel注册到多路复用器Selector上,注册SelectionKey.OP_READ;如果没有没有成功则说明服务端没有返回TCP握手应答消息,但所以此时需要将SocketChannel注册到多路复用器Selector上,注册SelectionKey.OP_CONNECT,当服务端返回TCP syn-ack消息后,Selector就能轮询到这个SocketChannel处于连接就绪状态。

3.在执行完doConnect()后,通过循环不断轮询多路复用器Selector。当有就绪的Channel时,执行handleInput()方法。

4.首先对SelectionKey进行判断,判断其属于什么状态,如果处于连接则说明服务端已返回ACK应答消息。这时我们需要对连接结果进行判断,调用SocketChannel的finishConnect()方法。如果返回值为true,说明客户端连接成功;如果返回false或者抛出IOException说明连接失败。在这里返回值为true,说明连接成功。将SocketChannel注册到多路复用器上,注册SelectionKey.OP_READ操作位,监听网络读操作,然后将请求消息给服务端。

此时会调用doWrite()方法,这里构造消息体然后将其编码写入到发送缓冲区中,最后调用SocketChannel的write方法进行发送。

5.如果客户端接收到了服务端的应答消息,则SocketChannel是可读的,这里我分配1M进行读取应答消息,调用SocketChannel的read()方法进行异步读取操作。如果读取到了消息则对消息进行解码最后输出,然后将stop设为true,线程退出循环。

6.线程退出循环后,需要对资源进行释放。

NIO优点总结:

1.客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。

2.SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用。

3.线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制,这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。因此它非常适合做高性能、高负载的网络服务器。

猜你喜欢

转载自blog.csdn.net/qq_37598011/article/details/83213425
今日推荐