項目需求
最近在開發(fā)一個 electron 程序,其中有用到和硬件通訊部分;硬件廠商給的是 .dll 鏈接庫做通訊橋接,
第一版本使用 C 寫的 Node.js 擴(kuò)展 ??;由于有異步任務(wù)的關(guān)系,實現(xiàn)使用了 N-API 提供的多線程做異步任務(wù)調(diào)度,
雖然功能實現(xiàn)了,但是也有些值得思考的點。
- 純 C 編程效率低,木有 trycatch 的語言調(diào)試難度也大 (磕磕絆絆的)
- 編寫好的 .node 擴(kuò)展文件,放在 electron 主進(jìn)程中運行會有一定的隱患稍有差錯會導(dǎo)致軟件閃退 (后來用子進(jìn)程隔離運行)
- 基于 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í)行就是開啟了一個單獨的進(jìn)程,通訊問題也就是進(jìn)程通訊問題。之前如果你用過 spawn 啟動過 Node.js 程序(.js文件),那么你肯定知道通訊使用 send 方法即可;這個是 Node.js 內(nèi)置的方式
我們啟動的進(jìn)程是 C# 程序,通訊問題只能我們自己來解決了;進(jìn)程通訊的方式有好多這里不展開。對于前端(web)攻城獅來講,我們最熟悉的莫過于 http 通訊方式了;就用它!
- C# 程序端啟動開啟一個
http服務(wù)等待 Node.js 端發(fā)送請求過來;根據(jù)參數(shù)決定要干啥 -
spawn啟動的應(yīng)用(進(jìn)程),會返回一個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)然程序不會自己停下來哈,畢竟子進(jìn)程的 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
