Practical Node.js (2018版) 第9章: 使用WebSocket建立实时程序,原生的WebSocket使用介绍,Socket.IO的基本使用介绍。

时间:2022-02-06 01:11:59

Real-Time Apps with WebSocket, Socket.IO, and DerbyJS

实时程序的使用变得越来越广泛,如传统的交易,游戏,社交,开发工具DevOps tools, 云服务,新闻。

之所以如此是因为技术的进步。更大的带宽用来传输数据和更多的计算来处理和取回数据。

HTML5的新实时连接标准叫做,WebSocket。

在浏览器,JavaScript给你一个对象WebSocket。这个对象是一个类,它有各类方法用于生成WebSocket protocol client。

WebSocket协议(ws://)不和HTTP或者HTTPS区别非常大。

因此,开发者需要一个特别的ws server。仅仅有一个HTTP server是不够的.(having a HTTP server won't cut it.)

并且你知道,Node.js是一个高效率,非块结构的输入输出平台。

使用Node执行implementing WebSocket servers非常好,因为Node是非常快速的也因为Node是JavaScript,  就像WebSocket clients(比如browser JavaScript)。

因此, Node非常适合当一个后端对儿,和browser和它的WebSocket API。

为了开始你的WebSocket和Node.js, 让我们keep things simple stupid,并学习下面的内容:

  • 什么是WebSocket
  • Native WebSocket and Node.js with ws module example(原生WebSocket的使用)
  • Socket.IO and Express.js example(这章只是简单的介绍了Socket.IO)
  • Collaborative online editor example with DerbyJS, Express.js, and MongoDB(未看)

What Is WebSocket?

WebSocket是一个特殊的关联频道channel: 浏览器(客户端)和服务器。

它是一个HTML5协议。

WebSocket的连接是持续的constant。而传统的HTTP请求总是由客户端发起,这意味着服务器有更新时,没法通知客户端(除了Server-side Events)。

WebSocket通过保持客户端和服务器的双向连接,更新就会被及时发出,而无需客户端to poll(通过投票/提问等方式来获得民意,轮询--定时/轮流的询问:客户端向服务器定时发送请求)。

详细的解释见:

http://www.websocket.org/aboutwebsocket.html

在现代浏览器使用WebSocket无需特定的库。

不同server端的API推荐表:

https://*.com/questions/1253683/what-browsers-support-html5-websocket-api

其中node,使用Socket.IO(45000✨最高)

再说一句,轮询poll也可以用于web apps的实时响应(这是过去的技巧)。

一些高级库如Socket.IO会在WebSocket不能使用时,回退到轮询的方式。比如出现连接问题或者用户的浏览器版本太低。

轮询相对简单,本章不讲。它可以执行使用一个setInterval()回退函数和一个服务器上的终端。尽管如此,polling不是真正的实时连接,它的每个请求都是separate。


Native WebSocket and Node.js with the ws Module Example

建立一个小的程序,包括建立一个原生WebSocket 执行项目,它在ws模块module的帮助下与Node.js server进行通话。

  • Browser WebSocket implementation
  • Node.js server with ws module implementation

主要代码在<script>内,我们会从global WebSocket实例化一个对象。

需要提供一个参数:server URL。 ⚠️协议类型改为WebSocket protocol,所以使用ws://

<script type="text/javascript">
var ws = new WebSocket('ws://localhost:3000')

只要连接被建立,我们发送一条信息给服务器

ws.onopen  =  function(event) {
ws.send('front-end message: ABC')
}

通常,信息被发送以响应用户行为。例如鼠标点击。当我们得到任何从WebSocket location来的信息,这随后的事件处理器被执行:

ws.onmessage = function(event) {
ws.send('server message: ', event.data)
}

另外,添加一个错误事件处理器,记录❌信息:

ws.onerror = function(event) {
console.log('server error message: ', event.data)
}

完全的代码:index.html

<html>
<head>
</head>
<body>
<script type="text/javascript">
var ws = new WebSocket('ws://localhost:3000');
ws.onopen = function(event) {
ws.send('front-end message: ABC');
};
ws.onerror = function(event) {
console.log('server error message: ', event.data);
};
ws.onmessage = function(event) {
console.log('server message: ', event.data);
};
</script>
</body>
</html>

websocket.onopen, websocket.onclose, websocket.onmessage, websocket.onerror,这几个是事件监听器,是函数。每个监听器都会接收一个事件。

Event: 'message'类型是data, 当从服务器收到一条信息后发出。

websocket.send(data, options, callback) 通过连接发送数据。

注意⚠️:https://github.com/websockets/ws#api-docs 上的使用案例的代码有过期的:

//比如:
ws.on('open', function open() {
ws.send('something');
});
//会报告❌ ws.on不是一个函数。
//查看https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback
// on不是一个方法,可以改用addEventListener(type, listener)

Node.js Server with ws Module Implementation

WebSocket.org 提供了echo service用于测试浏览器WebSocket, 但是我们可以建立自己的Node.js server,在ws库的帮助下:

https://github.com/websockets/ws

(http://npmjs.org/ws and https://github.com/einaros/ws)

你可以创建package.json并安装ws。

$ npm init -y
$ npm install ws //建立一个server.js文件。引进ws模块,并初始化服务器进入wss变量。
const WebSocketServer = require('ws').Server
const wss = new WebSocketServer({port: 3000}) 

类似前端代码,我们使用一个事件模式来等待连接。当这个连接准备好,在回调内我们发送字符串XYZ,并附加一个事件监听on('message')来监听从网页传来的信息。

wss.on('connection', (ws) => {
ws.send('XYZ')
ws.on('message', (message) => {
console.log('received: %s', message)
})
})

进一步,我们加一些逻辑:使用ws.send()和new Date提供当前时间给浏览器:

wss.on('connection', (ws) => {
ws.send('XYZ')
setInterval(()=>{
ws.send((new Date).toLocaleTimeString())
}, 1000)
ws.on('message', (message) => {
console.log('received: %s', message)
})
})

最后在terminal

$node server
$pwd
//print working directory name, 然后在浏览器打开文件index.html
//file:///Users/chen/node_practice/websocket/index.html

因为原生HTML5 webSocket是一个牛逼的技术。因为WebSocket是标准的协议,每个浏览器执行可能不同。另外,通常连接可能丢失并需要再建立!

为处理跨浏览器和后端的兼容,也包括再opening, 大量开发者使用Socket.IO库。



Socket.IO and Express.js Example

https://socket.io/

完全的Socket.IO库绝对值得一本书来讲。

它是实时,双向的,基于事件的通信。

  • Real-time analytics
  • 立即的会话和聊天。
  • Binary streaming: 可以发送任意blob到前端和后端:image, audio, video.
  • Document collaboration: 允许多个用户并发地编辑一个文档并且看其他人的改变。

下面的例子是一个基本的结构, 演示浏览器和服务器之间的频道连接。

最常见的实时web程序的关联,既是一些用户行为的响应,也是从服务器得到更新结果的响应。

本例子,网页渲染一个form。使用Express.js手脚架, Socket.IO,和Pug。

$express -c styl express-socket
$cd express-socket
//查看package.json,添加包:
$ npm install socket.io body-parser ejs

Socket.IO在一些方面可以被看作是另一个server,因为它处理socket connection并且不是标准的HTTP requests。

重构程序文件根目录下的app.js:

https://github.com/azat-co/practicalnode/blob/master/code/ch9/socket-express/app.js#L30

const http = require('http')
const express = require('express')
const path = require('path')
const logger = require('morgan')
const bodyParser = require('body-parser') const routes = require('./routes/index')
const app = express() // view engine setup
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'pug') app.use(logger('dev'))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: true}))
app.use(express.static(path.join(__dirname, 'public'))) app.use('/', routes) const server = http.createServer(app)
const io = require('socket.io').listen(server) io.sockets.on('connection', (socket) => {
socket.on('messageChange', (data) => {
console.log(data)
socket.emit('receive', data.message.split('').reverse().join(''))
})
}) app.set('port', process.env.PORT || 3000)
server.listen(app.get('port'), () => {
console.log(`Express server listening on port ${app.get('port')}`)
})

