项目演示: 像素绘板 请使用 Chrome 浏览器打开javascript
为何是绘板: v2ex前端
做为一名前端,总会有意无心接触到 NodeJS 、有意无心会去看文档、有意无心会注意到框架,但真当须要咱们须要在工做中善用它时,多半仍是要感叹一句“纸上得来终觉浅”。因此一周前我决定进行一个实践尝试,但愿能把以往无心中学到的知识融汇贯通,最终选择把之前的一个画板 Demo 重写并添加 server 端。vue
Webpack 之因此也被列出来,是由于本项目做为项目 luwuer.com
的一个模块,须要 webpack 来实现独立打包 java
node-canvas 是我目前遇到过最难安装的依赖,以致于我根本不想在 Windows 下安装他,它的功能依赖不少系统下默认不存在的包,在 Github 上也能看到不少 issue 的标签是 installation help。以 CentOS 7 纯净版为例,在安装它以前你须要安装如下这些依赖,值得注意的是 npm 文档上提供的命令没有 cairo 。node
# centos 前置条件
sudo yum install gcc-c++ cairo cairo-devel pango-devel libjpeg-turbo-devel giflib-devel
# 安装本体
yarn add canvas -D
复制代码
还有一个不明因此的坑,若是前置条件准备就绪后,安装本体仍然一直卡取包这一步(不报错),此时须要单独更新一下 npm webpack
参考文档很容易就能掌握基本用法,下方例子中先取到像素点数据生成 ImageData ,而后经过 putImageData 把历史数据画到 canvas 。ios
const {
createCanvas,
createImageData
} = require('canvas')
const canvas = createCanvas(canvasWidth, canvasHeight)
const ctx = canvas.getContext('2d')
// 初始化
const init = callback => {
Dot.queryDots().then(data => {
let imgData = new createImageData(
Uint8ClampedArray.from(data),
canvasWidth,
canvasHeight
)
// 移除 Smooth
ctx.mozImageSmoothingEnabled = false
ctx.webkitImageSmoothingEnabled = false
ctx.msImageSmoothingEnabled = false
ctx.imageSmoothingEnabled = false
ctx.putImageData(imgData, 0, 0, 0, 0, canvasWidth, canvasHeight)
successLog('canvas render complete !')
callback()
})
}
复制代码
本项目在设计上有两个必须用到推送的地方,一是其余用户的建点信息,二是全部用户发送的聊天消息。nginx
client
c++
// socket.io init
// transports: [ 'websocket' ]
window.socket = io.connect(window.location.origin.replace(/https/, 'wss'))
// 接收图片
window.socket.on('dataUrl', data => {
this.imageObject.src = data.url
this.loadInfo.push('渲染图像...')
this.init()
})
// 接收其余用户建点
window.socket.on('newDot', data => {
this.saveDot(
{
x: data.index % this.width,
y: Math.floor(data.index / this.width),
color: data.color
},
false
)
})
// 接收全部人的最新推送消息
window.socket.on('newChat', data => {
if (this.msgs.length === 50) {
this.msgs.shift()
}
this.msgs.push(data)
})
复制代码
server /bin/www
let http = require('http');
let io = require('socket.io')
let server = http.createServer(app.callback())
let ws = io.listen(server)
server.listen(port)
ws.on('connection', socket => {
// 创建链接的 client 加入房间 chatroom ,为了下方能够广播
socket.join('chatroom')
socket.emit('dataUrl', {
url: cv.getDataUrl()
})
socket.on('saveDot', async data => {
// 推送给其余用户,即广播
socket.broadcast.to('chatroom').emit('newDot', data)
saveDotHandle(data)
})
socket.on('newChat', async data => {
// 推送给全部用户
ws.sockets.emit('newChat', data)
newChatHandle(data)
})
})
复制代码
# 得到程序
git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt
# 自动生成证书(环境安装完毕后会有两次确认),证书目录 /etc/letsencrypt/live/{输入的第一个域名} 我这里是 /etc/letsencrypt/live/www.luwuer.com/
./letsencrypt-auto certonly --standalone --email html6@foxmail.com -d www.luwuer.com -d luwuer.com
复制代码
# 进入定时任务编辑
crontab -e
# 提交申请,我这里设置每两月一次,过时时间为三月
* * * */2 * cd /root/certificate/letsencrypt && ./letsencrypt-auto certonly --renew
复制代码
yum install -y nginx
复制代码
/etc/nginx/config.d/https.conf
server {
# 使用 HTTP/2,须要 Nginx1.9.7 以上版本
listen 443 ssl http2 default_server;
# 开启HSTS,并设置有效期为“6307200秒”(6个月),包括子域名(根据状况可删掉),预加载到浏览器缓存(根据状况可删掉)
add_header Strict-Transport-Security "max-age=6307200; preload";
# add_header Strict-Transport-Security "max-age=6307200; includeSubdomains; preload";
# 禁止被嵌入框架
add_header X-Frame-Options DENY;
# 防止在IE九、Chrome和Safari中的MIME类型混淆攻击
add_header X-Content-Type-Options nosniff;
# ssl 证书
ssl_certificate /etc/letsencrypt/live/www.luwuer.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.luwuer.com/privkey.pem;
# OCSP Stapling 证书
ssl_trusted_certificate /etc/letsencrypt/live/www.luwuer.com/chain.pem;
# OCSP Stapling 开启,OCSP是用于在线查询证书吊销状况的服务,使用OCSP Stapling能将证书有效状态的信息缓存到服务器,提升TLS握手速度
ssl_stapling_verify on;
#OCSP Stapling 验证开启
ssl_stapling on;
#用于查询OCSP服务器的DNS
resolver 8.8.8.8 8.8.4.4 valid=300s;
# DH-Key交换密钥文件位置
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# 指定协议 TLS
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# 加密套件,这里用了CloudFlare's Internet facing SSL cipher configuration
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
# 由服务器协商最佳的加密算法
ssl_prefer_server_ciphers on;
server_name ~^(\w+\.)?(luwuer\.com)$; # $1 = 'blog.' || 'img.' || '' || 'www.' ; $2 = 'luwuer.com'
set $pre $1;
if ($pre = 'www.') {
set $pre '';
}
set $next $2;
root /root/apps/$pre$next;
location / {
try_files $uri $uri/ /index.html;
index index.html;
}
location ^~ /api/ {
proxy_pass http://43.226.147.135:3000/;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# socket代理配置
location /socket.io/ {
proxy_pass http://43.226.147.135:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# location /weibo/ {
# proxy_pass https://api.weibo.com/;
# }
include /etc/nginx/utils/cache.conf;
}
server {
listen 80;
server_name www.luwuer.com;
rewrite ^(.*)$ https://$server_name$request_uri;
}
复制代码
首先需求是画板能够做画实际大小为 { width: 1024px, height: 512px }
,这就意味着有 1024 * 512 = 524,288 个像素点,或则有 524,288 * 4 = 2,097,152 个表示颜色的数字,这些数据量在不作压缩的状况下,最小存储方式是后者剔除掉 rgba 中的 a ,也就是一个长度为 524,288 * 3 = 1,572,864 的数组,若是赋值给变量占用内存大概 1.5M (数据来源于 Chrome Memory)。为了存储以上结构,我首先分了两种类型的存储结构:
虽然看起来结构2有点蠢,但起初我确实思考过这样的结构,那时我还不清楚原来取数据最耗时的不是查询而是 IO 。
后来我分别测试 1.1 和 1.2 这两种结构,而后直接否认告终构 2,由于在测试中我发现了 IO 耗时占总耗时超过 98% ,而结构 2 无疑不能由于单条数据取得绝对的性能优点。
结构 2 若是取数据不是毫秒级,就是死刑,由于这种结构下单个像素变更就须要存储整个图片数据
老实讲这个测试结果让我有些难以接受,问了好几个认识的后端为何性能这么差、有没有解决办法,但都没什么结果。更可怕的是,测试是在我 i7 CPU 的台式电脑上进行的,当我把测试环境放到单核服务器上时,取全表数据的耗时还要乘以 10 。好在只要想一个问题久了,即便有时只是想着这个问题发呆,也总能迸发出一些莫名的灵感。我想到了关键之一数据能够只在服务启动时取出放到内存中,像素发生改变时数据库和内存数据副本同步修改,因而得以继续开发下去。最终我选择了 1.1 的结构,选择缘由和下文的“数据传输”有关。
const mongoose = require('mongoose')
let schema = new mongoose.Schema({
index: {
type: Number,
index: true
},
r: Number,
g: Number,
b: Number
}, {
collection: 'dots'
})
复制代码
index
代替 x & y
以及移除 rgba
中的 a
在代码中再补上,都能显著下降 collection 的实际存储大小
在测试过程当中其实还有个特别奇怪的问题,就是单核小霸王服务器上,我若是一次性取出全部数据存储到一个 Array 中,程序会在中途奔溃,没有任何报错信息。起初我觉得是 CPU 满荷载久了致使的奔溃(top
查看硬件使用信息),因此还特地新租了一个服务器,想用一个群里的朋友提醒的“分布式”。再后面一段时间,我经过分页取数据,发现程序老是在取第二十万零几百条(一个固定数字)是陡然奔溃,因此为 CPU 证了清白。
PS:好在之前没分布式经验,否则一条路走到黑,可能如今都还觉得是 CPU 的问题呢。
上面有提到过,长度为 1,572,864 的颜色数组占用内存为 1.5M ,我猜测数据传输时也是这个大小。起初我想,我得把这个数据压缩压缩(不是指 gzip ),但因为不会,就想到了替代方案。前面已经为了不取数时高额的 IO 消耗,会在内存中存储一个数据副本,我想到这个数据我能够经过拼接(1.1 的结构相对而言 CPU 消耗少得多)生成 ImageData
再经过 ctx.putImageData
画到 Canvas 上,这就是关键之二把数据副本画在服务器上的一个 canvas 上。
而后就好办了,能够经过 ctx.toDataURL || fs.writeFile('{path}', canvas.toBuffer('image/jpeg')
把数据以图片的方式推送给客户端,图片自己的算法帮助咱们压缩了数据,不用本身捣鼓。事实上压缩率很是可观,前期画板上几乎都是重复颜色时,1.5M 数据甚至能够压缩到小于 10k,后期估计应该也在 300k 之内。
鉴于 DataURL 更方便,这里我采用的 DataURL 的方式传递图片数据。
Day 4 说的实际问题,我只能大概定位在 NodeJS 变量大小限制或对象个数限制,由于在我将 50w 长度 Array[Object] 转换为 200w 长度 Array[Number] 后问题消失了,知道具体缘由的大佬望不吝赐教。
记录是从日记里复制过来的,Day 6/7 确实是最艰难的两天,其实代码从一开始就没什么错,有问题的是又拍云的 CDN 加速,可怖的是我根本没想到罪魁祸首是他。其实在两天的重复测试中,由于实在是机关用尽,我也有两次怀疑 CDN 。第一次,我把域名解析到服务器 IP ,但测试结果仍然报错,以后就又恢复了加速。第二次是在第七天的早上五点,当时头很胀很难受就直接停了 CDN ,想着最后测试一下不行就去掉 CDN 的 https 证书用 http 访问。那时我才发现,在我 ping 域名肯定解析已经改变后(修改解析后大概 10 分钟),域名又会间隙性被从新解析到 CDN (这个反复缘由不知道为何,阿里云的域名解析服务),第一次测试不许应该就是这个缘由,稍长时间后就再也不会了。解决后我有意恢复 CDN 加速测试,但始终没找出到底是哪个配置致使了问题,因此最终我也没能恢复加速。