思考:像这样的消息功能怎么实现? 如果网页不刷新,服务端有新消息如何推送到浏览器? 解决方案,采用轮询的方式。即:通过js不断的请求服务器,查看是否有新数据,如果有,就获取到新数据。 这种解决方法是否存在问题呢? 当然是有的,如果服务端一直没有新的数据,那么js也是需要一直的轮询查询数据,这就是一种资源的浪费。 那么,有没有更好的解决方案? 有!那就是采用WebSocket技术来解决。
WebSocket 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助 HTTP请求完成。 WebSocket是真正实现了全双工通信的服务器向客户端推的互联网技术。 它是一种在单个TCP连 接上进行全双工通讯协议。Websocket通信协议与2011年倍IETF定为标准RFC 6455,Websocket API被W3C定为 标准。
全双工和单工的区别?
全双工(Full Duplex)是通讯传输的一个术语。通信允许数据在两个方向上同时传输,它在能力上相当 于两个单工通信方式的结合。全双工指可以同时(瞬时)进行信号的双向传输(A→B且B→A)。指 A→B的同时B→A,是瞬时同步的。单工、半双工(Half Duplex),所谓半双工就是指一个时间段内只有一个动作发生,举个简单例子, 一条窄窄的马路,同时只能有一辆车通过,当目前有两辆车对开,这种情况下就只能一辆先过,等到头 儿后另一辆再开,这个例子就形象的说明了半双工的原理。早期的对讲机、以及早期集线器等设备都是 基于半双工的产品。随着技术的不断进步,半双工会逐渐退出历史舞台。http协议是短连接,因为请求之后,都会关闭连接,下次重新请求数据,需要再次打开链接。
WebSocket协议是一种长链接,只需要通过一次请求来初始化链接,然后所有的请求和响应都是通过这个TCP链接 进行通讯。
在基础工程中新建module,命名为spring-websocket或其他名字都可以,这里可以根据自己的需求去创建相应的名称,创建过程如下:
创建模块 点击next,输入模块名称点击Finish就可以啦这里使用的是springboot框架,我们直接使用对应的starter就可以啦,我在pom文件中引入相应的配置如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- websocket依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> </dependencies> 创建项目启动类在src.main.java下创建自己定义的包名,这里定义的包名称是cn.org.spring.tools.websocket,在包下面创建springboot的启动类WebSocketApplication
@SpringBootApplication public class WebSocketApplication { public static void main(String[] args) { SpringApplication.run(WebSocketApplication.class); } } 创建webSocket配置类在cn.org.spring.tools.websocket新建一个package下新建config包,在config包中新建WebSocketConfig类,配置如下
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { /** * 注入拦截器 */ @Resource private MyHandshakeInterceptor myHandshakeInterceptor; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) { webSocketHandlerRegistry //添加myHandler消息处理对象,和websocket访问地址 .addHandler(myHandler(), "/ws") //设置允许跨域访问 .setAllowedOrigins("*") //添加拦截器可实现用户链接前进行权限校验等操作 .addInterceptors(myHandshakeInterceptor); } @Bean public WebSocketHandler myHandler() { return new MyWebSocketHandler(); } } 实现MyWebSocketHandler新建包handler,创建MyWebSocketHandler并集成TextWebSocketHandler,TextWebSocketHandler是主要处理string类型消息,这里我们可以继承其他的handler类,如BinaryWebSocketHandler
public class MyWebSocketHandler extends TextWebSocketHandler { //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static AtomicInteger onlineNum = new AtomicInteger(); //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。 private static ConcurrentHashMap<String, WebSocketSession> sessionPools = new ConcurrentHashMap<>(); @Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException { System.out.println("获取到消息 >> " + message.getPayload()); session.sendMessage(new TextMessage(String.format("收到用户:【%s】发来的【%s】", session.getAttributes().get("uid"), message.getPayload()))); } @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { System.out.println("获取到拦截器中用户ID : " + session.getAttributes().get("uid")); String uid = session.getAttributes().get("uid").toString(); //TODO: 重复链接没有进行处理 sessionPools.put(uid, session); addOnlineCount(); System.out.println(uid + "加入webSocket!当前人数为" + onlineNum); session.sendMessage(new TextMessage("欢迎连接到ws服务! 当前人数为:" + onlineNum)); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { System.out.println("断开连接!"); String uid = session.getAttributes().get("uid").toString(); sessionPools.remove(uid); subOnlineCount(); } /** * 添加链接人数 */ public static void addOnlineCount() { onlineNum.incrementAndGet(); } /** * 移除链接人数 */ public static void subOnlineCount() { onlineNum.decrementAndGet(); } } 创建拦截器Interceptor创建链接前权限验证拦截器Interceptor,这里我们继承HandshakeInterceptor具体实现如下:
@Component public class MyHandshakeInterceptor implements HandshakeInterceptor { /** * 握手之前,若返回false,则不建立链接 * * * @param request * @param response * @param wsHandler * @param attributes * @return */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) { //将用户id放入socket处理器的会话(WebSocketSession)中 ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request; //获取参数 String userId = serverHttpRequest.getServletRequest().getParameter("userId"); attributes.put("uid", userId); //可以在此处进行权限验证,当用户权限验证通过后,进行握手成功操作,验证失败返回false if (userId.equals("123")) { System.out.println("握手失败....."); return false; } System.out.println("开始握手。。。。。。。"); return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { System.out.println("握手成功啦。。。。。。"); } } 启动测试Springboot 默认启动端口为8080,可以采用默认端口,也可以自定义端口,这么我们新增application.yml配置文件,定义服务端口和应用名称;
server: port: 9001 spring: application: name: spring-websocket配置好后,启动服务;
这里给大家分享个ws在线测试工具,工具地址
我们尝试链接下我们的ws服务: 在地址栏输入地址ws//127.0.0.1:9001/ws?userId=123 点击链接 如出现一下信息说明链接成功。
连接成功后,进行发送消息测试看到如下信息说明服务端收到前端消息并返回了对应的消息
至此,我们集成websocket完成,代码中还有些细节需要去优化,大家在借鉴使用时不要直接copy,要结合自己的业务场景去实现细化它。
源码地址: 传送门