• 基于Netty的WebSocket开发网页版聊天室


    1、WebSocket简介

            WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,客户端和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

    应用场景十分广泛:

    1. 社交订阅
    2. 协同编辑/编程
    3. 股票基金报价
    4. 体育实况更新
    5. 多媒体聊天
    6. 在线教育

    2、WebSocket和HTTP的区别

            http协议是用在应用层的协议,他是基于tcp协议的,http协议建立连接也必须要有三次握手才能发送信息。 http连接分为短连接,长连接,短连接是每次请求都要三次握手才能发送自己的信息。即每一个request对应一个response。长连接是在一定的期限内保持连接。保持TCP连接不断开。客户端与服务器通信,必须要有客户端先发起, 然后服务器返回结果。客户端是主动的,服务器是被动的。 客户端要想实时获取服务端消息就得不断发送长连接到服务端.

            WebSocket实现了多路复用,他是全双工通信。在webSocket协议下服务端和客户端可以同时发送信息。 建立了WebSocket连接之后, 服务端可以主动发送信息到客户端。而且信息当中不必在带有head的部分信息了与http的长链接通信来说,这种方式,不仅能降低服务器的压力。而且信息当中也减少了部分多余的信息。

    3、导入基础环境

    1. 将资料中Netty-Springboot工程导入到idea

    2. 相关依赖 

    1. <dependency>
    2. <groupId>org.springframework.bootgroupId>
    3. <artifactId>spring-boot-starter-webartifactId>
    4. dependency>
    5. <dependency>
    6. <groupId>org.springframework.bootgroupId>
    7. <artifactId>spring-boot-starter-thymeleafartifactId>
    8. dependency>
    9. <dependency>
    10. <groupId>org.projectlombokgroupId>
    11. <artifactId>lombokartifactId>
    12. dependency>

    3. 静态资源

    4. yam配置 

    1. server:
    2. port: 8080
    3. resources:
    4. static-locations:
    5. - classpath:/static/
    6. spring:
    7. thymeleaf:
    8. cache: false
    9. checktemplatelocation: true
    10. enabled: true
    11. encoding: UTF-8
    12. mode: HTML5
    13. prefix: classpath:/templates/
    14. suffix: .html

    4、服务端开发

    1. 添加Netty依赖

    1. <dependency>
    2. <groupId>io.nettygroupId>
    3. <artifactId>netty-allartifactId>
    4. dependency>

    2. Netty相关配置

    1. netty:
    2. port: 8081
    3. path: /chat

    3. Netty配置类

    1. package com.lagou.config;
    2. import lombok.Data;
    3. import org.springframework.boot.context.properties.ConfigurationProperties;
    4. import org.springframework.stereotype.Component;
    5. @Component
    6. @ConfigurationProperties("netty")
    7. @Data
    8. public class NettyConfigure {
    9. private int port; // netty监听的端口
    10. private String path; // websocket访问路径
    11. }

    4. NettyWebSocketServer开发

    1. package com.lagou.netty;
    2. import com.lagou.config.NettyConfigure;
    3. import io.netty.bootstrap.ServerBootstrap;
    4. import io.netty.channel.ChannelFuture;
    5. import io.netty.channel.EventLoopGroup;
    6. import io.netty.channel.nio.NioEventLoopGroup;
    7. import io.netty.channel.socket.nio.NioServerSocketChannel;
    8. import io.netty.handler.logging.LogLevel;
    9. import io.netty.handler.logging.LoggingHandler;
    10. import org.springframework.beans.factory.annotation.Autowired;
    11. import org.springframework.stereotype.Component;
    12. import javax.annotation.PreDestroy;
    13. /**
    14. * netty服务器
    15. */
    16. @Component
    17. public class NettyWebSocketServer implements Runnable{
    18. @Autowired
    19. NettyConfigure nettyConfigure;
    20. @Autowired
    21. WebSocketChannelInit webSocketChannelInit;
    22. private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    23. private EventLoopGroup workerGroup = new NioEventLoopGroup();
    24. /**
    25. * 资源关闭--在容器销毁时关闭
    26. */
    27. @PreDestroy
    28. public void close() {
    29. bossGroup.shutdownGracefully();
    30. workerGroup.shutdownGracefully();
    31. }
    32. @Override
    33. public void run() {
    34. try {// 创建服务端启动助手
    35. ServerBootstrap serverBootstrap = new ServerBootstrap();
    36. // 设置线程组
    37. serverBootstrap.group(bossGroup, workerGroup);
    38. // 设置参数
    39. serverBootstrap.channel(NioServerSocketChannel.class)
    40. .handler(new LoggingHandler(LogLevel.DEBUG))
    41. .childHandler(webSocketChannelInit);
    42. // 启动
    43. ChannelFuture channelFuture = serverBootstrap.bind(nettyConfigure.getPort()).sync();
    44. System.out.println("--netty服务端启动成功--");
    45. channelFuture.channel().closeFuture().sync();
    46. } catch (InterruptedException e) {
    47. e.printStackTrace();
    48. bossGroup.shutdownGracefully();
    49. workerGroup.shutdownGracefully();
    50. }finally {
    51. bossGroup.shutdownGracefully();
    52. workerGroup.shutdownGracefully();
    53. }
    54. }
    55. }

    5. 通道初始化对象

    1. package com.lagou.netty;
    2. import com.lagou.config.NettyConfigure;
    3. import io.netty.channel.Channel;
    4. import io.netty.channel.ChannelInitializer;
    5. import io.netty.channel.ChannelPipeline;
    6. import io.netty.handler.codec.http.HttpObjectAggregator;
    7. import io.netty.handler.codec.http.HttpServerCodec;
    8. import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
    9. import io.netty.handler.stream.ChunkedWriteHandler;
    10. import org.springframework.beans.factory.annotation.Autowired;
    11. import org.springframework.stereotype.Component;
    12. /**
    13. * 通道初始化对象
    14. */
    15. @Component
    16. public class WebSocketChannelInit extends ChannelInitializer {
    17. @Autowired
    18. NettyConfigure nettyConfigure;
    19. @Autowired
    20. WebSocketHandler webSocketHandler;
    21. @Override
    22. protected void initChannel(Channel channel) throws Exception {
    23. ChannelPipeline pipeline = channel.pipeline();
    24. // 对Http协议的支持
    25. pipeline.addLast(new HttpServerCodec());
    26. // 对大数据流的支持
    27. pipeline.addLast(new ChunkedWriteHandler());
    28. // post请求三部分 request line / request header/ message body
    29. // HttpObjectAggregator将多个信息转化成单一的request或者response对象
    30. pipeline.addLast(new HttpObjectAggregator(8000));
    31. // 将Http协议升级为ws协议,websocket的支持
    32. pipeline.addLast(new WebSocketServerProtocolHandler(nettyConfigure.getPath()));
    33. // 自定义处理handler
    34. pipeline.addLast(webSocketHandler);
    35. }
    36. }

    6. 处理对象

    1. package com.lagou.netty;
    2. import io.netty.channel.Channel;
    3. import io.netty.channel.ChannelHandler;
    4. import io.netty.channel.ChannelHandlerContext;
    5. import io.netty.channel.SimpleChannelInboundHandler;
    6. import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
    7. import org.springframework.stereotype.Component;
    8. import java.util.ArrayList;
    9. import java.util.List;
    10. /**
    11. * 自定义处理类
    12. * TextWebSocketFrame : websocket数据是帧的形式处理
    13. */
    14. @Component
    15. @ChannelHandler.Sharable // 设置通道共享
    16. public class WebSocketHandler extends SimpleChannelInboundHandler {
    17. public static List channelList = new ArrayList<>();
    18. /**
    19. * 通道就绪事件
    20. *
    21. * @param ctx
    22. * @throws Exception
    23. */
    24. @Override
    25. public void channelActive(ChannelHandlerContext ctx) throws Exception {
    26. Channel channel = ctx.channel();
    27. // 当有新的客户端连接的时候,将通道放入集合
    28. channelList.add(channel);
    29. System.out.println("有新的连接");
    30. }
    31. /**
    32. * 读就绪事件
    33. *
    34. * @param ctx
    35. * @param textWebSocketFrame
    36. * @throws Exception
    37. */
    38. @Override
    39. protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame textWebSocketFrame) throws Exception {
    40. String msg = textWebSocketFrame.text();
    41. System.out.println("msg:" + msg);
    42. // 当前发送消息的通道,当前发送客户端的连接
    43. Channel channel = ctx.channel();
    44. for (Channel channel1 : channelList) {
    45. // 排除自身通道
    46. if (channel != channel1) {
    47. channel1.writeAndFlush(new TextWebSocketFrame(msg));
    48. }
    49. }
    50. }
    51. /**
    52. * 通道未就绪--channel 下线
    53. *
    54. * @param ctx
    55. * @throws Exception
    56. */
    57. @Override
    58. public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    59. Channel channel = ctx.channel();
    60. // 当已有客户端断开连接的时候,就一处对应的通道
    61. channelList.remove(channel);
    62. }
    63. /**
    64. * 异常处理事件
    65. *
    66. * @param ctx
    67. * @param cause
    68. * @throws Exception
    69. */
    70. @Override
    71. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    72. cause.printStackTrace();
    73. Channel channel = ctx.channel();
    74. // 移除集合
    75. channelList.remove(channel);
    76. }
    77. }

    7. 启动类

    1. package com.lagou;
    2. import com.lagou.netty.NettyWebSocketServer;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.boot.CommandLineRunner;
    5. import org.springframework.boot.SpringApplication;
    6. import org.springframework.boot.autoconfigure.SpringBootApplication;
    7. @SpringBootApplication
    8. public class NettySpringbootApplication implements CommandLineRunner {
    9. @Autowired
    10. NettyWebSocketServer nettyWebSocketServer;
    11. public static void main(String[] args) {
    12. SpringApplication.run(NettySpringbootApplication.class, args);
    13. }
    14. @Override
    15. public void run(String... args) throws Exception {
    16. new Thread(nettyWebSocketServer).start();
    17. }
    18. }

    8. 前端js开发

    1. $(function () {
    2. //这里需要注意的是,prompt有两个参数,前面是提示的话,后面是当对话框出来后,在对话框里的默认值
    3. var username = "";
    4. while (true) {
    5. //弹出一个输入框,输入一段文字,可以提交
    6. username = prompt("请输入您的名字", ""); //将输入的内容赋给变量 name ,
    7. if (username.trim() === "")//如果返回的有内容
    8. {
    9. alert("名称不能输入空")
    10. } else {
    11. $("#username").text(username);
    12. break;
    13. }
    14. }
    15. var ws = new WebSocket("ws://localhost:8081/chat");
    16. ws.onopen = function() {
    17. console.log("连接成功")
    18. }
    19. ws.onmessage = function (evt) {
    20. showMessage(evt.data);
    21. }
    22. ws.onclose = function () {
    23. console.log("连接关闭")
    24. }
    25. ws.onerror = function () {
    26. console.log("连接异常")
    27. }
    28. function showMessage(message) {
    29. var str = message.split(":");
    30. $("#msg_list").append(`
  • ${str[0]}
  • ${str[1]}
  • `);
  • // 置底
  • setBottom();
  • }
  • $('#my_test').bind({
  • focus: function (event) {
  • event.stopPropagation()
  • $('#my_test').val('');
  • $('.arrow_box').hide()
  • },
  • keydown: function (event) {
  • event.stopPropagation()
  • if (event.keyCode === 13) {
  • if ($('#my_test').val().trim() === '') {
  • this.blur()
  • $('.arrow_box').show()
  • setTimeout(() => {
  • this.focus()
  • }, 1000)
  • } else {
  • $('.arrow_box').hide()
  • //发送消息
  • sendMsg();
  • this.blur()
  • setTimeout(() => {
  • this.focus()
  • })
  • }
  • }
  • }
  • });
  • $('#send').on('click', function (event) {
  • event.stopPropagation()
  • if ($('#my_test').val().trim() === '') {
  • $('.arrow_box').show()
  • } else {
  • sendMsg();
  • }
  • })
  • function sendMsg() {
  • var message = $("#my_test").val();
  • $("#msg_list").append(`
  • ` + message + `
  • `);
  • $("#my_test").val('');
  • // 发送消息
  • message = username + ":" + message;
  • ws.send(message);
  • // 置底
  • setBottom();
  • }
  • // 置底
  • function setBottom() {
  • // 发送消息后滚动到底部
  • const container = $('.m-message')
  • const scroll = $('#msg_list')
  • container.animate({
  • scrollTop: scroll[0].scrollHeight - container[0].clientHeight + container.scrollTop() + 100
  • });
  • }
  • });
  • 5、注意

    上述只是列举了部分核心代码,完整代码示例见下方下载地址

    链接:https://pan.baidu.com/s/1LBgd_Xlk-19YIPZnP4XuQg?pwd=sj45 
    提取码:sj45 

  • 相关阅读:
    程序员新手村常见问题
    CSP-J 2020 第二轮 优秀的拆分(power)题解 —— 特别篇
    Impagliazzo five-worlds
    konisGraph学习。复杂查询优化记录
    Spring Cloud Alibaba(一)
    第二十三节:带你梳理Vue2:Vue插槽的认识和基本使用
    【Eclipse】Project interpreter not specified 新建项目时,错误提示,已解决
    Win11系统电脑节电模式如何打开
    Java网络通信
    信息学奥赛一本通(c++):1407:笨小猴
  • 原文地址:https://blog.csdn.net/weixin_52851967/article/details/126161531