欢迎来到DIVCSS5查找CSS资料与学习DIV CSS布局技术!
前段时间 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 等等很多实现的代码,这篇文章也从中学习了很多。

如需转载,请注明文章出处和来源网址:http://www.divcss5.com/html/h63544.shtml