小型直播系統系列-樂聊TV的開發(四)
這一節,我們講解一下基於websocket的彈幕實現:首先了解一下websocket協議
websocket
們知道,傳統的HTTP協議是無狀態的,每次請求(request)都要由客戶端(如 瀏覽器)主動發起,服務端進行處理後返回response結果,而服務端很難主動向客戶端發送數據;這種客戶端是主動方,服務端是被動方的傳統Web模式 對於信息變化不頻繁的Web應用來說造成的麻煩較小,而對於涉及實時信息的Web應用卻帶來了很大的不便,如帶有即時通信、實時數據、訂閱推送等功能的應 用。在WebSocket規範提出之前,開發人員若要實現這些實時性較強的功能,經常會使用折衷的解決方法:輪詢(polling)和Comet技術。其實後者本質上也是一種輪詢,只不過有所改進。
輪詢是最原始的實現實時Web應用的解決方案。輪詢技術要求客戶端以設定的時間間隔週期性地向服務端發送請求,頻繁地查詢是否有新的數據改動。明顯地,這種方法會導致過多不必要的請求,浪費流量和服務器資源。
Comet技術又可以分爲長輪詢和流技術。長輪詢改進了上述的輪詢技術,減小了無用的請求。它會爲某些數據設定過期時間,當數據過期後纔會向服務端發送請求;這種機制適合數據的改動不是特別頻繁的情況。流技術通常是指客戶端使用一個隱藏的窗口與服務端建立一個HTTP長連接,服務端會不斷更新連接狀態以保持HTTP長連接存活;這樣的話,服務端就可以通過這條長連接主動將數據發送給客戶端;流技術在大併發環境下,可能會考驗到服務端的性能。
這兩種技術都是基於請求-應答模式,都不算是真正意義上的實時技術;它們的每一次請求、應答,都浪費了一定流量在相同的頭部信息上,並且開發複雜度也較大。
伴隨着HTML5推出的WebSocket,真正實現了Web的實時通信,使B/S模式具備了C/S模式的實時通信能力。WebSocket的工作流程是這 樣的:瀏覽器通過JavaScript向服務端發出建立WebSocket連接的請求,在WebSocket連接建立成功後,客戶端和服務端就可以通過 TCP連接傳輸數據。因爲WebSocket連接本質上是TCP連接,不需要每次傳輸都帶上重複的頭部數據,所以它的數據傳輸量比輪詢和Comet技術小 了很多。本文不詳細地介紹WebSocket規範,主要介紹下WebSocket在Java Web中的實現。
websocket彈幕
@OnOpen
public void onOpen(Session session) {
session.setMaxTextMessageBufferSize((int) MAX_BIG_LONG);
addOnlineCount(); // 在線數加1--必須先加1---錯誤的時候會減1
this.session = session;
if (!parseQueryString(session)) // 如果未能取得用戶id和type,退出
return;
// 驗證賬號,防止僞造
this.chatUser = loginChatServer(chatUserId);
if (this.chatUser == null) {
closeSession(session);
return;
}
addChatUserToHashMap(roomId, chatUserId);
try {
// 發一個應答標記,表示已經成功登陸,沒有構造
sendMessage("SUCCESS");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Constant.ONLINECOUNT = onlineCount.toString();
}
onopen標識客戶端與服務器進行通訊連接,在此處可進行業務邏輯的處理,將所需賬號提出進行保存
private static Log logger = LogFactory.getLog(ChatServer.class);
/** AtomicInteger:線程安全的整數對象 */
private static AtomicInteger onlineCount = new AtomicInteger(0);// 線程安全整數對象
private static long MAX_BIG_LONG = 1024 * 4 * 1024;
/** roomId與一個集合的哈希。集合中存儲當前房間的所有用戶 */
private static ConcurrentHashMap<String, CopyOnWriteArraySet<String>> roomToChatUserHashMap = new ConcurrentHashMap<String, CopyOnWriteArraySet<String>>();
/** 用戶與chatServer實例的哈希。 */
private static ConcurrentHashMap<String, ChatServer> chatUserToChatServer = new ConcurrentHashMap<String, ChatServer>();
/** token驗證 **/
private String token;
/** 房間號 **/
private String roomId;
/** chatUser 主鍵Id **/
private String chatUserId;
/** chatUserd對象 **/
private ChatUser chatUser;
聲明兩個類級別的ConcurrentHashMap存儲房間號和chatUser之間的對應關係,一個房間對應多個chatUser賬號,一個chatUser對應一個chatServer實例,發送消息時只需進行遍歷這倆個map找到相應的實例對象,調用它的發消息即可成功的發送
/**
* 一個房間對應的一個chatuser列表 發消息時候進行遍歷操作
*
* @param chatUserId
* @param chatUserId
* @return
*/
private boolean addChatUserToHashMap(String roomId, String chatUserId) {
try {
CopyOnWriteArraySet<String> chatUserIdSet = null;
if (roomToChatUserHashMap.containsKey(roomId)) {
chatUserIdSet = roomToChatUserHashMap.get(roomId);
} else {
chatUserIdSet = new CopyOnWriteArraySet<String>();
}
chatUserIdSet.add(chatUserId);
roomToChatUserHashMap.put(roomId, chatUserIdSet);
chatUserToChatServer.put(chatUserId, this);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
這是進入房間時,將自身賬號加入到房間聊天羣組,用來發廣播消息時遍歷的
/**
* 從哈希中移除已經斷開的連接
*
* @param chatUserId
* @param terminalUuid
* @return
*/
private boolean removeChatUserFromRoomHashMap(String roomId, String chatUserId) {
try {
CopyOnWriteArraySet<String> chatUserIdSet = null;
if (roomToChatUserHashMap.containsKey(roomId)) {// 如果存在
chatUserIdSet = roomToChatUserHashMap.get(chatUserId);// 取得chatUserId的集合
} else {
return true;
}
chatUserIdSet.remove(chatUserId);// 從集合中移除
if (chatUserIdSet.size() == 0) {// 如果已經沒有連接終端
roomToChatUserHashMap.remove(roomId);// 則清除
} else {
roomToChatUserHashMap.put(roomId, chatUserIdSet);// 更新哈希
}
ChatServer chatServer = chatUserToChatServer.get(chatUserId);
// 釋放資源,清空chatServer
chatServer = null;
chatUserToChatServer.remove(chatUserId);// 將chatServer的實例從哈希中移除。
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
當斷開聊天室時,應該講其從聊天組移除掉
* 關閉websocket連接。
*
* @param session
* 要關閉的會話
*/
private void closeSession(Session session) {
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
/**
* 連接關閉調用的方法
*/
@OnClose
public void onClose(Session session) {
try {
removeChatUserFromRoomHashMap(this.roomId, this.chatUserId);
subOnlineCount(); // 在線數減1
} catch (Exception e) {
}
}
關閉調用的方法
@OnMessage
public void onMessage(String message, Session session) {
if (StringUtils.isBlank(message)) // 收到的是空串
return;
if (StringUtils.equals(Constant.SUCCESS_RESPONSE, message)) {
return;
}
Gson gson = null;
try {// 解析json串
gson = new Gson();
ChatMessage chatMessage = gson.fromJson(message, ChatMessage.class);
// 解析json出錯
if (chatMessage == null)
return;
/**
* 目的爲了以後對每個模塊進行拓展,所以分開寫
*
*/
// 如果是圖片類型的消息體
if (StringUtils.equals(chatMessage.getMessageType(), EnumMessageType.IMAGE.name())) {
try {
boolean dealImageResult = dealBinary(chatMessage);// 處理圖片結果
if (!dealImageResult) {
return;// 如果沒生成,則返回
}
} catch (Exception e) {
logger.error("處理圖片異常" + e.getMessage());
}
}
// 處理小視頻
else if ((StringUtils.equals(chatMessage.getMessageType(), EnumMessageType.VIDEO.name()))) {
try {
boolean dealVideoResult = dealBinary(chatMessage);
if (!dealVideoResult) {
return;
}
} catch (Exception e) {
logger.error("處理小視頻異常" + e.getMessage());
}
}...................代碼不全,需要的找我聯繫
進行通訊的模塊
不足之處
爲考慮多個終端登錄的情況,比如一個用戶雙開瀏覽器,這時候這套方案顯然是不可行的,具體的解決方案找我諮詢
彈幕聊天室
如果成功了之後就會是下面的場景
開始動手製作你自己的直播間和聊天室吧。