websocket 基本使用

1 websocket基本使用

websocket 是 javax.websocket下面的,不需要任何依赖,直接就可以使用

  @ServerEndpoint 标记声明一个websocket 服务 ,configurator 属性指定 鉴权 配置类,@ServerEndpoint 标记的类 为每个链接会创建一个该对象实例,也就是成员变量这个链接内私有。

  @OnOpen , @OnClose , @OnMessage , @OnError 4个事件方法,对应事件触发的时候调用 (除了@PathParam("path") 标记的参数以外,最多只能有 String message, Session session 两个参数)

  

package com.lomi.websocket;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;



/**
 * 每个连接会创建一个新的 ServerEndpoint 实例,所以成员变量 是当前ServerEndpoint私有的
 * websocket可以 带有用户参数的地方只有 自定义协议 和  PathParam,当然还有message 体
 * 
 * 
 * @author ZHANGYUKUN
 *
 *
 *
 *
 */
@ServerEndpoint(value="/websocket/{path}",configurator = SocketServerConfigurator.class)
@Component
public class SimpleWebSocket {
    private static final Logger logger = LoggerFactory.getLogger(SimpleWebSocket.class);
    
    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;
     
    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
    private static CopyOnWriteArraySet<SimpleWebSocket> webSocketSet = new CopyOnWriteArraySet<SimpleWebSocket>();
     
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    
    private static int i = 0;
     
    /**
     * 连接建立成功调用的方法
     * @param session  可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void onOpen(Session session){
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在线数加1
        logger.warn("有新连接加入!当前在线人数为" + getOnlineCount());
    }
     
    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(){
        webSocketSet.remove(this);  //从set中删除
        subOnlineCount();           //在线数减1    
        logger.warn("有一连接关闭!当前在线人数为" + getOnlineCount());
    }
     
    /**
     * 收到客户端消息后调用的方法
     * @param message 客户端发送过来的消息
     * @param session 可选的参数
     * @throws IOException 
     */
    @OnMessage
    public void onMessage(@PathParam("path") String path, String message, Session session) throws IOException {
        i++;
        logger.warn("来自客户端" + session.getId() + "的path:" + path+i);
        logger.warn("来自客户端" + session.getId() + "的消息:" + message+i);
        sendMessage(message+i);
    }
     
    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error){
        logger.warn("发生错误");
        error.printStackTrace();
    }
     
    /**
     * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
     * @param message
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException{
        this.session.getBasicRemote().sendText(message);
        //this.session.getAsyncRemote().sendText(message);
    }
 
    public static synchronized int getOnlineCount() {
        return onlineCount;
    }
 
    public static synchronized void addOnlineCount() {
        SimpleWebSocket.onlineCount++;
    }
     
    public static synchronized void subOnlineCount() {
        SimpleWebSocket.onlineCount--;
    }
}

配置鉴权类

package com.lomi.websocket;

import javax.servlet.http.HttpServletRequest;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * 鉴权
 * @author ZHANGYUKUN
 *
 */
public class SocketServerConfigurator extends ServerEndpointConfig.Configurator {
	private final Logger logger = LoggerFactory.getLogger(SocketServerConfigurator.class);

	/**
	 * token鉴权认证,我们可以在自定义协议里面带上用户标识,这样就可以识别用户了
	 *
	 * @param originHeaderValue
	 * @return
	 */
	@Override
	public boolean checkOrigin(String originHeaderValue) {
		ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		HttpServletRequest request = servletRequestAttributes.getRequest();
		String url = request.getServletPath();
		
		//请求url中可以带参数
		logger.warn( "请求url" +  url );
		
		//用自定义协议传达参数
		String token = request.getHeader("Sec-WebSocket-Protocol");
		logger.warn("Sec-WebSocket-Protocol是:{}" + token);
		servletRequestAttributes.getResponse().setHeader("Sec-WebSocket-Protocol",   token );
		
		return true;
	}

