Java-NIO服务器,说好的复制粘贴呢。。。

如题,尽可能的,通过复制粘贴能解决的代码一般拒绝手撸。

Java-NIO这个名字的高大上一开始让我完全摸不到头脑,然后越看越熟悉,越看越熟悉,最后一瞅代码:Selector,?这不就是python的select嘛。。。

select监听四种事件,字面意思理解即可

SelectionKey.OP_CONNECT 
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

事件被激活时,我们可以通过SelectionKey连接到这个Channel并做一些操作

当没有事件激活时,selector.select()方法会进入阻塞,当有事件激活时,select()会返回最新一次激活的事件数量(这迷惑了我足足半天)。阻塞解除时,使用selector.selectedKeys()取得所有被事件激活的频道,这段代码看起来像是这样(来源)

Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
}

这里涉及一个很基础的知识:在遍历集合时删除元素,应当使用迭代器的remove而不是集合的remove

我猜谁都不会认为这段代码足够漂亮,事实上,java11有更优雅的实现:

selector.select(key->{
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
});

考虑一个http服务器(请原谅我依然没搞懂https的底层原理),我们需要一个HttpServerSocket,但是等等,我找到的代码似乎使用的是ServerSocketChannel,无所谓,它看起来长这样:

Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();

// 配置为非阻塞模式
serverChannel.configureBlocking(false);

// 监听ACCEPT事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

// 把server暴露至port端口
ServerSocket serverSocket = serverChannel.socket();
serverSocket.bind(new InetSocketAddress(port));

这样一来,当Accept事件被激活时,我们知道它就是ServerSocketChannel,接收这个请求:

protected void accept(SelectionKey key) throws IOException {
	ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
	SocketChannel channel = ssc.accept();
	if (channel == null) return;
	channel.configureBlocking(false);
	channel.register(key.selector(), SelectionKey.OP_READ);
}

同之前一样,将channel设置为非阻塞模式,但是只对它注册READ事件,因为我们先读到请求信息才知道返回什么

现在read事件将很快的被激活,因此我们会瞬间结束select的阻塞并进入isReadable分支,处理读:

protected void readDataFromSocket(SelectionKey key) throws IOException {

	SocketChannel socketChannel = (SocketChannel) key.channel();
	
	List<byte[]> list = new ArrayList<>();
	ByteBuffer buffer = ByteBuffer.allocateDirect(L);
	while (socketChannel.read(buffer) > 0) {
		buffer.flip();
		byte[] bytes = new byte[L];
		buffer.get(bytes, 0, buffer.remaining());
		list.add(bytes);
		buffer.compact();
	}
	byte[] bytes = new byte[L * list.size()];
	for(int i=0; i<list.size(); i++) {
		System.arraycopy(list.get(i), 0, bytes, i*L, L);
	}
	System.out.println(new String(bytes, "utf-8"));

	key.interestOps(SelectionKey.OP_WRITE);
}

这段代码暴露了我有多愚蠢emm,但是我真的没有找到其它将缓冲区中的byte变成String的方法……好在至少通过这样的打印,我们可以看到,http的请求头成功的被送达和读取到了。

在函数的最后,我们将key的“兴趣点”设置为WRITE,并等待它再一次进入isWriteable分支,处理写:

protected void writeDataToSocket(SelectionKey key) throws IOException {
		
	SocketChannel socketChannel = (SocketChannel) key.channel();
	
	byte[] bytes = (header+"\r\nhello, this is some word").getBytes("utf-8");
	ByteBuffer sender = ByteBuffer.wrap(bytes);
	sender.put(bytes);
	sender.flip();
	socketChannel.write(sender);
		
	//socketChannel.shutdownOutput();
	socketChannel.close();
		
	key.cancel();
}

最后的cancel意味着我们不再需要跟踪和捕获这个key的事件,因为它已经被处理完了。在此之前,需要对socketChannel进行close或shutdownOutput,我不太确定究竟使用哪个,如果不这样,浏览器会认为响应报文没有结束。

那么到这里,一个不带有任何实用功能的基于java-nio的http服务器就算是完成了,以下是完整代码:

class SelectSockets {
	public static void main(String[] args) throws Exception {
		new SelectSockets().runServer(args);
	}
	
