您的位置:首页 > 编程语言 > Java开发

WebSocket实现群发和单聊--Springboot实现

2019-06-01 17:56 65 查看

阿里云协同办公工具Teambition 0元试用>>>

一:WebSocket原理

1、要谈WebSocket就不得不提起HTTP连接

    WebSocket是HTML5出的东西(协议,就是大家一起约定好的东西),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)首先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是Websocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充。有交集,但是并不是全部。当然他们都属于网络的7层协议中的应用层。

    另外Html5是指的一系列新的API,或者说新规范,新技术。Http协议本身只有1.0和1.1,而且跟Html本身没有直接关系。
    通俗来说,你可以用HTTP协议传输非Html数据,就是这样。再简单来说,层级不一样。

1ff1

    此外,ws协议是全双工协议,意味着不光服务器向客户端推送,客户端也可以像服务器发送请求。

2、Websocket是什么样的协议,具体有什么优点

    首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。
    1) HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么在HTTP1.0中,这次HTTP请求就结束了。
    在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。
    但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。
    2)首先Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手。在握手阶段是一样的。

    在三次握手进行通信的过程中,Websocket协议要比HTTP协议的握手请求中,多了几个东西。

    

这个就是Websocket的核心了,告诉Apache、Nginx等后端服务器:发起的是Websocket协议,即协议是ws:// 而不是http://。

之后,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:我要验证你是不是真的是Websocket。
然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。
最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。不过现在还好,已经定下来啦~大家都使用的一个东西。
3、Websocket的作用

在讲Websocket之前,我就顺带着讲下 long poll 和 ajax轮询 的原理。
首先是 ajax轮询 ,ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。相当于java代码的while循环,不段的发起请求,如果客户端连接数量过多的话,对服务器压力无疑是个重大的考验。

其次是 long poll,其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(就是请求到服务器后,一直不返回相应,也不超时),直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

通过上面这个例子,我们可以看出,这两种方式都不是最好的方式,需要很多资源。
一种需要更快的速度,一种需要更多的'电话'。这两种都会导致'电话'的需求越来越高。
HTTP还是一个无状态协议。通俗的说就是,服务器因为每天要接待太多客户了,是个健忘鬼,你一挂电话,他就把你的东西全忘光了,把你的东西全丢掉了。你第二次还得再告诉服务器一遍。
所以在这种情况下出现了,Websocket出现了。他解决了HTTP的这几个难题。

首先,被动性,当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦。所以我们其实一直建立者连接,一旦有消息了,服务器就会推送给客户端。

只需要经过一次HTTP请求,就可以做到源源不断的信息传送了。(在程序设计中,这种设计叫做回调,即:你有信息了再来通知我,而不是我傻乎乎的每次跑来问你)
这样的协议解决了上面同步有延迟,而且还非常消耗资源的这种情况。

那么为什么他会解决服务器上消耗资源的问题呢?
其实我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(JAVA等)来处理。
简单地说,我们有一个非常快速的接线员(Nginx),他负责把问题转交给相应的客服(Handler)。
本身接线员基本上速度是足够的,但是每次都卡在客服(Handler)了,老有客服处理速度太慢。,导致客服不够。
Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。
这样就可以解决客服处理速度过慢的问题了。
同时,在传统的方式上,要不断的建立,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁。
虽然接线员很快速,但是每次都要听这么一堆,效率也会有所下降的,同时还得不断把这些信息转交给客服,不但浪费客服的处理时间,而且还会在网路传输中消耗过多的流量/时间。
但是Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。
同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(当然客户端还是等主动发送信息过来的。。),没有信息的时候就交给接线员(Nginx),不需要占用本身速度就慢的客服(Handler)了。

至于怎么在不支持Websocket的客户端上使用Websocket。。答案是:不能。
但是可以通过上面说的 long poll 和 ajax 轮询来模拟出类似的效果。

二:代码实现

    首先是jar包引入,springboot可以很容易的实现websocket协议的配置(springboot版本2.0以上)

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

    接下来就是配置启动文件了

package cn.chinotan.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
* @program: test
* @description: WebSocketConfig启动配置
* @author: xingcheng
* @create: 2019-05-30 19:33
**/
@Configuration
public class WebSocketConfig {

@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}

}

    紧接着就是业务逻辑处理

package cn.chinotan.websocket;

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.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

