短轮询
function showTime(){
$.get("showTime",function (data) {
console.log(data);
$("#serverTime").html(data);
})
}
setInterval(showTime, 1000);
服务器推送技术-Comet
基于 AJAX 的长轮询(long-polling)方式,Spring带来的DeferedResult
/**
* @author yun
* 类说明:
*/
@Controller
@RequestMapping(produces="text/html;charset=UTF-8")
/*记得要在WebInitializer中增加servlet.setAsyncSupported(true);*/
public class PushNewsController {
private ExecutorService executorService
= Executors.newFixedThreadPool(1);
@RequestMapping("/pushnews")
public String news(){
return "pushNews";
}
@RequestMapping(value="/realTimeNews")
@ResponseBody
/*在WebInitializer中要加上servlet.setAsyncSupported(true);*/
public DeferredResult<String> realtimeNews(HttpServletRequest request){
final DeferredResult<String> dr = new DeferredResult<String>();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int index = new Random().nextInt(Const.NEWS.length);
dr.setResult(Const.NEWS[index]);
}
});
return dr;
}
}
启用异步:
/**
* @author yun
* 类说明:
*/
public class WebInit implements WebApplicationInitializer {
public void onStartup(ServletContext servletContext)
throws ServletException {
AnnotationConfigWebApplicationContext ctx
= new AnnotationConfigWebApplicationContext();
ctx.register(CometMvcConfig.class);
ctx.setServletContext(servletContext);
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher",
new DispatcherServlet(ctx));
servlet.addMapping("/");
servlet.setLoadOnStartup(1);
servlet.setAsyncSupported(true);//启用异步
}
}
js调用:
longLoop();
function longLoop() {
$.get("realTimeNews",function (data) {
console.log(data);
$("#realTimeNews").html(data);
longLoop();//马上再发起请求
})
}
基于长轮询的服务器推模型Server-sent-events(SSE)
js代码:
<script type="text/javascript" src="assets/js/jquery-1.9.1.min.js"></script>
<script type="text/javascript">
function showPrice(index,data){
$("#c"+index).html("当前价格:"+data);
var s = $("#s"+index).html();
$("#s"+index).html(s+data+" ");
}
if(!!window.EventSource){//判断浏览器支持度
//拿到sse的对象
var source = new EventSource('needPrice');
//接收到服务器的消息
source.onmessage=function (e) {
var dataObj=e.data;
var arr = dataObj.split(',');
$.each(arr, function (i, item) {
showPrice(i,item);
});
$("#hint").html("");
};
source.onopen=function (e) {
console.log("Connecting server!");
};
source.onerror=function () {
console.log("error");
};
}else{
$("#hint").html("您的浏览器不支持SSE!");
}
</script>
服务端代码:
/**
* @author yun
* 类说明:
*/
@Controller
public class NobleMetalController {
private static Logger logger = LoggerFactory.getLogger(NobleMetalController.class);
@RequestMapping("/nobleMetal")
public String stock(){
return "nobleMetal";
}
@RequestMapping(value="needPrice")
@ResponseBody
public void push(HttpServletResponse response){
response.setContentType("text/event-stream");
response.setCharacterEncoding("utf-8");
Random r = new Random();
int sendCount =0; /*服务器数据发送次数*/
try {
PrintWriter pw = response.getWriter();
while(true) {
if(pw.checkError()) {
System.out.println("客户端断开连接");
return;
}
Thread.sleep(1000);
//字符串拼接
StringBuilder sb = new StringBuilder("");
sb.append("retry:2000\n")
.append("data:")
.append((r.nextInt(1000)+50)+",")
.append((r.nextInt(800)+100)+",")
.append((r.nextInt(2000)+150)+",")
.append((r.nextInt(1500)+100)+",")
.append("\n\n");
pw.write(sb.toString());
pw.flush();
sendCount++;
if(sendCount >= 100) {
return;
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
spring为sse提供了支持,代码示例如下:
/**
* @author yun
* 类说明:
*/
@Controller
public class SseController {
private static Logger logger = LoggerFactory.getLogger(SseController.class);
private static Map<String,SseEmitter> sseEmitters
= new ConcurrentHashMap<>();
private ExecutorService executorService
= Executors.newFixedThreadPool(2);
@RequestMapping("/weChatPay")
public String stock(){
return "weChatPay";
}
@RequestMapping(value="/payMoney")
@ResponseBody
public SseEmitter pay(String weCharId){
SseEmitter emitter = new SseEmitter();
sseEmitters.put(weCharId,emitter);
executorService.submit(new Pay(weCharId) );
return emitter;
}
private static class Pay implements Runnable{
private String weCharId;
public Pay(String weCharId) {
this.weCharId = weCharId;
}
@Override
public void run() {
SseEmitter sseEmitter = sseEmitters.get(weCharId);
try {
logger.info("联系支付服务,准备扣款");
Thread.sleep(500);
sseEmitter.send("支付完成");
logger.info("准备通知自动售货机");
Thread.sleep(1500);//售货机的动作
sseEmitter.send("已通知自动售货机C9出货,请勿走开!");
sseEmitter.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
js代码:
<script type="text/javascript" src="assets/js/jquery-1.9.1.min.js"></script>
<script type="text/javascript">
function send(){
$("#payHint").html("正在处理中........");
$.ajax({
type: 'get',
url:'payMoney?weCharId=1234567',
dataType:'text',
success:function(e){
console.log(e);
var arr = e.split('data:');
var hint = '';
$.each(arr, function (i, item) {
hint = hint+item+'<br>';
});
$("#payHint").html(hint);
},
error:function(data){
$("#payHint").html(data);
}
});
}
</script>
websocket
WebSocket通信-STOMP
html引入的js脚本:
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>
<script th:src="@{wechat_room.js}"></script>
wechat_room.js:
var stompClient = null;
//加载完浏览器后调用connect(),打开通道
$(function(){
//打开双通道
connect()
})
//强制关闭浏览器时调用websocket.close(),进行正常关闭
window.onunload = function() {
disconnect()
}
//打开通道
function connect(){
//连接SockJS的endpoint名称为"endpointMark"
var socket = new SockJS('/endpointMark');
stompClient = Stomp.over(socket);//使用STMOP子协议的WebSocket客户端
stompClient.connect({},function(frame){//连接WebSocket服务端
console.log('Connected:' + frame);
//广播接收信息
stompTopic();
});
}
//关闭通道
function disconnect(){
if(stompClient != null) {
stompClient.disconnect();
}
console.log("Disconnected");
}
//一对多,发起订阅
function stompTopic(){
//通过stompClient.subscribe订阅目标(destination)发送的消息(广播接收信息)
stompClient.subscribe('/mass/getResponse',function(response){
var message=JSON.parse(response.body);
//展示广播的接收的内容接收
var response = $("#mass_div");
var userName=$("#selectName").val();
if(userName==message.name){
response.append("<div class='user-group'>" +
" <div class='user-msg'>" +
" <span class='user-reply'>"+message.chatValue+"</span>" +
" <i class='triangle-user'></i>" +
" </div>" +userName+
" </div>");
}else{
response.append(" <div class='admin-group'>"+
message.name+
"<div class='admin-msg'>"+
" <i class='triangle-admin'></i>"+
" <span class='admin-reply'>"+message.chatValue+"</span>"+
"</div>"+
"</div>");
}
});
}
//群发消息
function sendMassMessage(){
var postValue={};
var chatValue=$("#sendChatValue");
var userName=$("#selectName").val();
postValue.name=userName;
postValue.chatValue=chatValue.val();
//postValue.userId="0";
if(userName==1||userName==null){
alert("请选择你是谁!");
return;
}
if(chatValue==""||userName==null){
alert("不能发送空消息!");
return;
}
stompClient.send("/massRequest",{},JSON.stringify(postValue));
chatValue.val("");
}
//单独发消息
function sendAloneMessage(){
var postValue={};
var chatValue=$("#sendChatValue2");
var userName=$("#selectName").val();
var sendToId=$("#selectName2").val();
var response = $("#alone_div");
postValue.name=userName;//发送者姓名
postValue.chatValue=chatValue.val();//聊天内容
postValue.userId=sendToId;//发送给谁
if(userName==1||userName==null){
alert("请选择你是谁!");
return;
}
if(sendToId==1||sendToId==null){
alert("请选择你要发给谁!");
return;
}
if(chatValue==""||userName==null){
alert("不能发送空消息!");
return;
}
stompClient.send("/aloneRequest",{},JSON.stringify(postValue));
response.append("<div class='user-group'>" +
" <div class='user-msg'>" +
" <span class='user-reply'>"+chatValue.val()+"</span>" +
" <i class='triangle-user'></i>" +
" </div>" +userName+
" </div>");
chatValue.val("");
}
//一对一,发起订阅
function stompQueue(){
var userId=$("#selectName").val();
alert("监听:"+userId)
//通过stompClient.subscribe订阅目标(destination)发送的消息(队列接收信息)
stompClient.subscribe('/queue/' + userId + '/alone',
function(response){
var message=JSON.parse(response.body);
//展示一对一的接收的内容接收
var response = $("#alone_div");
response.append(" <div class='admin-group'>"+
message.name+
"<div class='admin-msg'>"+
" <i class='triangle-admin'></i>"+
" <span class='admin-reply'>"+message.chatValue+"</span>"+
"</div>"+
"</div>");
});
}
后台服务器代码:
配置类
/**
* @author yun
* 类说明:
*/
@Configuration
/*开启使用Stomp协议来传输基于消息broker的消息
这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样*/
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/*注册STOMP协议的节点(endpoint),并映射指定的url,
* 添加一个访问端点“/endpointMark”,客户端打开双通道时需要的url,
* 允许所有的域名跨域访问,指定使用SockJS协议。*/
registry.addEndpoint("/endpointMark")
.setAllowedOrigins("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
/*配置一个消息代理
* mass 负责群聊
* queue 单聊*/
registry.enableSimpleBroker(
"/mass","/queue");
//一对一的用户,请求发到/queue
registry.setUserDestinationPrefix("/queue");
}
}
controller类
/**
* @author yun
* 类说明:
*/
@Controller
public class StompController {
@Autowired
private SimpMessagingTemplate template;/*Spring实现的一个发送模板类*/
/*消息群发,接受发送至自massRequest的请求*/
@MessageMapping("/massRequest")
@SendTo("/mass/getResponse")
//SendTo 发送至 Broker 下的指定订阅路径mass ,
// Broker再根据getResponse发送消息到订阅了/mass/getResponse的用户处
public ChatRoomResponse mass(ChatRoomRequest chatRoomRequest){
System.out.println("name = " + chatRoomRequest.getName()
+" chatValue = " + chatRoomRequest.getChatValue());
ChatRoomResponse response=new ChatRoomResponse();
response.setName(chatRoomRequest.getName());
response.setChatValue(chatRoomRequest.getChatValue());
//this.template.convertAndSend();
return response;
}
/*单独聊天,接受发送至自aloneRequest的请求*/
@MessageMapping("/aloneRequest")
//@SendToUser
public ChatRoomResponse alone(ChatRoomRequest chatRoomRequest){
System.out.println("SendToUser = " + chatRoomRequest.getUserId()
+" FromName = " + chatRoomRequest.getName()
+" ChatValue = " + chatRoomRequest.getChatValue());
ChatRoomResponse response=new ChatRoomResponse();
response.setName(chatRoomRequest.getName());
response.setChatValue(chatRoomRequest.getChatValue());
//会发送到订阅了 /user/{用户的id}/alone 的用户处
// {userid}/alone ==> /queue/{userid}/alone
this.template.convertAndSendToUser(chatRoomRequest.getUserId()
+"","/alone",response);
return response;
}
}
请求实体类:
/**
* @author yun
* 类说明:聊天室的请求实体
*/
public class ChatRoomRequest {
private String name;
private String chatValue;
private String userId;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getChatValue() {
return chatValue;
}
public void setChatValue(String chatValue) {
this.chatValue = chatValue;
}
}
响应实体类:
/**
* @author yun
* 类说明:聊天室的应答实体
*/
public class ChatRoomResponse {
private String name;
private String chatValue;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getChatValue() {
return chatValue;
}
public void setChatValue(String chatValue) {
this.chatValue = chatValue;
}
}
websocket与spring整合
<script type="text/javascript">
var socket;
if (typeof (WebSocket) == "undefined") {
console.log("遗憾:您的浏览器不支持WebSocket");
} else {
console.log("恭喜:您的浏览器支持WebSocket");
//实现化WebSocket对象
//指定要连接的服务器地址与端口建立连接
//注意ws、wss使用不同的端口。我使用自签名的证书测试,
//无法使用wss,浏览器打开WebSocket时报错
//ws对应http、wss对应https。
socket = new WebSocket("ws://localhost:8080/ws/asset");
//连接打开事件
socket.onopen = function() {
console.log("Socket 已打开");
socket.send("消息发送测试(From Client)");
};
//收到消息事件
socket.onmessage = function(msg) {
console.log(msg);
//console.log(msg.data);
};
//连接关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function() {
alert("Socket发生了错误");
}
//窗口关闭时,关闭连接
window.unload=function() {
socket.close();
};
}
</script>
服务端代码:
/**
*@author yun
*类说明:提供WebSocket服务
*/
@ServerEndpoint(value = "/ws/asset")
@Component
public class WebSocketServer {
private static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
private static final AtomicInteger OnlineCount = new AtomicInteger(0);
// concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<Session>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
SessionSet.add(session);
int cnt = OnlineCount.incrementAndGet(); // 在线数加1
log.info("有连接加入,当前连接数为:{}", cnt);
SendMessage(session, "连接成功");
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
SessionSet.remove(session);
int cnt = OnlineCount.decrementAndGet();
log.info("有连接关闭,当前连接数为:{}", cnt);
}
/**
* 收到客户端消息后调用的方法
*
* @param message
* 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("来自客户端的消息:{}",message);
SendMessage(session, "收到消息,消息内容:"+message);
}
/**
* 出现错误
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误:{},Session ID: {}",error.getMessage(),session.getId());
error.printStackTrace();
}
/**
* 发送消息,实践表明,每次浏览器刷新,session会发生变化。
* @param session
* @param message
*/
public static void SendMessage(Session session, String message) {
try {
session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)",message,session.getId()));
} catch (IOException e) {
log.error("发送消息出错:{}", e.getMessage());
e.printStackTrace();
}
}
/**
* 群发消息
* @param message
* @throws IOException
*/
public static void BroadCastInfo(String message) throws IOException {
for (Session session : SessionSet) {
if(session.isOpen()){
SendMessage(session, message);
}
}
}
/**
* 指定Session发送消息
* @param sessionId
* @param message
* @throws IOException
*/
public static void SendMessage(String sessionId,String message) throws IOException {
Session session = null;
for (Session s : SessionSet) {
if(s.getId().equals(sessionId)){
session = s;
break;
}
}
if(session!=null){
SendMessage(session, message);
}
else{
log.warn("没有找到你指定ID的会话:{}",sessionId);
}
}
}
开启websocket配置:
/**
* @author yun
* 类说明:开启WebSocket支持
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
业务controller:
/**
*@author yun
*类说明: WebSocket服务器端推送消息示例Controller
*/
@RestController
@RequestMapping("/api/ws")
public class WsController {
/**
* 群发消息内容
* @param message
* @return
*/
@RequestMapping(value="/sendAll", method=RequestMethod.GET)
String sendAllMessage(@RequestParam(required=true) String message){
try {
WebSocketServer.BroadCastInfo(message);
} catch (IOException e) {
e.printStackTrace();
}
return "ok";
}
/**
* 指定会话ID发消息
* @param message 消息内容
* @param id 连接会话ID
* @return
*/
@RequestMapping(value="/sendOne", method=RequestMethod.GET)
String sendOneMessage(@RequestParam(required=true) String message,@RequestParam(required=true) String id){
try {
WebSocketServer.SendMessage(id,message);
} catch (IOException e) {
e.printStackTrace();
}
return "ok";
}
}