今天休了个年假面试,是某北方另一个项目组。这次面试又挂了,题目是要求写一个NIO服务器,这是我从来没写过的。所以今天特意练习一下。
第一版本,单线程
传统的IO叫做Blocking IO,简称BIO。如果开发一个服务器,那么new一个ServerSocket的话,那么这个ServerSocket再也绑定不了Channel了,就无法进行NIO了。在创建之后就是注册选择器了。最后就是用选择器选择key,每个key有对应的客户端SocketChannel。对象之间是下图这种关系:
单线程的代码,比较简单:
public class PlainNioServer {
public void serve(int port) throws IOException {
final ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
final ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(port));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes(StandardCharsets.UTF_8));
for (; ; ) {
try {
final int select = selector.select();
} catch (IOException e) {
e.printStackTrace();
break;
}
final Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("size = " + selectionKeys.size());
final Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
final SelectionKey selectionKey = iterator.next();
iterator.remove();
try {
System.out.println(selectionKey.hashCode());
if (selectionKey.isAcceptable()) {
final SocketChannel clientChannel = serverSocketChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE,
msg.duplicate());
System.out.println("Accepted from " + clientChannel);
} else if (selectionKey.isWritable()) {
final SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
System.out.println("Channel is " + clientChannel);
final ByteBuffer attachment = (ByteBuffer) selectionKey.attachment();
while (attachment.hasRemaining()) {
if (clientChannel.write(attachment) == 0) {
break;
}
}
clientChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
selectionKey.cancel();
final SelectableChannel channel = selectionKey.channel();
channel.close();
System.out.println("selection key is " + selectionKey + ", Closed " + channel);
}
}
}
}
}
第一版本,多线程
因为单线程都没写出来,所以自然面试官没有再出题,但是我对自己有更高的要求,我必须写个多线程版本出来。每个线程持有一个选择器,选择器可以注册多个。
因为选择器自带锁,所以不需要加锁,但是要注意,多线程条件下,accept可能会返回NULL。做个判断就好。因此,Java代码如下:
多个线程贡献的对象类:
public class MultiThreadServer {
private ServerSocketChannel serverSocketChannel;
public MultiThreadServer(int port) throws IOException {
this.serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
final ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(port));
}
public ServerSocketChannel getServerSocketChannel() {
return serverSocketChannel;
}
}
线程类
package com.youngthing.nio;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
/**
* 2022/3/13 17:24 创建
*
* @author 花书粉丝
*/
public class ServerWorker extends Thread {
private MultiThreadServer multiThreadServer;
private Selector selector;
public ServerWorker(MultiThreadServer multiThreadServer) throws IOException {
this.multiThreadServer = multiThreadServer;
this.selector = Selector.open();
multiThreadServer.getServerSocketChannel().register(selector, SelectionKey.OP_ACCEPT);
}
@Override
public void run() {
for (; ; ) {
try {
final int select = selector.select();
} catch (IOException e) {
e.printStackTrace();
break;
}
final Set<SelectionKey> selectionKeys = selector.selectedKeys();
final Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
final SelectionKey selectionKey = iterator.next();
iterator.remove();
try {
if (selectionKey.isAcceptable()) {
final SocketChannel clientChannel = multiThreadServer.getServerSocketChannel().accept();
if (clientChannel != null) {
clientChannel.configureBlocking(false);
ByteBuffer msg = ByteBuffer.wrap(("Hi!" + this + ","+selector+"\r\n").getBytes(StandardCharsets.UTF_8));
clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE,
msg.duplicate());
}
} else if (selectionKey.isWritable()) {
final SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
final ByteBuffer attachment = (ByteBuffer) selectionKey.attachment();
while (attachment.hasRemaining()) {
if (clientChannel.write(attachment) == 0) {
break;
}
}
clientChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
selectionKey.cancel();
final SelectableChannel channel = selectionKey.channel();
try {
channel.close();
} catch (IOException ex) {
ex.printStackTrace();
}
System.out.println("selection key is " + selectionKey + ", Closed " + channel);
}
}
}
}
}
启动类
package com.youngthing.nio;
import java.io.IOException;
/**
* 2022/3/13 15:30 创建
*
* @author 花书粉丝
*/
public class MultiThreadsNioMain {
public static void main(String[] args) throws IOException {
final MultiThreadServer server = new MultiThreadServer(8080);
for (int i = 0; i < 8; i++) {
new ServerWorker(server).start();
}
}
}