用Netty实现WebSocket网络聊天室

在这里插入图片描述

  最近学习Netty,学习到WebSocket一章节,将Netty中WebSocket的样例代码做了一些简单的改造,实现了一个简易的WebSocket网络聊天室,源码并非完全自己实现,只是将一些别人的代码做了下整合改造,今分享至此,希望对大家学习Netty有所帮助。
  首先介绍下什么是WebSocket,这就不得不先提到HTTP协议了。众所周知,在HTTP/2发布前,所有HTTP请求都是 请求-应答的 模式,这就意味着客户端只能向服务器要数据,然后服务端被动应答,而服务器无法主动将数据推送给客户端。这就导致一些高时效性的场景用HTTP就会有些问题,就拿实时聊天举例吧,客户端想知道近期有没有人说过话,就只能不断问服务器 有没有人发了消息? 有的话服务器就返回,没有就不返回,这种行为被称为轮询。 轮询的问题在于如果询问的时间间隔太长,消息的及时性无法得到保证,但如果时间太短,对服务器的压力就会大幅提升(因为不断要请求响应)。 有没有可能服务器有消息的时候,主动推送给客户端?
  WebSocket因此而诞生,它允许客户端和服务端之间在HTTP之上建立一个全双工的TCP长连接,这里的关键点在于全双工,意味着服务端也能通过这个连接给客户端发送即时消息,从而解决了轮询的性能和时效性矛盾的问题。了解过Socket编程的同学应该很容易理解了,WebSocket其实本质上就是Socket,只不过WebSocket是建立在HTTP协议之上的。
  回到我们的正题,如何用Netty+WebSocket写一个网络聊天室? 其实Netty里已经封装好了HTTP和WebSocket的实现,我们只需要实现部分聊天室的功能即可,接下来看下我实现的完整代码:
首先是ServerBootstrap的部分,这里是Netty的启动入口。

@Service
public class WebSocketServer {static final String WEBSOCKET_PATH = "/ws";private ChannelFuture f;@Resourceprivate WebSocketFrameHandler webSocketFrameHandler;@PostConstructprivate void init() {bind(Constant.SOCKET_PORT);}public static voud bind(int port) {EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast(new HttpServerCodec());  // netty中http协议的编解码pipeline.addLast(new HttpObjectAggregator(65536));  pipeline.addLast(new WebSocketServerCompressionHandler());pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true));pipeline.addLast(new WebSocketIndexPageHandler(WEBSOCKET_PATH));  // demo页面的handler,这个是非必须的,可以换成其他第三方的WebSocket客户端工具  pipeline.addLast(webSocketFrameHandler); // 聊天室的主要逻辑}});Channel f = b.bind(port).sync().channel();f.closeFuture().sync();} catch (Exception e) {} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}

  因为HttpServerCodec HttpObjectAggregator WebSocketServerCompressionHandler WebSocketServerProtocolHandler是Netty组件中提供的组件,其作用就是完成Http和WebSocket协议数据到Java对象的相互转换,这里就不再展开了,我们直接看下剩下的两个Handler。
  首先是WebSocketIndexPageHandler,这个也是我直接从Netty样例中Copy出来的,它的作用就是构建一个Http首页,这个首页实现了一个简单的WebSocket网页客户端,如果你不需要这个网页客户端,你也可直接删掉。

public class WebSocketIndexPageHandler extends SimpleChannelInboundHandler<FullHttpRequest> {private final String websocketPath;public WebSocketIndexPageHandler(String websocketPath) {this.websocketPath = websocketPath;}private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {// Generate an error page if response getStatus code is not OK (200).HttpResponseStatus responseStatus = res.status();if (responseStatus.code() != 200) {ByteBufUtil.writeUtf8(res.content(), responseStatus.toString());HttpUtil.setContentLength(res, res.content().readableBytes());}// Send the response and close the connection if necessary.boolean keepAlive = HttpUtil.isKeepAlive(req) && responseStatus.code() == 200;HttpUtil.setKeepAlive(res, keepAlive);ChannelFuture future = ctx.writeAndFlush(res);if (!keepAlive) {future.addListener(ChannelFutureListener.CLOSE);}}private static String getWebSocketLocation(ChannelPipeline cp, HttpRequest req, String path) {String protocol = "ws";if (cp.get(SslHandler.class) != null) {// SSL in use so use Secure WebSocketsprotocol = "wss";}return protocol + "://" + req.headers().get(HttpHeaderNames.HOST) + path;}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {// Handle a bad request.if (!req.decoderResult().isSuccess()) {sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), BAD_REQUEST,ctx.alloc().buffer(0)));return;}// Allow only GET methods.if (!GET.equals(req.method())) {sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), FORBIDDEN,ctx.alloc().buffer(0)));return;}// Send the index pageif ("/".equals(req.uri()) || "/index.html".equals(req.uri())) {String webSocketLocation = getWebSocketLocation(ctx.pipeline(), req, websocketPath);ByteBuf content = WebSocketServerIndexPage.getContent(webSocketLocation);FullHttpResponse res = new DefaultFullHttpResponse(req.protocolVersion(), OK, content);res.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");HttpUtil.setContentLength(res, content.readableBytes());sendHttpResponse(ctx, req, res);} else {sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), NOT_FOUND,ctx.alloc().buffer(0)));}}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {cause.printStackTrace();ctx.close();}
}

