当前位置:   article > 正文

Java 网络编程之TCP(四):基于NIO中的selector实现服务端,解决客户端异常断开导致服务端不断读取OP_READ问题

Java 网络编程之TCP(四):基于NIO中的selector实现服务端,解决客户端异常断开导致服务端不断读取OP_READ问题

上一篇文章中,没有使用Selector,实习服务端的读取多个客户端的数据;本文先使用Selector实现读取多个客户单数据的功能,然后做些扩展。

一、基于NIO Selector读取多个客户的数据

1.服务端:基于Selector处理客户端的连接事件:OP_READ,处理客户端的数据具备事件:OP_READ

2.客户端:和上一篇一样,基于BIO实现连接和发送数据

服务端代码:

  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. /**
  11. * 基于NIO实现服务端,通过Selector基于事件驱动客户端的读取
  12. *
  13. */
  14. class NIOSelectorServer {
  15. Selector selector;
  16. public static void main(String[] args) throws IOException {
  17. NIOSelectorServer server = new NIOSelectorServer();
  18. server.start(); // 开启监听和事件处理
  19. }
  20. public void start() {
  21. initServer();
  22. // selector非阻塞轮询有哪些感兴趣的事件到了
  23. doService();
  24. }
  25. private void doService() {
  26. if (selector == null) {
  27. System.out.println("server init failed, without doing read/write");
  28. return;
  29. }
  30. try {
  31. while (true) {
  32. while (selector.select() > 0) {
  33. Set<SelectionKey> keys = selector.selectedKeys(); // 感兴趣且准备好的事件
  34. Iterator<SelectionKey> iterator = keys.iterator(); // 迭代器遍历处理,后面要删除集合元素
  35. while (iterator.hasNext()) {
  36. SelectionKey key = iterator.next();
  37. iterator.remove(); // 删除当前元素,防止重复处理
  38. // 下面根据事件进行分别处理
  39. if (key.isAcceptable()) {
  40. // 客户端连接事件
  41. acceptHandler(key);
  42. } else if (key.isReadable()) {
  43. // 读取客户端数据
  44. readHandler(key);
  45. }
  46. }
  47. }
  48. }
  49. } catch (IOException exception) {
  50. exception.printStackTrace();
  51. }
  52. }
  53. private void initServer() {
  54. try {
  55. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
  56. serverSocketChannel.configureBlocking(false);
  57. serverSocketChannel.bind(new InetSocketAddress(9090));
  58. // 此时在selector上注册感兴趣的事件
  59. // 这里先注册OP_ACCEPT: 客户端连接事件
  60. selector = Selector.open();
  61. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
  62. System.out.println("server init success");
  63. } catch (IOException exception) {
  64. exception.printStackTrace();
  65. System.out.println("server init failied");
  66. }
  67. }
  68. public void acceptHandler(SelectionKey key) {
  69. ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 获取客户端的channel
  70. try {
  71. SocketChannel client = server.accept();
  72. client.configureBlocking(false); // 设置client非阻塞
  73. System.out.println("server receive a client :" + client);
  74. // 注册OP_READ事件,用于从客户端读取数据
  75. // 给Client分配一个buffer,用于读取数据,注意buffer的线程安全
  76. ByteBuffer buffer = ByteBuffer.allocate(1024); // buffer这个参数注册的时候也可以不用
  77. client.register(key.selector(), SelectionKey.OP_READ, buffer);
  78. } catch (IOException exception) {
  79. exception.printStackTrace();
  80. }
  81. }
  82. public void readHandler(SelectionKey key) {
  83. System.out.println("read handler");
  84. SocketChannel client = (SocketChannel) key.channel(); // 获取客户端的channel
  85. ByteBuffer buffer = (ByteBuffer) key.attachment(); // 获取Client channel关联的buffer
  86. buffer.clear(); // 使用前clear
  87. // 防止数据分包,需要while循环读取
  88. try {
  89. while (true) {
  90. int readLen = client.read(buffer);
  91. if (readLen > 0) {
  92. // 读取到数据了
  93. buffer.flip();
  94. byte[] data = new byte[buffer.limit()];
  95. buffer.get(data);
  96. System.out.println("server read data from " + client + ", data is :" + new String(data));
  97. } else if (readLen == 0) {
  98. // 没读到数据
  99. System.out.println(client + " : no data");
  100. break;
  101. } else if (readLen == -1) {
  102. // client 关闭连接
  103. System.out.println(client + " close");
  104. break;
  105. }
  106. }
  107. } catch (IOException exception) {
  108. // exception.printStackTrace();
  109. // client 关闭连接
  110. System.out.println(client + " disconnect");
  111. // todo:disconnect 导致一直有read事件,怎么办?
  112. }
  113. }
  114. }

