[Netty实践] 简单WebSocket服务实现

news2024/11/26 1:44:35

目录

一、介绍

二、依赖导入

三、基础类准备

四、Handler实现

五、WebSocketChannelInitializer实现

六、WebSocketServer实现

七、前端实现

八、测试

九、参考链接


一、介绍

关于WebSocket此处不进行过多介绍,本章主要着重通过Netty实现WebSocket通信服务端,并且实现一个简单的通过网页进行聊天的功能。

讲到WebSocket,这里简单介绍一下为什么要使用WebSocket。以往我们通过网页与服务器进行交互时,都是通过发起一个http/https请求,该请求是无状态的,发送请求后,等待获取服务器返回的结果之后,这次请求就结束了,客户端与服务端就断开了。如果此时服务端想向客户端推送消息的话,由于连接已经断开,服务端无法进行消息推送,此时可以通过使用WebSocket进行客户端与服务端建立长连接,当服务端想向客户端推送消息时,就可以向客户端进行消息推送了。

接下来就进行代码实现。

二、依赖导入

<dependencies>

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.101.Final</version>
        </dependency>

        <!--添加tomcat依赖模块.-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <version>2.0.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.41</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>

    </dependencies>

三、基础类准备

1、ChannelManager, 用于管理Channel等相关信息

public class ChannelManager {

    public final static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    public final static Map<Channel, WebSocketServerHandshaker> handShakerMap = new ConcurrentHashMap<>();

}

2、WebSocketRequestMessage,用于将客户端传递过来的json字符串转换该类的对象

@Data
@ToString
public class WebSocketRequestMessage {

    public String user;

    public String type;

    public String message;


}

3、WebSocketResponseMessage,用于返回给客户端的类型数据

public class WebSocketResponseMessage {

    public String user;

    public String type;

    public String message;

    public String date;

    public WebSocketResponseMessage(WebSocketRequestMessage webSocketRequestMessage) {
        this.user = webSocketRequestMessage.getUser();
        this.type = webSocketRequestMessage.getType();
        this.message = webSocketRequestMessage.getMessage();
        this.date = new Date().toString();
    }

}

四、Handler实现

建立WebSocket通信时,第一次发送的就是http请求,进行协议升级为WebSocket,需要实现针对该连接请求进行处理的handler,协议升级成功之后,后续发送的消息是数据帧,消息将由WebSocketFrame相关的Handler进行处理,为了方便理解,此处简单画个图:

1、DefaultHandler, 抽象类,重写一些基础的方法(处理触发的事件、建立连接、断开连接、遗异常处理的),供其他Handler继承,其他Handler则无需实现上述功能

public abstract class DefaultHandler<T> extends SimpleChannelInboundHandler<T> {

   
    // 配合心跳使用,由客户端进行发送心跳数据包
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent idleStateEvent = (IdleStateEvent) evt;

            // 触发读空闲, 关闭客户端
            if(idleStateEvent.state().equals(IdleState.READER_IDLE)) {
                System.out.println("触发读空闲,关闭channel");
                // 将消息传递CloseWebSocketFrame进行处理
                CloseWebSocketFrame closeWebSocketFrame = new CloseWebSocketFrame();
                ctx.fireChannelRead(closeWebSocketFrame);
            }

        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("与客户端建立连接: " + ctx.channel());
        ChannelManager.channelGroup.add(ctx.channel());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("与客户端断开连接: " + ctx.channel());
        ChannelManager.channelGroup.remove(ctx.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        System.out.println("发生异常: " + ctx.channel());
    }
    
}

2、FullHttpRequestHander

public class FullHttpRequestHandler extends DefaultHandler<FullHttpRequest> {

    private WebSocketServerHandshaker handShaker;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest httpRequest) throws Exception {
        System.out.println("客户端消息:" + httpRequest);

        if(httpRequest.decoderResult().isFailure() || !"websocket".equals(httpRequest.headers().get(HttpHeaderValues.UPGRADE))) {
            DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST);

            ctx.writeAndFlush(httpResponse);

            return;
        }

        WebSocketServerHandshakerFactory webSocketServerHandshakerFactory = new WebSocketServerHandshakerFactory("ws:/" + ctx.channel() + "/websocket", null, false);
        handShaker = webSocketServerHandshakerFactory.newHandshaker(httpRequest);
        if (null == handShaker) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            // 在此处为channel添加WebSocketFrameEncoder和WebSocketFrameDecoder
            handShaker.handshake(ctx.channel(), httpRequest);
        }

        ChannelManager.handShakerMap.put(ctx.channel(), handShaker);

    }

}

