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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章