客户端代码:

  1. import java.io.IOException;
  2. import java.io.InputStream;
  3. import java.io.OutputStream;
  4. import java.net.Socket;
  5. /**
  6. * 基于BIO的TCP网络通信的客户端,接收控制台输入的数据,然后通过字节流发送给服务端
  7. *
  8. * @author freddy
  9. */
  10. class ChatClient {
  11. public static void main(String[] args) throws IOException {
  12. // 连接server
  13. Socket serverSocket = new Socket("localhost", 9090);
  14. System.out.println("client connected to server");
  15. // 读取用户在控制台上的输入,并发送给服务器
  16. new Thread(new ClientThread(serverSocket)).start();
  17. // 接收服务端发送过来的数据
  18. try (InputStream serverSocketInputStream = serverSocket.getInputStream();) {
  19. byte[] buffer = new byte[1024];
  20. int len;
  21. while ((len = serverSocketInputStream.read(buffer)) != -1) {
  22. String data = new String(buffer, 0, len);
  23. System.out.println(
  24. "client receive data from server" + serverSocketInputStream + " data size:" + len + ": " + data);
  25. }
  26. }
  27. }
  28. }
  29. class ClientThread implements Runnable {
  30. private Socket serverSocket;
  31. public ClientThread(Socket serverSocket) {
  32. this.serverSocket = serverSocket;
  33. }
  34. @Override
  35. public void run() {
  36. // 读取用户在控制台上的输入,并发送给服务器
  37. InputStream in = System.in;
  38. byte[] buffer = new byte[1024];
  39. int len;
  40. try (OutputStream outputStream = serverSocket.getOutputStream();) {
  41. // read操作阻塞,直到有数据可读,由于后面还要接收服务端转发过来的数据,这两个操作都是阻塞的,所以需要两个线程
  42. while ((len = in.read(buffer)) != -1) {
  43. String data = new String(buffer, 0, len);
  44. System.out.println("client receive data from console" + in + " : " + new String(buffer, 0, len));
  45. if ("exit\n".equals(data)) {
  46. // 模拟客户端关闭连接
  47. System.out.println("client close :" + serverSocket);
  48. // 这里跳出循环后,try-with-resources 会自动关闭outputStream
  49. break;
  50. }
  51. // 发送数据给服务器端
  52. outputStream.write(new String(buffer, 0, len).getBytes()); // 此时buffer中是有换行符
  53. }
  54. } catch (IOException e) {
  55. throw new RuntimeException(e);
  56. }
  57. }
  58. }

测试:

先启动服务端,再启动2个客户端,客户端发送数据

  1. server init success
  2. server receive a client :java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13982]
  3. server receive a client :java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13989]
  4. read handler
  5. server read data from java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13982], data is :client1
  6. java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13982] : no data
  7. read handler
  8. server read data from java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13989], data is :client2
  9. java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13989] : no data

客户端1,exit 关闭连接

客户端日志:

  1. exit
  2. client receive data from consolejava.io.BufferedInputStream@72cdfe9a : exit
  3. client close :Socket[addr=localhost/127.0.0.1,port=9090,localport=13982]
  4. Exception in thread "main" java.net.SocketException: Socket closed
  5. at java.base/sun.nio.ch.NioSocketImpl.endRead(NioSocketImpl.java:248)
  6. at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:327)
  7. at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:350)
  8. at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:803)
  9. at java.base/java.net.Socket$SocketInputStream.read(Socket.java:966)
  10. at java.base/java.io.InputStream.read(InputStream.java:218)
  11. at com.huawei.io.chatroom.bio.ChatClient.main(ChatClient.java:30)
  12. Process finished with exit code 1

服务端日志:

  1. read handler
  2. java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13982] close

客户端2,异常关闭

直接关闭客户端2的服务;如果是用nc命令模拟的,直接Ctrl+C

服务端日志:

  1. java.net.SocketException: Connection reset
  2. at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:394)
  3. at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:411)
  4. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.readHandler(NIOSelectorServer.java:105)
  5. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.doService(NIOSelectorServer.java:54)
  6. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.start(NIOSelectorServer.java:32)
  7. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.main(NIOSelectorServer.java:26)
  8. java.net.SocketException: Connection reset
  9. at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:394)
  10. at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:411)
  11. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.readHandler(NIOSelectorServer.java:105)
  12. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.doService(NIOSelectorServer.java:54)
  13. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.start(NIOSelectorServer.java:32)
  14. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.main(NIOSelectorServer.java:26)
  15. java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:16672] disconnect
  16. read handler
  17. java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:16672] disconnect
  18. read handler
  19. java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:16672] disconnect
  20. read handler
  21. java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:16672] disconnect

可以看到,客户端2的异常关闭,会导致服务器端一直不断收到客户端的OP_READ事件,然后去调用java.nio.channels.SocketChannel#read(java.nio.ByteBuffer)导致抛出异常java.net.SocketException: Connection reset

问题分析:

出现该问题的原因是,客户端2的异常关闭后,服务器端第一次java.nio.channels.SocketChannel#read(java.nio.ByteBuffer)时收到java.net.SocketException: Connection reset,就应该识别出这是客户但异常关闭,需要调用对应的SocketChannel.close()方法关闭客户端;此方法会在对应的Selector上取消之前注册的事件;

修复后的代码如下:

// try {
// client.close();
// } catch (IOException ex) {
// System.out.println("close ex");
// }

此时客户端异常关闭后,不会再持续收到该客户端的OP_READ事件,而且新的客户端可以正常连接发送数据

服务端日志:

  1. read handler
  2. java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:26783] disconnect
  3. java.net.SocketException: Connection reset
  4. at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:394)
  5. at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:426)
  6. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.readHandler(NIOSelectorServer.java:105)
  7. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.doService(NIOSelectorServer.java:54)
  8. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.start(NIOSelectorServer.java:32)
  9. at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.main(NIOSelectorServer.java:26)
  10. server receive a client :java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:26892]
  11. read handler
  12. server read data from java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:26892], data is :client3
  13. java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:26892] : no data

待优化:

可以看到上面,在上面客户端异常端口连接的异常捕获中,再次捕获了client.close();的异常,这很不优雅呀。。。

需要看下别人的优秀代码怎么搞的

todo

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/503859
推荐阅读
相关标签
  

闽ICP备14008679号