	public static final int PORT_NUMBER = 1234;

	public void runServer(String[] args) throws Exception {

		int port = PORT_NUMBER;

		if (args.length > 0) { // 覆盖默认的监听端口
			port = Integer.parseInt(args[0]);
		}

		try (Selector selector = Selector.open(); ServerSocketChannel serverChannel = ServerSocketChannel.open()) {

			serverChannel.configureBlocking(false);// 设置非阻塞模式
			
			serverChannel.register(selector, SelectionKey.OP_ACCEPT);// 将ServerSocketChannel注册到Selector

			System.out.printf("Listening on port %d\n", port);
			ServerSocket serverSocket = serverChannel.socket();// 得到一个ServerSocket去和它绑定
			serverSocket.bind(new InetSocketAddress(port));// 设置server channel将会监听的端口

			while (true) selector.select(this::doSelection);
		}
	}

	protected void doSelection(SelectionKey key) {
		try {
			// 判断是否是一个连接到来
			if (key.isAcceptable()) {
				this.accept(key);
			}
			// 判断这个channel上是否有数据要读
			else if (key.isReadable()) {
				this.readDataFromSocket(key);
			}
			else if (key.isWritable()) {
				this.writeDataToSocket(key);
			}
		} catch (IOException e) {
			throw new RuntimeException("我不确定出了什么问题,这里有bug", e);
		}

	}
	
	
	protected void accept(SelectionKey key) throws IOException {
		ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
		SocketChannel channel = ssc.accept();
		// 注册读事件
		if (channel == null) return;
		// 设置通道为非阻塞
		channel.configureBlocking(false);
		// 将通道注册到选择器上
		channel.register(key.selector(), SelectionKey.OP_READ);
	}
	private static final int L = 1024;
	
	protected void readDataFromSocket(SelectionKey key) throws IOException {
		
		SocketChannel socketChannel = (SocketChannel) key.channel();
		
		// 嘛呀,处理输入这么麻烦的吗???
		List<byte[]> list = new ArrayList<>();
		ByteBuffer buffer = ByteBuffer.allocateDirect(L);
		while (socketChannel.read(buffer) > 0) {
			buffer.flip();
			byte[] bytes = new byte[L];
			buffer.get(bytes, 0, buffer.remaining());
			list.add(bytes);
			buffer.compact();
		}
		byte[] bytes = new byte[L * list.size()];
		for(int i=0; i<list.size(); i++) {
			System.arraycopy(list.get(i), 0, bytes, i*L, L);
		}
		System.out.println(new String(bytes, "utf-8"));
		
		key.interestOps(SelectionKey.OP_WRITE);
	}
	
	
	private final String header = "HTTP/1.1 200 OK\r\n" + 
			"Server: nginx/1.13.7\r\n" + 
			"Date: Sat, 30 Mar 2019 09:50:12 GMT\r\n" + 
			"Content-Type: text/html\r\n" + 
			//"Content-Length: 612\r\n" + 
			"Last-Modified: Sun, 17 Mar 2019 10:40:32 GMT\r\n" + 
			"Connection: keep-alive\r\n" + 
			"ETag: \"5c8e2420-264\"\r\n" + 
			"Accept-Ranges: bytes\r\n\r\n";

	protected void writeDataToSocket(SelectionKey key) throws IOException {
		
		SocketChannel socketChannel = (SocketChannel) key.channel();
		
		// 我该返回点什么
		byte[] bytes = (header+"hello, here is some word").getBytes("utf-8");
		ByteBuffer sender = ByteBuffer.wrap(bytes);
		sender.put(bytes);
		sender.flip();
		socketChannel.write(sender);
		
		//socketChannel.shutdownOutput();
		socketChannel.close();
		
		key.cancel();
	}

}

最后再次感谢网上找到的开源代码,感谢 Java NIO详解 、 java NIO原理及实例 、 Java NIO之Selector
@url http://www.importnew.com/22623.html
@url https://www.cnblogs.com/tengpan-cn/p/5809273.html
@url https://www.jianshu.com/p/94246fb98870

发布了11 篇原创文章 · 获赞 8 · 访问量 8851

猜你喜欢

转载自blog.csdn.net/qq_26946497/article/details/88916463