WebSocket

一.简单介绍

 

  随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。

 

  我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)Comet技术。其实后者本质上也是一种轮询,只不过有所改进。

 

  轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。

 

  Comet技术又可以分为长轮询流技术长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。

 

  这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。

 

  伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过JavaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。

SSM实现https://www.tpyyes.com/a/javaweb/2017/0119/41.html

springboot实现https://blog.csdn.net/zhangdehua678/article/details/78913839/

                     https://www.jianshu.com/p/409307982ef1 

 

编码实现


一、在SpringBoot中,添加WebSocket的配置

package com.yuanju.demo.config;


import com.yuanju.demo.interceptor.WebSocketInterceptor;
import com.yuanju.demo.utils.MyHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;


//实现接口来配置Websocket请求的路径和拦截器。
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //handler是webSocket的核心,配置入口

        registry.addHandler(new MyHandler(), "/myHandler/{ID}").setAllowedOrigins("*").addInterceptors(new WebSocketInterceptor());

    }


}


1.@Configuration:注解标识该类为Spring的配置类

2.@EnableWebSocket:开启注解接收和发送消息

3.实现WebSocketConfigurer接口,重写registerWebSocketHandlers方法,这是一个核心实现方法,配置websocket入口,允许访问的域、注册Handler、定义拦截器。客户端通过“/myHandler/{ID}”直接访问Handler核心类,进行socket的连接、接收、发送等操作,这里由于还加了个拦截器,所以建立新的socket访问时,都先进来拦截器再进去Handler类,“new WebSocketInterceptor()”是我实现的拦截器,“new MyHandler()”是我实现的一个Handler类。

二、WebSocketInterceptor拦截器的实现:

package com.yuanju.demo.interceptor;

import java.util.Map;

import javax.servlet.http.HttpSession;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

public class WebSocketInterceptor implements HandshakeInterceptor {

    //进入hander之前的拦截
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
        if (request instanceof ServletServerHttpRequest) {
            String ID = request.getURI().toString().split("ID=")[1];
            System.out.println("当前session的ID=" + ID);
            ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request;
            HttpSession session = serverHttpRequest.getServletRequest().getSession();
            map.put("WEBSOCKET_USERID", ID);
        }
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
        System.out.println("进来webSocket的afterHandshake拦截器!");
    }
}


1.实现了HandshakeInterceptor 接口,并实现了beforeHandshake该方法,该方法是在进入Handler核心类之前进行拦截。

这里主要实现的逻辑是:

截取客户端建立webSocket连接时发送的URL地址字符串,并通过对该字符串进行特殊标识截取操作,获取客户端发送的唯一标识(由自己定义的,一般是系统用户ID唯一标识,用以标识该用户),并把它以键值对的形式放到Session里,这样后期可以通过该session获取它对应的用户ID了。【一个session对应着一个webSocketSession】

三、MyHandler核心类的实现

package com.yuanju.demo.utils;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import net.sf.json.JSONObject;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;


@Service
public class MyHandler implements WebSocketHandler {

    //在线用户列表
    private static final Map<String, WebSocketSession> users;
    
    static {
        users = new HashMap<>();
    }

