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

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

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

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

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

服务端代码:

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事件,怎么办?}}
}

客户端代码:

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.mzph.cn/web/5351.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

windows pytorch安装

安装环境 WindowsAnacondaCudacuDNN Linux和Windows操作系统的安装存在差异&#xff0c;步骤会有所不同&#xff0c;本教程主要针对Windows系统进行示例。 Anaconda集成了许多方便的包和工具&#xff0c;使用会更加方便&#xff0c;特别适合科学计算&#xff0c;深度学习的数…

WSL及UBUNTU及xfce4安装

如何拥有Linux服务器&#xff1f; wsl 是适用于 Linux 的 Windows 子系统&#xff08;Windows Subsystem for Linux&#xff09;。是一个为在Windows 10和Windows Server 2019上能够原生运行Linux二进制可执行文件&#xff08;ELF格式&#xff09;的兼容层&#xff0c;可让开发…

LLM之RAG理论(十一)| 面向生产的RAG应用程序的12种调整策略指南

本文对文本RAG涉及到的主要12种关键“超参数”进行简单总结&#xff0c;主要包括摄取阶段&#xff08;数据清洗、数据分块、embedding模型选择、元数据过滤、多重索引和索引算法&#xff09;和推理阶段【检索和生成】&#xff08;查询转换、检索参数、高级检索策略、重排序、大…

C语言【动态内存】

1.为什么要有动态内存 我们现在掌握的内存开辟方法有&#xff1a; int val 20;//在栈空间开辟4个字节 char str[10]{0};//在栈空间开辟10个字节的连续的空间但是上述的方式有两个点要注意&#xff1a; 1.空间开辟的大小是固定的 2.数组在申明的时候&#xff0c;一定要指定数…

数据驱动,敏捷前行|MongoDB线下技术沙龙-杭州站活动

扫描海报中二维码或点击阅读原文&#xff0c;报名参加阿里云MongoDB在5月11日杭州举办的【数据驱动&#xff0c;敏捷前行——MongoDB企业开发加速器】线下沙龙活动&#xff0c;与MongoDB专家以及其他游戏行业同行一起探讨轻松获得游戏数据库高可用性和弹性的方法&#xff01; 在…

运维实施工程师常用技术面试题(系统与软件实施)

常用技术面试题(系统与软件实施) 9.1 计算机基础 你熟悉的远程有哪些方法?各种方法应该怎么配置? (1)最简单的QQ上有,打开对话框 上边有个 “应用”图标 点击“远程协助”。(2)系统自带的远程桌面服务,右击我的电脑—属性,点远程,把两个够都打上去。 (3)远程协助…

赶紧收藏!2024 年最常见 100道 Java 基础面试题(十九)

上一篇地址&#xff1a;赶紧收藏&#xff01;2024 年最常见 100道 Java 基础面试题&#xff08;十八&#xff09;-CSDN博客 三十七、守护线程是什么&#xff1f; 守护线程&#xff08;Daemon Thread&#xff09;是Java中的一种特殊类型的线程&#xff0c;它的目的是为其他线程…

安卓获取SHA

1&#xff1a;安卓通过签名key获取SHA 方式有两种&#xff0c; 1、电脑上来存在eclipse的用户或正在使用此开发工具的用户就简单了&#xff0c;直接利用eclipse 走打包流程&#xff0c;再打包的时候选择相应的签名&#xff0c;那么在当前面板的下面便会出现签名的相关信息。 2、…

springboot615基于springboot的旅游出行指南_655ms--论文

springboot615基于springboot的旅游出行指南_655ms--论文 springboot615基于springboot的旅游出行指南

23 重构:烟囱式、平台化、中台化的架构

上一讲里&#xff0c;我们介绍了两大类型的系统升级重构方案&#xff0c;还介绍了如何进行重构版本的上线&#xff0c;以及如何平滑地完成新老版本切换的方案。在本讲里&#xff0c;将会具体介绍如何判断系统发展到什么阶段需要重构&#xff0c;以及如何实施重构。 系统稳定性…

