Websocket实现私聊和群聊
- 1. websocket的概念
-
- 1.1. 全双工概念
- 2. websocket实现聊天室
-
- 2.1. WebSocket API
-
- 2.1.1. 构造方法
-
- 2.1.1.1. 语法
- 2.1.1.2. 参数
- 2.1.1.3. 抛出异常
- 2.1.2. 常量
- 2.1.3. 属性
- 2.1.4. 方法
- 2.1.5. 事件
- 3. websocket实现群聊或私聊或图片发送
-
- 3.1. 项目的最终目录结构
- 3.2. 依赖注入
- 3.3. yml文件配置
- 3.4. 注入ServerEndpointExporter
- 3.5. Controller代码
- 3.5. websocket实现群聊
-
- 3.5.1. 服务器端代码
- 3.5.2. 客户端代码
- 3.5.3. 测试结果
- 3.6. websocket实现私聊+群聊
-
- 3.6.1. 服务器端代码
- 3.6.2. 客户端代码
- 3.6.3. SocketConfig代码
- 3.6.4. 测试结果
- 3.7. websocket实现聊天图片发送
-
- 3.7.1. 服务端代码
- 3.7.2. 客户端代码
- 3.7.3. SocketConfig代码
- 3.7.4. 测试结果
1. websocket的概念
websocket是一种在单个TCP连接上进行全双工通信的协议。websocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在websocket API中浏览器和服务器只需要完成一次握手,两者之间就可以直接创建持久性的连接,并进行双向数据传输。
1.1. 全双工概念
全双工是通讯传输的一个术语。通信允许数据在两个方面上同时传输,它在能力上相当于两个单工通信方式的结合。全双工指可以同时进行信号的双向传输(A—>B的同时B—>A)。单工指:只允许A向B传送信息,而B不能向A传送。
2. websocket实现聊天室
2.1. WebSocket API
WebSocket对象提供了用户创建和管理WebSocket连接,以及通过该连接发送和接收数据的API。
2.1.1. 构造方法
2.1.1.1. 语法
WebSocket(url [, protocols]);
2.1.1.2. 参数
- url:要连接的url,WebSocket服务器的url
- protocols:可选参数,一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个WebSocket子协议。如果不指定协议字符串,则假定为空字符串。
2.1.1.3. 抛出异常
SECURITY_ERR:正在尝试连接的端口被阻止。
2.1.2. 常量
- WebSocket.CONNECTING == 0 连接中
- WebSocket.OPEN == 1 已连接
- WebSocket.CLOSING == 2 正在关闭
- WebSocket.CLOSED == 3 已关闭
2.1.3. 属性
-
WebSocket.binaryType
使用二进制的数据类型连接
-
WebSocket.bufferedAmount(只读)
未发送至服务器的字节数
-
WebSocket.extensions(只读)
服务器选择的扩展
-
WebSocket.onopen
用户指定连接成功后的回调函数
-
WebSocket.onmessage
用户指定从服务器接收到信息时的回调函数
-
WebSocket.onerror
用于指定连接失败后的回调函数
-
WebSocket.onclose
用于指定连接关闭后的回调函数
-
WebSocket.url
websocket的绝对路径
-
WebSocket.readyState(只读)
当前的连接状态
-
WebSocket.protocol(只读)
服务器选择的下属协议
2.1.4. 方法
-
WebSocket.close([code, reason])
关闭当前连接
-
WebSocket.send(data)
对要传输的数据进行排队
2.1.5. 事件
使用addEventListener()监听或将一个事件监听器赋值给本接口的oneventname属性,来监听下面的事件:
-
open
当一个WebSocket连接成功时触发,也可以通过onopen属性来设置
-
message
当一个WebSocket收发数据时触发,也可以通过onmessage属性来设置
-
error
当一个WebSocket连接因错误而关闭时触发,也可以通过onerror属性来设置
-
close
当一个WebSocket连接被关闭时触发,也可以通过onclose属性来设置
3. websocket实现群聊或私聊或图片发送
本文使用springboot+freemarker+websocket实现代码
3.1. 项目的最终目录结构
3.2. 依赖注入
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.AttackingApe.demo</groupId><artifactId>webSocket</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>net.sourceforge.nekohtml</groupId><artifactId>nekohtml</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.72</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins><resources><resource><directory>src/main/resources</directory></resource></resources></build> </project>
3.3. yml文件配置
server:port: 20713tomcat:uri-encoding: UTF-8 spring:freemarker:suffix: .ftlcharset: UTF-8content-type: text/htmlhttp:encoding:charset: UTF-8force: trueenabled: true
3.4. 注入ServerEndpointExporter
package com.AttackingApe.demo.webSocket.config;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** @author PengPan* @version 1.0* @date 2020/7/14 18:50*/ @Configuration public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter(){return new ServerEndpointExporter();} }
3.5. Controller代码
package com.AttackingApe.demo.webSocket.Controller;import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView;import java.io.IOException; import java.util.Map;/*** websocket应用* @author PengPan* @version 1.0* @date 2020/7/13 14:34*/ @RestController @Slf4j public class WebsocketController {@RequestMapping("/websocketTest")public ModelAndView sendMessage(Map<String, Object> map) throws IOException{try{log.info("跳转到websocket页面上");return new ModelAndView("webSocketClients", map);}catch (Exception e){log.info("页面跳转发生错误:{}", e.getMessage());map.put("msg", "请求错误");return new ModelAndView("error", map);}} }
3.5. websocket实现群聊
实现内容:连接websocket的用户都可以进行聊天并显示所有人聊天以及对应的昵称。
3.5.1. 服务器端代码
package com.AttackingApe.demo.webSocket.util;import com.AttackingApe.demo.webSocket.Pojo.SocketConfig; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component;import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CopyOnWriteArraySet;/*** @author PengPan* @version 1.0* @date 2020/7/15 18:23*/ @ServerEndpoint(value = "/websocket/{nickname}") @Component @Slf4j public class MyWebsocket {private static Map<String, Session> map = new HashMap<>();private static CopyOnWriteArraySet<MyWebsocket> clients = new CopyOnWriteArraySet<>();private Session session;private String nickname;@OnOpenpublic void onOpen(Session session, @PathParam("nickname") String nickname){this.session = session;this.nickname = nickname;clients.add(this);log.info("有新用户加入,当前人数为:", clients.size());this.session.getAsyncRemote().sendText(nickname + "已加入连接,当前人数为:" + clients.size());}@OnClosepublic void onClose(){clients.remove(this);log.info("有用户断开连接,当前人数为:{}", clients.size());}@OnMessagepublic void onMessage(String message, Session session, @PathParam("nickname") String nickname){log.info("来自客户端:{}发来的消息:{}", nickname, message);broadcast(nickname + ":" + message);}@OnErrorpublic void onError(Session session, Throwable error){log.error("出现错误");error.printStackTrace();}/*** 自定义群发消息* @param message*/public void broadcast(String message){for (MyWebsocket websocket : clients){//异步发送消息websocket.session.getAsyncRemote().sendText(message);}} }
3.5.2. 客户端代码
<!DOCTYPE HTML> <html> <head><meta charset="UTF-8"><title>My WebSocket</title><style>#message{margin-top:40px;border:1px solid gray;padding:20px;}</style> </head> <body> 昵称:<input type="text" id="nickname"/> <button οnclick="conectWebSocket()">连接WebSocket</button> <button οnclick="closeWebSocket()">断开连接</button> <hr /> <br /> 消 息:<input id="text" type="text" /><button οnclick="send()">发送消息</button> <div id="message"></div> </body> <script type="text/javascript">var websocket = null;function conectWebSocket(){var nickname = document.getElementById("nickname").value;if(nickname == "" || nickname == null){alert("请输入昵称");return;}//判断当前浏览器是否支持WebSocketif ('WebSocket'in window) {websocket = new WebSocket("ws://10.4.4.83:20713/websocket/" + nickname);} else {alert('Not support websocket')}//连接发生错误的回调方法websocket.onerror = function() {setMessageInnerHTML("error");};//连接成功建立的回调方法websocket.onopen = function(event) {setMessageInnerHTML("Loc MSG: 成功建立连接");}//接收到消息的回调方法websocket.onmessage = function(event) {setMessageInnerHTML(event.data);}//连接关闭的回调方法websocket.onclose = function() {setMessageInnerHTML("Loc MSG:关闭连接");}//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。window.onbeforeunload = function() {websocket.close();}}//将消息显示在网页上function setMessageInnerHTML(innerHTML) {document.getElementById('message').innerHTML += innerHTML + '<br/>';}//关闭连接function closeWebSocket() {websocket.close();}//发送消息function send() {var message = document.getElementById('text').value;websocket.send(message);} </script> </html>
3.5.3. 测试结果
3.6. websocket实现私聊+群聊
实现内容:A可以在公屏上输入内容,大家都可以看见。也可以选择输入频道,只和每一个人聊天。
3.6.1. 服务器端代码
package com.AttackingApe.demo.webSocket.util;import com.AttackingApe.demo.webSocket.Pojo.SocketConfig; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component;import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CopyOnWriteArraySet;/*** @author PengPan* @version 1.0* @date 2020/7/15 18:23*/ @ServerEndpoint(value = "/websocket/{nickname}") @Component @Slf4j public class MyWebsocket {private static Map<String, Session> map = new HashMap<>();private static CopyOnWriteArraySet<MyWebsocket> clients = new CopyOnWriteArraySet<>();private Session session;private String nickname;@OnOpenpublic void onOpen(Session session, @PathParam("nickname") String nickname){this.session = session;this.nickname = nickname;map.put(session.getId(), session);clients.add(this);log.info("有新用户加入,当前人数为:", clients.size());this.session.getAsyncRemote().sendText(nickname + "已成功连接(其频道号为:" + session.getId() + "),当前在线人数为:" + clients.size());}@OnClosepublic void onClose(){clients.remove(this);log.info("有用户断开连接,当前人数为:{}", clients.size());}@OnMessagepublic void onMessage(String message, Session session, @PathParam("nickname") String nickname){log.info("来自客户端:{}发来的消息:{}", nickname, message);SocketConfig socketConfig;ObjectMapper objectMapper = new ObjectMapper();try{socketConfig = objectMapper.readValue(message, SocketConfig.class);if(socketConfig.getType() == 1){ //私聊socketConfig.setFromUser(session.getId());Session fromSession = map.get(socketConfig.getFromUser());Session toSession = map.get(socketConfig.getToUser());if(toSession != null){ //接受者存在,发送以下消息给接受者和发送者fromSession.getAsyncRemote().sendText(nickname + ":" + socketConfig.getMsg());toSession.getAsyncRemote().sendText(nickname + ":" + socketConfig.getMsg());}else{ //发送者不存在,发送以下消息给发送者fromSession.getAsyncRemote().sendText("频道号不存在或对方不在线");}}else{ //群聊broadcast(nickname + ":" + socketConfig.getMsg());}}catch (Exception e){log.error("发送消息出错");e.printStackTrace();}}@OnErrorpublic void onError(Session session, Throwable error){log.error("出现错误");error.printStackTrace();}/*** 自定义群发消息* @param message*/public void broadcast(String message){for (MyWebsocket websocket : clients){//异步发送消息websocket.session.getAsyncRemote().sendText(message);}} }
3.6.2. 客户端代码
<!DOCTYPE HTML> <html> <head><meta charset="UTF-8"><title>My WebSocket</title><style>#message{margin-top:40px;border:1px solid gray;padding:20px;}</style> </head> <body> 昵称:<input type="text" id="nickname"/> <button οnclick="conectWebSocket()">连接WebSocket</button> <button οnclick="closeWebSocket()">断开连接</button> <hr /> <br /> 消 息:<input id="text" type="text" /> 频道号:<input id="toUser" type="text"/> <button οnclick="send()">发送消息</button> <div id="message"></div> </body> <script type="text/javascript">var websocket = null;function conectWebSocket(){var nickname = document.getElementById("nickname").value;if(nickname == "" || nickname == null){alert("请输入昵称");return;}//判断当前浏览器是否支持WebSocketif ('WebSocket'in window) {websocket = new WebSocket("ws://10.4.4.83:20713/websocket/" + nickname);} else {alert('Not support websocket')}//连接发生错误的回调方法websocket.onerror = function() {setMessageInnerHTML("error");};//连接成功建立的回调方法websocket.onopen = function(event) {setMessageInnerHTML("Loc MSG: 成功建立连接");}//接收到消息的回调方法websocket.onmessage = function(event) {setMessageInnerHTML(event.data);}//连接关闭的回调方法websocket.onclose = function() {setMessageInnerHTML("Loc MSG:关闭连接");}//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。window.onbeforeunload = function() {websocket.close();}}//将消息显示在网页上function setMessageInnerHTML(innerHTML) {document.getElementById('message').innerHTML += innerHTML + '<br/>';}//关闭连接function closeWebSocket() {websocket.close();}//发送消息function send() {var message = document.getElementById('text').value;var toUser = document.getElementById('toUser').value;var socketConfig = {msg:message,toUser:toUser};if(toUser == "" || toUser == null){socketConfig.type = 0;}else{socketConfig.type = 1;}websocket.send(JSON.stringify(socketConfig));} </script> </html>
3.6.3. SocketConfig代码
package com.AttackingApe.demo.webSocket.Pojo;import lombok.Data;/*** @author PengPan* @version 1.0* @date 2020/7/16 15:41*/ @Data public class SocketConfig {//聊天类型 0:群聊 1:私聊private int type;//发送者private String fromUser;//接受者private String toUser;//消息private String msg;//消息类型 1:文本 2:图片private int code; }
3.6.4. 测试结果
3.7. websocket实现聊天图片发送
实现内容:聊天的时候可以传入图片。
3.7.1. 服务端代码
package com.AttackingApe.demo.webSocket.util;import com.AttackingApe.demo.webSocket.Pojo.SocketConfig; import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component;import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CopyOnWriteArraySet;/*** @author PengPan* @version 1.0* @date 2020/7/15 18:23*/ @ServerEndpoint(value = "/websocket/{nickname}") @Component @Slf4j public class MyWebsocket {private static Map<String, Session> map = new HashMap<>();private static CopyOnWriteArraySet<MyWebsocket> clients = new CopyOnWriteArraySet<>();private Session session;private String nickname;@OnOpenpublic void onOpen(Session session, @PathParam("nickname") String nickname){this.session = session;this.nickname = nickname;map.put(session.getId(), session);clients.add(this);log.info("有新用户加入,当前人数为:", clients.size());JSONObject jsonObject = new JSONObject();jsonObject.put("msg",nickname + "已成功连接(其频道号为:" + session.getId() + "),当前在线人数为:" + clients.size());jsonObject.put("code", 1);this.session.getAsyncRemote().sendText(jsonObject.toJSONString());}@OnClosepublic void onClose(){clients.remove(this);log.info("有用户断开连接,当前人数为:{}", clients.size());}@OnMessagepublic void onMessage(String message, Session session, @PathParam("nickname") String nickname){log.info("来自客户端:{}发来的消息:{}", nickname, message);SocketConfig socketConfig;ObjectMapper objectMapper = new ObjectMapper();JSONObject jsonObject = new JSONObject();jsonObject.put("code", JSONObject.parseObject(message).get("code"));try{socketConfig = objectMapper.readValue(message, SocketConfig.class);jsonObject.put("nickname", nickname + ":");if(socketConfig.getType() == 1){ //私聊socketConfig.setFromUser(session.getId());Session fromSession = map.get(socketConfig.getFromUser());Session toSession = map.get(socketConfig.getToUser());if(toSession != null){ //接受者存在,发送以下消息给接受者和发送者jsonObject.put("msg", socketConfig.getMsg());fromSession.getAsyncRemote().sendText(jsonObject.toJSONString());toSession.getAsyncRemote().sendText(jsonObject.toJSONString());}else{ //发送者不存在,发送以下消息给发送者jsonObject.put("msg", "频道号不存在或对方不在线");fromSession.getAsyncRemote().sendText("频道号不存在或对方不在线");}}else{ //群聊jsonObject.put("msg", socketConfig.getMsg());broadcast(jsonObject.toJSONString());}}catch (Exception e){log.error("发送消息出错");e.printStackTrace();}}@OnErrorpublic void onError(Session session, Throwable error){log.error("出现错误");error.printStackTrace();}/*** 自定义群发消息* @param message*/public void broadcast(String message){for (MyWebsocket websocket : clients){//异步发送消息websocket.session.getAsyncRemote().sendText(message);}} }
3.7.2. 客户端代码
<!DOCTYPE HTML> <html> <head><meta charset="UTF-8"><title>My WebSocket</title><style>#message{margin-top:40px;border:1px solid gray;padding:20px;}</style> </head> <body> 昵称:<input type="text" id="nickname"/> <button οnclick="conectWebSocket()">连接WebSocket</button> <button οnclick="closeWebSocket()">断开连接</button> <hr /> <br /> 消 息:<input id="text" type="text" /> 频道号:<input id="toUser" type="text"/> <button οnclick="send()">发送消息</button> <input type="file" id="file" οnchange="chooseFile()"/> <div id="message"></div> </body> <script type="text/javascript">var websocket = null;function conectWebSocket(){var nickname = document.getElementById("nickname").value;if(nickname == "" || nickname == null){alert("请输入昵称");return;}//判断当前浏览器是否支持WebSocketif ('WebSocket'in window) {websocket = new WebSocket("ws://10.4.4.83:20713/websocket/" + nickname);} else {alert('Not support websocket')}//连接发生错误的回调方法websocket.onerror = function() {setMessageInnerHTML("error");};//连接成功建立的回调方法websocket.onopen = function(event) {setMessageInnerHTML("Loc MSG: 成功建立连接");}//接收到消息的回调方法websocket.onmessage = function(event) {var json = JSON.parse(event.data);if(json.code == 1){setMessageInnerHTML(json.nickname + json.msg);}else if(json.code == 2){setMessageInnerHTML(json.nickname);setIconInnerHTML(json.msg);}}//连接关闭的回调方法websocket.onclose = function() {setMessageInnerHTML("Loc MSG:关闭连接");}//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。window.onbeforeunload = function() {websocket.close();}}//将文本消息显示在网页上function setMessageInnerHTML(innerHTML) {document.getElementById('message').innerHTML += innerHTML + '<br/>';}//将图片消息显示在网页上function setIconInnerHTML(innerHTML) {document.getElementById('message').innerHTML = document.getElementById('message').innerHTML + '<img width="150px" src='+innerHTML+'>' + '<br/>';}//关闭连接function closeWebSocket() {websocket.close();}//发送文本消息function send() {var message = document.getElementById('text').value;var toUser = document.getElementById('toUser').value;var socketConfig = {code:1,msg:message,toUser:toUser};if(toUser == "" || toUser == null){socketConfig.type = 0;}else{socketConfig.type = 1;}websocket.send(JSON.stringify(socketConfig));}//发送图片消息function chooseFile() {var fileList = document.getElementById("file").files;var type = fileList[0].type;var toUser = document.getElementById('toUser').value;if(fileList.length > 0){var fileReader = new FileReader();fileReader.readAsDataURL(fileList[0]);fileReader.onload = function (e) {var socketConfig = {msg: e.target.result,toUser: toUser,code: 2};if (toUser == "" || toUser == null) {socketConfig.type = 0;} else {socketConfig.type = 1;}websocket.send(JSON.stringify(socketConfig));}}} </script> </html>
3.7.3. SocketConfig代码
package com.AttackingApe.demo.webSocket.Pojo;import lombok.Data;/*** @author PengPan* @version 1.0* @date 2020/7/16 15:41*/ @Data public class SocketConfig {//聊天类型 0:群聊 1:私聊private int type;//发送者private String fromUser;//接受者private String toUser;//消息private String msg;//消息类型 1:文本 2:图片private int code; }
3.7.4. 测试结果
# websocket# javascript