在传统http思惟当中,有(浏览器1,服务器,浏览器2三个角色),如何实现浏览器1发送消息,而后,浏览器2接收看到浏览器1发送的消息,反之同样。html
http能不能实现这种聊天的效果?前端
答案固然是能的,可是比较麻烦。node
由于 http是基于 请求 -------- 响应 这种模型的jquery
服务器没有办法“主动”给浏览器发送消息的git
因此只能在浏览器2当中,加入ajax轮询,不断的请求服务器,以获取到最新的消息,来展现到客户端github
websocket是一种网络协议,容许客户端和服务端全双工的进行网络通信,服务器能够给客户端发消息,客户端也能够给服务器发消息。web
至关于浏览器和客户端之间创建的是一种长连接,不会断开,在这个管道内可以互相的,不断的进行信息交互 (而传统http是:浏览器请求数据,服务端响应数据后,连接断开)ajax
在HTML5中,浏览器已经实现了websocket的API,直接使用便可。WebSocket-MDNexpress
// 参数1: url:链接的websocket属性 // 参数2: protocol,可选的,指定链接的协议 // var socket = new WebSocket('ws://echo.websocket.org') =>官方提供的地址 var Socket = new WebSocket(url, [protocol] );
事件 | 事件处理程序 | 描述 |
---|---|---|
open | Socket.onopen | 链接创建触发 |
message | Socket.onmessage | 客户端接收服务端数据时触发 |
error | Socket.onerror | 通讯发生错误时触发 |
close | Socket.onclose | 链接关闭时触发 |
方法 | 描述 |
---|---|
Socket.send() | 使用链接发送数据 |
Socket.close() | 关闭链接 |
先简单了解一下,有关于nodejs-websocket有哪些api,和怎么使用json
前端页:
<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 便可启动服务
前端页:
<!-- 用于收集输入内容 --> <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) })
一次给全部用户广播消息的功能,都须要经过一个存有全部用户的数组属性来本身封装,支持的事件少,返回给客户端的数据也只能是字符串格式,提供的api少
可是:这其实也不能说是websocket的缺点,由于它自己就是提供基础能力而出现的,并非为了解决咱们业务代码的便利而出现,因此,一般在websocket的特性的时候,咱们经常会用框架来更简单,高效的实现咱们所须要的功能 -> 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的时候,不论是前端仍是后端,都是这样接收和发送事件来进行数据的传输
github项目代码地址:
1.使用了socket.io express => app.js (参照socket.io官方demo) 2.在index.html 中引入socket.io 等包 3.使用express处理静态资源,而且重定向访问 / 根目录时 => 静态资源文件夹(public)下的index.html node app.js便可启动项目
===前端=== 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('登陆成功')
===前端=== //监听登陆成功后,服务器有把我的信息返回了 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 })
===后端=== //使用socket.io的内置api给每一个用户发送消息,把头像和名称发送给每个人 io.emit('addUser', data) //data传给前端的不只有消息数据,还有消息类型type:0,1,2... 前端根据数据的类型,渲染出不一样的消息样式 //=> 好比先经过消息类型建立好元素,而且加入样式,再将样式插入到页面当中
//后端判断当登陆成功以后,继续emit一个事件,将全部用户数据都扔给前端
//使用内置,用户离开触发的事件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) 告诉全部人,有人离开的聊天室
//使用到了一个方法 :element.scrollIntoView() -> 原生dom方法,不是jq方法 //找到盒子当中的最后一个dom对象,跳转到那个高度 //children(':last')找最后一个子元素 $('.box-bd').children(':last').get(0).scrollIntoView(false)
前端: <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
具体查看 jquery-emoji 插件文档