項目需求
最近在開發(fā)一個 electron 程序,其中有用到和硬件通訊部分丁频;硬件廠商給的是 .dll
鏈接庫做通訊橋接飒责,
第一版本使用 C 寫的 Node.js 擴展 ??修械;由于有異步任務(wù)的關(guān)系颁褂,實現(xiàn)使用了 N-API 提供的多線程做異步任務(wù)調(diào)度评汰,
雖然功能實現(xiàn)了,但是也有些值得思考的點痢虹。
- 純 C 編程效率低,木有 trycatch 的語言調(diào)試難度也大 (磕磕絆絆的)
- 編寫好的 .node 擴展文件主儡,放在 electron 主進程中運行會有一定的隱患稍有差錯會導(dǎo)致軟件閃退 (后來用子進程隔離運行)
- 基于 N-API 方式去編寫 Node.js 插件會顯得有所束縛奖唯,木有那種隨心所欲寫 C 的那種“順暢”;尤其是多線程部分
綜上考慮糜值,加上通訊功能又是調(diào)用 .dll
文件丰捷,索性轉(zhuǎn)戰(zhàn) C#,對于 windows 來說再合適不過了寂汇;但是問題是 C# 咋編譯到 Node.js 中病往?
答案是“編譯不了”。
插件實現(xiàn)的功能只是收到命令后調(diào)用 .dll
去操作硬件骄瓣,再時時能把結(jié)果返回即可停巷。
基于這個需求我們用 C# 去調(diào)用 .dll
文件,然后再解決派發(fā)命令榕栏、實時獲取結(jié)果的通訊問題就OK了畔勤,剩下的就都是好處啦
- C# 編寫難度低于 C,又是 windows 親兒子扒磁,基于
.NET Framework
編譯后的程序僅 19KB (C實現(xiàn)同樣功能編出來的.node文件 565KB) - 基于 C# 的插件獨立于 Node.js 運行環(huán)境庆揪,程序出了問題不會影響 electron 應(yīng)用
- 木有任何的編程束縛,~親想咋寫就咋寫
通訊問題
說這個之前我們還忽略了一個問題妨托,這個 C# 的程序(.exe文件)如果啟動缸榛?
既然是一個程序(.exe文件),我們雙擊即可執(zhí)行兰伤;既然雙擊即可執(zhí)行瘦锹,我們就可以用 child_process
模塊提供的
spawn 去拉起程序(代替鼠標(biāo)雙擊);
好小渊!程序已經(jīng)啟動了铺罢,那么該到了如果通訊的環(huán)節(jié)了。
spawn
的執(zhí)行就是開啟了一個單獨的進程,通訊問題也就是進程通訊問題负懦。之前如果你用過 spawn
啟動過 Node.js 程序(.js文件)筒捺,那么你肯定知道通訊使用 send
方法即可;這個是 Node.js 內(nèi)置的方式
我們啟動的進程是 C# 程序纸厉,通訊問題只能我們自己來解決了系吭;進程通訊的方式有好多這里不展開。對于前端(web)攻城獅來講颗品,我們最熟悉的莫過于 http
通訊方式了肯尺;就用它!
- C# 程序端啟動開啟一個
http
服務(wù)等待 Node.js 端發(fā)送請求過來躯枢;根據(jù)參數(shù)決定要干啥 -
spawn
啟動的應(yīng)用(進程)则吟,會返回一個ChildProcessWithoutNullStreams
(這個我也不能很明確的理解);能夠接收到標(biāo)準(zhǔn)的stdio
輸入/輸出
那我們就利用這點使用ChildProcessWithoutNullStreams.stdout.on('data', chunk => console.log(chunk.toString()))
的方式就可以收到 C# 通過stdio
即Console.WriteLine()
發(fā)過來的數(shù)據(jù)锄蹂;
哇氓仲!好方便~ - 可能有人會想到用雙工的
web socket
實現(xiàn)通訊,很棒得糜!實現(xiàn)方式確實有很多種敬扛,這里用Console.WriteLine()
通過標(biāo)準(zhǔn)的stdio
方式實現(xiàn),算不算是一個開發(fā)成本不高的討巧做法呢朝抖!
大致流程
- 完整代碼
- 如果覺得這篇文章有難度啥箭,可以看簡單版的哦 Node.js 利用 stdio 標(biāo)準(zhǔn)輸入/輸出實現(xiàn)與 C# 程序通訊
開發(fā)環(huán)境
- C# 代碼部分使用 Visual Studio 2017
- test.js 代碼部分使用 VsCode
代碼實現(xiàn)
-
C# 部分
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Net.Sockets; using System.Threading; using System.Text.RegularExpressions; namespace NodeAddons { class Program { static TcpListener listener; static int port = 8899; static string now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); static void Main(string[] args) { listener = new TcpListener(IPAddress.Any, port); listener.Start(); // 啟用服務(wù)器線程 new Thread(new ThreadStart(StartServer)).Start(); Console.WriteLine("Http server run at {0}.", port); } // Http 服務(wù)器 static void StartServer() { while(true) { // 這里會阻塞線程,直到接受到一個請求 Socket socket = listener.AcceptSocket(); // 將請求單獨開一個線程處理治宣;while(true)會回到等待下一個請求狀態(tài)急侥,周而復(fù)始 new Thread(new ParameterizedThreadStart(HandleRequest)).Start(socket); } } // 處理一個請求 static void HandleRequest(object args) { Socket socket = (Socket)args; byte[] receive = new byte[1024]; socket.Receive(receive, receive.Length, SocketFlags.None); string httpRawTxt = Encoding.ASCII.GetString(receive); // 通過 stdio(Console.WriteLine) 實現(xiàn)與 node.js 通訊 // ## 開頭、結(jié)尾炼七,方便區(qū)分這個條輸出是給 node.js 通訊用的 Console.WriteLine("##" + httpRawTxt + "##"); SendToBrowser(ref socket, now); } // 發(fā)送數(shù)據(jù) static void SendToBrowser(ref Socket socket, string body) { string header = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Content-Length: " + body.Length + "\r\n" + "Access-Control-Allow-Origin: *\r\n" // 支持跨域 + "\r\n"; // 響應(yīng)頭與響應(yīng)體分界 byte[] data = Encoding.ASCII.GetBytes(header + body); if (socket.Connected) { int res = socket.Send(data, data.Length, SocketFlags.None); if (res == -1) { Console.WriteLine("Socket Error cannot Send Packet."); } else { Console.WriteLine(">> [{0}]", now); } socket.Close(); } } } }
-
Node.js 部分
const http = require('http'); const cp = require('child_process'); const path = require('path'); // const handel = cp.spawn(path.join(__dirname, 'dist/NodeAddons.exe')); const handel = cp.spawn(path.join(__dirname, 'dist/NodeAddons_WithConsole.exe')); handel.stdout.on('data', chunk => { const str = chunk.toString(); // 約定 ##數(shù)據(jù)## 的字符串為通訊數(shù)據(jù) let res = str.match(/##([\S\s]*)##/g); if (!Array.isArray(res)) return; res = res[0].match(/(?<=(\?))(.*)(?=(\sHTTP\/1.1))/); if (!Array.isArray(res)) return; console.log('[stdout queryString]', res[0]); }); function query(param, cb) { http.get(`http://127.0.0.1:8899/?${(new URLSearchParams(param)).toString()}`, res => { res.on('data', chunk => { cb(chunk.toString()); }); }); } query({ name: 'anan', age: 29, time: Date.now() }, httpRawTxt => { console.log('[http response]', httpRawTxt); }); // 監(jiān)聽 Ctrl + c process.on('SIGINT', () => { handel.kill(); process.exit(0); });
測試一下
-
當(dāng)然程序不會自己停下來哈缆巧,畢竟子進程的 http 服務(wù)一直在運行!
$ node test.js [stdout queryString] name=anan&age=29&time=1595134635733 [http response] 2020-07-19 12:57:15
-
看下真實項目中任務(wù)管理器
1595376201(1).png