Comet概念
首先我们看维基百科的定义: Comet是一种用于web的推送技术,能使服务器实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有两种实现方式,长轮询和iframe流。在H5的标准中定义了WebSocket方式,在得到浏览器支持之后,WebSocket将会取代Comet成为服务器推送的方法。 根据这个定义,短轮询其实不算是一种Comet,因为短轮询是依赖于客户端发起请求的。
长轮询
原理与概念
长轮询的原理就是客户端发起HTTP请求,请求的超时时间设置的长一些。当服务端收到请求时,如果此时没有需要发送给客户端的消息,则暂时不返回响应。当有数据需要推送给客户端时,拿到正在等待的连接,将数据通过此连接发送。当客户端收到请求、或者即将超时服务端主动返回空结果、服务端完成一次数据返回后,一次长轮询结束。客户端再发起请求。
长轮询和短轮询的区别是:短轮询中服务器对请求立即响应。而长轮询中服务器等待新的数据到来才响应。因此长轮询需要的网络更少。
示例
需求: 提供一个发送消息的接口。如果收信人在线则立即发送。否则将消息保存,等收信人上线后再发送。收信人使用长轮询接口来拉去自己信箱中的消息。
@RestController
public class LongPollController {
private final long timeOutInMilliSec = 100000L;
@Autowired
private MsgDist dist;
/**
* 长轮询接口。
* 客户端的超时时间需要比服务器的timeOutInMilliSec设置的长一些
*
* @param uid 消息所属用户id
* @return
*/
@GetMapping("/longpoll")
public DeferredResult<String> poll(@RequestParam("uid") Integer uid) {
// 当我们返回DeferredResult时,请求线程将被释放,并将其交给worker线程处理
DeferredResult<String> result = new DeferredResult<>(timeOutInMilliSec, "");
dist.pollMsgs(uid, result);
return result;
}
/**
* 消息发送接口
*
* @param toUid 收信息人用户id
* @param msg 需要发送的消息
* @return
*/
@PostMapping("/send")
public String sendMsg(@RequestParam("to_uid") Integer toUid, @RequestParam("msg") String msg) {
dist.sendMsgToUserBox(toUid, msg);
return "ok";
}
}
@Component
public class MsgDist {
private ConcurrentHashMap<Integer, DeferredResult<String>> uid2deferredResult;
private ConcurrentHashMap<Integer, LinkedList<String>> uid2box;
public MsgDist() {
this.uid2deferredResult = new ConcurrentHashMap<>();
this.uid2box = new ConcurrentHashMap<>();
Timer timer = new Timer();
MsgSender sender = new MsgSender();
timer.schedule(sender, 1, 1);
}
/**
* 将消息放到用户信箱,交给定时任务发送
*
* @param uid
* @param msg
*/
public void sendMsgToUserBox(int uid, String msg) {
synchronized (this){
LinkedList<String> box = this.uid2box.getOrDefault(uid, new LinkedList<String>());
box.add(msg);
this.uid2box.put(uid, box);
}
}
/**
* 拉取消息
*
* @param uid
* @param deferredResult
*/
public void pollMsgs(int uid, DeferredResult<String> deferredResult) {
this.uid2deferredResult.put(uid, deferredResult);
}
/**
* 定时尝试将用户信息中的数据发送给用户
*/
class MsgSender extends TimerTask {
@Override
public void run() {
Iterator<Map.Entry<Integer, LinkedList<String>>> iterator = uid2box.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, LinkedList<String>> entry = iterator.next();
LinkedList<String> box = entry.getValue();
// 信箱无数据
if (box == null || box.size() == 0) {
continue;
}
Integer uid = entry.getKey();
// 用户没有长轮询请求
DeferredResult<String> deferredResult = uid2deferredResult.get(uid);
if (deferredResult == null) {
continue;
}
// 将多条消息拼接后发送
StringBuffer willSendMsg = new StringBuffer("");
box.forEach(str -> {
willSendMsg.append(str);
willSendMsg.append(";");
});
deferredResult.setResult(willSendMsg.toString());
uid2box.remove(uid);
}
}
}
}
关于DeferredResult的拓展阅读 Guide to DeferredResult in Spring
基于iframe的长连接流模式
前端在页面中嵌入一个inframe并甚至其src,src指向服务端的接口。服务端通过此接口不断的返回数据。
Server-Send Events
前端通过标准的EventSource API来和后端建立连接。后端可以通过这个连接发送任意的字符串数据。SSE的MIME请求类型是text/event-stream.
SSE通信是单向的。相较于长连接,它可以分多次将数据返回。
示例
@RestController
public class SseController {
private final long timeOutInMilliSec = 100000L;
@Autowired
private SseServer sseServer;
/**
* 建立连接
*
* @param uid
* @return
*/
@GetMapping("/sse_connect")
public SseEmitter connect(Integer uid) {
SseEmitter emitter = new SseEmitter(timeOutInMilliSec);
sseServer.subscribe(uid, emitter);
return emitter;
}
/**
* 发送消息
*
* @param toUid
* @param msg
* @return
*/
@PostMapping("/sse_send")
public String sendMsg(@RequestParam("to_uid") Integer toUid, @RequestParam("msg") String msg) {
sseServer.sendMsgToUser(toUid, msg);
return "ok";
}
}
@Component
public class SseServer {
private ConcurrentHashMap<Integer, SseEmitter> uid2sseEmitter;
public SseServer() {
this.uid2sseEmitter = new ConcurrentHashMap<>();
}
public void sendMsgToUser(Integer uid, String msg) {
SseEmitter emitter = this.uid2sseEmitter.get(uid);
if (emitter != null) {
try {
emitter.send(msg);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public void subscribe(Integer uid, SseEmitter emitter) {
this.uid2sseEmitter.put(uid, emitter);
emitter.onTimeout(()->{
this.uid2sseEmitter.remove(uid);
});
emitter.onError((throwable)->{
this.uid2sseEmitter.remove(uid);
});
}
}
websocket
websocket支持双向通信,后续数据通信无需再传递冗余的http头部信息。
示例
- 引入
websocket依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 编写endpoint
@ServerEndpoint("/ws")
public class WsChannel {
private Session session;
@OnMessage
public void onMessage(String msg) {
System.out.println("recv:" + msg);
this.session.getAsyncRemote().sendText("hello");
}
@OnOpen
public void onOpen(Session session, EndpointConfig endpointConfig) {
this.session = session;
System.out.println("new connect");
}
@OnClose
public void onClose(CloseReason closeReason) {
System.out.println("close,close code:" + closeReason.getCloseCode());
}
@OnError
public void onError(Throwable throwable) throws IOException {
System.out.println("on error");
this.session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, throwable.getMessage()));
}
}
- 导出endpoint
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
ServerEndpointExporter exporter = new ServerEndpointExporter();
exporter.setAnnotatedEndpointClasses(WsChannel.class);
return exporter;
}
}
注意:这是一个很简单例子,仅演示websocket库如何使用。实际工程中还需要处理Session管理,自定义编解码,握手处理器等逻辑