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

項目需求

最近在開發(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# 通過 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)然程序不會自己停下來哈,畢竟子進(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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容