/**
* @program: test
* @description: WebSocketServer 服务器
* @author: xingcheng
* @create: 2019-05-30 19:34
**/
@ServerEndpoint("/websocket/{sid}")
@Component
@Slf4j
public class WebSocketServer {

/**
* 静态变量,用来记录当前在线连接数。线程安全。
*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static CopyOnWriteArraySet&
ff8
lt;WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();

/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;

/**
* 接收sid
*/
private String sid = "";

/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
// 加入set中
webSocketSet.add(this);
// 在线数加1
addOnlineCount();
log.info("有新窗口开始监听:" + sid + ",当前在线人数为" + getOnlineCount());
this.sid = sid;
sendMessage("连接成功: " + sid);
}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
//从set中删除
webSocketSet.remove(this);
//在线数减1
subOnlineCount();
log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
}

/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到来自窗口" + sid + "的信息:" + message);
//群发消息
for (WebSocketServer item : webSocketSet) {
item.sendMessage(message);
}
}

/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}

/**
* 实现服务器主动推送
*/
public void sendMessage(String message) {
try {
this.session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("消息推送失败");
e.printStackTrace();
}
}

/**
* 群发自定义消息
*/
public static void sendInfo(String message, String sid) {
log.info("推送消息到窗口" + sid + ",推送内容:" + message);
for (WebSocketServer item : webSocketSet) {
//这里可以设定只推送给这个sid的,为null则全部推送
if (sid == null) {
item.sendMessage(message);
} else if (item.sid.equals(sid)) {
item.sendMessage(message);
}
}
}

public static int getOnlineCount() {
return onlineCount.get();
}

public static void addOnlineCount() {
WebSocketServer.onlineCount.addAndGet(1);
}

public static void subOnlineCount() {
WebSocketServer.onlineCount.decrementAndGet();
}
}

可以看到,通过springboot的注解,可以很容易的实现ws协议的编写,类似于controller,并且还有对应的生命周期监听注解

最后服务器端,我们配置一个推送请求,通过平常的http协议编写,模拟服务器想客户端推送过程

package cn.chinotan.controller;

import cn.chinotan.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.List;

/**
* @program: test
* @description: websocket控制器
* @author: xingcheng
* @create: 2019-06-01 15:13
**/
@RestController
@RequestMapping("/webSocket")
public class WebSocketController {

@Autowired
WebSocketServer webSocketServer;

@GetMapping("/server/push/{sid}")
public void push(@PathVariable("sid") Integer sid) {
if (0 == sid) {
WebSocketServer.sendInfo(sid + "
1ff8
:全员注意啦!", null);
} else {
WebSocketServer.sendInfo(sid + ":注意啦! -> " + (sid + 1), String.valueOf(sid + 1));
}
}

}

最后,我们编写客户端的代码,采用html

<html>

<head>
<title>WebSocket测试页面</title>
</head>

<body>
<script>
var socket;
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else{
console.log("您的浏览器支持WebSocket");
let sid = getUrlParam("sid");
//实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接
if (!sid) {
alert("sid不存在")
};
socket = new WebSocket("ws://localhost:9000/websocket/" + sid);
//打开事件
socket.onopen = function() {
console.log("Socket 已打开");
socket.send("这是来自客户端的消息" + location.href + new Date());
};
//获得消息事件
socket.onmessage = function(msg) {
console.log(msg.data);
//发现消息进入    开始处理前端触发逻辑
};
//关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function() {
alert("Socket发生了错误");
//此时可以尝试刷新页面
}
//离开页面时,关闭socket
//jquery1.8中已经被废弃,3.0中已经移除
// $(window).unload(function(){
//     socket.close();
//});
}

function getUrlParam(paraName) {
    var url = document.location.toString();
    var arrObj = url.split("?");

    if (arrObj.length > 1) {
      var arrPara = arrObj[1].split("&");
      var arr;

      for (var i = 0; i < arrPara.length; i++) {
        arr = arrPara[i].split("=");

        if (arr != null && arr[0] == paraName) {
          return arr[1];
        }
      }
      return "";
    }
    else {
      return "";
    }
  }
</script>
</body>
</html>

接下来就可以进行测试了:

首先,我们启动在谷歌浏览器上启动三个窗口,连接地址分别是file:///Users/xingcheng/Downloads/websocket.html?sid=21,file:///Users/xingcheng/Downloads/websocket.html?sid=22,file:///Users/xingcheng/Downloads/websocket.html?sid=23,代表3个不同的客户端

来看服务器和客户端的交互过程

服务器:

客户端:

可以看到,连接的过程全部监听到了

接下来模拟服务器的推送过程:群聊,访问连接:http://localhost:9000/webSocket/server/push/0

服务器:

客户端:

最后,验证单聊:22客户端通过服务器给23发送消息,推送地址:http://localhost:9000/webSocket/server/push/22

服务器:

客户端:

 

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息