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(); // 获取客户端的channel
        try {
            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(); // 获取客户端的channel
        ByteBuffer buffer = (ByteBuffer) key.attachment(); // 获取Client channel关联的buffer
        buffer.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 {
        // 连接server
        Socket 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;
    }

    @Override
    public 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 会自动关闭outputStream
                    break;
                }
                // 发送数据给服务器端
                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 :client1

java.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 :client2

java.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 : exit

client close :Socket[addr=localhost/127.0.0.1,port=9090,localport=13982]
Exception in thread "main" java.net.SocketException: Socket closed
	at 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 reset
	at 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 reset
	at 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 reset
	at 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 :client3

java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:26892] : no data

待优化:

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

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

todo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/578521.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

新科技辅助器具赋能视障生活:让盲人出行融入日常

随着科技日新月异的发展&#xff0c;一款名为蝙蝠避障专为改善盲人日常生活的盲人日常生活辅助器具应运而生&#xff0c;它通过巧妙整合实时避障与拍照识别功能&#xff0c;成功改变了盲人朋友们的生活格局&#xff0c;为他们提供了更为便捷、高效的生活体验。 这款非同…

DevOps(十五)如何创建参数化的Jenkins Job

一、Jenkins参数化 在Jenkins中创建参数化的Job允许你在构建过程中动态输入一些值&#xff0c;这样可以让构建过程更加灵活和通用。以下是创建参数化Jenkins Job的步骤&#xff1a; 1、 创建新的Job 登录到Jenkins控制台。点击左侧的“新建任务”或“Create new jobs”。输入…

RocketMQ 部署

RocketMQ 部署 1、安装依赖&#xff08;Java&#xff09; [rootMicroservices ~]# mkdir -p /data/businessServer/ [rootMicroservices ~]# cd /data/businessServer/# 获取安装包&#xff08;下载较慢&#xff09; [rootMicroservices businessServer]# wget https://githu…

【Redis 开发】(Feed流的模式,GEO数据结构,BitMap,HyperLogLog)

Redis FeedTimeline GEOBitMapHyperLogLog Feed Feed流产品有两种常见模式: Timeline:不做内容筛选&#xff0c;简单的按照内容发布时间排序&#xff0c;常用于好友或关注。例如朋友圈 优点:信息全面&#xff0c;不会有缺失。并且实现也相对简单 缺点:信息噪音较多&#xff0c…

「51媒体」城市推介会,地方旅游推荐,怎么做好媒体宣传

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 城市推介会和地方旅游推荐是城市形象宣传的重要组成部分&#xff0c;通过有效的媒体宣传可以提升城市的知名度和吸引力。&#xff1a; 一&#xff0c;活动内容层面&#xff1a; 突出亮点…

公认最好的随身WiFi的格行5G随身WiFi真实测评!格行5G和纽曼5G随身WiFi哪个好?5G随身WiFi推荐第一名

随着5G信号基站的铺设逐渐完善&#xff0c;各大通讯移动公司也都适时的推出了属于自己的5G随身WiFi。其中老牌企业纽曼与格行的5G随身WiFi最受大家的欢迎。那么二者到底谁才是5G设备中的王者呢&#xff1f;今天就做一个全面测评。 一、首先是颜值党们最为关注的外观问题 纽曼5…

Java中Synchronized的锁升级

锁升级过程 当JVM启动后&#xff0c;一个共享资源对象直到有线程第一个访问时&#xff0c;这段时间内是处于无锁状态&#xff0c;对象头的Markword里偏向锁标识位是0&#xff0c;锁标识位是01。 Tips&#xff1a;当一个共享资源首次被某个线程访问时&#xff0c;锁就会从无锁状…

记录AE学习查漏补缺(持续补充中。。。)

记录AE学习查漏补缺 常用win下截图WinShifts导入AI/PS工程文件将图层上移一个位置或者下移一个位置展示/关闭图层标线/标度放大面板适应屏幕大小 CtrlAltF 关键帧熟记关键参数移动锚点位置加选一个关键参数快速回到上下一帧隐藏/显示图层关键帧拉长缩短关键帧按着鼠标左键不松手…

新款闯关游戏制作

目前制作4关, cpp. #include "c.h" #include "Level1.h" using namespace std; int main() {srand(time(0)); initgraph(600, 600); BeginBatchDraw();IMAGE a; loadimage(&a, _T("1.jpg")); putimage(0, 0, &a);setbkmode(TRANSPAREN…

