WebSocket


WebSocket

什么是WebSocket

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

实现原理

在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” 。在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

优点

在以前的消息推送机制中,用的都是 Ajax 轮询(polling),在特定的时间间隔由浏览器自动发出请求,将服务器的消息主动的拉回来,这种方式是非常消耗资源的,因为它本质还是http请求,而且显得非常笨拙。而WebSocket 在浏览器和服务器完成一个握手的动作,在建立连接之后,服务器可以主动传送数据给客户端,客户端也可以随时向服务器发送数据。

WebSocket 和 Socket 区别

WebSocket:

  1. websocket通讯的建立阶段是依赖于http协议的。最初的握手阶段是http协议,握手完成后就切换到websocket协议,并完全与http协议脱离了。
  2. 建立通讯时,也是由客户端主动发起连接请求,服务端被动监听。
  3. 通讯一旦建立连接后,通讯就是“全双工”模式了。也就是说服务端和客户端都能在任何时间自由得发送数据,非常适合服务端要主动推送实时数据的业务场景。
  4. 交互模式不再是“请求-应答”模式,完全由开发者自行设计通讯协议。
  5. 通信的数据是基于“帧(frame)”的,可以传输文本数据,也可以直接传输二进制数据,效率高。当然,开发者也就要考虑封包、拆包、编号等技术细节。

Socket:

  1. 服务端监听通讯,被动提供服务;客户端主动向服务端发起连接请求,建立起通讯。

  2. 每一次交互都是:客户端主动发起请求(request),服务端被动应答(response)。

  3. 服务端不能主动向客户端推送数据。

  4. 通信的数据是基于文本格式的。二进制数据(比如图片等)要利用base64等手段转换为文本后才能传输。

1、依赖

<!--SpringBoot整合WebSocket-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!--json转换工具 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.49</version>
</dependency>

2、服务端

(1)配置类

@Component
public class WebSocketConfig {

    /**
     * ServerEndpointExporter 作用
     *
     * 这个Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint
     *
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

(2)具体代码实现

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.ConcurrentHashMap;

/**
 *
 * @ServerEndpoint 这个注解有什么作用?
 *
 * 这个注解用于标识作用在类上,它的主要功能是把当前类标识成一个WebSocket的服务端
 * 注解的值用户客户端连接访问的URL地址
 *
 */

@Slf4j
@Component
@ServerEndpoint("/websocket/{name}")
public class WebSocket {

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

    /**
     * 标识当前连接客户端的用户名
     */
    private String name;

    /**
     *  用于存所有的连接服务的客户端,这个对象存储是安全的
     */
    private static ConcurrentHashMap<String,WebSocket> webSocketSet = new ConcurrentHashMap<>();


    @OnOpen
    public void OnOpen(Session session, @PathParam(value = "name") String name){
        this.session = session;
        this.name = name;
        // name是用来表示唯一客户端,如果需要指定发送,需要指定发送通过name来区分
        webSocketSet.put(name,this);
        log.info("[WebSocket] 连接成功,当前连接人数为:={}",webSocketSet.size());
    }


    @OnClose
    public void OnClose(){
        webSocketSet.remove(this.name);
        log.info("[WebSocket] 退出成功,当前连接人数为:={}",webSocketSet.size());
    }

    @OnMessage
    public void OnMessage(String message){
        log.info("[WebSocket] 收到消息:{}",message);
        //判断是否需要指定发送,具体规则自定义
        if(message.indexOf("TOUSER") == 0){
            String name = message.substring(message.indexOf("TOUSER")+6,message.indexOf(";"));
            AppointSending(name,message.substring(message.indexOf(";")+1,message.length()));
        }else{
            GroupSending(message);
        }

    }

