一,前言
目前在很多網(wǎng)站為了實(shí)現(xiàn)推送技術(shù)所用的技術(shù)都是 Ajax 輪詢。輪詢是在特定的的時(shí)間間隔(如每1秒),由瀏覽器對服務(wù)器發(fā)出HTTP請求,然后由服務(wù)器返回最新的數(shù)據(jù)給客戶端的瀏覽器。HTTP 協(xié)議是一種無狀態(tài)的、無連接的、單向的應(yīng)用層協(xié)議。它采用了請求/響應(yīng)模型。通信請求只能由客戶端發(fā)起,服務(wù)端對請求做出應(yīng)答處理。
然而,這種通信模型有一個(gè)弊端: HTTP協(xié)議無法實(shí)現(xiàn)服務(wù)器主動(dòng)向客戶端發(fā)起消息。這種單向請求的特點(diǎn)注定了如果服務(wù)器有連續(xù)的狀態(tài)變化,客戶端要獲知就非常麻煩。大多數(shù)web 應(yīng)用程序?qū)⑼ㄟ^頻繁的異步AJAX請求實(shí)現(xiàn)長輪詢。輪詢的效率低,非常浪費(fèi)資源(因?yàn)楸仨毑煌_B接,或者 HTTP連接始終打開)。因此在這種情況下WebSocket應(yīng)運(yùn)而生。
二,WebSocket介紹
WebSocket協(xié)議是基于TCP的一種新的網(wǎng)絡(luò)協(xié)議,是 HTML5 開始提供的一種在單個(gè) TCP 連接上進(jìn)行全雙工通訊的協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工(full-duplex)通信--允許服務(wù)器主動(dòng)發(fā)送信息給客戶端,使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單。彌補(bǔ)了Http協(xié)議在持久通信能力上的不足。
WebSocket通信協(xié)議于2011年被IETF定為標(biāo)準(zhǔn)RFC6455,并被RFC7936所補(bǔ)充規(guī)范。
WebSocket 使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)。在 WebSocket API 中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸。
在 WebSocket API 中,瀏覽器和服務(wù)器只需要做一個(gè)握手的動(dòng)作,然后,瀏覽器和服務(wù)器之間就形成了一條快速通道。兩者之間就直接可以數(shù)據(jù)互相傳送。
HTML5 定義的 WebSocket 協(xié)議,能更好的節(jié)省服務(wù)器資源和帶寬,并且能夠更實(shí)時(shí)地進(jìn)行通訊。
?瀏覽器通過 JavaScript 向服務(wù)器發(fā)出建立 WebSocket 連接的請求,連接建立以后,客戶端和服務(wù)器端就可以通過 TCP 連接直接交換數(shù)據(jù)。
當(dāng)你獲取 Web Socket 連接后,你可以通過 send() 方法來向服務(wù)器發(fā)送數(shù)據(jù),并通過onmessage 事件來接收服務(wù)器返回的數(shù)據(jù)。
1.1實(shí)現(xiàn)原理
在實(shí)現(xiàn)WebSocket連線過程中,需要通過瀏覽器發(fā)出WebSocket連線請求,然后服務(wù)器發(fā)出回應(yīng),這個(gè)過程通常稱為"握手" 。在 WebSocket API,瀏覽器和服務(wù)器只需要做一個(gè)握手的動(dòng)作,然后,瀏覽器和服務(wù)器之間就形成了一條快速通道。兩者之間就直接可以數(shù)據(jù)互相傳送。在此WebSocket 協(xié)議中,為我們實(shí)現(xiàn)即時(shí)服務(wù)帶來了兩大好處:
-
Header:互相溝通的Header是很小的-大概只有 2 Bytes
-
Server Push:服務(wù)器的推送,服務(wù)器不再被動(dòng)的接收到瀏覽器的請求之后才返回?cái)?shù)據(jù),而是在有新數(shù)據(jù)時(shí)就主動(dòng)推送給瀏覽器。
1.2 WebSocket協(xié)議
該協(xié)議有兩部分:握手和數(shù)據(jù)傳輸。握手是基于http協(xié)議的。
一個(gè)典型的WebSocket握手請求如下:
客戶端請求
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
來自服務(wù)器的握手看起來像如下形式:
HTTP/1.1 101 switching Protocols
Upgrade: websocket
Connection: upgrade
Sec-websocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+XOo=
Sec-websocket-Extensions: permessage-def1ate
字段說明:
頭名稱 | 說明 |
---|---|
Connection | 必須設(shè)置 Upgrade,表示客戶端希望連接升級。 |
Upgrade | 字段必須設(shè)置 Websocket,表示希望升級到 Websocket 協(xié)議。 |
sec-websocket-version | 表示支持的 Websocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均應(yīng)當(dāng)棄用。 |
sec-websocket-Key | 是隨機(jī)的字符串,服務(wù)器端會用這些數(shù)據(jù)來構(gòu)造出一個(gè) SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一個(gè)特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后計(jì)算 SHA-1 摘要,之后進(jìn)行 BASE-64 編碼,將結(jié)果做為 “Sec-WebSocket-Accept” 頭的值,返回給客戶端。如此操作,可以盡量避免普通 HTTP 請求被誤認(rèn)為 Websocket 協(xié)議。 |
sec-websocket-Extensions | 協(xié)議擴(kuò)展類型 |
Origin | 字段是可選的,通常用來表示在瀏覽器中發(fā)起此 Websocket 連接所在的頁面,類似于 Referer。但是,與 Referer 不同的是,Origin 只包含了協(xié)議和主機(jī)名稱。 |
WebSocket 連接的過程是:
首先,客戶端發(fā)起http請求,經(jīng)過3次握手后,建立起TCP連接;http 請求里存放 WebSocket 支持的版本號等信息,如:Upgrade、Connection、WebSocket-Version等;
然后,服務(wù)器收到客戶端的握手請求后,同樣采用HTTP協(xié)議回饋數(shù)據(jù);
最后,客戶端收到連接成功的消息后,開始借助于TCP傳輸信道進(jìn)行全雙工通信。
三,WebSocket在B/S端的實(shí)現(xiàn)基礎(chǔ)
2.1客戶端(瀏覽器)實(shí)現(xiàn)
2.1.1 WebSocket對象
實(shí)現(xiàn)webSockets的 web瀏覽器將通過webSocket對象公開所有必需的客戶端功能(主要指支持 Html5的瀏覽器)。以下API用于創(chuàng)建websocket對象:
?var ws = new websocket(ur1);
參數(shù)url格式說明: ws : / /ip地址:端口號/資源名稱
2.1.2 WebSocket事件
事件 | 事件處理程序 | 描述 |
---|---|---|
open | websocket對象.onopen | 連接建立時(shí)觸發(fā) |
message | websocket對象.onmessage | 客戶端接收服務(wù)端數(shù)據(jù)時(shí)觸發(fā) |
error | websocket對象.onerror | 通信發(fā)生錯(cuò)誤時(shí)觸發(fā) |
close | websocket對象.onclose | 連接關(guān)閉時(shí)觸發(fā) |
2.1.3WebSocket方法
WebSocket對象的相關(guān)方法:
方法 | 描述 |
---|---|
send() | 使用連接發(fā)送數(shù)據(jù),WebSocket.send() 方法將需要通過 WebSocket 鏈接傳輸至服務(wù)器的數(shù)據(jù)排入隊(duì)列,并根據(jù)所需要傳輸?shù)?data bytes 的大小來增加bufferedAmount 的值。若數(shù)據(jù)無法傳輸(例如數(shù)據(jù)需要緩存而緩沖區(qū)已滿)時(shí),套接字會自行關(guān)閉。 |
close() |
WebSocket.close() 方法關(guān)閉 WebSocket 連接或連接嘗試(如果有的話)。如果連接已經(jīng)關(guān)閉,則此方法不執(zhí)行任何操作。 |
2.1.4客戶端實(shí)例
WebSocket 協(xié)議本質(zhì)上是一個(gè)基于 TCP 的協(xié)議。
為了建立一個(gè) WebSocket 連接,客戶端瀏覽器首先要向服務(wù)器發(fā)起一個(gè) HTTP 請求,這個(gè)請求和通常的 HTTP 請求不同,包含了一些附加頭信息,其中附加頭信息"Upgrade: WebSocket"表明這是一個(gè)申請協(xié)議升級的 HTTP 請求,服務(wù)器端解析這些附加的頭信息然后產(chǎn)生應(yīng)答信息返回給客戶端,客戶端和服務(wù)器端的 WebSocket 連接就建立起來了,雙方就可以通過這個(gè)連接通道自由的傳遞信息,并且這個(gè)連接會持續(xù)存在直到客戶端或者服務(wù)器端的某一方主動(dòng)的關(guān)閉連接?;趈s和html的基礎(chǔ)實(shí)現(xiàn)如下:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>測試</title>
<script type="text/javascript">
function WebSocketTest()
{
if ("WebSocket" in window)
{
alert("您的瀏覽器支持 WebSocket!");
// 打開一個(gè) web socket
var ws = new WebSocket("ws://localhost:8099/chat");
ws.onopen = function()
{
// Web Socket 已連接上,使用 send() 方法發(fā)送數(shù)據(jù)
ws.send("發(fā)送數(shù)據(jù)");
alert("數(shù)據(jù)發(fā)送中...");
};
ws.onmessage = function (evt)
{
var received_msg = evt.data;
alert("數(shù)據(jù)已接收...");
};
ws.onclose = function()
{
// 關(guān)閉 websocket
alert("連接已關(guān)閉...");
};
}
else
{
// 瀏覽器不支持 WebSocket
alert("您的瀏覽器不支持 WebSocket!");
}
}
</script>
</head>
<body>
<div id="sse">
<a href="javascript:WebSocketTest()">運(yùn)行 WebSocket</a>
</div>
</body>
</html>
2.2 服務(wù)端實(shí)現(xiàn)
Tomcat的7.0.5 版本開始支持webSocket,并且實(shí)現(xiàn)了Java WebSocket規(guī)范(JSR356)。
Java websocket應(yīng)用由一系列的websocketEndpoint組成。Endpoint是一個(gè)java對象代表websocket鏈接的一端,對于服務(wù)端,我們可以視為處理具體websocket消息的接口,就像Servlet之與http請求一樣。
我們可以通過兩種方式定義Endpoint:
-
第一種是編程式,即繼承類javax.websocket.Endpoint并實(shí)現(xiàn)其方法。
-
第二種是注解式,即定義一個(gè)服務(wù)組件,并添@serverEndpoint相關(guān)注解。
Endpoint實(shí)例在webSocket握手時(shí)創(chuàng)建,并在客戶端與服務(wù)端鏈接過程中有效,最后在鏈接關(guān)閉時(shí)結(jié)束。在Endpoint接口中明確定義了與其生命周期相關(guān)的方法,規(guī)范實(shí)現(xiàn)者確保生命周期的各個(gè)階段調(diào)用實(shí)例的相關(guān)方法。生命周期方法如下:
方法 | 含義描述 | 注解 |
---|---|---|
onClose | 當(dāng)會話關(guān)閉時(shí)調(diào)用。 | onclose |
onOpen | 當(dāng)開啟一個(gè)新的會話時(shí)調(diào)用,該方法是客戶端與服務(wù)端握手成功后調(diào)用的方法。 | onopen |
onError | 當(dāng)連接過程中異常時(shí)調(diào)用。 | onError |
服務(wù)端如何接收客戶端發(fā)送的數(shù)據(jù)呢?
通過為 session添加MessageHandler消息處理器來接收消息,當(dāng)采用注解方式定義Endpoint時(shí),我們還可以通過@onMessage 注解指定接收消息的方法。
服務(wù)端如何推送數(shù)據(jù)給客戶端呢?
發(fā)送消息則由RemoteEndpoint 完成,其實(shí)例由session維護(hù),根據(jù)使用情況,我們可以通過session.getBasicRemote獲取同步消息發(fā)送的實(shí)例,然后調(diào)用其sendXxx ()方法就可以發(fā)送消息,可以通過session. getAsyncRemote 獲取異步消息發(fā)送實(shí)例。
服務(wù)端代碼示例:
//該注解用來指定一個(gè)URI,客戶端可以通過這個(gè)URI來連接到WebSocket。類似Servlet的注解mapping。無需在web.xml中配置。
@ServerEndpoint("/webSocket/{id}")
@Component("webSocket")
public class WebSocket {
private static Logger logger = Logger.getLogger(WebSocket.class);
//靜態(tài)變量,用來記錄當(dāng)前在線連接數(shù)。應(yīng)該把它設(shè)計(jì)成線程安全的。
private static int onlineCount = 0;
//與某個(gè)客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù)
private Session session;
//concurrent包的線程安全Map,用來存放每個(gè)客戶端對應(yīng)的MyWebSocket對象。若要實(shí)現(xiàn)服務(wù)端與單一客戶端通信的話,可以使用Map來存放,其中Key可以為用戶標(biāo)識
private static ConcurrentMap<String, WebSocket> webSocketMap = new ConcurrentHashMap<>();
private static ConcurrentMap<String, WebSocket> webSocketMapAdmin = new ConcurrentHashMap<>();
public Session getSession() {
return session;
}
public static WebSocket getWebSocket(String id) {
return webSocketMap.get(id);
}
/**
* 連接建立成功調(diào)用的方法
*
* @param session 可選的參數(shù)。session為與某個(gè)客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù)
*/
@OnOpen
public void onOpen(Session session, @PathParam("id") String id) {
this.session = session;
//String sessionId = session.getId();
webSocketMap.put(id, this); //加入map中
if (id.contains("admin")) {// 后臺登陸用戶,加入list
webSocketMapAdmin.put(id, this);
}
addOnlineCount(); //在線數(shù)加1
System.out.println("有新連接加入!當(dāng)前在線人數(shù)為" + getOnlineCount());
}
/**
* 連接關(guān)閉調(diào)用的方法
*/
@OnClose
public void onClose(@PathParam("id") String id) {
webSocketMap.remove(id); //從map中刪除
webSocketMapAdmin.remove(id);
subOnlineCount(); //在線數(shù)減1
System.out.println("有一連接關(guān)閉!當(dāng)前在線人數(shù)為" + getOnlineCount());
}
/**
* 收到客戶端消息后調(diào)用的方法
*
* @param message 客戶端發(fā)送過來的消息
* @param session 可選的參數(shù)
*/
@OnMessage
public static void onMessage(String message, Session session) {
//群發(fā)消息
if (webSocketMapAdmin.size() > 0) {
for (WebSocket item : webSocketMapAdmin.values()) {
try {
//System.out.println(item.session.getId());
item.session.getBasicRemote().sendText(message);
} catch (IOException e) {
logger.error("IO異常");
continue;
}
}
}
}
/**
* 發(fā)生錯(cuò)誤時(shí)調(diào)用
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
//System.out.println("發(fā)生錯(cuò)誤");
logger.error("發(fā)生錯(cuò)誤");
}
}
四,WebSocket在聊天室功能中的實(shí)現(xiàn)
3.1服務(wù)端代碼
這里以前后端分離的SpringBoot項(xiàng)目集成聊天功能為例,首先需要在SpringBoot的項(xiàng)目中導(dǎo)入WebSocket依賴。
? ? ? ?<dependency>
? ? ? ? ? ?<groupId>org.springframework.boot</groupId>
? ? ? ? ? ?<artifactId>spring-boot-starter-websocket</artifactId>
? ? ? ?</dependency>
然后需要?jiǎng)?chuàng)建ServerEndpointExporter并注入Spring容器,如果想在使用內(nèi)嵌容器的Spring Boot應(yīng)用中使用@ServerEndpoint
,我們需要聲明一個(gè)單獨(dú)的ServerEndpointExporter的Bean
:
@Bean
public ServerEndpointExporter serverEndpointExporter() {
? return new ServerEndpointExporter();
}
該bean將使用底層的WebSocket容器注冊任何被@ServerEndpoint
注解的beans。
創(chuàng)建一個(gè)WebSocket服務(wù),并通過@ServerEndpoint
指明該類作為服務(wù)端點(diǎn),直接構(gòu)建請求接口即可,并在其中完成消息處理的業(yè)務(wù)。
package com.yy.util;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author young
* Date 2023/6/7 12:34
* Description: WebSocket服務(wù)
*/
@Component
@ServerEndpoint("/chat/{nickname}")
public class WebSocketServer {
Logger log = LoggerFactory.getLogger(WebSocketServer.class);
protected static final Map<String, Session> SESSION_MAP = new ConcurrentHashMap<>(10);
@OnOpen
public void onOpen(Session session, @PathParam("nickname") String username) {
SESSION_MAP.put(username, session);
log.info("有新用戶加入---》{},當(dāng)前在線人數(shù)為:{}", username, SESSION_MAP.size());
JSONObject result = new JSONObject();
JSONArray array = new JSONArray();
result.set("users", array);
SESSION_MAP.keySet().forEach(s -> {
JSONObject object = new JSONObject();
object.set("nickname", s);
array.add(object);
});
log.info("存入的對象集合信息{}", Arrays.toString(result.values().toArray()));
// 后臺發(fā)送消息給所有的客戶端
sendAllMessage(JSONUtil.toJsonStr(result));
}
/**
* 收到客戶端消息后調(diào)用的方法
* 后臺收到客戶端發(fā)送過來的消息
* onMessage 是一個(gè)消息的中轉(zhuǎn)站
* 接受 瀏覽器端 socket.send 發(fā)送過來的 json數(shù)據(jù)
* @param message 客戶端發(fā)送過來的消息
*/
@OnMessage
public void onMessage(String message, Session session, @PathParam("nickname") String username) {
log.info("服務(wù)端收到用戶{}的消息{}", username, message);
JSONObject obj = JSONUtil.parseObj(message);
// to表示發(fā)送給哪個(gè)用戶
String toUser = obj.getStr("to");
// 發(fā)送的消息文本 hello
String text = obj.getStr("text");
// 根據(jù) to用戶名來獲取 session,再通過session發(fā)送消息文本
Session toSession = SESSION_MAP.get(toUser);
if (ObjectUtil.isNotEmpty(toSession)) {
JSONObject object = new JSONObject();
object.set("from", username);
object.set("text", text);
this.sendMsg(object.toString(), toSession);
log.info("發(fā)送給{}的消息---》{}", toUser, object);
} else {
log.info("消息發(fā)送失敗,未找到用戶{}", toUser);
}
}
@OnClose
public void onClose(Session session, @PathParam("username") String username) {
SESSION_MAP.remove(username);
log.info("有一個(gè)鏈接關(guān)閉,移除{}的用戶session,當(dāng)前在線人數(shù)為:{}", username, SESSION_MAP.size());
}
@OnError
public void onError(Session session, Throwable throwable) {
log.info("消息發(fā)生錯(cuò)誤");
throwable.printStackTrace();
}
private void sendMsg(String message, Session session) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
log.error("服務(wù)端發(fā)送消息失敗", e);
}
}
private void sendAllMessage(String message) {
SESSION_MAP.values().forEach(s -> {
try {
s.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("服務(wù)端發(fā)送消息失敗", e);
}
});
}
}
此時(shí)一個(gè)簡易的服務(wù)端就完成了??蛻舳税l(fā)送消息時(shí)綁定該接口就能完成消息交互了。
3.2客戶端代碼
客戶端這塊主要在前端處理當(dāng)前用戶與遠(yuǎn)程用戶聊天時(shí),通過創(chuàng)建WebSocekt實(shí)例與服務(wù)端交互實(shí)現(xiàn)消息的組裝傳遞與轉(zhuǎn)發(fā),從而達(dá)到用戶相互聊天的功能。這里頁面主要通過Vue和ElementPlus完成。大致前端代碼如下:
<template>
<div class="ml20" style="padding: 10px; margin-bottom: 50px;">
<el-row>
<el-col :span="4">
<el-card style="width: 300px; height: 300px; color: #333">
<div style="padding-bottom: 10px; border-bottom: 1px solid #ccc">
在線用戶<span style="font-size: 12px">(點(diǎn)擊聊天氣泡開始聊天)</span>
</div>
<div style="padding: 10px 0" v-for="user in users" :key="user.username">
<span style="padding-bottom: 10px; border-bottom: 1px solid #48a6f3">{{ user.nickname }}</span>
<el-icon color="#409EFC" class="no-inherit" style="margin-left: 10px;text-align: center; font-size: 16px; cursor: pointer"
@click="chatUser = user.nickname">
<ChatLineRound />
</el-icon>
<span style="font-size: 12px;color: limegreen; margin-left: 5px" v-if="user.nickname === chatUser">聊天中...</span>
</div>
</el-card>
</el-col>
<el-col :span="20">
<div
style="width: 800px; margin: 0 auto; background-color: white; border-radius: 5px; box-shadow: 0 0 10px #ccc">
<div style="text-align: center; line-height: 50px;">
Web聊天室({{ chatUser }})
</div>
<div style="height: 300px;
overflow:auto;
border-top: 1px solid #ccc"
v-html="content">
</div>
<div style="height: 200px">
<textarea v-model="text"
placeholder="在此輸入信息……"
style="height: 120px;
width: -webkit-fill-available;
padding: 20px;
border: none;
background-color: #f7f7fa;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
outline: none"
@keyup.enter="send"
>
</textarea>
<div style="text-align: right; padding-right: 10px">
<el-button type="primary" size="small" @click="send">發(fā)送</el-button>
</div>
</div>
</div>
</el-col>
</el-row>
<div class="fixed3">
<a href="#"><img src="../../assets/kefu.png" style="border:5px solid #0f99e9;border-radius: 20%;" alt=""/></a>
</div>
</div>
</template>
<script>
import {mixin} from "../../mixins/index";
import { ChatLineRound} from "@element-plus/icons-vue";
let socket;
export default {
name: "ChatHome",
mixins:[mixin],
components:{
ChatLineRound
},
data() {
return {
circleUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
user: localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {},
isCollapse: false,
users: [],
chatUser: '',
text: "",
messages: [],
content: ''
}
},
created() {
this.init()
},
methods: {
send() {
if (!this.chatUser) {
this.$message({type: 'warning', message: "請選擇聊天對象"})
return;
}
if (!this.text) {
this.$message({type: 'warning', message: "請輸入內(nèi)容"})
} else {
if (typeof (WebSocket) == "undefined") {
console.log("您的瀏覽器不支持WebSocket");
} else {
console.log("您的瀏覽器支持WebSocket");
// 組裝待發(fā)送的消息 json
// {"from": "張三", "to": "李四", "text": "聊天文本"}
let message = {from: this.user.nickname, to: this.chatUser, text: this.text}
socket.send(JSON.stringify(message));
// 將組裝好的json發(fā)送給服務(wù)端,由服務(wù)端進(jìn)行轉(zhuǎn)發(fā)
this.messages.push({user: this.user.nickname, text: this.text})
// 構(gòu)建消息內(nèi)容,本人消息
this.createContent(null, this.user.nickname, this.text)
this.text = '';
}
}
},
createContent(remoteUser, nowUser, text) {
// 這個(gè)方法是用來將 json的聊天消息數(shù)據(jù)轉(zhuǎn)換成 html的。
let html
// 當(dāng)前用戶消息
if (nowUser) {
// nowUser 表示是否顯示當(dāng)前用戶發(fā)送的聊天消息,綠色氣泡
html = "<div class=\"el-row\" style=\"padding: 5px 0;\">\n" +
" <div class=\"el-col el-col-22\" style=\"text-align: right;margin-top: auto;margin-bottom: auto; padding-right: 10px\">\n"
+ " <div class=\"tip left\" style=\" width: auto;\n" +
" height: 30px;\n" +
" background: #48a6f3;\n" +
" padding: 5px 20px;\n" +
" margin: 4px;\n" +
" line-height: 30px;\n" +
" font-size: 14px;\n" +
" border-radius: 10px;\n" +
" margin-left: 10px;\n" +
" position: relative;\n" +
" float: right;\">"
+ text
+ "</div>\n"
+ " </div>\n"
+ " <div class=\"el-col el-col-2\" style=\"text-align: left;margin-top: auto;padding-left: 10px;\">\n"
+ " <span class=\"el-avatar el-avatar--circle\" style=\"height: 40px; width: 40px; line-height: 40px;\">\n"
+ " <img :src=\"http://img.mp.itc.cn/upload/20161123/2fb03a6584f24901acc5e02d19ece787_th.jpeg\" style=\"object-fit: cover;\">\n"
+ " </span>\n" + " </div>\n" + "</div>";
} else if (remoteUser) {
// remoteUser表示遠(yuǎn)程用戶聊天消息,藍(lán)色的氣泡
html = "<div class=\"el-row\" style=\"padding: 5px 0;\">\n"
+ " <div class=\"el-col el-col-2\" style=\"text-align: right;margin-top: auto;\">\n"
+ " <span class=\"el-avatar el-avatar--circle\" style=\"height: 40px; width: 40px; line-height: 40px;\">\n"
+ " <img src=\"http://img.mp.itc.cn/upload/20161123/2fb03a6584f24901acc5e02d19ece787_th.jpeg\" style=\"object-fit: cover;\">\n"
+ " </span>\n"
+ " </div>\n"
+ " <div class=\"el-col el-col-22\" style=\"text-align: left; padding-left: 10px;margin-top: auto;margin-bottom: auto;\">\n"
+ " <div class=\"tip right\" style=\"width: auto;\n" +
" height: 30px;\n" +
" background: #eeeeee;\n" +
" padding: 5px 20px;\n" +
" margin: 4px;\n" +
" line-height: 30px;\n" +
" font-size: 14px;\n" +
" border-radius: 10px;\n" +
" margin-left: 10px;\n" +
" position: relative;\n" +
" float: left;\">"
+ text
+ "</div>\n"
+ " </div>\n"
+ "</div>";
}
console.log(html)
this.content += html;
},
init() {
let nickname = this.user.nickname;
let _this = this;
if (typeof (WebSocket) == "undefined") {
console.log("您的瀏覽器不支持WebSocket");
} else {
console.log("您的瀏覽器支持WebSocket");
let socketUrl = "ws://localhost:8084/chat/" + nickname;
if (socket != null) {
socket.close();
socket = null;
}
// 開啟一個(gè)websocket服務(wù)
socket = new WebSocket(socketUrl);
//打開事件
socket.onopen = function () {
console.log("websocket已打開");
};
// 瀏覽器端收消息,獲得從服務(wù)端發(fā)送過來的文本消息
socket.onmessage = function (message) {
console.log("收到數(shù)據(jù)====" + message.data)
let chatData = JSON.parse(message.data)
// 對收到的json數(shù)據(jù)進(jìn)行解析, 類似這樣的: {"users": [{"username": "張三"},{ "username": "李四"}]}
if (chatData.users) {
// 獲取在線人員信息,并且排除自身,自己不會出現(xiàn)在自己的聊天列表里
_this.users = chatData.users.filter(user => user.nickname !== nickname)
} else {
// 如果服務(wù)器端發(fā)送過來的json數(shù)據(jù) 不包含 users 這個(gè)key,那么發(fā)送過來的就是聊天文本json數(shù)據(jù)
// {"from": "張三", "text": "hello"}
if (chatData.from === _this.chatUser) {
_this.messages.push(chatData)
// 構(gòu)建消息內(nèi)容
_this.createContent(chatData.from, null, chatData.text)
}
}
};
//關(guān)閉事件
socket.onclose = function () {
console.log("websocket已關(guān)閉");
};
//發(fā)生了錯(cuò)誤事件
socket.onerror = function () {
console.log("websocket發(fā)生了錯(cuò)誤");
}
}
}
}
}
</script>
<style scoped>
.tip {
color: white;
text-align: center;
border-radius: 10px;
font-family: sans-serif;
padding: 10px;
width: auto;
display: inline-block !important;
display: inline;
}
.fixed3{
position: absolute;
right: 14px;
top: 400px;
}
.right {
background-color: deepskyblue;
}
.left {
background-color: forestgreen;
}
</style>
然后就可以進(jìn)行簡單測試了。
3.3聊天功能測試
打開兩個(gè)不同的瀏覽器,并登錄兩個(gè)用戶進(jìn)入到聊天室界面。這里需要注意的是:因?yàn)楣P者這里用戶使用的是相同的WebSocket連接,因此在同一個(gè)瀏覽器中登錄多個(gè)用戶,可能會出現(xiàn)信息被覆蓋的問題。
因?yàn)閃ebSocket是基于長連接的通信協(xié)議,而同一個(gè)瀏覽器內(nèi)的WebSocket連接是共享的,多個(gè)用戶在同一個(gè)瀏覽器中使用相同的WebSocket連接進(jìn)行通信,導(dǎo)致消息會被混淆。所以需要在兩個(gè)不同瀏覽器中才看得到實(shí)現(xiàn)效果。
如果你的用戶采用了不同的連接方式,或者建立不同的獨(dú)立的WebSocket連接,就沒這回事了。
測試效果如下:A用戶聊天界面:
?B用戶聊天界面如下:
?至此一個(gè)簡單聊天室就實(shí)現(xiàn)了,但是這里面的消息記錄都是與原生html拼接的,因此頭像,用戶名并沒有與用戶真實(shí)信息連同展示,并且還存在許多不足之處:
首先,因?yàn)橛玫亩际且粋€(gè)WebSocket連接,因此用戶只有在不同瀏覽器中登錄才能實(shí)現(xiàn)該功能,否則就會出現(xiàn)信息覆蓋。
其次,當(dāng)用戶數(shù)>2時(shí),聊天區(qū)消息記錄并沒有及時(shí)清除,也就是說,該模式更像一個(gè)群聊。一對一與一對多的聊天功能實(shí)現(xiàn)可參考如下方式:
實(shí)現(xiàn)一對一聊天功能:
-
當(dāng)一個(gè)客戶端建立連接時(shí),服務(wù)器為每個(gè)客戶端分配一個(gè)唯一的標(biāo)識符。
-
當(dāng)客戶端發(fā)送消息時(shí),服務(wù)器將消息標(biāo)識符和消息內(nèi)容保存下來,并通過WebSocket服務(wù)器將消息發(fā)送給特定的目標(biāo)客戶端。
實(shí)現(xiàn)一對多聊天功能:
-
當(dāng)一個(gè)客戶端建立連接時(shí),服務(wù)器為每個(gè)客戶端分配一個(gè)唯一的標(biāo)識符。
-
當(dāng)客戶端發(fā)送消息時(shí),服務(wù)器將消息標(biāo)識符和消息內(nèi)容保存下來,并通過WebSocket服務(wù)器將消息發(fā)送給所有連接的客戶端。
另外,聊天消息并未持久化,也就是說,當(dāng)頁面重新打開時(shí)消息記錄就被清空了,因?yàn)閃ebSockset斷連了,相應(yīng)的記錄也就沒有了。如果需要消息記錄保存的話,仍然需要數(shù)據(jù)庫參與其中。
最后,消息記錄采用html拼接形式展示出來的,因此無論是聊天用戶頭像還是昵稱都并未與用戶本身信息綁定,比較呆板。
留給大家一些思考~
五,總結(jié)
WebSocket 是為了在 web 應(yīng)用上進(jìn)行雙通道通信而產(chǎn)生的協(xié)議,相比于輪詢HTTP請求的方式,WebSocket 有節(jié)省服務(wù)器資源,效率高等優(yōu)點(diǎn)。
WebSocket 中 Sec-WebSocket-Key 的生成算法是拼接服務(wù)端和客戶端生成的字符串,進(jìn)行SHA1哈希算法,再用base64編碼。
WebSocket 協(xié)議握手是依靠 HTTP 協(xié)議的,依靠于 HTTP 響應(yīng)101進(jìn)行協(xié)議升級轉(zhuǎn)換。
它的優(yōu)缺點(diǎn)在于:
-
優(yōu)點(diǎn):WebSocket協(xié)議一旦建議后,互相溝通所消耗的請求頭是很小的服務(wù)器可以向客戶端推送消息了
-
缺點(diǎn):少部分瀏覽器不支持,瀏覽器支持的程度與方式有區(qū)別(IE10)
應(yīng)用場景如下:
-
即時(shí)聊天通信
-
多玩家游戲
-
在線協(xié)同編輯/編輯
-
實(shí)時(shí)數(shù)據(jù)流的拉取與推送
-
體育/游戲?qū)崨r
-
實(shí)時(shí)地圖位置
-
即時(shí)Web應(yīng)用程序:即時(shí)Web應(yīng)用程序使用一個(gè)Web套接字在客戶端顯示數(shù)據(jù),這些數(shù)據(jù)由后端服務(wù)器連續(xù)發(fā)送。在WebSocket中,數(shù)據(jù)被連續(xù)推送/傳輸?shù)揭呀?jīng)打開的同一連接中,這就是為什么WebSocket更快并提高了應(yīng)用程序性能的原因。
-
游戲應(yīng)用程序:在游戲應(yīng)用程序中,你可能會注意到,服務(wù)器會持續(xù)接收數(shù)據(jù),而不會刷新用戶界面。屏幕上的用戶界面會自動(dòng)刷新,而且不需要建立新的連接,因此在WebSocket游戲應(yīng)用程序中非常有幫助。文章來源:http://www.zghlxwxcb.cn/news/detail-510218.html
-
聊天應(yīng)用程序:聊天應(yīng)用程序僅使用WebSocket建立一次連接,便能在訂閱戶之間交換,發(fā)布和廣播消息。它重復(fù)使用相同的WebSocket連接,用于發(fā)送和接收消息以及一對一的消息傳輸。文章來源地址http://www.zghlxwxcb.cn/news/detail-510218.html
到了這里,關(guān)于基于WebSocket的簡易聊天室的基本實(shí)現(xiàn)梳理的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!