websocket是一种在单个TCP连接上进行全双工通信的协议。websocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在websocket API中浏览器和服务器只需要完成一次握手,两者之间就可以直接创建持久性的连接,并进行双向数据传输。
全双工是通讯传输的一个术语。通信允许数据在两个方面上同时传输,它在能力上相当于两个单工通信方式的结合。全双工指可以同时进行信号的双向传输(A—>B的同时B—>A)。单工指:只允许A向B传送信息,而B不能向A传送。
WebSocket对象提供了用户创建和管理WebSocket连接,以及通过该连接发送和接收数据的API。
WebSocket(url [, protocols]);
SECURITY_ERR:正在尝试连接的端口被阻止。
使用addEventListener()监听或将一个事件监听器赋值给本接口的oneventname属性,来监听下面的事件:
本文使用springboot+freemarker+websocket实现代码
<?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>
server:
port: 20713
tomcat:
uri-encoding: UTF-8
spring:
freemarker:
suffix: .ftl
charset: UTF-8
content-type: text/html
http:
encoding:
charset: UTF-8
force: true
enabled: true
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 {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
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);
}
}
}
实现内容:连接websocket的用户都可以进行聊天并显示所有人聊天以及对应的昵称。
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;
@OnOpen
public 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());
}
@OnClose
public void onClose(){
clients.remove(this);
log.info("有用户断开连接,当前人数为:{}", clients.size());
}
@OnMessage
public void onMessage(String message, Session session, @PathParam("nickname") String nickname){
log.info("来自客户端:{}发来的消息:{}", nickname, message);
broadcast(nickname + ":" + message);
}
@OnError
public 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);
}
}
}
<!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 onclick="conectWebSocket()">连接WebSocket</button>
<button onclick="closeWebSocket()">断开连接</button>
<hr />
<br />
消 息:<input id="text" type="text" />
<button onclick="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;
}
//判断当前浏览器是否支持WebSocket
if ('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 + '
';
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
//发送消息
function send() {
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</html>
实现内容:A可以在公屏上输入内容,大家都可以看见。也可以选择输入频道,只和每一个人聊天。
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;
@OnOpen
public 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());
}
@OnClose
public void onClose(){
clients.remove(this);
log.info("有用户断开连接,当前人数为:{}", clients.size());
}
@OnMessage
public 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();
}
}
@OnError
public 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);
}
}
}
<!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 onclick="conectWebSocket()">连接WebSocket</button>
<button onclick="closeWebSocket()">断开连接</button>
<hr />
<br />
消 息:<input id="text" type="text" />
频道号:<input id="toUser" type="text"/>
<button onclick="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;
}
//判断当前浏览器是否支持WebSocket
if ('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 + '
';
}
//关闭连接
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>
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;
}
实现内容:聊天的时候可以传入图片。
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;
@OnOpen
public 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());
}
@OnClose
public void onClose(){
clients.remove(this);
log.info("有用户断开连接,当前人数为:{}", clients.size());
}
@OnMessage
public 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();
}
}
@OnError
public 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);
}
}
}
<!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 onclick="conectWebSocket()">连接WebSocket</button>
<button onclick="closeWebSocket()">断开连接</button>
<hr />
<br />
消 息:<input id="text" type="text" />
频道号:<input id="toUser" type="text"/>
<button onclick="send()">发送消息</button>
<input type="file" id="file" onchange="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;
}
//判断当前浏览器是否支持WebSocket
if ('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 + '
';
}
//将图片消息显示在网页上
function setIconInnerHTML(innerHTML) {
document.getElementById('message').innerHTML = document.getElementById('message').innerHTML + '
' + '
';
}
//关闭连接
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>
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;
}