spring boot中使用websocket实现点对点通信与服务器推送

WebSocket介绍

   websocket是html中一种新的协议,它实现了真正的长连接,实现了浏览器与服务器的全双工通信(指在通信的任意时刻,线路上存在A到B和B到A的双向信号传输)。 现在我们接触的协议大多是htttp协议,在浏览器中通过http协议实现了单向的通信,浏览器发出请求,服务器在响应,一次客户端与服务器的请求就结束了,服务器不能主动响应客户端,主动往客户端返回数据,而在某些需求上要实时刷新数据,获取服务器上的最新数据,显示给客户端。为了实现这样的需求,大多数公司使用了轮询的技术。轮询技术,在特定的时间间隔(如1秒)由浏览器发出http request,服务器再将最新数据返回给浏览器,实现了数据的实时刷新,很明显,通过这种技术实现的伪长连接,存在着一些缺陷,每隔一段时间的http request,不见得每一次的请求都是有意义的,因为客户端不会知道服务器上的数据有没有更新,这样在多次请求当中肯定会存在无效的请求(上一次请求回来的数据跟本次的完全一样)。 可见轮询这种技术,存在很大的弊端,而websocket实现了真正的长连接,服务器可以主动向客户端发送数据,正是这样的特点,就能很好的实现这种需求,当服务器有数据变化时,服务器就可以将新的数据返回给客户端,没有无效的请求回复

环境配置和工具

STS     ,JDK1.8,     Spring boot 2.0.4

新建spring boot项目,在pom.xml文件中加入WebSocket依赖

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
        <!-- websocket依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

WebSocket配置类

**
 * 使用@ServerEndpoint创立websocket endpoint
 * 首先要注入 ServerEndpointExporter,
 * 这个bean会自动注册使用了 @ServerEndpoint 注
 * 解声明的 Web Socket endpoint。
 * 要注意,如果使用独立的 Servlet 容器,
 * 而不是直接使用 Spring Boot 的内置容器,
 * 就不要注入 ServerEndpointExporter,
 * 因为它将由容器自己提供和管理
 */
@Configuration
public class WebSocketConfig{
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

注意!!!这里要说明一下,由于我们是点对点通信,不同于广播式通信的是,必须要区分不同的客户端,那么我们怎么来让服务端区分出不同的客户端呢?

经过查找资料和试验,我找到了两种可行获取客户端userId的方法。

一种是通过在Server取HttpSession中的值获取当前用户

一种是直接在客户端建立连接时附带上用户的值。

先说第一种,新建一个MyEndpointConfigure类,代码如下,

/**
 * 
 * @author lipengbin
 *
 */
public class MyEndpointConfigure extends ServerEndpointRegistration.Configurator implements ApplicationContextAware
{
    private static volatile BeanFactory context;

    @Override
    public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException
    {
         return context.getBean(clazz);
    }
 
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
    {
        System.out.println("auto load"+this.hashCode());
        MyEndpointConfigure.context = applicationContext;
    }
}

然后在配置类里面添加如下代码,用来向spring注册服务

@Bean
    public MyEndpointConfigure newConfigure()
    {
        return new MyEndpointConfigure();
    }

最后我们编写WebSocket服务端的类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
 
/**
 * Created by jack on 2017/10/25.
 */
/**
 * websocket的具体实现类
 */
@ServerEndpoint(value = "/websocket",configurator = MyEndpointConfigure.class)
@Component
public class WebSocketServer {
	
    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;
    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    String str="";	
    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session,EndpointConfig config) {
        this.session = session;
        HttpSession httpSession= (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在线数加1
        try {
            //sendMessage(CommonConstant.CURRENT_WANGING_NUMBER.toString());
            sendMessage("服务端连接成功");
        } catch (IOException e) {
            System.out.println("IO异常");
        }
    }
 
    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);  //从set中删除
        subOnlineCount();   
       
        System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
      
    }
 
    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("来自客户端的消息:" + message);
        str=message;
//        System.out.println("onMessage sessionId is : "+session.getId());
        //群发消息
//        for (WebSocketServer item : webSocketSet) {
//            try {                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
//                item.sendMessage(message);
//            } catch (IOException e) {
//                e.printStackTrace();
//            }
//        }
    }
 
    /**
     * 发生错误时调用
     */
    @OnError
    public void onError(Session session, Throwable error) {
    	for (WebSocketServer item : webSocketSet) {
            try {
                item.sendMessage("响应超时");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        error.printStackTrace();
    }
 
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText("服务端消息:"+message);
        //this.session.getAsyncRemote().sendText(message);
    }
 
    /**
     * 群发自定义消息
     */
    public static void sendInfo(String message) throws IOException {
        for (WebSocketServer item : webSocketSet) {
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                continue;
            }
        }
    }
 
    public static synchronized int getOnlineCount() {
        return onlineCount;
    }
 
    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }
 
    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }
}

