16.WebSocket聊天室

基于SpringBoot 2.6.11

1.WebSocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议,可以在html页面直接使用。

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

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

过去,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过TCP连接直接交换数据。

当获取WebSocket连接后,可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。

WebSocket在传输的过程中不再使用http协议,而是Stomp协议

1.1 STOMP

STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。

STOMP协议的前身是TTMP协议(一个简单的基于文本的协议),专为消息中间件设计。

STOMP是一个非常简单和容易实现的协议,其设计灵感源自于HTTP的简单性。尽管STOMP协议在服务器端的实现可能有一定的难度,但客户端的实现却很容易。例如,可以使用Telnet登录到任何的STOMP代理,并与STOMP代理进行交互。

1.2 WebSocket 事件

以下是 WebSocket 对象的相关事件。

事件 事件处理程序 描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发

1.3 WebSocket 方法

以下是 WebSocket 对象的相关方法。

方法 描述
Socket.send() 使用连接发送数据
Socket.close() 关闭连接

2.用WebSocket实现网页端聊天室

导入依赖

md-end-block 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

创建配置类

md-end-block 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author: Elivs.Xiang
 * @Email: 406862257@qq.com
 * @create 2022-12-06 15:09
 * @verson IDEA 2020.3
 */

@Configuration
public class WebsocketConfig {
    @Bean    //在容器中创建bean对象,在WebSocketUtil中需要用到的RemoteEndpoint对象
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

创建工具类

md-end-block 复制代码
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.websocket.Session;
import javax.websocket.RemoteEndpoint;
public class WebSocketUtil {
	//HashMap:不支持多线程,并发情况下线程不安全
	public static final Map<String, Session> MESSAGEMAP = new ConcurrentHashMap<>();
	
	//发送消息给客户端
	public static void sendMessage(Session session,String message) {
		if (session!=null) {
			final RemoteEndpoint.Basic basic = session.getBasicRemote();
			if (basic!=null) {
				try {
					basic.sendText(message);//发送消息回客户端
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	//将消息给所有聊天室的人
	//循环发送
	public static void sendMessageToAll(String message) {
		MESSAGEMAP.forEach((sessionId,session)->sendMessage(session, message));
	}
}

创建handler

md-end-block 复制代码
import java.io.IOException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import com.example.web.util.WebSocketUtil;
import org.springframework.web.bind.annotation.RestController;

@RestController
@ServerEndpoint("/WebSocketHandler/{userName}")		//表示接受的是STOMP协议提交的数据
public class WebSocketHandler {
	
	//建立连接
	@OnOpen
	public void openSession(@PathParam("userName")String userName,Session session) {
		//消息
		String message = "欢迎:"+userName+"加入群聊";
		//加入聊天室
		WebSocketUtil.MESSAGEMAP.put(userName, session);
		//发送消息
		WebSocketUtil.sendMessageToAll(message);
	}
	
	@OnMessage
	public void onMessage(@PathParam("userName")String userName,String message) {
		message = userName+":"+message;
		WebSocketUtil.sendMessageToAll(message);
	}
	
	//离开聊天室
	@OnClose
	public void onClose(@PathParam("userName")String userName,Session session) {
		//将当前用户从map中移除 注销
		WebSocketUtil.MESSAGEMAP.remove(userName);
		//群发消息
		WebSocketUtil.sendMessageToAll("用户:"+userName+"离开聊天室");
		//关闭session
		try {
			session.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	//连接异常
	@OnError
	public void onError(Session session,Throwable throwable) {
		try {
			session.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

主启动类开启websocket

md-end-block 复制代码
@SpringBootApplication
@EnableWebSocket
public class WebApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }
}

前端代码

md-end-block 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
    <link rel="stylesheet" type="text/css" href="css/index.css">
    <script type="text/javascript" src="js/jquery.min.js"></script>
    <script type="text/javascript">
        $(document).ready(function(){
            let url = "ws://192.168.7.131:9988/WebSocketHandler/";
            let ws = null;
            //连接服务器
            $("#join").click(function(){
                let userName = $("#userName").val();
                let newUrl = url + userName;	//传递用户名
                console.info(newUrl);
                //创建对象,连接服务器
                ws = new WebSocket(newUrl); 	//html5中提供了

                //给open事件绑定方法
                ws.onopen = function(){
                    console.info("连接成功");
                }
                //接收到数据
                ws.onmessage = function(result){
                    var textarea = document.getElementById('textarea');
                    textarea.append(result.data+"\n");
                    //将文本域的滚动条滚动到最后
                    textarea.scrollTop = textarea.scrollHeight;
                }
                //关闭连接
                ws.onclose = function(){
                    $("#textarea").append("用户:"+userName+"离开聊天室"+"\n");
                    console.info("关闭连接");
                }
            });
            //发送消息
            $("#send").click(function(){
                //将输入框中的消息发送给服务器,并且显示到消息框中
                var messageInput = $("#message");
                var message = messageInput.val();
                if(ws!=null){
                    ws.send(message);	//发送消息
                    messageInput.val("");
                }
            });
            //断开连接
            $("#out").click(function(){
                if(ws!=null){
                    ws.close();
                }
            });
        })
    </script>
</head>
<body>
<div id="box">
    <p>蜗牛聊天室</p>
    <textarea rows="10" cols="50" disabled="disabled" id="textarea"></textarea><br>
    <div class="infoBox">
        用户名:<input type="text" id="userName"><br><br>
        <button style="color: green;" id="join">加入聊天室</button>
        &nbsp;&nbsp;&nbsp;&nbsp;
        <button style="color: red;" id="out">离开聊天室</button>
    </div>
    <br><br>
    <div class="infoBox">
        消&nbsp;&nbsp;&nbsp;息:<input type="text" id="message"><br><br>
        <button id="send">发送消息</button>
    </div>
    <br>

</div>
</body>
</html>

css

md-end-block 复制代码
#box{
	width: 500px;
	background: pink;
	text-align: center;
}
.infoBox{
	text-align:left;
	position: relative;
	left: 62px;
}
#message{
	width: 322px;
}
#send{
	position:relative;
	left:50px;
	height:30px;
	width: 326px;
}

启动项目报错

md-end-block 复制代码
Caused by: java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available

在测试类上的@SpringBootTest中加上

webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT

md-end-block 复制代码
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class WebApplicationTests {
    @Test
    void contextLoads() {
    }
}

整合Gateway

在application.yml中配置websocket路由

md-end-block 复制代码
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: chat  #
          uri: "ws://127.0.0.1:8000"   # ws 协议    需要指定IP,通过微服务名字会报503
          predicates:
            - Path=/WebSocketHandler/**
相关推荐
热爱跑步的恒川3 小时前
【论文复现】基于图卷积网络的轻量化推荐模型
网络·人工智能·开源·aigc·ai编程
云飞云共享云桌面3 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
音徽编程6 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
幺零九零零7 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
23zhgjx-NanKon7 小时前
华为eNSP:QinQ
网络·安全·华为
23zhgjx-NanKon7 小时前
华为eNSP:mux-vlan
网络·安全·华为
点点滴滴的记录7 小时前
RPC核心实现原理
网络·网络协议·rpc
Lionhacker8 小时前
网络工程师这个行业可以一直干到退休吗?
网络·数据库·网络安全·黑客·黑客技术
程思扬9 小时前
为什么Uptime+Kuma本地部署与远程使用是网站监控新选择?
linux·服务器·网络·经验分享·后端·网络协议·1024程序员节
ZachOn1y9 小时前
计算机网络:运输层 —— 运输层概述
网络·tcp/ip·计算机网络·运输层