    /**
     * 群发
     * @param message
     */
    public void GroupSending(String message){
        for (String name : webSocketSet.keySet()){
            try {
                webSocketSet.get(name).session.getBasicRemote().sendText(message);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    /**
     * 指定发送
     * @param name
     * @param message
     */
    public void AppointSending(String name,String message){
        try {
            webSocketSet.get(name).session.getBasicRemote().sendText(message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

(3)使用

@RestController
@RequestMapping("/socket")
public class SocketController {

    @Resource
    private WebSocket webSocket;

    @GetMapping("/send")
    public void send(){
        HashMap<String, String> map = new HashMap<>();
        map.put("key1","value1");
        map.put("key2","value2");

        String s = JSON.toJSONString(map);

        //群发信息
        webSocket.GroupSending(s);
    }
}

3、HTML+JS 实现客户端

<body>
    <input type="button" name="WebSocket" onclick="f()" value="WebSocket连接">

<script>
    function f() {
   var websocket = null;
        var host = document.location.host;
        var username = "rewind";

        // console.log(username)
        //判断当前浏览器是否支持WebSocket
        if ('WebSocket' in window) {
            console.log("浏览器支持Websocket"+username)
            //  ws://localhost:8080/websocket/rewind
            websocket = new WebSocket('ws://'+host+'/websocket/'+username);
        } else {
            console.log('当前浏览器 Not support websocket')
        }

        //连接发生错误的回调方法
        websocket.onerror = function() {
            console.log("WebSocket连接发生错误")
            setMessageInnerHTML("WebSocket连接发生错误");
        };

        //连接成功建立的回调方法
        websocket.onopen = function() {
            console.log("WebSocket连接成功")
            setMessageInnerHTML("WebSocket连接成功");
        }

        //接收到消息的回调方法
        websocket.onmessage = function(event) {
            console.log("接收到消息的回调方法")
            console.log("这是后台推送的消息:"+event.data);
            //websocket.close();
            //console.log("webSocket已关闭!")
        }

        //连接关闭的回调方法
        websocket.onclose = function() {
            setMessageInnerHTML("WebSocket连接关闭");
        }

        //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
        window.onbeforeunload = function() {
            closeWebSocket();
        }

        //关闭WebSocket连接
        function closeWebSocket() {
            websocket.close();
        }

        //将消息显示在网页上
        function setMessageInnerHTML(innerHTML) {
            document.getElementById('message').innerHTML += innerHTML + '<br/>';
        }
    }
</script>
</body>
document.location 这个对象包含了当前URL的信息
location.host 获取port号
location.hostname 设置或获取主机名称
location.href 设置或获取整个URL
location.port设置或获取URL的端口号
location.search 设置或获取href属性中跟在问号后面的部分
-------------------------------------------------------------------------------
js中window.location的应用
URL即:统一资源定位符 (Uniform Resource Locator, URL)
完整的URL由这几个部分构成:
scheme://host:port/path?query#fragment
scheme:通信协议
常用的http,ftp,maito等
host:主机
服务器(计算机)域名系统 (DNS) 主机名或 IP 地址。
port:端口号
整数,可选,省略时使用方案的默认端口,如http的默认端口为80。
path:路径
由零或多个"/"符号隔开的字符串,一般用来表示主机上的一个目录或文件地址。
query:查询
可选,用于给动态网页(如使用CGI、ISAPI、PHP/JSP/ASP/ASP.NET等技术制作的网页)传递参数,可有多个参数,用"&"符号隔开,每个参数的名和值用"="符号隔开。
fragment:信息片断
字符串,用于指定网络资源中的片断。例如一个网页中有多个名词解释,可使用fragment直接定位到某一名词解释。(也称为锚点.)

4、后台实现客户端

(1)依赖

<!--websocket作为客户端-->
<dependency>
    <groupId>org.java-websocket</groupId>
    <artifactId>Java-WebSocket</artifactId>
    <version>1.3.5</version>
</dependency>

(2)配置类

import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.net.URI;

/**
 * @Description: 配置websocket后台客户端
 */
@Slf4j
@Component
public class WebSocketConfig {

    @Bean
    public WebSocketClient webSocketClient() {
        try {
            WebSocketClient webSocketClient = new WebSocketClient(new URI("ws://localhost:8085/websocket/test"),new Draft_6455()) {
                @Override
                public void onOpen(ServerHandshake handshakedata) {
                    log.info("[websocket] 连接成功");
                }

                @Override
                public void onMessage(String message) {
                    log.info("[websocket] 收到消息={}",message);

                }

                @Override
                public void onClose(int code, String reason, boolean remote) {
                    log.info("[websocket] 退出连接");
                }

                @Override
                public void onError(Exception ex) {
                    log.info("[websocket] 连接错误={}",ex.getMessage());
                }
            };
            webSocketClient.connect();
            return webSocketClient;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

(3)客户端发送消息

1、首先写一个接口,里面有指定发送和群发消息两个方法。

2、实现发送的接口,区分指定发送和群发由服务端来决定(小编在服务端写了,如果带有TOUSER标识的,则代表需要指定发送给某个websocket客户端)

3、最后采用get方式用浏览器请求,也能正常发送消息

public interface WebSocketService {

    /**
     * 群发
     * @param message
     */
     void groupSending(String message);

    /**
     * 指定发送
     * @param name
     * @param message
     */
     void appointSending(String name,String message);
}
import org.java_websocket.client.WebSocketClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Description: websocket接口实现类
 */
@Component
public class ScoketClient implements WebSocketService{

    @Autowired
    private WebSocketClient webSocketClient;

    @Override
    public void groupSending(String message) {
        // 这里我加了6666-- 是因为我在index.html页面中,要拆分用户编号和消息的标识,只是一个例子而已
        // 在index.html会随机生成用户编号,这里相当于模拟页面发送消息
        // 实际这样写就行了 webSocketClient.send(message)
        webSocketClient.send(message+"---6666");
    }

    @Override
    public void appointSending(String name, String message) {
        // 这里指定发送的规则由服务端决定参数格式
        webSocketClient.send("TOUSER"+name+";"+message);
    }
}
import com.example.socket.code.ScoketClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Description: 测试后台websocket客户端
 */
@RestController
@RequestMapping("/websocket")
public class IndexController {

    @Autowired
    private ScoketClient webScoketClient;

    @GetMapping("/sendMessage")
    public String sendMessage(String message){
        webScoketClient.groupSending(message);
        return message;
    }
}

5、单元测试报错

在SpringBoot项目中集成了WebSocket,在进行单元测试的时候,出现了以下错误:

javax.websocket.server.ServerContainer not available

SpringBootTest在启动的时候不会启动服务器,所以WebSocket自然会报错,这个时候需要添加选项webEnvironment,以便提供一个测试的web环境。如下:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

  目录