注意这一行

@ServerEndpoint(value = "/websocket",configurator = MyEndpointConfigure.class)

加上了它。我们就可以用

@OnOpen
        public void onOpen(Session session, EndpointConfig config){
            HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
            }

来获取httpSession对象了,然后直接取出登录时存储的用户对象就可以了。

但是!!!这么做有问题啊

原则上来讲我在server获取的httpsession中取出来的用户对象就是现在正和服务端建立连接的对象,因为这种情况的操作肯定是先登录,然后直接建立连接,可是在实际中多用户同时登录时就不一定是这样子了。因为登录是客户端发起的操作,建立连接也是客户端发起的操作,且不说在客户端这两个操作是否是紧密相连,就算是紧密相连,从服务器验证成功(此时已经放入currentUser对象)返回登录结果给客户端到客户端向服务端发起连接这中间因为网络原因也是会消耗一定时间的。那么这时候一件尴尬的事情就发生了:此时,另一个用户也在登录,并且在之前用户两个操作期间完成了登录验证操作,那么第一个用户连接建立之后取出的use对象就不是这个用户的而是第二个用户的,这就乱套了。这种方法相当于是 ,用户A先对服务器说,记住了,我叫A,然后过了一会儿来说,我要建立连接,我是刚刚告诉你名字那个人。那如果B在A离开那会儿也告诉了服务器我叫B,那么服务器就会把A当成B了。

但是,哎,不慌!我们还有planB呢

服务端可以用@PathParam获取用户对象,如此这般,用户在建立WebSocket连接的时候告诉服务器自己的用户id,这样服务器就肯定不会把用户搞错了。

    服务端注解的地方改成这样写

@ServerEndpoint(value = "/websocket/{userId}")

    建立连接的方法参数

 @OnOpen
    //public void onOpen(Session session,EndpointConfig config) {
    public void onOpen(@PathParam("userId")String userId,Session session){

        this.session = session;
        String[] userArray = userId.split(",");
        this.userid = userArray[0];
        webSocketMap.put(userid, this);
        
        addOnlineCount();           //在线数加1
       
		send("服务端连接成功", this.userid);
		System.out.println("服务端连接成功");
    }

    客户端建立连接时的请求如下

ws = "ws://localhost:8080"  + "/websocket"+"/${userId}";

    userId就是你的用户id,可以从session中获取。使用这种方法获取用户id,也就不用再配置类里添加下面这段代码了

@Bean
    public MyEndpointConfigure newConfigure()
    {
        return new MyEndpointConfigure();
    }

以上,实现了一个websocket作为服务端,html页面作为客户端的一个websocket连接的例子,下面介绍java作为客户端的例子。

spring boot项目引入依赖

<dependency>
	 <groupId>org.java-websocket</groupId>
	 <artifactId>Java-WebSocket</artifactId>
	 <version>1.3.4</version>
	 <scope>test</scope>
</dependency>

建立一个测试类

import org.java_websocket.WebSocket;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
 

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
 
/**
 * Created by jack on 2018.9.25
 */
public class WebsocketClient {
    public static WebSocketClient client;
    public static void main(String[] args) throws InterruptedException {
        try {
            client = new WebSocketClient(new URI("ws://localhost:8081/websocket"),new Draft_6455()) {
        	//client = new WebSocketClient(new URI("ws://192.168.87.59:80/websocket"),new Draft_6455()) {
                @Override
                public void onOpen(ServerHandshake serverHandshake) {
                    System.out.println("打开链接");
                }
 
                @Override
                public void onMessage(String s) {
                    System.out.println("收到消息"+s);
                }
 
                @Override
                public void onClose(int i, String s, boolean b) {
                    System.out.println("链接已关闭");
                }
 
                @Override
                public void onError(Exception e) {
                    e.printStackTrace();
                    System.out.println("发生错误已关闭");
                }
            };
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
 
        client.connect();
        System.out.println(client.getDraft());
       while(!client.getReadyState().equals(WebSocket.READYSTATE.OPEN)){
        System.out.println("还没有打开");
        Thread.sleep(1000);
       }
        
        try {
        	for(int i=0;i<1000;i++){
        		String str = "打开了"+i;
        		System.out.println(str);
        		send("hello world".getBytes("utf-8"));
        		Thread.sleep(3000);
        		client.send(str);
        	}
            
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        
       
       }
    
 
 
    public static void send(byte[] bytes){
        client.send(bytes);
    }
 
}

这里我还是用的第一种获取session的方法,改成第二种只需要在建立连接的时候后面加上Id即可。

运行结果如下

 

参考:https://blog.csdn.net/wonderful_life_mrchi/article/details/52535463

           https://blog.csdn.net/qq_33171970/article/details/55001587

https://blog.csdn.net/j903829182/article/details/78342941

           

猜你喜欢

转载自blog.csdn.net/weixin_40693633/article/details/82838129