    //新增socket
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("成功建立连接");
        String ID = session.getUri().toString().split("ID=")[1];
        System.out.println(ID);
        if (ID != null) {
            users.put(ID, session);
            session.sendMessage(new TextMessage("成功建立socket连接"));
            System.out.println(ID);
            System.out.println(session);
        }
        System.out.println("当前在线人数:" + users.size());
    }

    //接收socket信息
    @Override
    public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
        try {
            JSONObject jsonobject = JSONObject.fromObject(webSocketMessage.getPayload());
            System.out.println(jsonobject.get("id"));
            System.out.println(jsonobject.get("message") + ":来自" + (String) webSocketSession.getAttributes().get("WEBSOCKET_USERID") + "的消息");
            sendMessageToUser(jsonobject.get("id") + "", new TextMessage("服务器收到了,hello!"));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 发送信息给指定用户
     *
     * @param clientId
     * @param message
     * @return
     */
    public boolean sendMessageToUser(String clientId, TextMessage message) {
        if (users.get(clientId) == null) return false;
        WebSocketSession session = users.get(clientId);
        System.out.println("sendMessage:" + session);
        if (!session.isOpen()) return false;
        try {
            session.sendMessage(message);
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 广播信息
     *
     * @param message
     * @return
     */
    public boolean sendMessageToAllUsers(TextMessage message) {
        boolean allSendSuccess = true;
        Set<String> clientIds = users.keySet();
        WebSocketSession session = null;
        for (String clientId : clientIds) {
            try {
                session = users.get(clientId);
                if (session.isOpen()) {
                    session.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
                allSendSuccess = false;
            }
        }

        return allSendSuccess;
    }


    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        if (session.isOpen()) {
            session.close();
        }
        System.out.println("连接出错");
        users.remove(getClientId(session));
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        System.out.println("连接已关闭:" + status);
        users.remove(getClientId(session));
    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

    /**
     * 获取用户标识
     *
     * @param session
     * @return
     */
    private Integer getClientId(WebSocketSession session) {
        try {
            Integer clientId = (Integer) session.getAttributes().get("WEBSOCKET_USERID");
            return clientId;
        } catch (Exception e) {
            return null;
        }
    }
}


1.实现了WebSocketHandler接口,并实现了关键的几个方法。

① afterConnectionEstablished(接口提供的):建立新的socket连接后回调的方法。主要逻辑是:将成功建立连接的webSocketSssion放到定义好的常量[private static final Map<String, WebSocketSession> users;]中去。这里也截取客户端访问的URL的字符串,拿到标识,以键值对的形式讲每一个webSocketSession存到users里,以记录每个Socket。

② handleMessage(接口提供的):接收客户端发送的Socket。主要逻辑是:获取客户端发送的信息。这里之所以可以获取本次Socket的ID,是因为客户端在第一次进行连接时,拦截器进行拦截后,设置好ID,这样也说明,双方在相互通讯的时候,只是对第一次建立好的socket持续进行操作。

③ sendMessageToUser(自己定义的):发送给指定用户信息。主要逻辑是:根据用户ID从常量users(记录每一个Socket)中,获取Socket,往该Socket里发送消息,只要客户端还在线,就能收到该消息。

④sendMessageToAllUsers (自己定义的):这个广播消息,发送信息给所有socket。主要逻辑是:跟③类型,只不过是遍历整个users获取每一个socket,给每一个socket发送消息即可完广播发送

⑤handleTransportError(接口提供的):连接出错时,回调的方法。主要逻辑是:一旦有连接出错的Socket,就从users里进行移除,有提供该Socket的参数,可直接获取ID,进行移除。这个在客户端没有正常关闭连接时,会进来,所以在开发客户端时,记得关闭连接

⑥afterConnectionClosed(接口提供的):连接关闭时,回调的方法。主要逻辑:一旦客户端/服务器主动关闭连接时,将个socket从users里移除,有提供该Socket的参数,可直接获取ID,进行移除。

 

后台的开发就开发完了,大家有没有发现比基于STOMP协议实现要灵活得多?

 

四、客户端页面的实现【基于H5】

不需要加入任何的JS包

<!DOCTYPE html>
<html>
  <head>
    <title>socket.html</title>
	
    <meta name="keywords" content="keyword1,keyword2,keyword3">
    <meta name="description" content="this is my page">
    <meta name="content-type" content="text/html" charset="UTF-8">
    <!--<link rel="stylesheet" type="text/css" href="./styles.css">-->
 
  </head>
  
  <body>
	
	Welcome<br/>
<input id="text" type="text" /><button onclick="send()">Send</button>    <button onclick="closeWebSocket()">Close</button>
<div id="message">
</div>
    <!-- 公共JS -->
    <script type="text/javascript" src="../webSocket/jquery.min.js"></script>
 
	
<script type="text/javascript">
var userID="888";
var websocket=null;
 $(function() {
  
  //创建WebSocket
  connectWebSocket();
  })
  
   //强制关闭浏览器  调用websocket.close(),进行正常关闭
 window.onunload = function() {
   	 
   //关闭连接   
   closeWebSocket();    
    }
  
  //建立WebSocket连接
  function connectWebSocket(){
  	
	console.log("开始...");
      
      //建立webSocket连接
       websocket = new WebSocket("ws://127.0.0.1:9091/cloud-sbjm/myHandler/ID="+userID);
      
      //打开webSokcet连接时,回调该函数
       websocket.onopen = function () {      
        console.log("onpen");  
       }
       
       //关闭webSocket连接时,回调该函数
       websocket.onclose = function () {
       //关闭连接    
        console.log("onclose");
       }
	
	   //接收信息
       websocket.onmessage = function (msg) {
        console.log(msg.data);
       }
  }
  
  //发送消息
  function send(){
     var postValue={};
     postValue.id=userID;
     postValue.message=$("#text").val(); 	 	
     websocket.send(JSON.stringify(postValue));
  }
  //关闭连接
  function closeWebSocket(){
  	 if(websocket != null) {
            websocket.close();
        }
  }
 
</script>
  </body>
</html>

页面比较简单,简单解释一下:

1.new WebSocket("ws://127.0.0.1:9091/cloud-sbjm/myHandler/ID="+userID),与服务器建立webSocket连接,后面的ID="+userID,是动态参数,跟服务器配置Handler的访问地址时对应"/myHandler/{ID}"。

2.H5也提供多个回调函数

onopen:打开webSokcet连接时,回调该函数

onclose:关闭webSocket连接时,回调该函数

onmessage:服务器给该socket发送消息时,回调该函数,获取消息

websocket.send(JSON.stringify(postValue));:给Socket发送消息,服务器获取

websocket.close();客户端主要关闭连接,会触发客户端的onclose方法和服务器的afterConnectionClosed方法

 

 

到此服务端的开发也完成了,下面执行一下程序效果图:

一、建立连接

客户端:

服务器:

 

二、发送消息

客户端:

服务器:

 

三、服务器主动推送消息

服务器代码:

 

到此已经完成了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值