然后需要一个前端的template,修改views/index.pug

⚠️这里使用pug, 上面的代码需要修改:

app.set('view engine', 'pug')
extends layout

block content
h1= title
p Welcome to
span.received-message #{title}
input(type='text', class='message', placeholder='what is on your mind?', onkeyup='send(this)')
script(src="/socket.io/socket.io.js")
script.
var socket = io.connect('http://localhost:3000');
socket.on('receive', function (message) {
console.log('received %s', message);
document.querySelector('.received-message').innerText = message;
});
var send = function(input) {
console.log(input.value)
var value = input.value;
console.log('sending %s to server', value);
socket.emit('messageChange', {message: value});
}

最后开启服务器node app.js在浏览器和服务器之间建立起来Websocket,实时连接。无需请求和等待。


官网socket.io上的guide的例子:

非常简单,使用html。(还有完全的代码,here

https://socket.io/get-started/chat/

//新建一个文件夹进入
$ npm init -y
$ npm i express //新建index.js文件
//index.js

var app = require('express')()
var http = require('http').createServer(app)
// var http = require('http').Server(app) app.get('/', function(req, res) {
res.send('<h1>Hello World!</h1>')
}) http.listen(3000, function() {
console.log('listening on *:3000');
})
  • Express初始化一个app对象,然后把它提供给http模块来创建一个http.Server实例
  • 定义一个路径‘/’
  • 监听端口3000

