当遇到并发服务场景时,我们可以采取如下措施:
一、同步阻塞IO实现
public class DemoServer extends Thread {
private ServerSocket serverSocket;
public int getPort() {
return serverSocket.getLocalPort();
}
public void run() {
try {
serverSocket = new ServerSocket(0);
while (true) {
// 非常占用内存资源,每个客户端启用一个线程,十分不合理
Socket socket = serverSocket.accept();
RequesHandler requesHandler = new RequesHandler(socket);
requesHandler.start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
;
}
}
}
public static void main(String[] args) throws IOException {
DemoServer server = new DemoServer();
server.start();
try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
BufferedReader buferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
buferedReader.lines().forEach(s -> System.out.println(s));
}
}
}
// 简化实现,不做读取,直接发送字符串
class RequesHandler extends Thread {
private Socket socket;
RequesHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (PrintWriter out = new PrintWriter(socket.getOutputStream());) {
out.println("Hello world!");
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
每次 new 一个线程或者销毁一个线程是有明显的开销的,每个线程都有单独的线程结构,非常占用内存资源,每个客户端启用一个线程是十分不合理的, 因此可以采用线程池的方式进行优化。
// 也是阻塞IO,采用线程池的方式处理请求,当来一个新的客户端连接时,
// 将请求 Socket 封装成一个 task ,放到线程池中取执行。
serverSocket = new ServerSocket(0);
executor = Executors.newFixedThreadPool(8);
while (true) {
Socket socket = serverSocket.accept();
RequesHandler requesHandler = new RequesHandler(socket);
executor.execute(requesHandler);
}
二、NIO实现
NIO(非阻塞IO) 多路复用机制
public class NIOServer extends Thread {
public void run() {
try (Selector selector = Selector.open(); ServerSocketChannel serverSocket = ServerSocketChannel.open();) {// 创建Selector和Channel
serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
serverSocket.configureBlocking(false);
// 注册到Selector,并说明关注点
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();// 阻塞等待就绪的Channel,这是关键点之一
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 生产系统中一般会额外进行就绪状态检查
sayHelloWorld((ServerSocketChannel) key.channel());
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void sayHelloWorld(ServerSocketChannel server) throws IOException {
try (SocketChannel client = server.accept();) {
ByteBuffer readBuffer = ByteBuffer.allocate(32);
client.read(readBuffer);
System.out.println("Server received : " + new String(readBuffer.array()));
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
writeBuffer.put("hello xiaoming".getBytes());
writeBuffer.flip();
client.write(writeBuffer);
//client.write(Charset.defaultCharset().encode("Hello world!"));
}
}
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.start();
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
ByteBuffer writeBuffer = ByteBuffer.allocate(32);
ByteBuffer readBuffer = ByteBuffer.allocate(32);
writeBuffer.put("hello".getBytes());
writeBuffer.flip();
while (true) {
writeBuffer.rewind();
socketChannel.write(writeBuffer);
// readBuffer.clear();
socketChannel.read(readBuffer);
System.out.println("Client received : " + new String(readBuffer.array()));
}
} catch (IOException e) {
}
}
/**
* @return
*/
private int getPort() {
return 8888;
}
三、总结
在前面两个例子中:阻塞IO和伪异步IO,一个是使用 new 线程的方式,另外一个是采用线程池管理的方式, IO都是同步阻塞模式,所以需要多线程以实现多任务处理。
而 NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的Channel,来决定做什么,仅仅select阶段是阻塞的,可以有效避免大量客户端连接时频繁线程切换带来的问题,应用的扩展能力有了非常大的提高。