3、TextWebSocketFrameHandler,当WebSocket通信建立成功之后,在此阶段发送的文本消息在该handler中进行处理,如果发送的是二进制流,那么请自己自行实现一个处理BinaryWebSocketFrame类型数据的Handler

public class TextWebSocketFrameHandler extends DefaultHandler<TextWebSocketFrame> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {

        WebSocketRequestMessage webSocketRequestMessage = JSON.parseObject(msg.text(), WebSocketRequestMessage.class);

        if("ping".equals(webSocketRequestMessage.getType())) {

            System.out.println("ping message");

            return;
        }

        System.out.println(ctx.channel() + ", 客户端消息:" + msg);

        WebSocketResponseMessage webSocketResponseMessage = new WebSocketResponseMessage(webSocketRequestMessage);

        TextWebSocketFrame textWebSocketFrame = new TextWebSocketFrame(JSON.toJSONString(webSocketResponseMessage));

        ChannelManager.channelGroup.writeAndFlush(textWebSocketFrame);
    }
}

4、CloseWebSocketFrameHandler,客户端进行关闭时,将通过该Handler进行处理

public class CloseWebSocketFrameHandler extends DefaultHandler<CloseWebSocketFrame> {


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, CloseWebSocketFrame msg) throws Exception {
        System.out.println("客户端要断开");
        WebSocketServerHandshaker handShaker = ChannelManager.handShakerMap.get(ctx.channel());
        handShaker.close(ctx.channel(), msg.retain());
    }

}

五、WebSocketChannelInitializer实现

public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {


    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();

        // 将建立连接时发送的第一次http请求数据装换为转换为 HttpRequest
        pipeline.addLast(new HttpServerCodec());
        // 将拆分的http消息(请求内容、请求体)聚合成一个消息
        pipeline.addLast(new HttpObjectAggregator(65536));
        // 块写出
        pipeline.addLast(new ChunkedWriteHandler());

        // 配置读空闲Handler, 3秒该Channel没有产生读将会触发读空闲事件
        pipeline.addLast(new IdleStateHandler(3, 0, 0));

        pipeline.addLast(new FullHttpRequestHandler());
        pipeline.addLast(new TextWebSocketFrameHandler());
        pipeline.addLast(new CloseWebSocketFrameHandler());
    }

}

六、WebSocketServer实现

public class WebSocketServer {

    public void bind(Integer port) {

        EventLoopGroup parent = new NioEventLoopGroup();
        EventLoopGroup child = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(parent, child)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childHandler(new WebSocketChannelInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();

            System.out.println("web socket server 启动成功...");

            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            parent.shutdownGracefully();
            child.shutdownGracefully();
        }


    }

}

七、前端实现

接下来写一个简单的前端web页,无须引入框架,只需要使用原生HTML5和JavaScript即可。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	
	<body>
		
		<button id = "login-btn" onclick="login()">登录</button>
		<button id = "logout-btn" onclick="logout()">退出</button>
		<br/>
		<p id="connection-status">连接状态:关闭</p>
		<p id="username-tag">user:</p>
		
		<input id="send-msg-box" type="text"/>
		<button onclick="send()">发送</button>
		
		<p>==================================群发消息=====================================</p>
		
		<textarea  id="msg-box"></textarea >
		
	</body>
	
	<script>
		var socket;
		var user;
		
		function login() {
			if (window.WebSocket){
				
				socket = new WebSocket("ws://localhost:8888/websocket");
			
				socket.onerror = function(event) {
					document.getElementById("connection-status").innerHTML = "连接状态:异常";
					document.getElementById("channel-tag").innerHTML = "channel:";
				}
				
				socket.onopen = function(event) {
					document.getElementById("connection-status").innerHTML = "连接状态:正常";
					var max = 999
					var min = 100
					user = 'admin' + (Math.floor(Math.random() * (max - min + 1)) + min)
					document.getElementById("username-tag").innerHTML = "user: " + user

;
					
					// 每隔 2 秒钟发送一次 Ping 帧
					setInterval(function() {
					  if (socket.readyState === socket.OPEN) {
								
							var pingMessage = {
								type: 'ping',
								message: ''
							}
							
							socket.send(JSON.stringify(pingMessage))
					  }
					}, 2000);
				}
			
				socket.onclose = function(event){
				    document.getElementById("connection-status").innerHTML = "连接状态:关闭";
					document.getElementById("username-tag").innerHTML = "user:"
				}
				
				socket.onmessage = function(event){
					var message = document.getElementById("msg-box").value;
				
					var response = JSON.parse(event.data)
				
					message = message + '\n' + response.date + " " + response.user + " " + response.message;
					
					document.getElementById("msg-box").value = message;
					
				}
				
			} else {
				alter("不支持websocket")
			}
		}
		
		function send() {
			
			if(socket == null) {
				alert('连接未建立')
				return;
			}
			
			if (socket.readyState == WebSocket.OPEN) {
					var value = document.getElementById("send-msg-box").value;
			        
					var textMessage = {
						type: 'text',
						user: user,
						message: value
					}
					
					//alert(message);
					//console.log(message)
			        socket.send(JSON.stringify(textMessage));
			} else {
				alert('连接未建立')
			}
		}
		
		
		function logout() {
			
			if(socket == null) {
				alert('连接未建立')
				return;
			}
			
			if (socket.readyState == WebSocket.OPEN) {
				socket.close();
			} else {
				alert('连接未建立')
			}
		}
	
	</script>
	
