如题,尽可能的,通过复制粘贴能解决的代码一般拒绝手撸。
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