項目演示: 像素繪板 請使用 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 加速測試,但始終沒找出到底是哪個配置致使了問題,因此最終我也沒能恢復加速。