【Vue】如何创建一个Vue-cli程序

一、准备工作 1、下载Node.js 官网地址 https://nodejs.org/en 2、查看版本 cmd下通过node-v,查看版本号&#xff1b; cmd下通过npm-v,查看是否打印版本号。 3、安装淘宝加速器 npm install cnpm -g 4、安装Vue-cli cnpm install vue-cli -g 二、创建Vue程序 1、创建一个V…

【数据分析面试】32.矩阵元素求和 (Python: for…in…语句)

题目&#xff1a;矩阵元素求和 &#xff08;Python) 假设给定一个整数矩阵。你的任务是编写一个函数&#xff0c;返回矩阵中所有元素的和。 示例 1&#xff1a; 输入&#xff1a; matrix [[1, 2, 3], [4, 5, 6], [7, 8, 9]]输出&#xff1a; matrix_sum(matrix) -> 45…

Android 12 Starting window的添加与移除

添加&#xff1a; 04-13 16:29:55.931 2944 7259 D jinyanmeistart: at com.android.server.wm.StartingSurfaceController.createSplashScreenStartingSurface(StartingSurfaceController.java:87) 04-13 16:29:55.931 2944 7259 D jinyanmeistart: at com.android.server.wm.…

记录些 LLM 常见的问题和解析

1、提示校准为什么有助于减轻基于提示的学习中的偏见? 提示校准包括调整提示&#xff0c;尽量减少产生的输出中的偏差。 其他&#xff1a;微调修改模型本身&#xff0c;而数据增强扩展训练数据&#xff0c;梯度裁剪防止在训练期间爆炸梯度。 2、是否需要为所有基于文本的LL…

必应bing国内广告开户注册教程!

今天搜索引擎广告成为企业推广产品与服务、提升品牌知名度的重要渠道之一。作为全球第二大搜索引擎&#xff0c;必应Bing凭借其高质量的用户群体和广泛的国际覆盖&#xff0c;为广告主提供了独特的市场机遇。在中国&#xff0c;虽然必应的市场份额相对较小&#xff0c;但对于寻…

鸿蒙官网学习3

鸿蒙官网学习3 每日小提示项目的模块类型跨设备预览调试阶段应用的替换方式有两种 打开老的demo工程报错UIAbility 每日小提示 项目的模块类型 moduleType分为三种&#xff0c;只有1&#xff0c;2的模块支持直接调试和运行 entryfeaturehar 跨设备预览 需要手动在config.j…

在开发软件以便未来本地化到其他语言时需要考虑的事项

我们准备了一份关于开发软件以便未来本地化到其他语言时需要考虑的事项的简要指南。这非常重要&#xff0c;因为您的软件在其他国家市场上的销售可能会带来比本国市场更多的收入。 在开发软件时考虑到未来本地化到其他语言的一些重要方面包括&#xff1a; 设计多语言支持&…

C++—DAY4

在Complex类的基础上&#xff0c;完成^&#xff0c;<<&#xff0c;>>&#xff0c;~运算符的重载 #include <iostream>using namespace std; class Complex {int rel;int vir; public:Complex(){}Complex(int rel,int vir):rel(rel),vir(vir){}void show(){c…

【每日刷题】Day26

【每日刷题】Day26 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. [NOIP2008]笨小猴_牛客题霸_牛客网 (nowcoder.com) 2. 添加逗号_牛客题霸_牛客网 (nowcoder.com) …

windows/linux 安装php的 sql server 扩展

Windowsphpstudyphp7.1 下载&#xff1a;ODBC、下载php 的sql server 扩展 路径&#xff1a;下载地址 版本&#xff1a;我的是7.1 对应的ODBC 是13&#xff0c;php 的sql server 扩展为4.3 安装&#xff1a;msodbcsql 直接安装、sqlsrv43 安装完把 扩展复制到php71 的扩展文…

Python脚本抢票【笔记】

Python脚本抢票【笔记】 前言版权推荐Python脚本抢票【Python】microsoft edge驱动器下载以及使用最后 前言 2024-4-17 18:19:15 以下内容源自《【笔记】》 仅供学习交流使用 版权 禁止其他平台发布时删除以下此话 本文首次发布于CSDN平台 作者是CSDN日星月云 博客主页是ht…
最新文章