文章目錄
- 一、WebSocket簡介
- 二、Netty-SocketIO 服務端Demo
- 1、添加配置類SocketIOServer
- 2、添加消息結構類MessageInfo
- 3、添加消息處理類MessageEventHandler
- 4、添加啟動類ServerRunner
- 三、SocketIO 用戶端Demo
一、WebSocket簡介
WebSocket是HTML5新增的一種全雙工通信協定,用戶端和服務端基于TCP握手連接配接成功後,兩者之間就可以建立持久性的連接配接,實作雙向資料傳輸。
傳統HTTP和WebSocket的不同點:
HTTP是單向資料流,用戶端向服務端發送請求,服務端響應并傳回資料;
WebSocket連接配接後可以實作用戶端和服務端雙向資料傳遞。
由于是新的協定,HTTP的url使用"http//"或"https//"開頭;WebSocket的url使用"ws//"開頭。
傳統HTTP和WebSocket的相同點:
都需要建立TCP連接配接,都是屬于七層協定中的應用層協定。
傳統通過HTTP請求模拟雙向資料傳遞的方式是http+Polling(輪詢)和http+Long Polling(長輪詢)。輪詢(Polling)就是用戶端定時發送get/post請求向服務端請求資料,這種方式能滿足一定的需求,但是存在一些問題,例如如果服務端沒有新資料,用戶端請求到的資料都是舊資料,這樣不僅浪費了帶寬資源,而且占用CPU記憶體。
LongPolling(長輪詢)就是在Polling上的一些改進,即如果服務端沒有新資料傳回給用戶端,服務端會把目前的這個服務請求保持住(hold),當有新資料時則傳回新資料,如果超過一定時間服務端仍沒有新資料,則服務端傳回逾時請求,用戶端接收到逾時請求,然後在發送服務請求,一直循環執行。
雖然一定程度解決了帶寬資源和CPU記憶體浪費情況,但是當服務端資料更新很快,這和輪詢(Polling)沒有本質上的差別,而且http資料包的頭部資料量往往很大(通常有400多個位元組),但是真正被伺服器需要的資料卻很少(有時隻有10個位元組左右),這樣的資料包在網絡上周期性的傳輸,難免對網絡帶寬是一種浪費。在高并發的情況下,這對伺服器是一個很大的挑戰。綜合上面輪詢的種種問題,WebSocket全雙工通信成為一種很好的解決政策。
二、Netty-SocketIO 服務端Demo
實際應用中,如果需要WebSocket進行雙向資料通信,SocketIO是一個非常好的選擇。它是用JavaScript語言實作的WebSocket架構,簡單易用,穩定可靠。SocketIO不是WebSocket,它隻是将WebSocket和輪詢 (Polling)機制以及其它的實時通信方式封裝成了通用的接口,并且在服務端實作了這些實時機制的相應代碼。也就是說,WebSocket僅僅是SocketIO實作實時通信的一個子集。
常用的方式是前端使用SocketIO,後端使用node.js實作SocketIO的接口。但由于目前服務端使用JAVA,是以我使用的是Netty-SocketIO開源庫,基于Netty網絡庫編寫的WebSocket實作。
下面是從項目工程中提煉出的Netty-SocketIO服務端Demo:
首先在pom.xml中添加相應的依賴庫:
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.13</version>
</dependency>
<dependency>
<groupId>io.socket</groupId>
<artifactId>socket.io-client</artifactId>
<version>1.0.0</version>
</dependency>
1、添加配置類SocketIOServer
添加SocketIO配置類NettySocketConfig.java,用于填寫nettysocket的相關配置資訊, 注冊netty-socketio服務端,相關代碼如下:
package nssc.simulation.DataTransmission.socket;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Slf4j
public class NettySocketConfig {
@Bean
public SocketIOServer socketIOServer() {
/*
* 建立Socket,并設定監聽端口
*/
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
// 設定主機名,預設是0.0.0.0
// config.setHostname("localhost");
// 設定監聽端口
config.setPort(9090);
// 協定更新逾時時間(毫秒), 預設10000, HTTP握手更新為ws協定逾時時間
config.setUpgradeTimeout(10000);
// Ping消息間隔(毫秒), 預設25000, 用戶端向伺服器發送一條心跳消息間隔
config.setPingInterval(60000);
// Ping消息逾時時間(毫秒), 預設60000, 這個時間間隔内沒有接收到心跳消息就會發送逾時事件
config.setPingTimeout(180000);
final SocketIOServer server = new SocketIOServer(config);
return server;
}
@Bean
public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
return new SpringAnnotationScanner(socketServer);
}
}
2、添加消息結構類MessageInfo
添加消息結構類MessageInfo.java,用于接收前台使用者資訊,相關代碼如下:
消息類型按需自己設計,這裡我定義的是byte[]位元組數組。
package nssc.simulation.DataTransmission.socket;
import lombok.ToString;
import org.springframework.stereotype.Component;
@Component
@ToString
public class MessageInfoStructure {
//消息類型
private String msgType;
//消息内容
private byte[] msgContent;
public String getMsgType() {
return msgType;
}
public void setMsgType(String msgType) {
this.msgType = msgType;
}
public byte[] getMsgContent() {
return msgContent;
}
public void setMsgContent(byte[] msgContent) {
this.msgContent = msgContent;
}
}
3、添加消息處理類MessageEventHandler
添加消息處理類MessageEventHandler.Java,用于前後端消息事件的互動,相關代碼如下:
package nssc.simulation.DataTransmission.socket;
import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.HandshakeData;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.corundumstudio.socketio.annotation.OnEvent;
import lombok.extern.slf4j.Slf4j;
import nssc.simulation.EmulationDataTransmission.udp.UdpCommonSendSave;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Component
@Slf4j
public class NettySocketEventHandler {
public static ConcurrentMap<String, SocketIOClient> socketIOClientMap =
new ConcurrentHashMap<>();
//socket事件消息接收入口
@OnEvent(value = "message_event") //value值與前端自行商定
public void onEvent(SocketIOClient client, AckRequest ackRequest, MessageInfoStructure data) throws Exception {
//根據msgType類别進行資料類型判斷,
if (data.getMsgType().equals("XXXXXData")){ //資料類型辨別
client.sendEvent("message_event", "已成功接收資料"); //向前端發送接收資料成功辨別
//data.getMsgContent()擷取前端推送資料
//......這裡可填寫接收資料後的相關業務邏輯代碼
}
}
//socket添加@OnDisconnect事件,用戶端斷開連接配接時調用,重新整理用戶端資訊
@OnDisconnect
public void onDisconnect(SocketIOClient client) {
log.info("--------------------用戶端已斷開連接配接--------------------");
client.disconnect();
}
//socket添加connect事件,當用戶端發起連接配接時調用
@OnConnect
public void onConnect(SocketIOClient client) {
if (client != null)
{
HandshakeData client_mac = client.getHandshakeData();
String mac = client_mac.getUrl();
//存儲SocketIOClient,用于向不同用戶端發送消息
socketIOClientMap.put(mac, client);
log.info("--------------------用戶端連接配接成功---------------------");
} else {
log.error("用戶端為空");
}
}
/**
* 廣播消息 函數可在其他類中調用
*/
public static void sendBroadcast(byte[] data) {
for (SocketIOClient client : socketIOClientMap.values()) { //向已連接配接的所有用戶端發送資料,map實作用戶端的存儲
if (client.isChannelOpen()) {
client.sendEvent("message_event", data);
}
}
}
}
4、添加啟動類ServerRunner
在項目服務啟動的時候啟動socket.io服務, 新增啟動類ServerRunner.java,相關代碼如下所示:
package nssc.simulation.DataTransmission.socket;
import com.corundumstudio.socketio.SocketIOServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(value=1)
@Slf4j
public class NettySocketRunnable implements CommandLineRunner {
private final SocketIOServer server;
@Autowired
public NettySocketRunnable(SocketIOServer server) {
this.server = server;
}
@Override
public void run(String... args) throws Exception {
log.info("--------------------前端socket.io通信啟動成功!---------------------");
server.start();
}
}
三、SocketIO 用戶端Demo
用戶端Demo這裡參考了Github上的官方開源庫SocketIO,按照項目所需功能可以後續進行修改
<script src="js/socket.io/socket.io.js"></script>
<script src="js/moment.min.js"></script>
<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script>
/**
* 前端js的 socket.emit("事件名","參數資料")方法,是觸發後端自定義消息事件的時候使用的,
* 前端js的 socket.on("事件名",匿名函數(伺服器向用戶端發送的資料))為監聽伺服器端的事件
**/
var socket = io.connect('http://localhost:9092');
//監聽伺服器連接配接事件
socket.on('connect', function() {
output('<span class="connect-msg">Client has connected to the server!</span>');
});
//監聽伺服器端發送消息事件
socket.on('message_event', function(data) {
message(data)
//console.log("伺服器發送的消息是:"+data);
});
//監聽伺服器關閉服務事件
socket.on('disconnect', function() {
output('<span class="disconnect-msg">The client has disconnected!</span>');
});
function sendDisconnect() {
socket.disconnect();
}
//點選發送消息觸發
function sendMessage() {
var message = $('#msg').val();
$('#msg').val('');
var jsonObject = {userName: userName,
message: message};
socket.emit('chatevent', jsonObject);
}
</script>