Java网络编程IO模型
    IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO,NIO,AIO
    一、BIO(Blocking IO)
    1.同步单线程阻塞模型,服务器同时只能处理一个请求。可以看作是银行里只有一个办事窗口。
    image.png
    代码示例:

    1. //问题:一次只能处理一个请求,没用实用性可言
    2. import java.io.IOException;
    3. import java.net.ServerSocket;
    4. import java.net.Socket;
    5. public class SocketServer {
    6. public static void main(String[] args) throws IOException {
    7. ServerSocket serverSocket = new ServerSocket(9000);
    8. while (true) {
    9. System.out.println("等待连接。。");
    10. //同步单线程阻塞模型
    11. Socket clientSocket = serverSocket.accept();
    12. System.out.println("有客户端连接了。。");
    13. handler(clientSocket);
    14. }
    15. }
    16. private static void handler(Socket clientSocket) throws IOException {
    17. byte[] bytes = new byte[1024];
    18. System.out.println("准备read。。");
    19. //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
    20. int read = clientSocket.getInputStream().read(bytes);
    21. System.out.println("read完毕。。");
    22. if (read != -1) {
    23. System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
    24. }
    25. clientSocket.getOutputStream().write("HelloClient".getBytes());
    26. clientSocket.getOutputStream().flush();
    27. }
    28. }

    image.png
    2.同步多线程模型,一个客户端连接对应一个处理线程,服务器同时可以处理多个请求。但如果突然来了10000个客户,线程就很多了,线程是很宝贵的资源,这种通过多线程来实现的功能很脆弱。当然也可以搞个线程池如100个,但这样服务器同时也只能处理100个请求了,并发能力也不高了。
    image.png
    代码示例:

    1. import java.io.IOException;
    2. import java.net.ServerSocket;
    3. import java.net.Socket;
    4. public class SocketServer {
    5. public static void main(String[] args) throws IOException {
    6. ServerSocket serverSocket = new ServerSocket(9000);
    7. while (true) {
    8. System.out.println("等待连接。。");
    9. //同步单线程阻塞模型
    10. final Socket clientSocket = serverSocket.accept();
    11. System.out.println("有客户端连接了。。");
    12. //handler(clientSocket);
    13. //同步多线程阻塞模型
    14. new Thread(new Runnable() {
    15. @Override
    16. public void run() {
    17. try {
    18. handler(clientSocket);
    19. } catch (IOException e) {
    20. e.printStackTrace();
    21. }
    22. }
    23. }).start();
    24. }
    25. }
    26. private static void handler(Socket clientSocket) throws IOException {
    27. byte[] bytes = new byte[1024];
    28. System.out.println("准备read。。");
    29. //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
    30. int read = clientSocket.getInputStream().read(bytes);
    31. System.out.println("read完毕。。");
    32. if (read != -1) {
    33. System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
    34. }
    35. clientSocket.getOutputStream().write("HelloClient".getBytes());
    36. clientSocket.getOutputStream().flush();
    37. }
    38. }

    image.png

    1. import java.io.IOException;
    2. import java.net.Socket;
    3. //客户端代码
    4. public class SocketClient {
    5. public static void main(String[] args) throws IOException {
    6. Socket socket = new Socket("localhost", 9000);
    7. //向服务端发送数据
    8. socket.getOutputStream().write("HelloServer".getBytes());
    9. socket.getOutputStream().flush();
    10. System.out.println("向服务端发送数据结束");
    11. byte[] bytes = new byte[1024];
    12. //接收服务端回传的数据
    13. socket.getInputStream().read(bytes);
    14. System.out.println("接收到服务端的数据:" + new String(bytes));
    15. socket.close();
    16. }
    17. }

    总结:通过多线程方式解决了只能同时处理一个请求的问题,可以并行处理多个请求了。
    同时带来了新的问题:
    1、handler代码里的read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源
    2、如果线程很多,会导致服务器线程太多,压力太大,比如C10K(10000连接)问题
    应用场景:
    BIO 方式适用于连接数目比较小且固定的架构, 这种方式对服务器资源要求比较高, 但程序简单易理解。

    二、NIO(Non Blocking IO)
    同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理,JDK1.4开始引入。
    应用场景:
    NIO方式适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯,编程比较复杂
    NIO非阻塞代码示例:

    1. import java.io.IOException;
    2. import java.net.InetSocketAddress;
    3. import java.nio.ByteBuffer;
    4. import java.nio.channels.ServerSocketChannel;
    5. import java.nio.channels.SocketChannel;
    6. import java.util.ArrayList;
    7. import java.util.Iterator;
    8. import java.util.List;
    9. public class NioServer {
    10. // 保存客户端连接
    11. static List<SocketChannel> channelList = new ArrayList<>();
    12. public static void main(String[] args) throws IOException, InterruptedException {
    13. // 创建NIO ServerSocketChannel,与BIO的serverSocket类似
    14. ServerSocketChannel serverSocket = ServerSocketChannel.open();
    15. serverSocket.socket().bind(new InetSocketAddress(9000));
    16. // 设置ServerSocketChannel为非阻塞
    17. serverSocket.configureBlocking(false);
    18. System.out.println("服务启动成功");
    19. while (true) {
    20. // 非阻塞模式accept方法不会阻塞,否则会阻塞
    21. // NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
    22. SocketChannel socketChannel = serverSocket.accept();
    23. if (socketChannel != null) { // 如果有客户端进行连接
    24. System.out.println("连接成功");
    25. // 设置SocketChannel为非阻塞
    26. socketChannel.configureBlocking(false);
    27. // 保存客户端连接在List中
    28. channelList.add(socketChannel);
    29. }
    30. // 遍历连接进行数据读取
    31. Iterator<SocketChannel> iterator = channelList.iterator();
    32. while (iterator.hasNext()) {
    33. SocketChannel sc = iterator.next();
    34. ByteBuffer byteBuffer = ByteBuffer.allocate(128);
    35. // 非阻塞模式read方法不会阻塞,否则会阻塞
    36. int len = sc.read(byteBuffer);
    37. // 如果有数据,把数据打印出来
    38. if (len > 0) {
    39. System.out.println("接收到消息:" + new String(byteBuffer.array()));
    40. } else if (len == -1) { // 如果客户端断开,把socket从集合中去掉
    41. iterator.remove();
    42. System.out.println("客户端断开连接");
    43. }
    44. }
    45. }
    46. }
    47. }

    总结:通过设置NIO非阻塞在while里不断的处理连接请求,并将连接请求放到channelList保存起来,从而解决了BIO创建多线程的问题,在循环里不断的判断channelList里有没数据,有就处理就可以了。
    同时带来了新的问题:
    1、如果连接数太多的话,会有大量的无效遍历,假如有10000个连接,其中只有1000个连接有写数据,但是由于其他9000个连接并没有断开,我们还是要每次轮询遍历一万次,其中有十分之九的遍历都是无效的,这显然不是一个让人很满意的状态。

    NIO引入多路复用器代码示例:

    1. import java.io.IOException;
    2. import java.net.InetSocketAddress;
    3. import java.nio.ByteBuffer;
    4. import java.nio.channels.SelectionKey;
    5. import java.nio.channels.Selector;
    6. import java.nio.channels.ServerSocketChannel;
    7. import java.nio.channels.SocketChannel;
    8. import java.util.Iterator;
    9. import java.util.Set;
    10. public class NioSelectorServer {
    11. public static void main(String[] args) throws IOException, InterruptedException {
    12. // 创建NIO ServerSocketChannel
    13. ServerSocketChannel serverSocket = ServerSocketChannel.open();
    14. serverSocket.socket().bind(new InetSocketAddress(9000));
    15. // 设置ServerSocketChannel为非阻塞
    16. serverSocket.configureBlocking(false);
    17. // 打开Selector处理Channel,即创建epoll
    18. Selector selector = Selector.open();
    19. // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
    20. serverSocket.register(selector, SelectionKey.OP_ACCEPT);
    21. System.out.println("服务启动成功");
    22. while (true) {
    23. // 阻塞等待需要处理的事件发生
    24. selector.select();
    25. // 获取selector中注册的全部事件的 SelectionKey 实例
    26. Set<SelectionKey> selectionKeys = selector.selectedKeys();
    27. Iterator<SelectionKey> iterator = selectionKeys.iterator();
    28. // 遍历SelectionKey对事件进行处理
    29. while (iterator.hasNext()) {
    30. SelectionKey key = iterator.next();
    31. // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
    32. if (key.isAcceptable()) {
    33. ServerSocketChannel server = (ServerSocketChannel) key.channel();
    34. SocketChannel socketChannel = server.accept();
    35. socketChannel.configureBlocking(false);
    36. // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
    37. socketChannel.register(selector, SelectionKey.OP_READ);
    38. System.out.println("客户端连接成功");
    39. } else if (key.isReadable()) { // 如果是OP_READ事件,则进行读取和打印
    40. SocketChannel socketChannel = (SocketChannel) key.channel();
    41. ByteBuffer byteBuffer = ByteBuffer.allocate(128);
    42. int len = socketChannel.read(byteBuffer);
    43. // 如果有数据,把数据打印出来
    44. if (len > 0) {
    45. System.out.println("接收到消息:" + new String(byteBuffer.array()));
    46. } else if (len == -1) { // 如果客户端断开连接,关闭Socket
    47. System.out.println("客户端断开连接");
    48. socketChannel.close();
    49. }
    50. }
    51. //从事件集合里删除本次处理的key,防止下次select重复处理
    52. iterator.remove();
    53. }
    54. }
    55. }
    56. }

    总结:多路复用器Selector帮我们实现了有消息数据的集合,我们不用去遍历channelList做无效的判断处理了,消息事件处理交给了操作系统内核(操作系统中断程序实现),我们只需从selector中取不同的消息出来做不同的消费就可以了,这极大程度上提高了效率。

    三、AIO(NIO 2.0)
    异步非阻塞, 由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用
    应用场景:
    AIO方式适用于连接数目多且连接比较长(重操作)的架构,JDK7 开始支持
    AIO代码示例:

    1. import java.io.IOException;
    2. import java.net.InetSocketAddress;
    3. import java.nio.ByteBuffer;
    4. import java.nio.channels.AsynchronousServerSocketChannel;
    5. import java.nio.channels.AsynchronousSocketChannel;
    6. import java.nio.channels.CompletionHandler;
    7. public class AIOServer {
    8. public static void main(String[] args) throws Exception {
    9. final AsynchronousServerSocketChannel serverChannel =
    10. AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));
    11. serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
    12. @Override
    13. public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
    14. try {
    15. System.out.println("2--"+Thread.currentThread().getName());
    16. // 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端
    17. serverChannel.accept(attachment, this);
    18. System.out.println(socketChannel.getRemoteAddress());
    19. ByteBuffer buffer = ByteBuffer.allocate(1024);
    20. socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    21. @Override
    22. public void completed(Integer result, ByteBuffer buffer) {
    23. System.out.println("3--"+Thread.currentThread().getName());
    24. buffer.flip();
    25. System.out.println(new String(buffer.array(), 0, result));
    26. socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
    27. }
    28. @Override
    29. public void failed(Throwable exc, ByteBuffer buffer) {
    30. exc.printStackTrace();
    31. }
    32. });
    33. } catch (IOException e) {
    34. e.printStackTrace();
    35. }
    36. }
    37. @Override
    38. public void failed(Throwable exc, Object attachment) {
    39. exc.printStackTrace();
    40. }
    41. });
    42. System.out.println("1--"+Thread.currentThread().getName());
    43. Thread.sleep(Integer.MAX_VALUE);
    44. }
    45. }

    image.png

    1. import java.net.InetSocketAddress;
    2. import java.nio.ByteBuffer;
    3. import java.nio.channels.AsynchronousSocketChannel;
    4. public class AIOClient {
    5. public static void main(String... args) throws Exception {
    6. AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
    7. socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get();
    8. socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes()));
    9. ByteBuffer buffer = ByteBuffer.allocate(512);
    10. Integer len = socketChannel.read(buffer).get();
    11. if (len != -1) {
    12. System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
    13. }
    14. }
    15. }

    BIO、 NIO、 AIO 对比:
    image.png

    为什么Netty使用NIO而不是AIO?
    在Linux系统上,AIO的底层实现仍使用Epoll,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化,Linux上AIO还不够成熟。Netty是异步非阻塞框架,Netty在NIO上做了很多异步的封装。

    同步异步与阻塞非阻塞
    同步:所有的请求与消息处理均是由同一个线程处理的。
    异步:请求与消息处理不是同一个线程处理的,从上面AIO的运行结果就可以很显示看出来同步与异步的区别了,main线程执行启动AIO程序并打印了数字1,而连接成功事件与接收到消息事件均不是main线程处理的。

    (段子)
    老张爱喝茶,废话不说,煮开水。
    出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
    1 老张把水壶放到火上,立等水开。(同步阻塞)
    老张觉得自己有点傻
    2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
    老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
    3 老张把响水壶放到火上,立等水开。(异步阻塞)
    老张觉得这样傻等意义不大
    4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
    老张觉得自己聪明了。
    所谓同步异步,只是对于水壶而言。
    普通水壶,同步;响水壶,异步。
    虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。
    同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
    所谓阻塞非阻塞,仅仅对于老张而言。
    立等的老张,阻塞;看电视的老张,非阻塞。