如果允许node index.js会在terminal看到:

listening on *:3000

打开浏览器:输入localhost:3000显示的是Hello World!

创建一个index.html。

重构route handler,使用sendFile方法:

app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});

写index.html

<!doctype html>
<html>
<head>
<title>Socket.IO chat</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font: 13px Helvetica, Arial; }
form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" /><button>Send</button>
</form>
</body>
</html>

集成Socket.IO

Socket.IO由2部分组成

  • A server: 集成Node.js HTTP Server:  socket.io
  • A client library: 在浏览器加载: socket.io-client

在开发阶段, socket.io自动地服务客户端:

npm i socket.io

然后更新index.js:

var app = require('express')()
var http = require('http').createServer(app)
// var http = require('http').Server(app)
var io = require('socket.io')(http) app.get('/', function(req, res) {
// res.send('<h1>Hello World!</h1>')
res.sendFile(__dirname + '/index.html')
}) io.on('connection', function(socket) {
console.log('a user connected')
}) http.listen(3000, function() {
console.log('listening on *:3000');
})

通过传递http对象(HTTP server实例),初始化一个socket.io实例。

然后监听connection事件。

现在在index.html,在</body>前面增加代码:

<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
</script>

加载了socket.io-client,并揭露出一个io全局对象,然后连接。

注意⚠️此时,没有给io()指定一个URL, 这样它默认的连接的host。window.location

重新刷新网页,可在terminal上看到连接后输出到控制台的提示信息:a user connected

还可以

const io = require('socket.io-client');
// or with import syntax
import io from 'socket.io-client';

每个socket也可以激活一个断开事件disconnect:

//index.js
io.on('connection', function(socket){
console.log('a user connected');
socket.on('disconnect', function(){
console.log('user disconnected');
});
});

如果再刷新几次网页可以看到:

//terminal
a user connected
user disconnected
a user connected
user disconnected
a user connected

Emitting event

Socket.IO背后的主要思想是: 你可以发送和接收任意事件,任意数据。

任意对象可以被编码为JSON, 同时binary data也被支持。binary data

修改index.html:

    <script>
var socket = io()
var el = document.querySelector("form")
el.addEventListener("submit", (event) => {
event.preventDefault();
socket.emit('chat message', document.getElementById("m").value)
document.getElementById("m").value = ""
})
</script>

socket发出一个'chat message'信息

修改index.js:

io.on('connection', function(socket){
socket.on('chat message', function(msg){
console.log('message: ' + msg);
});
});

socket接收到'chat message'后,回调函数打印值。


Client

socket.emit(eventName, ...args, callback)

发出一个事件,可以添加任意参数。事件是自定义的string,

Server

socket.on(eventName, callback)

用于接收从浏览器传入的信息。

server.disconnect(close)

close: Boolean, 返回Socket。断开client。


Broadcasting

下一个目标是发送一个事件给服务器上其他的用户。

为了发送一个事件到每个人,Socket.IO给我们了这个方法:

io.emit()

io.emit('some event', { for: 'everyone' });

如果你想要发送一条信息,到所有人,但不包括发送者本身,使用Flag: broadcast

io.on('connection', function(socket){
socket.broadcast.emit('hi');
});

如果是上面的例子,可以这么模拟一下,看看broadcast的作用:

打开两个浏览器窗口localhost:3000,然后修改代码:

index.html增加一个监听事件'hi', 并输出信息到网页的脚本:

      socket.on('hi', (msg) => {
console.log(msg)
var x = document.getElementById("messages")
var node = document.createElement('li')
var textnode = document.createTextNode(msg)
node.appendChild(textnode)
x.appendChild(node)
})

index.js:

io.on('connection', function(socket) {
//...增加