思考:像这样的消息功能怎么实现?
如果网页不刷新,服务端有新消息如何推送到浏览器?
解决方案,采用轮询的方式。即:通过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
或其他名字都可以,这里可以根据自己的需求去创建相应的名称,创建过程如下:
这里使用的是springboot框架,我们直接使用对应的starter就可以啦,我在pom文件中引入相应的配置如下:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-websocket
在src.main.java
下创建自己定义的包名,这里定义的包名称是cn.org.spring.tools.websocket
,在包下面创建springboot的启动类WebSocketApplication
@SpringBootApplication
public class WebSocketApplication {
public static void main(String[] args) {
SpringApplication.run(WebSocketApplication.class);
}
}
在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();
}
}
新建包handler
,创建MyWebSocketHandler
并集成TextWebSocketHandler
,TextWebSocketHandler
是主要处理string类型消息,这里我们可以继承其他的handler类,如BinaryWebSocketHandler
public class MyWebSocketHandler extends TextWebSocketHandler {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static AtomicInteger onlineNum = new AtomicInteger();
//concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
private static ConcurrentHashMap 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
,这里我们继承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 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,要结合自己的业务场景去实现细化它。