AutoBackgroundBackButton 在ScrollView上方自动根据返回键按钮下方内容动态改变颜色。自动变色返回键

在日常有时候有一些为了优化体验的需求。AutoBackgroundBackButton 一个可以根据按钮下方背景颜色动态的改版返回键自定义ImageView。这里只展示了黑白切换方式&#xff0c;你如果还有其他需求可以参考颜色校验来自己实现切换对应颜色按钮。【例如白色背景展示黑色样式&#xf…

【Git】修改提交记录的日期和提交信息

1. 查看日志 git log 2. 修改最近一次提交的时间 git commit --amend --date"2024-05-01T09:30:000800" -am ":memo: 更新 TODO.md" 3. 修改最近一次提交的时间并使用指定提交记录的信息 git commit --amend --date"2024-05-01T09:30:000800&quo…

深入了解Java中的Thread类

在Java编程中&#xff0c;Thread类是一个核心的类&#xff0c;用于创建和管理线程。线程是程序执行的最小单元&#xff0c;多线程编程可以提高程序的并发性和效率。 本文将深入介绍Java中的Thread类&#xff0c;包括其基本概念、创建线程的方法、线程状态的转换、线程同步与通…

Python urllib 爬虫入门(1)

本文主要为Python urllib类库函数和属性介绍及一些简单示例。 目录 urllib爬取网页 简单示例 写入文件 其他读取方法 readline函数 readlines函数 response属性 当前环境信息 返回状态码 返回url地址 对url进行编码与解码 写入文件 总结 urllib爬取网页 通过pyth…

保障互联网基础:深度解析DNS安全

目录 前言 一. DNS 概述 二. DNS 安全威胁 1..DNS欺骗 2.DNS缓存污染 3.DNS放大攻击 4.DNS隧道 5.危害 5.1数据盗窃和财务损失 5.2声誉损害和品牌蚀刻 5.3合规和监管问题 5.4系统停机和生产力损失 三. DNS 安全解决方案 1.DNSSEC&#xff08;域名系统安全扩展&…

PotatoPie 4.0 实验教程(35) —— FPGA实现摄像头图像二值化膨胀效果

手机扫码 链接直达 https://item.taobao.com/item.htm?ftt&id776516984361 什么是图像二值化膨胀&#xff0c;有什么作用&#xff1f; 图像二值化膨胀是图像处理中的一种基本操作&#xff0c;它用于扩展和增强二值图像中的白色区域。具体而言&#xff0c;二值化膨胀操作…

【论文笔记】Training language models to follow instructions with human feedback A部分

Training language models to follow instructions with human feedback A 部分 回顾一下第一代 GPT-1 &#xff1a; 设计思路是 “海量无标记文本进行无监督预训练少量有标签文本有监督微调” 范式&#xff1b;模型架构是基于 Transformer 的叠加解码器&#xff08;掩码自注意…

LeetCode55:跳跃游戏

题目描述 给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 解题思想 每次…

update_min_vruntime()流程图

linux kernel scheduler cfs的update_min_vruntime() 看起来还挺绕的。含义其实也简单&#xff0c;总一句话&#xff0c;将 cfs_rq->min_vruntime 设置为&#xff1a; max( cfs_rq->vruntime, min(leftmost_se->vruntime, cfs_rq->curr->vruntime) )。 画个流…

解决chunk-vendors.js文件太大,首屏加载很慢

首先介绍一款插件script-ext-html-webpack-plugin 可以动态插入script标签到HTML模板文件中&#xff0c;帮助开发者更好地控制脚本的加载和执行顺序&#xff0c;从而提高页面性能和用户体验。此外&#xff0c;该插件还允许开发者将JavaScript文件分为不同的块(chunk)并在HTML文…