IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO,NIO,AIO
同步阻塞模型,一个客户端连接对应一个处理线程
BIO服务端
public class SocketServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9000); while (true) { System.out.println("等待连接。。"); //阻塞方法 Socket clientSocket = serverSocket.accept(); System.out.println("有客户端连接了。。"); handler(clientSocket); /*new Thread(new Runnable() { @Override public void run() { try { handler(clientSocket); } catch (IOException e) { e.printStackTrace(); } } }).start();*/ } } private static void handler(Socket clientSocket) throws IOException { byte[] bytes = new byte[1024]; System.out.println("准备read。。"); //接收客户端的数据,阻塞方法,没有数据可读时就阻塞 int read = clientSocket.getInputStream().read(bytes); System.out.println("read完毕。。"); if (read != -1) { System.out.println("接收到客户端的数据:" + new String(bytes, 0, read)); } clientSocket.getOutputStream().write("HelloClient".getBytes()); clientSocket.getOutputStream().flush(); } }
BIO客户端
public class SocketClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("localhost", 9000); //向服务端发送数据 socket.getOutputStream().write("HelloServer".getBytes()); socket.getOutputStream().flush(); System.out.println("向服务端发送数据结束"); byte[] bytes = new byte[1024]; //接收服务端回传的数据 socket.getInputStream().read(bytes); System.out.println("接收到服务端的数据:" + new String(bytes)); socket.close(); } }
缺点:
适用场景:
使用简单,适用于连接少且固定的场景,服务器开销较大。
同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理,JDK1.4开始引入。
NIO方式适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯,编程比较复杂。
NIO服务端:
public class NioServer { // 保存客户端连接 static List<SocketChannel> channelList = new ArrayList<>(); public static void main(String[] args) throws IOException, InterruptedException { // 创建NIO ServerSocketChannel,与BIO的serverSocket类似 ServerSocketChannel serverSocket = ServerSocketChannel.open(); serverSocket.socket().bind(new InetSocketAddress(9000)); // 设置ServerSocketChannel为非阻塞 serverSocket.configureBlocking(false); System.out.println("服务启动成功"); while (true) { // 非阻塞模式accept方法不会阻塞,否则会阻塞 // NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数 SocketChannel socketChannel = serverSocket.accept(); if (socketChannel != null) { // 如果有客户端进行连接 System.out.println("连接成功"); // 设置SocketChannel为非阻塞 socketChannel.configureBlocking(false); // 保存客户端连接在List中 channelList.add(socketChannel); } // 遍历连接进行数据读取 Iterator<SocketChannel> iterator = channelList.iterator(); while (iterator.hasNext()) { SocketChannel sc = iterator.next(); ByteBuffer byteBuffer = ByteBuffer.allocate(128); // 非阻塞模式read方法不会阻塞,否则会阻塞 int len = sc.read(byteBuffer); // 如果有数据,把数据打印出来 if (len > 0) { System.out.println("接收到消息:" + new String(byteBuffer.array())); } else if (len == -1) { // 如果客户端断开,把socket从集合中去掉 iterator.remove(); System.out.println("客户端断开连接"); } } } } }
解决了BIO模型的问题,但效率太低,每次会轮询所有IO线程。
引入selector的NIO服务端:
public class NioSelectorServer { public static void main(String[] args) throws IOException, InterruptedException { // 创建NIO ServerSocketChannel ServerSocketChannel serverSocket = ServerSocketChannel.open(); serverSocket.socket().bind(new InetSocketAddress(9000)); // 设置ServerSocketChannel为非阻塞 serverSocket.configureBlocking(false); // 打开Selector处理Channel,即创建epoll Selector selector = Selector.open(); // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣 serverSocket.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服务启动成功"); while (true) { // 阻塞等待需要处理的事件发生 selector.select(); // 获取selector中注册的全部事件的 SelectionKey 实例 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); // 遍历SelectionKey对事件进行处理 while (iterator.hasNext()) { SelectionKey key = iterator.next(); // 如果是OP_ACCEPT事件,则进行连接获取和事件注册 if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel socketChannel = server.accept(); socketChannel.configureBlocking(false); // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件 socketChannel.register(selector, SelectionKey.OP_READ); System.out.println("客户端连接成功"); } else if (key.isReadable()) { // 如果是OP_READ事件,则进行读取和打印 SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(128); int len = socketChannel.read(byteBuffer); // 如果有数据,把数据打印出来 if (len > 0) { System.out.println("接收到消息:" + new String(byteBuffer.array())); } else if (len == -1) { // 如果客户端断开连接,关闭Socket System.out.println("客户端断开连接"); socketChannel.close(); } } //从事件集合里删除本次处理的key,防止下次select重复处理 iterator.remove(); } } } }
NIO 有三大核心组件: Channel(通道), Buffer(缓冲区),Selector(多路复用器)
NIO底层在JDK1.4版本是用linux的内核函数select()或poll()来实现,跟上面的NioServer代码类似,selector每次都会轮询所有的sockchannel看下哪个channel有读写事件,有的话就处理,没有就继续遍历,JDK1.5开始引入了epoll基于事件响应机制来优化NIO。
总结:NIO整个调用流程就是Java调用了操作系统的内核函数来创建Socket,获取到Socket的文件描述符,再创建一个Selector对象,对应操作系统的Epoll描述符,将获取到的Socket连接的文件描述符的事件绑定到Selector对应的Epoll文件描述符上,进行事件的异步通知,这样就实现了使用一条线程,并且不需要太多的无效的遍历,将事件处理交给了操作系统内核(操作系统中断程序实现),大大提高了效率。
Redis线程模型:
Redis就是典型的基于epoll的NIO线程模型(nginx也是),epoll实例收集所有事件(连接与读写事件),由一个服务端线程连续处理所有事件命令。
应用场景:
AIO方式适用于连接数目多且连接比较长(重操作)的架构,JDK7 开始支持。
AIO服务端
public class AIOServer { public static void main(String[] args) throws Exception { final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000)); serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() { @Override public void completed(AsynchronousSocketChannel socketChannel, Object attachment) { try { System.out.println("2--"+Thread.currentThread().getName()); // 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端 serverChannel.accept(attachment, this); System.out.println(socketChannel.getRemoteAddress()); ByteBuffer buffer = ByteBuffer.allocate(1024); socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer buffer) { System.out.println("3--"+Thread.currentThread().getName()); buffer.flip(); System.out.println(new String(buffer.array(), 0, result)); socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes())); } @Override public void failed(Throwable exc, ByteBuffer buffer) { exc.printStackTrace(); } }); } catch (IOException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, Object attachment) { exc.printStackTrace(); } }); System.out.println("1--"+Thread.currentThread().getName()); Thread.sleep(Integer.MAX_VALUE); } }
为什么Netty使用NIO而不是AIO?
目前Linux的AIO底层优化不够完善,基于AIO的IO模型效率提升不明显,Netty基于NIO模型开发,封装了一些异步通信的特性。
提交评论