在WEB項目中,服務器向WEB頁面推送消息是一種常見的業務需求。PC端的推送技術可以使用socket建立一個長連接來實現。傳統的web服務都是客戶端發出請求,服務端給出響應。但是現在直觀的要求是允許特定時間內在沒有客戶端發起請求的情況下服務端主動推送消息到客戶端。最近的預警系統中,需要服務端向預警系統推送商品行情和K線相關的數據,所以對常用的WEB端推送方式進行調研。常見的手段主要包括以下幾種:
- 輪詢(俗稱“拉”,polling):Ajax 隔一段時間向服務器發送請求,詢問數據是否發生改變,從而進行增量式的更新。輪詢的時間間隔成了一個問題:間隔太短,會有大量的請求發送到服務器,會對服務器負載造成影響;間隔太長業務數據的實時性得不到保證。連。使用輪詢的優點是實現邏輯簡單,缺點是無效請求的數量多,在用戶量較大的情況下,服務器負載較高。因此輪詢的方式通常在並發數量較少、並且對消息實時性要求不高的情況下使用。
- 長輪詢技術(long-polling):客戶端向服務器發送Ajax請求,服務器接到請求后hold住連接,直到有新消息或超時(設置)才返回響應信息並關閉連接,客戶端處理完響應信息后再向服務器發送新的請求。長輪詢技術的優點是消息實時性高,無消息的情況下不會進行頻繁的請求;缺點是服務端維持和客戶端的連接會消耗掉一部分資源。
- 插件提供socket方式:比如利用Flash XMLSocket,Java Applet套接口,Activex包裝的socket。優點是對原生socket的支持,和PC端和移動端的實現方式相似;缺點是瀏覽器需要安裝相應的插件。
- WebSocket:是HTML5開始提供的一種瀏覽器與服務器間進行全雙工通訊的網絡技術。其優點是更好的節省服務器資源和帶寬並達到實時通訊;缺點是目前還未普及,瀏覽器支持不好;
綜上,考慮到瀏覽器兼容性和性能問題,采用長輪詢(long-polling)是一種比較好的方式。netty-socketio是一個開源的Socket.io服務器端的一個java的實現, 它基於Netty框架。 項目地址為: https://github.com/mrniko/netty-socketio
以下基於Netty-socketIO實現一個簡單的聊天室功能,首先引入依賴:
<dependency> <groupId>com.corundumstudio.socketio</groupId> <artifactId>netty-socketio</artifactId> <version>1.7.3</version> </dependency>
定義Listen,用戶監聽Oncennect、disconnect和OnMSG事件:
@Service("eventListenner") public class EventListenner { @Resource(name = "clientCache") private SocketIOClientCache clientCache; @Resource(name = "socketIOResponse") private SocketIOResponse socketIOResponse; @OnConnect public void onConnect(SocketIOClient client) { System.out.println("建立連接"); } @OnEvent("OnMSG") public void onSync(SocketIOClient client, MsgBean bean) { System.out.printf("收到消息-from: %s to:%s\n", bean.getFrom(), bean.getTo()); clientCache.addClient(client, bean); SocketIOClient ioClients = clientCache.getClient(bean.getTo()); System.out.println("clientCache"); if (ioClients == null) { System.out.println("你發送消息的用戶不在線"); return; } socketIOResponse.sendEvent(ioClients,bean); } @OnDisconnect public void onDisconnect(SocketIOClient client) { System.out.println("關閉連接"); } }
定義消息發送類:
@Service("socketIOResponse") public class SocketIOResponse { public void sendEvent(SocketIOClient client, MsgBean bean) { System.out.println("推送消息"); client.sendEvent("OnMSG", bean); } }
定義Cache用於保存所有和Web端的連接:
@Service("clientCache") public class SocketIOClientCache { //String:EventType類型 private Map<String,SocketIOClient> clients=new ConcurrentHashMap<String,SocketIOClient>(); //用戶發送消息添加 public void addClient(SocketIOClient client,MsgBean msgBean){ clients.put(msgBean.getFrom(),client); } //用戶退出時移除 public void remove(MsgBean msgBean) { clients.remove(msgBean.getFrom()); } //獲取所有 public SocketIOClient getClient(String to) { return clients.get(to); } }
定義Server:
//繼承InitializingBean,使Spring加載完配置文件,自動運行如下方法 @Service("chatServer") public class ChatServer implements InitializingBean{ @Resource private EventListenner eventListenner; public void afterPropertiesSet() throws Exception { Configuration config = new Configuration(); config.setPort(9098); SocketConfig socketConfig = new SocketConfig(); socketConfig.setReuseAddress(true); socketConfig.setTcpNoDelay(true); socketConfig.setSoLinger(0); config.setSocketConfig(socketConfig); config.setHostname("localhost"); SocketIOServer server = new SocketIOServer(config); server.addListeners(eventListenner); server.start(); System.out.println("啟動正常"); } }
定義MSGbean:
public class MsgBean { private String from; private String to; private String content; public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public String getTo() { return to; } public void setTo(String to) { this.to = to; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public String toString() { return "MsgBean [from=" + from + ", to=" + to + ", content=" + content + "]"; } }
定義Main:
public class Main { public static void main(String[] args) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:/applicationContext-push.xml"); context.start(); context.getBean("chatServer"); } }
HTML頁面:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Socketio chat</title> <script src="./jquery-1.7.2.min.js" type="text/javascript"></script> <script type="text/javascript" src="./socket.io.js"></script> <style> body { padding: 20px; } #console { height: 400px; overflow: auto; } .username-msg { color: orange; } .connect-msg { color: green; } .disconnect-msg { color: red; } .send-msg { color: #888 } </style> </head> <body> <h1>Netty-socketio chat demo</h1> <br /> <div id="console" class="well"></div> <form class="well form-inline" onsubmit="return false;"> <input id="from" class="input-xlarge" type="text" placeholder="from. . . " /> <input id="to" class="input-xlarge" type="text" placeholder="to. . . " /> <input id="content" class="input-xlarge" type="text" placeholder="content. . . " /> <button type="button" onClick="sendMessage()" class="btn">Send</button> <button type="button" onClick="sendDisconnect()" class="btn">Disconnect</button> </form> </body> <script type="text/javascript"> var socket = io.connect('http://localhost:9098'); socket.on('connect',function() { output('<span class="connect-msg">Client has connected to the server!</span>'); }); socket.on('OnMSG', function(data) { output('<span class="username-msg">' + data.content + ' : </span>'); }); socket.on('disconnect',function() { output('<span class="disconnect-msg">The client has disconnected! </span>'); }); function sendDisconnect() { socket.disconnect(); } function sendMessage() { var from = $("#from").val(); var to = $("#to").val(); var content = $('#content').val(); socket.emit('OnMSG', { from : from, to : to, content : content }); } function output(message) { var currentTime = "<span class='time' >" + new Date() + "</span>"; var element = $("<div>" + currentTime + " " + message + "</div>"); $('#console').prepend(element); } </script> </html>