讓 deno 支持 HTTP 服務
來自專欄餓了么前端51 人贊了文章
前段時間 ry 大佬公開了他目前投入其中的開源項目 deno
, 還在演講中細數 Node.js
「十宗罪」, 一時間圈子裡那是『紅旗招展』、『人山人海』, 眾說紛紜, 也鬧出了很多「笑話」, 當然看標題就知道這篇文章說的不是這些。
The main difference is that Node works and Deno does not work : )
Deno is a prototype / experiment.
對於現階段的 deno
, 正如作者所言, 並不是一個正常投入生產的項目, 還在試驗階段。不過, 正因為如此, 現在倉庫的代碼量不多, 正是我們學習和玩耍的好時機, 可以很簡單地進行改造, 而不用太過擔心玩崩了。
上面是 Node.js
開發者 Parsa Ghadimi 畫的 deno
的架構圖 , 裡面的內容解釋大家可以在網上很容易找到, 我就不多講。
deno
依賴 Google 出品 protobuf
進行跨語言通信, 還有 ry 自己開發的 v8worker2
在 deno
則是 Golang
與 v8
進行溝通的橋樑(對了, deno
使用 Golang
替代了 C++
, 通過這個項目來學習下 Golang
也是不錯的)。
大家通過 README 可以了解到怎麼對項目進行編譯, 當然也可以找現成的 docker
鏡像進行操作。
下面進入正題, 現在的 deno
只支持很少的幾個功能, 並不支持搭建 HTTP
服務, 如果想要用 deno
搭建 HTTP
服務要怎麼辦呢?
只能自己進行開發支持, 我詳細介紹下怎麼樣讓 deno
可以搭建一個簡單的伺服器
// helloServer.tsimport { Request, Response, createHttpServer } from "deno";const server = createHttpServer((req: Request, res: Response) => { res.write(`[${req.method}] ${req.path} Hello world!`); res.end();});server.listen(3000);
上面是我們期望創建伺服器的代碼, 接下來我們根據這段代碼一點點實現
Request, Response, createHttpServer
上面說過, deno
現在並沒有這些類和方法, 我們要構建這些對象和方法。
注: 這裡並不是要寫一個功能完善的模塊, 有很多東西我都會省略掉
// http.tsimport { main as pb } from "./msg.pb";import { pubInternal, sub } from "./dispatch";const enc = new TextEncoder();const servers: {[key: number]: HttpServer} = {};export class Request { method: string; path: string; constructor(msg: pb.Msg) { this.path = msg.httpReqPath; this.method = msg.httpReqMethod; }}export class Response{ requestChannel: string; constructor(msg: pb.Msg) { this.requestChannel = `http/${msg.httpReqId}`; }}let serverId = 0;export class HttpServer { port: number; private id: number; private requestListener: (req: Request, res: Response) => void; constructor(requestListener: (req: Request, res: Response) => void) { this.requestListener = requestListener; this.id = serverId ++; servers[this.id] = this; }}export function createHttpServer( requestListener: (req: Request, res: Response) => void): HttpServer { const server = new HttpServer(requestListener); return server;}
在根目錄創建 http.ts
, 在其中進行定義。
Request
中有 method
、path
兩個屬性, 簡單起見, 瀏覽器請求中還有 body
、header
等等其他實際中會用到的屬性我都忽略了。
Response
中 requestChannel
是用於通過 deno
訂閱/發布模式返回結果的, 後面能看到具體什麼用。
HttpServer
中包括綁定的埠 port
, 在構造函數中, 生成對 HttpServer
生成實例進行標識的 id
, 及綁定對請求進行處理的函數 requestListener
。
方法 createHttpServer
則是用 requestListener
創建 server
實例
server.listen
在有了 HttpServer
也綁定了 requestListenner
之後, 要監聽埠
// http.ts...export class HttpServer { ... listen(port: number) { this.port = port; pubInternal("http", { command: pb.Msg.Command.HTTP_SERVER_LISTEN, httpListenPort: port, httpListenId: this.id }); }}...
其中, pubInternal
方法需要兩個參數 channel
和 msgObj
, 上面的代碼就是將監聽埠命令及所需的配置發布到 Golang
代碼中 http
這個頻道。
// msg.proto...message Msg { enum Command { ... HTTP_RES_WRITE = 14; HTTP_RES_END = 15; HTTP_SERVER_LISTEN = 16; } ... // HTTP int32 http_listen_port = 140; int32 http_listen_id = 141; bytes http_res_write_data = 142; int32 http_server_id = 143; string http_req_path = 144; string http_req_method = 145; int32 http_req_id = 146;}...
在 msg.proto
文件(protobuf
的定義文件)中對需要用到的 Command
以及 Msg
的屬性進行定義, 需要注意的是, 屬性值需要使用下劃線命名, 在編譯 deno
時會會根據這個文件生成對應的 msg.pb.d.ts
、msg.pb.js
及 msg.pb.go
分別讓 ts
及 Golang
代碼使用, 這裡對後續需要用到的定義都展示了, 後面不再贅述。
// http.gopackage denoimport ( "fmt" "net/http" "github.com/golang/protobuf/proto")var servers = make(map[int32]*http.Server)func InitHTTP() { Sub("http", func(buf []byte) []byte { msg := &Msg{} check(proto.Unmarshal(buf, msg)) switch msg.Command { case Msg_HTTP_SERVER_LISTEN: httpListen(msg.HttpListenId, msg.HttpListenPort) default: panic("[http] unsupport message " + string(buf)) } return nil })}func httpListen(serverID int32, port int32) { handler := buildHTTPHandler(serverID) server := &http.Server{ Addr: fmt.Sprintf(":%d", port), Handler: http.HandlerFunc(handler), } servers[serverID] = server wg.Add(1) go func() { server.ListenAndServe() wg.Done() }()}
同樣在根目錄創建 http.go
文件。
InitHTTP
中訂閱 http
channel, 在傳入的 msg.command
為 Msg_HTTP_SERVER_LISTEN
時調用 httpListen
進行埠監聽(還記得之前 msg.proto
中定義的枚舉 Command
么, 在生成的 msg.proto.go
中會加上 Msg
前綴)。
httpListen
中用模塊 net/http
新建了一個 httpServer
, 對埠進行監聽, 其中 Handler
後面再說。
wg
是個 sync.WaitGroup
, 在 dispatch.go
中保證調度任務完成.
請求到來
在上面的代碼中已經成功創建了 httpServer
, 接下來瀏覽器發送 HTTP
請求來到 http.go
中新建的 server
時, 需要將請求轉交給 ts
代碼中定義的 requestListener
進行響應。
// http.go...var requestID int32 = 0func buildHTTPHandler(serverID int32) func(writer http.ResponseWriter, req *http.Request) { return func(writer http.ResponseWriter, req *http.Request) { requestID++ id, requestChan := requestID, fmt.Sprintf("http/%d", requestID) done := make(chan bool) Sub(requestChan, func(buf []byte) []byte { msg := &Msg{} proto.Unmarshal(buf, msg) switch msg.Command { case Msg_HTTP_RES_WRITE: writer.Write(msg.HttpResWriteData) case Msg_HTTP_RES_END: done <- true } return nil }) msg := &Msg{ HttpReqId: id, HttpServerId: serverID, HttpReqPath: req.URL.Path, HttpReqMethod: req.Method, } go PubMsg("http", msg) <-done }}
buildHTTPHandler
會生成個 Handler
接收請求, 對每個請求生成 requestChan
及 id
。
訂閱 requestChan
接收 ts
代碼中 requestListener
處理請求後返回的結果, 在 msg.Command
為 Msg_HTTP_RES_WRITE
寫入返回的 body
, 而 Msg_HTTP_RES_END
返回結果給瀏覽器。
通過 PubMsg
可以將構造出的 msg
傳遞給 ts
代碼, 這裡需要 ts
代碼對 http
進行訂閱, 接收 msg
。
// http.ts...const servers: {[key: number]: HttpServer} = {};export function initHttp() { sub("http", (payload: Uint8Array) => { const msg = pb.Msg.decode(payload); const id = msg.httpServerId; const server = servers[id]; server.onMsg(msg); });}...export class HttpServer { ... onMsg(msg: pb.Msg) { const req = new Request(msg); const res = new Response(msg); this.requestListener(req, res); }}...
這裡在初始化 initHttp
中, 訂閱了http
, 得到之前 Golang
代碼傳遞過來的 msg
, 獲取對應的 server
, 觸發對應 onMsg
。
onMsg
中根據 msg
構建 Request
和 Response
的實例, 傳遞給 createHttpServer
時的處理函數 requestListener
。
在處理函數中調用了 res.write
和 res.end
, 同樣需要在 type.ts
里進行定義。
// http.ts...export class Response{ ... write(data: string) { pubInternal(this.requestChannel, { command: pb.Msg.Command.HTTP_RES_WRITE, httpResWriteData: enc.encode(data) }); } end() { pubInternal(this.requestChannel, { command: pb.Msg.Command.HTTP_RES_END }); }}...
而之前 Response
的構造方法中賦值的 requestChannel
作用就在於調用 res.write
和 res.end
時, 能將 command
和 httpResWriteDate
傳遞給 Golang
中相應的 handler
, 所以這個值需要和 Golang
代碼中 Handler
中訂閱的 requestChan
相一致。
最後
到這裡, 整個流程就已經走通了, 接下來就是要在 ts
和 Golang
代碼中執行模塊初始化
// main.go...func Init() { ... InitHTTP() ...}...
// main.ts...import { initHttp } from "./http";(window as any)["denoMain"] = () => { ... initHttp() ...}...
然後在 deno.ts
中拋出 Request
、Response
和 createHttpServer
, 以供調用。
// deno.ts...export { createHttpServer, Response, Request } from "./http";
另外需要在 deno.d.ts
進行類型定義, 這個不詳細說明了。
通過 make
進行編譯即可, 在每次編譯之前最好都要 make clean
清理之前的編譯結果。
通過命令 ./deno helloServer.ts
啟動伺服器, 就可以在瀏覽器訪問了。
最後附上一張 ts
代碼和 Golang
代碼通過訂閱/發布模式進行交互的靈魂草圖 ?? ??
最後的最後
這篇文章對很多代碼細節原理並沒有詳細解釋,網上已經有很多文章對 deno
的底層實現進行介紹,大家自行查閱。
如果大家要進行 deno
的開發工作或者學習的話,可以多多參考 pr 中的眾多優秀內容,其中已經有 http
、await
、tcp
等等很多實現的代碼,這篇文章也從中學習了很多。
祝大家端午節快樂,玩得開心,就到這裡了??
推薦閱讀:
※HTTP入門
※HTTP中的重定向和請求轉發的區別
※wireshark如何按照域名過濾?
※HTTP與HTTPS的區別
※關於XMLHTTP和XML實現無刷新提交