  上面这部分代码还依赖于一个静态的页面,我对这个页面做了简单的改造,方便大家愉快地一起聊天。


public final class WebSocketServerIndexPage {public static ByteBuf getContent(String webSocketLocation) {return Unpooled.copiedBuffer("\n"+ "<html><head><title>Web Socket Test</title></head>\n"+ "<body>\n"+ "<script type=\"text/javascript\">\n"+ "var socket;\n"+ "if (!window.WebSocket) {\n"+ "  window.WebSocket = window.MozWebSocket;\n"+ "}\n"+ "if (window.WebSocket) {\n"+ "  socket = new WebSocket(\"" + webSocketLocation + "\");\n"+ "  socket.onmessage = function(event) {\n"+ "    var ta = document.getElementById('responseText');\n"+ "    ta.value = ta.value + '\\n' + event.data\n"+ "    ta.scrollTop = ta.scrollHeight\n"+ "  };\n"+ "  socket.onopen = function(event) {\n"+ "    var ta = document.getElementById('responseText');\n"+ "    ta.value = \"Web Socket opened!\";\n"+ "    ta.scrollTop = ta.scrollHeight\n"+ "  };\n"+ "  socket.onclose = function(event) {\n"+ "    var ta = document.getElementById('responseText');\n"+ "    ta.value = ta.value + \"Web Socket closed!\";\n"+ "    ta.scrollTop = ta.scrollHeight\n"+ "  };\n"+ "} else {\n"+ "  alert(\"Your browser does not support Web Socket.\");\n"+ "}\n"+ "\n"+ "function send(message) {\n"+ "  if (!window.WebSocket) { return; }\n"+ "  if (socket.readyState == WebSocket.OPEN) {\n"+ "    socket.send(message);\n"+ "    document.getElementById('msgForm').value = ''\n"+ "  } else {\n"+ "    alert(\"The socket is not open.\");\n"+ "  }\n"+ "}\n"+ "</script>\n"+ "<textarea id=\"responseText\" style=\"width:500px;height:300px;\"></textarea>\n"+ "<form οnsubmit=\"return false;\">\n"+ "<input type=\"text\" name=\"message\" id=\"msgForm\"/><input type=\"button\" value=\"Send\"\n"+ "       οnclick=\"send(this.form.message.value)\" />\n"+ "</form>\n"+ "</body>\n"+ "</html>\n", CharsetUtil.US_ASCII);}
}

  改造后的页面效果长这样,虽然有些简陋,但还是可以收发消息。
在这里插入图片描述

