使用 C# 開發(fā) node.js 插件

項目需求

最近在開發(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# 通過 stdioConsole.WriteLine() 發(fā)過來的數(shù)據(jù)锄蹂;
    哇氓仲!好方便~
  • 可能有人會想到用雙工的 web socket 實現(xiàn)通訊,很棒得糜!實現(xiàn)方式確實有很多種敬扛,這里用 Console.WriteLine() 通過標(biāo)準(zhǔn)的 stdio 方式實現(xiàn),算不算是一個開發(fā)成本不高的討巧做法呢朝抖!

大致流程

process.png

開發(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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末豌拙,一起剝皮案震驚了整個濱河市陕悬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌按傅,老刑警劉巖捉超,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異唯绍,居然都是意外死亡拼岳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門况芒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惜纸,“玉大人,你說我怎么就攤上這事∧桶妫” “怎么了祠够?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長粪牲。 經(jīng)常有香客問我古瓤,道長,這世上最難降的妖魔是什么腺阳? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任落君,我火速辦了婚禮,結(jié)果婚禮上亭引,老公的妹妹穿的比我還像新娘绎速。我一直安慰自己,他們只是感情好焙蚓,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布朝氓。 她就那樣靜靜地躺著,像睡著了一般主届。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上待德,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天君丁,我揣著相機與錄音,去河邊找鬼将宪。 笑死绘闷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的较坛。 我是一名探鬼主播印蔗,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼丑勤!你這毒婦竟也來了华嘹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤法竞,失蹤者是張志新(化名)和其女友劉穎耙厚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岔霸,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡薛躬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了呆细。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片型宝。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出趴酣,到底是詐尸還是另有隱情梨树,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布价卤,位于F島的核電站劝萤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏慎璧。R本人自食惡果不足惜床嫌,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望胸私。 院中可真熱鬧厌处,春花似錦、人聲如沸岁疼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捷绒。三九已至瑰排,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間暖侨,已是汗流浹背椭住。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留字逗,地道東北人京郑。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像葫掉,于是被迫代替她去往敵國和親些举。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355