前段时间 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.ts
import { 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.ts
import { 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.go
package deno
import (
"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 = 0
func 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
启动服务器, 就可以在浏览器访问了。
Hello world!
最后附上一张 ts
代码和 Golang
代码通过订阅/发布模式进行交互的灵魂草图 😇 🤪
『草』图
最后的最后
这篇文章对很多代码细节原理并没有详细解释,网上已经有很多文章对 deno
的底层实现进行介绍,大家自行查阅。
如果大家要进行 deno
的开发工作或者学习的话,可以多多参考 pr 中的众多优秀内容,其中已经有 http
、await
、tcp
等等很多实现的代码,这篇文章也从中学习了很多。
祝大家端午节快乐,玩得开心,就到这里了🤩