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 一样,服务器端只会占用一个端口,但是一个客户端会占用一个端口。