傳統的Http是基於請求-響應式的協議,需要客戶端主動向用戶發送請求,才能得到服務器的響應,而在請求同步響應結束後,Http也會關閉,此時服務器便不能再向客戶端主動發送消息了。即若客戶端想得到服務端的消息,就必須首先發送請求才能得到消息回覆。
在有些場景下,如股票價格實時顯示、直播、在線聊天等場景,則需要服務器主動向客戶端推送消息,顯然Http協議並不太適合完全這項工作,而Netty-SocketIO是基於Netty框架下用Java實現Socket通信的組件,可用於服務器主動推送消息到客戶端的情形。
文章基於Netty-Socket實現Java後臺+socket.io.js前端實現服務器消息推送。
後臺服務器部分
- 引入netty-socketio,在maven中引入jar包,注意版本,筆者剛開始嘗試1.6.x的版本,不能正常使用,換成1.7.7版本之後就好了。
<!-- netty socketio -->
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.7</version>
</dependency>
- Socket服務器代碼,socket服務地址端口設爲本機
localhost:8089
,並在spring Bean加載的時候就開啓服務。socket服務需要添加監聽事項,本文用spring注入listeners
@Service
public class SocketService implements InitializingBean{
@Autowired
private EventListennter listeners;
public void startServer() {
Configuration config = new Configuration();
config.setHostname("localhost");
config.setPort(8089);
SocketIOServer server = new SocketIOServer(config);
server.addConnectListener(new ConnectListener() {// 添加客戶端連接監聽器
@Override
public void onConnect(SocketIOClient client) {
System.err.println(client.getRemoteAddress() + " web客戶端接入");
}
});
server.addListeners(listeners);
server.start();
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("start socket");
this.startServer();
}
}
- 監聽器:添加監聽事項event
@Component
public class EventListennter {
//維護每個客戶端的SocketIOClient
private Map<String, List<SocketIOClient>> clients = new ConcurrentHashMap<>();
@OnConnect
public void onConnect(SocketIOClient client) {
System.err.println("建立連接");
}
@OnEvent("token")
public void onToken(SocketIOClient client, SocketIOMessage message) {
List<SocketIOClient> socketList = clients.get(message.getToken());
if (null == socketList || socketList.isEmpty()) {
List<SocketIOClient> list = new ArrayList<>();
list.add(client);
clients.put(message.getToken(), list);
}
System.err.println("get token Message is " + message.getToken());
}
/**
* 新事務
* @param client 客戶端
* @param message 消息
*/
@OnEvent("newAlert")
public void onAlert(SocketIOClient client, SocketIOMessage message) {
//send to all users
Collection<List<SocketIOClient>> clientsList = clients.values();
for (List<SocketIOClient> list : clientsList) {
for (SocketIOClient socketIOClient : list) {
socketIOClient.sendEvent("newAlert", message);
}
}
}
/**
* 通知所有在線客戶端
*/
public void sendAllUser() {
Set<Entry<String,List<SocketIOClient>>> entrySet = clients.entrySet();
for (Entry<String, List<SocketIOClient>> entry : entrySet) {
String key = entry.getKey();
List<SocketIOClient> value = entry.getValue();
for (SocketIOClient socketIOClient : value) {
SocketIOMessage message = new SocketIOMessage();
message.setMessage("send All user Msg" + key);
socketIOClient.sendEvent("newAlert", message);
}
}
}
@OnDisconnect
public void onDisconnect(SocketIOClient client) {
System.err.println("關閉連接");
}
}
- 消息類封裝
public class SocketIOMessage {
private String token;
private String message;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
- Controller類,觸發給所有客戶端發送消息
@Controller
@RequestMapping("/server")
public class SocketController {
@Autowired
private EventListennter eventListennter;
@RequestMapping("/send")
public void sendMsg() {
System.err.println("send Msg....");
eventListennter.sendAllUser();
}
}
前端部分實現
- 引入jquery和socket.io.js
<html>
<head>
<meta charset="UTF-8">
<title>socket test</title>
<script src="./jquery.min.js" type="text/javascript"></script>
<script type="text/javascript" src="./socket.io.js"></script>
</head>
<body>
<h1>Netty-socketio demo</h1>
<br />
<div id="console" class="well"></div>
<form class="well form-inline" onsubmit="return false;">
<input id="token" class="input-xlarge" type="text" placeholder="token . . " />
<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>
<script type="text/javascript">
var socket = io.connect('http://localhost:8089');
socket.on('connect',function() {
alert("user connect");
console.log("user connect");
});
socket.on('newAlert', function(data) {
alert("receive alert");
console.log("receive alert..." + data.message);
});
socket.on('disconnect',function() {
alert("user disconnect");
console.log("user disconnect");
});
function sendDisconnect() {
socket.disconnect();
}
function sendMessage() {
console.log("send message token");
socket.emit('token', {
token : $('#token').val(),
message : 'message token'
});
}
</script>
</body>
</html>
運行結果
瀏覽器打開3~4個頁面連接
連接後,觸發服務器給所有用戶(客戶端)發送消息,收到結果如下
注意事項
- netty-SocketIO中添加監聽器,有這樣一個方法
addEventListener
,這個方法的第3個傳參是一個接口DataListener
,如下采用匿名類實現,DataListener需要實現onData(SocketIOClient client, SocketIOMessage data, AckRequest ackSender)
其中的AckRequest可以用來同步返回給客戶端(其實這個類是封裝了SocketIOClient對象),其中AckRequest.sendAckData(Ojbect obj)
底層是調用了SocketIOClient
的send方法,所以其本質與SocketIOClient是一樣的。
server.addEventListener("test", SocketIOMessage.class, new DataListener<SocketIOMessage>() {
@Override
public void onData(SocketIOClient client, SocketIOMessage data,
AckRequest ackSender) throws Exception {
System.err.println("receive from web " + data.toString());
SocketIOMessage send = new SocketIOMessage();
send.setMessage("test Ack");
send.setToken("server token");
ackSender.sendAckData(send);
}
});
- 用
SocketIOClient
的sendEvent
方法在socket.io.js客戶端可用socket.on('event', function(){....})
接收處理,那麼AckRequest
方法發送的消息怎麼接收呢?
其實,socket.io.js
客戶端的emit
函數可以傳3個參數,最後1個參數便回調處理AckRequest
返回的同步消息。
function sendTest() {
console.log("send test...");
socket.emit('test', {
token : $('#content').val(),
message: 'test ackData'
},
function(data) { //處理AckRequest返回的消息
console.log(data.message);
});
}