前言
网络协议系列文章
Http 协议系列 ----OSI 参考模型, 协议原理构成, 三次握手, 四次挥手, 连接管理
Http 协议系列 -- 字符编码, cookie, 缓存, 疑难杂症
Http 协议系列 -- 进阶 Https 基础
webSocket 协议入门基础
以下全部代码可以看 https://github.com/Lin-Kyle/sockit-chat-demo , 个人环境为
日期: 2018/06/25
Chrome 极速版: 版本号 2.0.6.38
- nodejs: v10.2.0
- boostrap4
- socket.io2
界面实现
我们直接使用 boostrap4 实现一个简单界面效果, 不要再细节花费多余的功夫. 组件里有个 List group 可以将就拿来实现, 另外底部固定一个输入框完成. 因为不需要用到插件, 只引入样式即可
Bootstrap4 https://v4.bootCSS.com/
->user.html
- <!DOCTYPE html>
- <html lang="en" dir="ltr">
- <head>
- <meta charset="utf-8">
- <title></title>
- <link rel="stylesheet" href="../bootstrap.min.css">
- <style media="screen">
- html,
- body {
- height: 100%;
- }
- .submitWrappre {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- border: 1px solid #999;
- }
- </style>
- </head>
- <body>
- <div class="list-group">
- <a class="list-group-item list-group-item-action flex-column align-items-start active">
- <div class="d-flex w-100 justify-content-between">
- <h5 class="mb-1">List group item heading</h5>
- <small>3 days ago</small>
- </div>
- <p class="mb-1">Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit.</p>
- <small>Donec id elit non mi porta.</small>
- </a>
- // 为了间隔开
- <a class="list-group-item list-group-item-action flex-column align-items-start">
- <div class="d-flex w-100 justify-content-between">
- <h5 class="mb-1">List group item heading</h5>
- <small class="text-muted">3 days ago</small>
- </div>
- <p class="mb-1">Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit.</p>
- <small class="text-muted">Donec id elit non mi porta.</small>
- </a>
- </div>
- // 为了间隔开
- <form class="submitWrappre">
- <div class="form-group">
- <textarea class="form-control" id="exampleFormControlTextarea1" rows="2"></textarea>
- <button type="button" class="btn btn-primary btn-sm" style="float:right">Small button</button>
- </div>
- </form>
- </body>
- </html>
以上代码可以看 https://github.com/Lin-Kyle/sockit-chat-demo 的 lesson1.
Socket.io
Socket.IO enables real-time bidirectional event-based communication.
It works on every platform, browser or device, focusing equally on reliability and speed.
SOCKET.IO 2.0 https://socket.io/
socket.io 具有实时双向基于事件通讯, 能兼容不同平台, 浏览器或设备, 同样关注可靠性和速度.
我们可以直接在浏览器和服务器都用 socket.io 库使用 websocket 通信. 使用方法很简单, 文档已经很详细了, 就不复述了.
同时在同目录下新建一个 server.js 脚本, 我们用 nodejs 创建一个服务器, 先安装依赖 socket.io 和 path.
- yarn add socket.io path
- //OR
- npm i -D socket.io path
path.join 方法将多个参数值字符串结合成一个绝对路径字符串, 我们需要利用它简单判断请求资源路径
- req.url === '/' ? './user.html' : (path.join(__dirname, '../', req.url));
- // ./user.html 同层 html
- // C:\project\sockit-chat\xx 其他上层资源
我们创建一个服务器并且监听 80 端口, 简单判断请求资源在当前目录下相对路径返回给前端.
- var app = require('http').createServer(handler);
- var fs = require('fs');
- var path = require("path");
- // 为了间隔开
- app.listen(80);
- // 为了间隔开
- function handler(req, res) {
- var _path = req.url === '/'
- ? './user.html'
- : (path.join(__dirname, '../', req.url));
- // 为了间隔开
- fs.readFile(_path, function(err, data) {
- if (err) {
- res.writeHead(500);
- return res.end('Error loading index.html');
- }
- res.writeHead(200);
- res.end(data);
- });
- }
然后引入 socket.io 库并且监听 my other event 事件, 同时触发 news 事件传播信息给前端.
- var io = require('socket.io')(app);
- io.on('connection', function(socket) {
- socket.emit('news', {hello: 'world'});
- socket.on('my other event', function(data) {
- console.log(data);
- });
- });
前端写法一样
- ->user.html
- <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.dev.js" charset="utf-8"></script>
- <script type="text/javascript">
- var socket = io('http://localhost');
- socket.on('news', function(data) {
- console.log(data);
- socket.emit('my other event', {
- my: 'data'
- });
- });
- </script>
以上代码可以看 https://github.com/Lin-Kyle/sockit-chat-demo 的 lesson2.
在当前目录下打开命令行, 输入 node server, 启动服务器之后浏览器打开 http://localhost/(默认端口 80 可省略). 看到命令行打印 { my: 'data' }, 浏览器控制台打印 Object {hello: "world"} 即为成功.
界面响应
我们现在已经有了界面和通信实现了, 现在先把界面响应实现出来.
1, 列表简化, 把不需要的结构删除, 只保留用户身份, 时间, 内容即可.
2, 输入框填写内容, 点击按键触发通信同时输入框清空.
3, 不管是发送还是接收都要在列表末尾添加进去内容, 通过. active 区分用户身份.
- <div class="list-group">
- <a class="list-group-item list-group-item-action flex-column align-items-start active">
- <div class="d-flex w-100 justify-content-between">
- <h5 class="mb-1">Me</h5>
- <small>3 days ago</small>
- </div>
- <p class="mb-1">Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit.</p>
- </a>
- // 为了间隔开
- <a class="list-group-item list-group-item-action flex-column align-items-start">
- <div class="d-flex w-100 justify-content-between">
- <h5 class="mb-1">Other</h5>
- <small class="text-muted">3 days ago</small>
- </div>
- <p class="mb-1">Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit.</p>
- </a>
- </div>
- var socket = io('http://localhost'),
- $content = document.getElementById('exampleFormControlTextarea1'),
- $send = document.getElementById('send');
- // 为了间隔开
- $send.onclick = function() {
- socket.emit('user_send', {
- content: $content.value
- });
- $content.value = '';
- }
这里我用了 ES6 的模板写法, 如果你们浏览器不支持就乖乖用原生写法吧
- /**
- * [addMsg description]
- * @param {[bol]} identity [身份, 1 本人, 0 非本人]
- * @param {[type]} val [description]
- */
- function addMsg(identity, val) {
- var node = document.createElement("a"),
- str =
- `<a class="list-group-item list-group-item-action flex-column align-items-start ${identity ?'active':''}">
- <div class="d-flex w-100 justify-content-between">
- <h5 class="mb-1">${identity ? 'Me' : 'Other'}</h5>
- <small class="text-muted">${new Date().toLocaleString()}</small>
- </div>
- <p class="mb-1">${val}</p>
- </a>`;
- node.innerHTML = str;
- $list.appendChild(node);
- }
以上代码可以看 https://github.com/Lin-Kyle/sockit-chat-demo 的 lesson3.
聊天实现
1, 先把列表界面清除. 只保留容器;
2, 定义前端发送给服务器的事件名称为 user_send, 保存在对象 content 属性;
3, 定义服务器发送给前端的事件名称为 user_receive, 保存在对象 content 属性;
- ->user.html
- socket.on('user_receive', function(data) {
- console.log('user_receive:' + data.content);
- addMsg(0, data.content);
- });
- // 为了间隔开
- $send.onclick = function() {
- socket.emit('user_send', {
- content: $content.value
- });
- addMsg(1, $content.value);
- $content.value = '';
- }
- ->server.js
每个连接都有一个单独的 socket 对象, 我们创建一个 user_list 保存每个 socket 对象, 当我们接收到一条信息就遍历发送给其他的 socket 对象达到聊天效果.
- const user_list = [];
- io.on('connection', (socket) => {
- if (!user_list.includes(socket)) user_list.push(socket);
- socket.on('user_send', (data) => {
- user_list.map(item => {
- if (item !== socket)
- item.emit('user_receive', {content: data.content})
- })
- });
- });
以上代码可以看 https://github.com/Lin-Kyle/sockit-chat-demo 的 lesson4.
在当前目录下打开命令行, 输入 node server, 启动服务器之后浏览器打开多个 http://localhost / 页面, 然后随便在输入框填写东西, 你就会发现其他页面也会自动接收相应数据了.
最终完成代码
- ->user.html
- <!DOCTYPE html>
- <html lang="en" dir="ltr">
- <head>
- <meta charset="utf-8">
- <title></title>
- <link rel="stylesheet" href="../bootstrap.min.css">
- <style media="screen">
- html,
- body {
- height: 100%;
- }
- .submitWrappre {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- border: 1px solid #999;
- }
- </style>
- </head>
- <body>
- <div class="list-group"></div>
- // 为了间隔开
- <form class="submitWrappre">
- <div class="form-group">
- <textarea class="form-control" id="exampleFormControlTextarea1" rows="2"></textarea>
- <button type="button" class="btn btn-primary btn-sm" style="float:right" id="send">Small button</button>
- </div>
- </form>
- // 为了间隔开
- <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.dev.js" charset="utf-8"></script>
- <script type="text/javascript">
- var socket = io('http://localhost'),
- $content = document.getElementById('exampleFormControlTextarea1'),
- $send = document.getElementById('send'),
- $list = document.getElementsByClassName('list-group')[0];
- // 为了间隔开
- socket.on('user_receive', function(data) {
- console.log('user_receive:' + data.content);
- addMsg(0, data.content);
- });
- // 为了间隔开
- $send.onclick = function() {
- socket.emit('user_send', {
- content: $content.value
- });
- addMsg(1, $content.value);
- $content.value = '';
- }
- // 为了间隔开
- /**
- * [addMsg description]
- * @param {[bol]} identity [身份, 1 本人, 0 非本人]
- * @param {[type]} val [description]
- */
- function addMsg(identity, val) {
- var node = document.createElement("a"),
- str =
- `<a class="list-group-item list-group-item-action flex-column align-items-start ${identity ?'active':''}">
- <div class="d-flex w-100 justify-content-between">
- <h5 class="mb-1">${identity ? 'Me' : 'Other'}</h5>
- <small class="text-muted">${new Date().toLocaleString()}</small>
- </div>
- <p class="mb-1">${val}</p>
- </a>`;
- node.innerHTML = str;
- $list.appendChild(node);
- }
- </script>
- </body>
- </html>
- ->server.js
- const app = require('http').createServer(handler),
- io = require('socket.io')(app),
- fs = require('fs'),
- path = require("path");
- // 为了间隔开
- app.listen(80);
- // 为了间隔开
- function handler(req, res) {
- const _path = req.url === '/'
- ? './user.html'
- : (path.join(__dirname, '../', req.url))
- fs.readFile(_path, (err, data) => {
- if (err) {
- res.writeHead(500);
- return res.end('Error loading index.html');
- }
- res.writeHead(200);
- res.end(data);
- });
- }
- // 为了间隔开
- const user_list = [];
- io.on('connection', (socket) => {
- if (!user_list.includes(socket)) user_list.push(socket);
- socket.on('user_send', (data) => {
- user_list.map(item => {
- if (item !== socket)
- item.emit('user_receive', {content: data.content})
- })
- });
- });
后话
主要思路已经交代完了, 全文下来其实还是很简单的, 特别是 socket.io 封装好了 API, 我们只需要傻瓜式使用即可, 实际开发我们还有很多步骤需要做的, 例如:
注册登录唯一账号密码并且需要验证;
登录数据保存数据库, nodejs 需要连接数据库, 支持多种数据库使用;
针对特定账号发送信息;
支持表情图;
微信的撤回删除等操作;
总之可以优化的地方很多, 你们可以自行研究.
来源: http://www.qdfuns.com/article/40831/18843630b0a73d009fe9ed56e70156a8.html