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

ops/2024/10/21 14:37:23/

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

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

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

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

服务端代码:

java">import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;/*** 基于NIO实现服务端,通过Selector基于事件驱动客户端的读取**/
class NIOSelectorServer {Selector selector;public static void main(String[] args) throws IOException {NIOSelectorServer server = new NIOSelectorServer();server.start(); // 开启监听和事件处理}public void start() {initServer();// selector非阻塞轮询有哪些感兴趣的事件到了doService();}private void doService() {if (selector == null) {System.out.println("server init failed, without doing read/write");return;}try {while (true) {while (selector.select() > 0) {Set<SelectionKey> keys = selector.selectedKeys(); // 感兴趣且准备好的事件Iterator<SelectionKey> iterator = keys.iterator(); // 迭代器遍历处理,后面要删除集合元素while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove(); // 删除当前元素,防止重复处理// 下面根据事件进行分别处理if (key.isAcceptable()) {// 客户端连接事件acceptHandler(key);} else if (key.isReadable()) {// 读取客户端数据readHandler(key);}}}}} catch (IOException exception) {exception.printStackTrace();}}private void initServer() {try {ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(9090));// 此时在selector上注册感兴趣的事件// 这里先注册OP_ACCEPT: 客户端连接事件selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("server init success");} catch (IOException exception) {exception.printStackTrace();System.out.println("server init failied");}}public void acceptHandler(SelectionKey key) {ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 获取客户端的channeltry {SocketChannel client = server.accept();client.configureBlocking(false); // 设置client非阻塞System.out.println("server receive a client :" + client);// 注册OP_READ事件,用于从客户端读取数据// 给Client分配一个buffer,用于读取数据,注意buffer的线程安全ByteBuffer buffer = ByteBuffer.allocate(1024); // buffer这个参数注册的时候也可以不用client.register(key.selector(), SelectionKey.OP_READ, buffer);} catch (IOException exception) {exception.printStackTrace();}}public void readHandler(SelectionKey key) {System.out.println("read handler");SocketChannel client = (SocketChannel) key.channel(); // 获取客户端的channelByteBuffer buffer = (ByteBuffer) key.attachment(); // 获取Client channel关联的bufferbuffer.clear(); // 使用前clear// 防止数据分包,需要while循环读取try {while (true) {int readLen = client.read(buffer);if (readLen > 0) {// 读取到数据了buffer.flip();byte[] data = new byte[buffer.limit()];buffer.get(data);System.out.println("server read data from " + client + ", data is :" + new String(data));} else if (readLen == 0) {// 没读到数据System.out.println(client + " : no data");break;} else if (readLen == -1) {// client 关闭连接System.out.println(client + " close");break;}}} catch (IOException exception) {// exception.printStackTrace();// client 关闭连接System.out.println(client + " disconnect");// todo:disconnect 导致一直有read事件,怎么办?}}
}

客户端代码:

java">import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;/*** 基于BIO的TCP网络通信的客户端,接收控制台输入的数据,然后通过字节流发送给服务端** @author freddy*/
class ChatClient {public static void main(String[] args) throws IOException {// 连接serverSocket serverSocket = new Socket("localhost", 9090);System.out.println("client connected to server");// 读取用户在控制台上的输入,并发送给服务器new Thread(new ClientThread(serverSocket)).start();// 接收服务端发送过来的数据try (InputStream serverSocketInputStream = serverSocket.getInputStream();) {byte[] buffer = new byte[1024];int len;while ((len = serverSocketInputStream.read(buffer)) != -1) {String data = new String(buffer, 0, len);System.out.println("client receive data from server" + serverSocketInputStream + " data size:" + len + ": " + data);}}}
}class ClientThread implements Runnable {private Socket serverSocket;public ClientThread(Socket serverSocket) {this.serverSocket = serverSocket;}@Overridepublic void run() {// 读取用户在控制台上的输入,并发送给服务器InputStream in = System.in;byte[] buffer = new byte[1024];int len;try (OutputStream outputStream = serverSocket.getOutputStream();) {// read操作阻塞,直到有数据可读,由于后面还要接收服务端转发过来的数据,这两个操作都是阻塞的,所以需要两个线程while ((len = in.read(buffer)) != -1) {String data = new String(buffer, 0, len);System.out.println("client receive data from console" + in + " : " + new String(buffer, 0, len));if ("exit\n".equals(data)) {// 模拟客户端关闭连接System.out.println("client close :" + serverSocket);// 这里跳出循环后,try-with-resources 会自动关闭outputStreambreak;}// 发送数据给服务器端outputStream.write(new String(buffer, 0, len).getBytes()); // 此时buffer中是有换行符}} catch (IOException e) {throw new RuntimeException(e);}}
}

测试:

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

server init success
server receive a client :java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13982]
server receive a client :java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13989]
read handler
server read data from java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13982], data is :client1java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13982] : no data
read handler
server read data from java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13989], data is :client2java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:13989] : no data