  最核心的就是WebSocketFrameHandler这个类了,所有的逻辑都是在这里面的,其实也不复杂,就是在连接建立后,给这个连接分配一个随机名字,将某个人发的消息转发到其他已有的连接上,另外及时清理掉断开的连接,防止资源泄露,代码很简单,相信你一看就懂。

@Service
@Slf4j
@ChannelHandler.Sharable
public class WebSocketFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {// 我直接取了天龙八部里的名字,给每个聊天的人随机分配一个 private final List<String> names =List.of("刀白凤", "丁春秋", "马夫人", "马五德", "小翠", "于光豪", "巴天石", "不平道人", "邓百川", "风波恶","甘宝宝", "公冶乾", "木婉清", "少林老僧", "太皇太后", "天狼子", "天山童姥", "王语嫣", "乌老大","无崖子", "云岛主", "云中鹤", "止清", "白世镜", "包不同", "本参", "本观", "本相", "本因", "出尘子","冯阿三", "兰剑", "古笃诚", "过彦之", "平婆婆", "石清露", "石嫂", "司空玄", "司马林", "玄慈","玄寂", "玄苦", "玄难", "玄生", "玄痛", "叶二娘", "竹剑", "左子穆", "华赫艮", "乔峰", "李春来","李傀儡", "李秋水", "刘竹庄", "朴者和尚", "祁六三", "全冠清", "阮星竹", "西夏宫女", "许卓诚","朱丹臣", "努儿海", "阿碧", "阿洪", "阿胜", "阿朱", "阿紫", "波罗星", "陈孤雁", "鸠摩智", "来福儿","孟师叔", "宋长老", "苏星河", "苏辙", "完颜阿古打", "耶律洪基", "耶律莫哥", "耶律涅鲁古","耶律重元", "吴长风", "吴光胜", "吴领军", "辛双清", "严妈妈", "余婆婆", "岳老三", "张全祥","单伯山", "单季山", "单叔山", "单小山", "单正", "段延庆", "段誉", "段正淳", "段正明", "范禹","范百龄", "范骅", "苟读", "和里布", "何望海", "易大彪", "郁光标", "卓不凡", "宗赞王子", "哈大霸","姜师叔", "枯荣长老", "梦姑", "姚伯当", "神山上人", "神音", "狮鼻子", "室里", "项长老", "幽草","赵钱孙", "赵洵", "哲罗星", "钟灵", "钟万仇", "高升泰", "龚光杰", "贾老者", "康广陵", "秦红棉","虚竹", "容子矩", "桑土公", "唐光雄", "奚长老", "徐长老", "诸保昆", "崔百泉", "崔绿华", "符敏仪","黄眉和尚", "菊剑", "聋哑婆婆", "梅剑", "萧远山", "游骥", "游驹", "游坦之", "程青霜", "傅思归","葛光佩", "缘根", "智光大师", "鲍千灵", "褚万里", "瑞婆婆", "端木元", "黎夫人", "薛慕华", "慕容博","慕容复", "谭公", "谭婆", "谭青", "摘星子", "慧方", "慧观", "慧净", "慧真", "穆贵妃", "赫连铁树");// 名字到连接的映射private final Map<String, ChannelHandlerContext> name2ctx = new ConcurrentHashMap<>();// 连接到名字的映射private final Map<ChannelHandlerContext, String> ctx2name = new ConcurrentHashMap<>();@Overridepublic void channelRegistered(ChannelHandlerContext ctx) throws Exception {// 先分配当前没有在使用中的名字Optional<String> nameOp = names.stream().filter(x -> !name2ctx.containsKey(x)).findFirst();if (!nameOp.isPresent()) {// 如果没分配到名字,直接断开连接,这么写的话,同时在线的人数取决于名字列表的大小ctx.writeAndFlush(new TextWebSocketFrame("当前连接人数过多,请稍后重试!"));log.info("当前连接人数过多,请稍后重试!");ctx.close();return;}String name = nameOp.get();name2ctx.put(name, ctx);ctx2name.put(ctx, name);broadcast(name + "加入了群聊!");}@Overridepublic void channelUnregistered(ChannelHandlerContext ctx) throws Exception {String name = ctx2name.getOrDefault(ctx, "");name2ctx.remove(name);ctx2name.remove(ctx);broadcast(name + "离开了群聊!");}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {// 收到消息后将消息群发给所有人if (frame instanceof TextWebSocketFrame) {String request = ((TextWebSocketFrame) frame).text();String name = ctx2name.getOrDefault(ctx, "");broadcast(name + ":" + request);} else {String message = "unsupported frame type: " + frame.getClass().getName();throw new UnsupportedOperationException(message);}}/*** 将消息群发给所有在线的人*/private void broadcast(String msg) {log.info("msg:{}", msg);name2ctx.entrySet().parallelStream().forEach(e -> {String name = e.getKey();ChannelHandlerContext ctx = e.getValue();if (ctx.channel().isActive()) {ctx.writeAndFlush(new TextWebSocketFrame(msg));} else {// 广播时清理掉不活跃的连接ctx2name.remove(ctx);name2ctx.remove(name);}});}
}