</html>

八、测试

1、启动服务端

new WebSocketServer().bind(8888);

控制台打印如下信息

web socket server 启动成功...

2、打开两个web页分别进行登录点击

点击登录之后,可以看到连接状态正常,以及随机分配了一个用户名

3、发送消息

在任一客户端进行消息发送,都能在所有客户端聊天框中看到如图信息:

4、关闭客户端,可手动点击关闭按钮,或则通过关闭网页以及浏览器,即可断开连接

九、参考链接

JS实时通信三把斧系列之一: websocket - 知乎

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

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

相关文章

在线客服系统:解决常见问题的实用工具与解决方案

市场得不断发展促使着消费者服务意识的觉醒&#xff0c;越来越多的消费者在购买产品的时候不仅看产品的功能、外观、性能&#xff0c;还关注品牌的服务质量。在线客服系统的出现帮助企业解决了客户服务难的问题。接下来&#xff0c;我们具体聊一聊在线客服系统能解决哪些问题&a…

每日一题——LeetCode888

方法一 个人方法&#xff1a; 交换后要达到相同的数量&#xff0c;那么意味着这个相同的数量就是两个人总数的平均值&#xff0c;假设A总共有4个&#xff0c;B总共有8个&#xff0c;那么最后两个人都要达到6个&#xff0c;如果A的第一盒糖果只有1个&#xff0c;那么B就要给出6…

铁山靠之——HarmonyOS基础 - 1.0

HarmonyOS学习第一章 一、HarmonyOS简介1.1 安装和使用DevEco Studio1.2 环境配置1.3 项目创建1.4 运行程序1.5 基本工程目录1.5.1 工程级目录1.5.2 模块级目录1.5.3 app.json51.5.4 module.json51.5.5 main_pages.json 二、TypeScript快速入门2.1 简介2.2 基础类型2.2.1 布尔值…

通过 Nginx 代理实现网页内容替换

突发奇想&#xff0c;用 Nginx 代理一个网站&#xff0c;把网站的一些关键字替换掉&#xff0c;蛮有意思的。 如下图&#xff1a; 一、编译安装 Nginx 一般 Nginx 中不包含 subs_filter 文本替换的模块&#xff0c;需要自己手动编译安装&#xff0c;步骤如下。 克隆 subs_fi…

linux cpu调度分析

一、cpu调度调试方法 echo 0 > /sys/kernel/debug/tracing/tracing_on echo > /sys/kernel/debug/tracing/trace echo 30720 > /sys/kernel/debug/tracing/buffer_size_kb echo nop > /sys/kernel/debug/tracing/current_tracer echo sched_switch sched_wakeup s…

移除石子使总数最小(LeetCode日记)

LeetCode-1962-移除石子使总数最小 题目信息: 给你一个整数数组 p i l e s piles piles &#xff0c;数组 下标从 0 0 0 开始 &#xff0c;其中 p i l e s [ i ] piles[i] piles[i] 表示第 i i i 堆石子中的石子数量。另给你一个整数 k k k &#xff0c;请你执行下述操作…

Win11右键菜单显示全部的方法

Win11右键菜单显示全部的方法&#xff1a;1. 用鼠标右键点击“开始”按钮&#xff08;或者按WinX键&#xff09;&#xff0c;选择点击 “Windows 终端&#xff08;管理员&#xff09;”。 2.在终端应用程序里粘贴这串代码【reg.exe add “HKCU\Software\Classes\CLSID{86ca1aa…

实现一个最简单的内核

更好的阅读体验&#xff0c;请点击 YinKai s Blog | 实现一个最简单的内核。 ​ 这篇文章带大家实现一个最简单的操作系统内核—— Hello OS。 PC 机的引导流程 ​ 我们这里将借助 Ubuntu Linux 操纵系统上的 GRUB 引导程序来引导我们的 Hello OS。 ​ 首先我们得了解一下&a…