	/**
	 * Modify the WebSocket handshake response 修改websocket 返回值
	 *
	 * @param sec
	 * @param request
	 * @param response
	 */
	@Override
	public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
		super.modifyHandshake(sec, request, response);

	}
}

  测试客户端html代码:

    websocket地址格式:ws://localhost/te/websocket/pathParam/ ,他们的协议 是 ws ,如果使用安全套接字证书就是wss://localhost/te/websocket/pathParam/,类似 http 和 https

    客户端创建 websocket 的时候可以指定私有协议: new WebSocket("ws://localhost/te/websocket/pathParam",["token"])

    客户端创建 websocket 支持 pathParam: new WebSocket("ws://localhost/te/websocket/pathParam/?id=1")

    客户端创建 websocket 的时候可以指定请求参数: new WebSocket("ws://localhost/te/websocket/pathParam/1")

    当然这些参数都需要服务器端支持

<!DOCTYPE HTML>
<html>
  <head>
    <title>My WebSocket</title>
  </head>
   
  <body>
    Welcome<br/>
    <input id="text" type="text" /><button onclick="send()">Send</button>    <button onclick="closeWebSocket()">Close</button>
    <div id="message">
    </div>
  </body>
   
  <script type="text/javascript">
      var websocket = null;
       
      //判断当前浏览器是否支持WebSocket
      if('WebSocket' in window){
          //使用子协议 websocket = new WebSocket("ws://localhost/te/websocket/pathParam/",["token"]);
          websocket = new WebSocket("ws://localhost/te/websocket/pathParam/");
      }
      else{
          alert('Not support websocket')
      }
       
      //连接发生错误的回调方法
      websocket.onerror = function(){
          setMessageInnerHTML("error");
      };
       
      //连接成功建立的回调方法
      websocket.onopen = function(event){
          setMessageInnerHTML("open");
      }
       
      //接收到消息的回调方法
      websocket.onmessage = function(event){
          setMessageInnerHTML(event.data);
      }
       
      //连接关闭的回调方法
      websocket.onclose = function(){
          setMessageInnerHTML("close");
      }
       
      //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
      window.onbeforeunload = function(){
          websocket.close();
      }
       
      //将消息显示在网页上
      function setMessageInnerHTML(innerHTML){
          document.getElementById('message').innerHTML += innerHTML + '<br/>';
      }
       
      //关闭连接
      function closeWebSocket(){
          websocket.close();
      }
       
      //发送消息
      function send(){
          var message = document.getElementById('text').value;
          websocket.send(message);
      }
  </script>
</html>

  



 

2 websocket在使用内嵌容器的spring中使用需要指定WebsocketExporter,否者不能识别 websocket

  需要注意的是  @ServerEndpoint 标记的 类不要被AOP 切到,否者或异常

package com.lomi.websocket;

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

/**
 * 
 * @author ZHANGYUKUN
 *
 */
@Configuration
public class WebsocketExporter {
	
	/**
	 * 必须有这个 @ServerEndpoint 才生效,ServerEndpointExporter 会把检查并注册 @ServerEndpoint (如果不是内嵌的tomcat,是外置的tomcat,不需要这个)
	 * @return
	 */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

  

 

3 Sec-WebSocket-Protocol 自定义协议参数传递

  客户端如果带了Sec-WebSocket-Protocol参数,服务器端响应必须带上同样的Sec-WebSocket-Protocol值才能正常的建立链接。否者服务器端的连接会立马断开。

 

 

4 性能使用就meter测试,本地机器可以轻松支持2000线程,10条消息/线程/秒 的请求。(线程多余3000,请求每秒一条消息都卡,感觉是window端口的问题,按理说一个客户端请求占一个端口,window 端口有6W多个,不应该卡才对,可可能是就jmeter的限制,线程多了就卡死)

  2000线程,每秒10 条,应该远远不是 我本机 websocket 的性能瓶颈,目前测试的瓶颈应该是 客户端限制的,这时候 java程序的cpu占用,内存占用都不高。

  如果直接用nginx 做websocket  service 代理,明显会受到 客户端端口的限制(LVS 的 三种负载均衡策略给出了解决方案(DR模式)

  

 

备注:和普通tcp socket 一样,服务器端只会占用一个端口,但是一个客户端会占用一个端口。

 

posted on 2022-04-20 12:39  zhangyukun  阅读(450)  评论(0编辑  收藏  举报

导航