  这里特别提醒下,想实现群聊,那WebSocketFrameHandler必须标记为Sharable,并且全局共享一个对象,所以需要注意下线程安全的问题,这里我都用了ConcurrentHashMap。
  以上就是完整的代码了,有兴趣可以自己跑一跑,另外这个网络聊天室我已经部署的我的服务器上了,也可以直接点开体验下 http://xindoo.xyz:8083/。

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

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

相关文章

聋哑六年级计算机课教学进度计划,小学六年级下册信息技术教学计划三篇

【导语】六年级的学生&#xff0c;通过三年的信息技术课的学习&#xff0c;已经对信息技术这门课很熟悉了&#xff0c;他们已经懂得了一些关于信息技术课的基础知识&#xff0c;掌握了计算机的基本操作&#xff0c;能利用画图软件绘出简单的图画&#xff0c;学习了Word文字处理…

计算机学院五名学生开发手语app,我院开发的手语合成系统有望让聋哑人用上手机...

高交会上参观者就系统性能向开发人员提问 网络信息中心宣(文/肖琼林 吴兴勤 图/吴兴勤)我院计算机科学与技术学科部智能计算研究中心开发的基于虚拟人技术的中国手语合成系统&#xff0c;有望成为千百万聋哑人的福音。 中国有数量巨大的聋哑人群体&#xff0c;据北京市手语研究…

【博主推荐】HTML浪漫表白求爱(附源码)

HTML浪漫表白求爱 带花询问表白HTML源码带花询问表白HTML效果展示爱心花环表白HTML源码爱心花环表白HTML效果展示动画图片表白HTML源码动画图片表白HTML效果展示酷炫爱心表白HTML源码酷炫爱心表白HTML效果展示树形爱心表白HTML源码树形爱心表白HTML效果展示唯美爱心表白HTML源码…

4链提币接口通道源码

介绍&#xff1a; 淘到的多签提币接口 可以借鉴参考 仅用于研究 请与下载24小时内删除 您的任何行和后果与本站无关 建议任何操作都走官方&#xff01; 云盘下载地址&#xff1a; http://zijieyunpan.cn/yXYGwOTuCoT 演示&#xff1a;

最新仿闲鱼链接闲鱼验货宝+独立后台管理

后台一键生成链接&#xff0c;独立后台管理 教程&#xff1a;修改数据库config/Conn 不会可以看源码里有教程 下载程序&#xff1a;https://pan.baidu.com/s/16lN3gvRIZm7pqhvVMYYecQ?pwd6zw3

转转闲鱼源码搭建教程

教程&#xff1a;修改数据库账号密码直接使用。 下载程序&#xff1a;https://pan.baidu.com/s/16lN3gvRIZm7pqhvVMYYecQ?pwd6zw3

chatgpt赋能python:Python区域找图教程:快速定位您需要的图像

Python 区域找图教程&#xff1a;快速定位您需要的图像 在计算机视觉和机器人领域中&#xff0c;区域找图是一项重要的任务。它可以帮助我们找到图像中的特定区域&#xff0c;从而提高我们对图像的理解和分析能力。在本教程中&#xff0c;我们将学习如何使用Python中的OpenCV库…

基于ChatGPT生成的泊松圆盘采样代码 Python纯算法源码

先上代码直接运行 # -*- coding: utf-8 -*- """ --********************************************************************-- --file: 柏森采样示例代码 --author: donganning --create time: 2023/3/30 14:24 --description: --***************************…

IdentityServer 4 自定义身份校验/通过 token获取用户信息

Demo https://github.com/MartinAaron/data_collection 1、自定义身份校验 public static IEnumerable<Client> GetClients(){return new List<Client>{//grant_type basicnew Client{ClientId "xczx",AccessTokenLifetime 36000,AllowedGrantTypes G…

微信小程序登录 + 基于token的身份验证

官方时序图如下&#xff1a; https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html 图里其实说的很清楚了&#xff0c;清理下流程&#xff1a; 1.前端调用wx.login()获取code值 2.前端通过调用wx.getUserInfo获取iv、rawData、signature、e…

Token:用户身份验证的令牌

一、Token是什么 Token&#xff0c;就是用户身份验证的令牌&#xff0c;代表执行某些操作的权利的对象&#xff0c;本质上是服务端生成的一串加密字符串、用于客户端进行请求的“令牌”。 当用户第一次使用账号密码成功登 陆后&#xff0c;服务器就生成一个token和token失效…

调用华为API实现身份证识别

调用华为API实现身份证识别 1、作者介绍2、调用华为API实现身份证识别2.1 算法介绍2.1.1OCR简介2.1.2身份证识别原理2.1.3身份证识别应用场景 2.2 调用华为API流程 3、代码实现3.1安装相关的包3.2代码复现3.3实验结果 1、作者介绍 雷千龙&#xff0c;男&#xff0c;西安工程大…

二十二、身份验证与权限

一、 准备工作 为了讲清楚身份验证与权限&#xff0c;我们再创建一个应用projects,设计模型如下&#xff1a; class Project(models.Model):name models.CharField(项目名称, max_length20, help_text项目名称)desc models.CharField(项目描述, max_length200, help_text项目…

人脸核身基础版 SDK 接入 > 合作方后台上送身份信息

文章目录 一、概述二、实现流程2.1. 获取获取 access_token2.2. 获取 SIGN ticket2.3. 生成签名2.4. 上送身份信息2.5. 获取 NONCE ticket 三、实战3.1. 获取获取 access_token3.2. 获取 SIGN ticket3.3. 生成签名3.4. 上送身份信息3.5. 获取 NONCE ticket 四、开源地址 一、概…

身份证验证接口API(仅需一行代码,公安部实时接口)

身份确认在互联网越来越普及&#xff0c;在二手、电商、贷款、交友、招聘等主流互联网应用中都有明确的刚需&#xff0c;传统的 身份证核验方式是用户上传身份证图片&#xff0c;客服后台人肉审核&#xff0c;但问题在于根本无从确认身份证的真实性&#xff0c;也不能确认该身份…

危!GPT将影响80%工作岗位,挣得越多越危险?OpenAI发布重磅研究

源&#xff5c;新智元 GPT-4发布没几天&#xff0c;OpenAI直接告诉所有人&#xff0c;GPTs是通用技术&#xff0c;80%的美国人的工作受到影响。想要保命&#xff0c;且看这34大「铁饭碗」。 前脚刚推出GPT-4&#xff0c;OpenAI后脚就发布了35页论文官宣&#xff1a; 80%的美国人…

音视频技术开发周刊 | 286

每周一期&#xff0c;纵览音视频技术领域的干货。 新闻投稿&#xff1a;contributelivevideostack.com。 比尔盖茨&#xff1a;AI 时代开启 盖茨谈到AI如何改善人类的不平等现象&#xff0c;并在医疗、教育等方面做出贡献&#xff0c;但也存在风险。 GPT-4 Copilot X震撼来袭&a…

对话 ClickHouse CTO Alexey:目光不仅限于成为最快的数据库 | 近匠

作为世界上最快的 OLAP 列式数据库之一&#xff0c;ClickHouse 能在毫秒级的时间内处理数百亿行的数据。ClickHouse 公司在官网上&#xff0c;也是简单扼要地介绍了自己的数据库&#xff1a;“Fast”。 ClickHouse 的灵魂人物 AlexeybMilovidov&#xff0c;则是一位将“慢”践行…

马斯克放话:没有我就没有 OpenAI,连名字都是我起的!

整理 | 郑丽媛 出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09; 在 ChatGPT、GPT- 4 的爆火下&#xff0c;近来 OpenAI 这个名字已逐渐成为科技圈的“高频热词”&#xff0c;其创立初期与马斯克之间的“恩怨”也被时常提及。 有人说自从马斯克走后&#xff0c;Open…

阿里云版Chat-gpt申请内测

不允许你还不知道阿里云的chat-gpt&#xff0c;快去申请 -- 内测申请地址&#xff1a;通义千问