传统的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);
});
}