burpsuite与sqlmap联动(sqlipy配置)

首先我们需要在burpsuite的 扩展-选项 里配置两个路径&#xff1a; 第一个路径为 jython-standalone-2.7.3.jar 的路径 这个jar文件我们需要自己下载&#xff0c;下载地址&#xff1a;https://www.jython.org/ 点击 download 点击 Jython Standalone 下载好之后将这个jar文件…

Django之DRF框架三,序列化组件

一、序列化类的常用字段和字段参数 常用字段 字段名字段参数CharFieldmax_lengthNone, min_lengthNone, allow_blankFalse, trim_whitespaceTrueIntegerFieldmax_valueNone, min_valueNoneFloatFieldmax_valueNone, min_valueNoneBooleanFieldNullBooleanFieldFloatFieldmax_…

基于Python的音乐数据可视化与推荐系统开发

基于Python的音乐数据可视化与推荐系统开发 导言&#xff1a; 音乐是人们生活中不可或缺的一部分&#xff0c;而对于音乐数据的收集、分析和可视化正逐渐成为技术领域的热点。本文介绍了一款基于Python开发的音乐数据可视化与推荐系统&#xff0c;通过爬取千千音乐网站的数据&a…

C# 实现虚拟数字人

随着Ai技术的提升和应用&#xff0c;虚拟数字人被广泛应用到各行各业中。为我们的生活和工作提供了非常多的便利和色彩。 通过设置虚拟数字人的位置大小&#xff0c;可以让数字人可以在电脑屏幕各个位置显示&#xff1a; 虚拟数字人素材&#xff1a; 虚拟数字人(实际有语音&am…

双向A*算法-python

GitHub - LittleFox99/B_A_star: Bidirectional A Star 其中a&#xff0c;b分别为双向A*搜索的权重 #-*- coding:utf-8 -*- # Time : 2020/11/11 1:21 下午 # Author : LittleFox99 # File : a_star.py # 参考&#xff1a; # https://blog.csdn.net/lustyoung/article/d…

前缀和+单调双队列+贪心:LeetCode2945:找到最大非递减数组的长度

本文涉及知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 单调双队列 贪心 题目 给你一个下标从 0 开始的整数数组 nums 。 你可以执行任意次操作。每次操作中&#xff0c;你需要选择一个 子数组 &#xff0c;并将这个子数组用它所…

资深13年测试整理,性能测试指标-评估方法,一篇搞懂...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、软件性能的关注…

深入探讨DNS数据包注入与DNS中毒攻击检测 (C/C++代码实现)

DNS数据包注入和DNS中毒攻击是网络安全领域中的两个重要主题。DNS&#xff08;域名系统&#xff09;是互联网中的一项核心服务&#xff0c;负责将域名转换为与之相对应的IP地址。 DNS数据包注入是指攻击者通过篡改或伪造DNS请求或响应数据包来干扰或破坏DNS服务的过程。攻击者…

webots仿真报警[ERROR] [1703399199.459991029]: Sampling period is not valid.

一、故障现象 在运行interace传感器使能程序时&#xff0c;报警[ERROR] [1703399199.459991029]: Sampling period is not valid. [ERROR] [1703399199.460080083]: Failed to enable lidar.并发生崩溃。 二、解决方式 1、尝试将程序中的TIME_STEP数值改为与WOrldInfo中的bas…

LeetCode 1954. 收集足够苹果的最小花园周长

一、题目 1、题目描述 给你一个用无限二维网格表示的花园&#xff0c;每一个 整数坐标处都有一棵苹果树。整数坐标 (i, j) 处的苹果树有 |i| |j| 个苹果。 你将会买下正中心坐标是 (0, 0) 的一块 正方形土地 &#xff0c;且每条边都与两条坐标轴之一平行。 给你一个整数 need…

Cross-Drone Transformer Network for Robust Single Object Tracking论文阅读笔记

Cross-Drone Transformer Network for Robust Single Object Tracking论文阅读笔记 Abstract 无人机在各种应用中得到了广泛使用&#xff0c;例如航拍和军事安全&#xff0c;这得益于它们与固定摄像机相比的高机动性和广阔视野。多无人机追踪系统可以通过从不同视角收集互补的…

红日靶场-2

目录 前言 外网渗透 外网渗透打点 1、arp探测 2、nmap探测 3、nikto探测 4、gobuster目录探测 WebLogic 10.3.6.0 1、版本信息 2、WeblogicScan扫描 3、漏洞利用 4、哥斯拉连接 内网渗透 MSF上线 1、反弹连接 2、内网扫描 3、frpc内网穿透 4、ms17-010 5、ge…