客户端1,exit 关闭连接

客户端日志:

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

服务端日志:

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

客户端2,异常关闭

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

服务端日志:

java.net.SocketException: Connection resetat java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:394)at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:411)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.readHandler(NIOSelectorServer.java:105)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.doService(NIOSelectorServer.java:54)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.start(NIOSelectorServer.java:32)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.main(NIOSelectorServer.java:26)
java.net.SocketException: Connection resetat java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:394)at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:411)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.readHandler(NIOSelectorServer.java:105)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.doService(NIOSelectorServer.java:54)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.start(NIOSelectorServer.java:32)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.main(NIOSelectorServer.java:26)
java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:16672] disconnect
read handler
java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:16672] disconnect
read handler
java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:16672] disconnect
read handler
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事件,而且新的客户端可以正常连接发送数据

服务端日志:

read handler
java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:26783] disconnect
java.net.SocketException: Connection resetat java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:394)at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:426)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.readHandler(NIOSelectorServer.java:105)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.doService(NIOSelectorServer.java:54)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.start(NIOSelectorServer.java:32)at com.huawei.io.chatroom.nio.sel.NIOSelectorServer.main(NIOSelectorServer.java:26)
server receive a client :java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:26892]
read handler
server read data from java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:26892], data is :client3java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:26892] : no data

待优化:

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

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

todo


http://www.ppmy.cn/ops/25380.html

相关文章

echarts地图绘制

概览 此篇文章为echart进行地图绘制&#xff0c;并有城市与城市之前的路径图&#xff0c;适用于物流方面&#xff0c;以下示例为大致的思路梳理&#xff0c;实际应用场景需要拓展更改 一. echarts配置完整代码示例 {"geo": {"map": "world",&…

【Linux】tr命令删除空格,sed替换空行

这行代码执行了一系列命令来处理文件 /Users/soulteary/《哈利波特》.txt 的内容&#xff0c;并将处理后的结果保存到 data.txt 中。让我们逐步解释这行代码&#xff1a; cat /Users/soulteary/《哈利波特》.txt&#xff1a;这部分使用 cat 命令来读取指定路径下的文件内容。ca…

通过AI助手实现一个nas定时任务更新阿里云域名解析

一.通过AI助手实现一个ip-domain.py的脚本 起一个Python脚本&#xff0c;ip-domain.py&#xff1b;注意已安装Python3.的运行环境&#xff1b;将下面阿里云相关配置添加&#xff0c;注意这里引用了两个包&#xff0c;requests和alibabacloud_alidns20150109&#xff1b;执行前…

前端获取资源的方式(ajax、fetch)及其区别

前端获取资源的方式及其区别 一、使用 ajax 请求1. 什么是 ajax 请求2. ajax请求原理 二、使用fetch请求1. 什么是fetch请求2. fetch请求原理 三、fetch和ajax的区别 一、使用 ajax 请求 1. 什么是 ajax 请求 Ajax 即 Asynchronous Javascript And XML&#xff08;异步JavaScr…

【酱浦菌-爬虫项目】爬取百度文库文档

1. 首先&#xff0c;定义了一个变量url&#xff0c;指向百度文库的搜索接口 ‘https://wenku.baidu.com/gsearch/rec/pcviewdocrec’。 2. 然后&#xff0c;设置了请求参数data&#xff0c;包括文档ID&#xff08;docId&#xff09;和查询关键词&#xff08;query&#xff09;。…

UE4内存优化

内存查看命令​ 可以通过Stat MemoryPlatform查看对应的内存信息 Total Virtual虚拟内存的总量 Available Virtual可用的虚拟内存 Total Physical 物理内存的总量 Available Physical 可用物理内存总量 Peak Used Virtual 表示应用程序或游戏在运行过程中达到的虚拟内存使用峰…

Spring Boot实现接口签名验证

项目场景&#xff1a; 开放接口是指不需要登录凭证就允许被第三方系统调用的接口。为了防止开放接口被恶意调用&#xff0c;开放接口一般都需要验签才能被调用。 在Spring Boot中实现接口校验签名通常是为了保证接口请求的安全性和数据的完整性。签名校验通常涉及对请求参数的签…

MySQL随便聊----之SQL的简单了解

一、含义 结构化查询语言&#xff0c;针对所有关系型数据库进行操作的语法 每一种数据库操作语法都存在不同的地方,操作相同的其实就是SQL语法,不同语法称之为该数据库操作软件的"方言" 二、通用语法 1. SQL 语句可以单行或多行书写&#xff0c;以分号结尾。 2. 可使…