如題,儘可能的,通過複製粘貼能解決的代碼一般拒絕手擼。
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