1.先提一個思考?
在傳統http思維當中,有(瀏覽器1,服務器,瀏覽器2三個角色),如何實現瀏覽器1發送消息,然后,瀏覽器2接收看到瀏覽器1發送的消息,反之一樣。
http能不能實現這種聊天的效果?
答案當然是能的,但是比較麻煩。
因為 http是基於 請求 -------- 響應 這種模型的
服務器沒有辦法“主動”給瀏覽器發送消息的
所以只能在瀏覽器2當中,加入ajax輪詢,不斷的請求服務器,以獲取到最新的消息,來展示到客戶端
2.websocket介紹
websocket是一種網絡協議,允許客戶端和服務端全雙工的進行網絡通訊,服務器可以給客戶端發消息,客戶端也可以給服務器發消息。
相當於瀏覽器和客戶端之間建立的是一種長鏈接,不會斷開,在這個管道內能夠互相的,不斷的進行信息交互 (而傳統http是:瀏覽器請求數據,服務端響應數據后,鏈接斷開)
3.websocket的使用
在HTML5中,瀏覽器已經實現了websocket的API,直接使用即可。WebSocket-MDN
3.1 創建websocket連接
// 參數1: url:連接的websocket屬性
// 參數2: protocol,可選的,指定連接的協議
// var socket = new WebSocket('ws://echo.websocket.org') =>官方提供的地址
var Socket = new WebSocket(url, [protocol] );
3.2 websocket的一些事件
事件 | 事件處理程序 | 描述 |
---|---|---|
open | Socket.onopen | 連接建立觸發 |
message | Socket.onmessage | 客戶端接收服務端數據時觸發 |
error | Socket.onerror | 通信發生錯誤時觸發 |
close | Socket.onclose | 連接關閉時觸發 |
3.3 websocket方法
方法 | 描述 |
---|---|
Socket.send() | 使用連接發送數據 |
Socket.close() | 關閉連接 |
4.使用node.js開發websocket服務
先簡單了解一下,有關於nodejs-websocket有哪些api,和怎么使用
前端頁:
<style>
div {
width: 200px;
height: 200px;
border: 1px solid #000;
}
</style>
<!-- 用於收集輸入內容 -->
<input type="text" placeholder="請輸入需要發送的內容" />
<!-- 用於發送websocket請求 -->
<button>websocket測試</button>
<!-- 用於顯示websock服務器的響應 -->
<div class="show"></div>
<script>
var input = document.querySelector('input')
var button = document.querySelector('button')
var div = document.querySelector('div')
// 1. 創建websocket對象, 這個地址是官方提供的地址
// var socket = new WebSocket('ws://echo.websocket.org')
var socket = new WebSocket('ws://localhost:3000')
// 2. 給websocket注冊事件
socket.addEventListener('open', function() {
// 與服務端建立連接的時候觸發
div.innerText = '恭喜你,與服務端建立連接了'
})
// 如何給服務器發送消息
button.addEventListener('click', function() {
socket.send(input.value)
input.value = ''
})
// 如果接收服務器的數據
socket.addEventListener('message', function(e) {
console.log('接收到服務器的數據了', e)
// 將接收到服務器的數據展示出來
div.innerText = e.data
})
socket.addEventListener('close', () => {
div.innerHTML = '與服務器斷開連接'
})
</script>
服務端(app.js): 使用前需下載nodejs-websocket包
//1.導入nodejs-websocket包
const ws = require('nodejs-websocket')
const PORT = 3000
//2.創建一個服務
//2.1 如何處理用戶的請求
//每次只要有用戶連接,函數就會被執行,會給當前連接的用戶創建 一個connect對象
var server = ws.createServer(connect =>{
console.log('有用戶連接上來了')
//每當接收到用戶傳遞過來的數據,這個text事件就會被觸發
connect.on('text',data=>{
console.log('接收到了用戶的數據:'+data)
//返回客戶發送的消息
connect.send('用戶發送的消息是:'+ data)
})
//只要websocket連接斷開了,close事件就會觸發
connect.on('close',()=>{
console.log('連接斷開了')
})
//在處理用戶斷開連接后,除了處理colse事件,還必需處理error事件,不然會報錯
connect.on('error',()=>{
console.log('用戶連接異常')
})
})
server.listen(PORT,()=>{
console.log('服務啟動成功,監聽了端口:'+PORT)
})
node app.js 即可啟動服務
5.websocket開發簡易聊天室
前端頁:
<!-- 用於收集輸入內容 -->
<input type="text" placeholder="請輸入需要發送的內容" />
<!-- 用於發送websocket請求 -->
<button>websocket測試</button>
<!-- 用於顯示websock服務器的響應 -->
<div class="show"></div>
<script>
var input = document.querySelector('input')
var button = document.querySelector('button')
var div = document.querySelector('div')
// 1. 創建websocket對象, 這個地址是官方提供的地址
// var socket = new WebSocket('ws://echo.websocket.org')
var socket = new WebSocket('ws://localhost:3000')
// 2. 給websocket注冊事件
socket.addEventListener('open', function() {
// 與服務端建立連接的時候觸發
div.innerText = '恭喜你,與服務端建立連接了'
})
// 如何給服務器發送消息
button.addEventListener('click', function() {
socket.send(input.value)
input.value = ''
})
// 如果接收服務器的數據
socket.addEventListener('message', function(e) {
//將服務端返回的json字符串,轉成對象
var data = JSON.parse(e.data)
//創建一個div(此時只是創建,並沒有插入到頁面當中)
var dv = document.createElement('div')
//在床的div中插入數據
dv.innerHTML = data.msg + '----' + data.date
//不同的消息類型改變消息的樣式
if (data.type === 0) {
dv.style.color = 'green'
}
if (data.type === 1) {
dv.style.color = 'red'
}
if (data.type === 2) {
dv.style.color = 'gray'
}
//最后將帶有數據的盒子插入到頁面當中
div.appendChild(dv)
})
socket.addEventListener('close', () => {
div.innerHTML = '與服務器斷開連接'
})
</script>
服務端(app.js):
const ws = require('nodejs-websocket')
//端口
const PORT = 3000
//返回客戶端是哪種信息的標記
const TYPE_MSG = 0
const TYPE_ENTER = 1
const TYPE_LEAVE = 2
//用戶的數量,用戶進入+1,用戶離開-1
let userCount = 0
const server = ws.createServer(connect => {
console.log('有新用戶連接了')
// 每次有新用戶連接,需要給所有用戶發送一條新增用戶的消息
userCount++
//給每個connect對象添加一個name名稱
connect.userName = 'user' + userCount
// 給所有的用戶進行廣播,客戶端可根據不同的type值渲染不同樣式的消息
broadcast({
type: TYPE_ENTER,
msg: connect.userName + '進入了聊天室',
date: new Date().toLocaleTimeString()
})
connect.on('text', msg => {
// 如果接收到用戶的數據, 需要發送給所有的用戶
broadcast({
type: TYPE_MSG,
msg: msg,
date: new Date().toLocaleTimeString()
})
})
connect.on('close', () => {
console.log('用戶斷開連接')
userCount--
// 給所有的用戶發送一條用戶離開的消息
broadcast({
type: TYPE_LEAVE,
msg: `${connect.userName}離開了聊天室`,
date: new Date().toLocaleTimeString()
})
})
connect.on('error', () => {
console.log('連接失敗')
})
})
//廣播消息的方法 connections:存有全部用戶,相當於connect數組集合
function broadcast(msg) {
server.connections.forEach(conn => {
//原生的websocekt的sendText()不允許返回對象數據,只能返回字符串
conn.sendText(JSON.stringify(msg))
})
}
server.listen(PORT, () => {
console.log('服務器啟動成功了', PORT)
})
6.原生websocket的缺點
一次給所有用戶廣播消息的功能,都需要通過一個存有所有用戶的數組屬性來自己封裝,支持的事件少,返回給客戶端的數據也只能是字符串格式,提供的api少
但是:這其實也不能說是websocket的缺點,因為它本身就是提供基礎能力而出現的,並不是為了解決我們業務代碼的便利而出現,所以,通常在websocket的特性的時候,我們常常會用框架來更簡單,高效的實現我們所需要的功能 -> socket.io
7.socket.io的基本用法
前端頁面(當前頁面是創建在與app.js同級目錄下):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
哈哈
<script src="/socket.io/socket.io.js"></script>
<script>
// 連接socket服務
// 參數:服務器地址
var socket = io('http://localhost:3000')
// 接受服務器返回的數據
// socket.on('send', data => {
// console.log(data)
// })
socket.emit('hehe', { name: 'zs', age: 18 })
socket.on('send', function(data) {
console.log(data)
})
</script>
</body>
</html>
app.js:
const http = require('http');
const fs = require('fs');
const app = http.createServer();
app.listen(3000,()=>{
console.log('服務器啟動成功')
});
//將創建的node服務傳入到socket.io方法中
const io = require('socket.io')(app);
function handler (req, res) {
fs.readFile(__dirname + '/index.html',
(err, data) => {
if (err) {
res.writeHead(500);
return res.end('Error loading index.html');
}
res.writeHead(200);
res.end(data);
});
}
//監聽用戶連接的事件
io.on('connection', (socket) => {
console.log('有用戶進入了')
//socket.emit 方法表示給到瀏覽器發送數據
//參數1:事件的名字(自定義)
socket.emit('send', { hello: 'world' });
socket.on('hehe', (data) => {
console.log(data);
});
});
總結:socket.io在前后端通用,socket表示用戶連接,socket.emit 表示觸發某個事件,socket.on表示監聽某個事件,在使用socket.io的時候,不管是前端還是后端,都是這樣接收和發送事件來進行數據的傳輸
8.實現一個多功能的聊天室
github項目代碼地址:
1.使用了socket.io express => app.js (參照socket.io官方demo)
2.在index.html 中引入socket.io 等包
3.使用express處理靜態資源,並且重定向訪問 / 根目錄時 => 靜態資源文件夾(public)下的index.html
node app.js即可啟動項目
9.聊天室的前后端業務代碼總結
9.1 簡單登錄
===前端===
1.選擇頭像=>
$('#login_avatar li').on('click', function() {
$(this)
.addClass('now')
.siblings()
.removeClass('now')
})
//點擊添加now類,讓被點擊的頭像出現選擇框
2.點擊登錄按鈕,發送頭像和用戶名給服務器=>
$('#loginBtn').on('click', function(){
// 獲取用戶名
var username = $('#username').val().trim()
if (!username) {
alert('請輸入用戶名')
return
}
// 獲取選擇的頭像
var avatar = $('#login_avatar li.now img').attr('src')
// 需要告訴socket io服務,登錄,並且傳入信息
socket.emit('login', {
username: username,
avatar: avatar
})
})
===后端===
對用戶重復登陸的處理 =>
// 記錄所有已經登錄過的用戶
const users = []
//尋找users數組中有沒有重復用戶名
let user = users.find(item => item.username === data.username)
if (user) {
// 表示用戶存在, 登錄失敗. 服務器需要給當前用戶響應,告訴登錄失敗
socket.emit('loginError', { msg: '登錄失敗' })
// console.log('登錄失敗')
} else {
// 表示用戶不存在, 登錄成功
users.push(data)
// 告訴用戶,登錄成功
socket.emit('loginSuccess', data)
// console.log('登錄成功')
9.2顯示個人用戶信息
===前端===
//監聽登錄成功后,服務器有把個人信息返回了
socket.on('loginSuccess', data => {
// 需要顯示聊天窗口
// 隱藏登錄窗口
$('.login_box').fadeOut()
$('.container').fadeIn()
// 設置個人信息
console.log(data)
$('.avatar_url').attr('src', data.avatar)
$('.user-list .username').text(data.username)
username = data.username
avatar = data.avatar
})
9.3顯示加入群聊的消息
===后端===
//使用socket.io的內置api給每個用戶發送消息,把頭像和名稱發送給每一個人
io.emit('addUser', data)
//data傳給前端的不僅有消息數據,還有消息類型type:0,1,2... 前端根據數據的類型,渲染出不同的消息樣式
//=> 比如先通過消息類型創建好元素,並且加入樣式,再將樣式插入到頁面當中
9.4用戶列表和聊天人數
//后端判斷當登錄成功之后,繼續emit一個事件,將所有用戶數據都扔給前端
9.5離開聊天室
//使用內置,用戶離開觸發的事件api
//=>
1.把當前y用戶的信息從users中刪除調用
2.告訴所有人,有人離開了聊天室
3.告訴所有人,userlist發生更新了
//=>
當登錄成功的時候,就將當前用戶的信息存儲起來
socket.username = data.username
socket.avatar = data.avatar
當用戶離開的時候,判斷離開的用戶是全局用戶中的哪一個
let idx = users.findIndex(item => item.username === socket.username)
根據下標刪除這個用戶
users.splice(idx,1)
告訴所有人,有人離開的聊天室
9.6消息總是在最底部開始顯示
//使用到了一個方法 :element.scrollIntoView() -> 原生dom方法,不是jq方法
//找到盒子當中的最后一個dom對象,跳轉到那個高度
//children(':last')找最后一個子元素
$('.box-bd').children(':last').get(0).scrollIntoView(false)
9.7發送圖片
前端:
<a> <label> <input ...></label></a>標簽包住一個隱藏的input type="file" 標簽,這樣,我們在點擊a標簽的時候也就是點擊了被隱藏的input選擇文件標簽
$('input_file').on('change',function(){
//拿到上傳的文件
var file = this.files[0]
//需要把文件發送到服務器,借助於h5新增的fileReader
var fr = new FileReader()
//讀取這個文件
fr.readAsDataURL(file)
//讀取成功
fr.onload = function(){
//fr.result為圖片讀取成功后的結果
console.log(fr.result)
socket.emit('send',{
username:name,
avater:avater,
img:fr.result
})
}
})
后端
接收到用戶上傳的圖片數據后,直接廣播給所有用戶,通過事件將數據發出
前端:
所有用戶接收到圖片消息,將圖片數據append進聊天盒子
$('...').append(' <img src="${data.img}"> ')
bug :發送圖片消息,聊天框總是不在底部開始顯示消息
原因:因為圖片還沒加載完成,就調用了scrollIntoView方法
解決:監聽最后一張圖片的加載,在調用scrollIntoView
9.8 jquery-emoji 表情包的使用
具體查看 jquery-emoji 插件文檔