从零实现一个在线面试工具

奶爸在一次偶然的机会中看到了 showmebug.com,程序员线上面试工具。看过首页视频后,就在默默的进行头脑风暴,这架势,奶爸是见过的呀,在线执行代码的工具烂大街了,然后就是加了个在线视频而已嘛。

image.png

由于上面这个想法,复刻一个 showmebug 的坑就算开挖了,首先就是先初始化一下框架,前端 Vue 后端 Go,选了个近期看到的 halfmoon 的 UI 框架,这挖坑又填坑的路算是开始了。

image.png

Web 服务

Web 框架

奶爸在前后端分离开发这也算是很有经验了,在公司做后端项目一般都是用 gin,奶爸的业余爱好喜欢尝试些新鲜事物,比如这一次使用了 fiber 作为 web 框架。在使用 fiber 的几个项目中,奶爸发现了他的两个优点:

  1. 路由

    • 在 fiber 中,app.Get("/a/*")app.Get("/a/b") 是可以并存的
    • 而在 gin 中,如果你的路由定义是上面这种情况,那么会出现路由冲突,对于博客类型的这种需要自定义文章链接又有固定的管理员 URL 的话,是极其不便的(来自 Solitudes 项目的扎心痛诉)。
  2. 错误处理

    • 在 fiber 中,直接在 handler 中 return 一个 error 就可以交由全局的错误处理方法进行处理,包括 response 的格式化、error 信息的收集过滤及展示,对于 API 来说十分便捷。
    • 同样与 gin 做比较,那我们只能自己定义一个通用方法去统一处理 response 了

同样奶爸也发现了 fiber 的一个缺点,就是 WebSocket,fiber 生态中的 webSocket 是直接 switch protocol 的,而不是先交由你来处理进行鉴权之类的,而是先 switch protocol 再把 conn 丢给你……

前后端分离

奶爸在业余爱好的探索中,做过很多前后端分离的项目,结果现在奶爸直接将前后端直接打包到同一容器中了(当然这仅限于广大跟我有同样爱好的同好们,并不适用于企业,毕竟不好针对 API 做负载均衡)

奶爸使用了 fiber 来 serve 前端文件,它的路由看起来是这样的 😄

if frontendDevProxyEnv == "" {
	app.Static("/", "dist")
	app.Use(handler.NotFund)
} else {
	proxy := httputil.NewSingleHostReverseProxy(frontendHost)
	// proxy.Transport = xhttputil.NewTransport(func(body string) string {
	// 	return strings.ReplaceAll(body, `/js/`, frontendHost.String()+"js/")
	// })
	app.Use(adaptor.HTTPHandlerFunc(func(req http.ResponseWriter, resp *http.Request) {
		proxy.ServeHTTP(req, resp)
	}))
}

* 如果设置了 frontendDevProxyEnv 它就转换为本地开发模式了,直接反代前端预览 npm run serve 端口,这样会极大程度的给我们全干工程师提供便利

用户系统

直接 GitHub OAuth2 登录

协同编辑

因为奶爸之前的 Solitudes 博客引擎项目选择的是黑客派的 D&V 夫妻的 Vditor 编辑器,所以在做这个项目的时候奶爸第一个想到的也是使用 Vditor 来进行协同编辑,所以一开始考虑的方案是基于 Vditor 在后端实现一个 OT 算法,然后看了下 OT 算法相关的东西 和 Vditor 的 API,放弃了,Vditor 和 后端实现 OT 两个方案都放弃了 😢

一方面是后端实现 OT,并没有想花太多时间在这上面,如果奶爸后端实现了 OT,那必然前端跟后面视频会议上面会更下功夫,本着实用原则,放弃了。

另一方面是 Vditor 毕竟年轻,还不成熟,只是在 Markdown 上面有很大改善。之前奶爸还奇怪为什么 Quill 编辑器为什么会有 delta 这种东西,原来是可以分片编辑,在协作场景下性能会有质的提升。CodeMirror 和 Monaco 等编辑器都有分片编辑的 API。

然后到处网上冲浪的奶爸看到了这个 issue:https://github.com/Vanessa219/vditor/issues/699 学到了 OT 之外的 另一个词汇:CRDT,是近年来才出现的强一致性协同编辑算法,而且是实现在客户端侧,只需要服务端转发一下信令即可。

最终找到了 YJS 这个库,结合 monaco 编辑器实现了在线协同编辑的部分。在实际测试过程中奶爸发现 YJS 社区维护的几个信令服务器不是那么的稳定,导致 YJS 的 WebRTC provider 的联通率不高,会出现视频都出来了编辑器没跟上的情况。

之后奶爸学习了 y-websocket 的代码将原本代码中冗余部分加建立 WebSocket 连接的部分剔除掉,然后接入了项目内部的 WebSocket 链接,把 send 换成了 param,把 onmessage 换成了 callback,就这样实现了自己的 yjs 的 WebSocket provider。commit:🍻 awesome: custom yjs websocket provider

WebRTC

这个工具的在线视频会话部分使用 WebRTC 实现的,因为奶爸不太喜欢使用免费的云服务额度,以开源技术方案为先。从一个 WebRTC 聊天的 demo 开始,奶爸从浏览器调用音视频设备,到通过信令服务器建立起连接,一点一点的把在线视频做起来了。

因为没有 TURN 服务器的原因,仅通过 NAT 穿透来建立连接,在一些情况下还是会连接失败。还有连接质量差的问题出现。抛开这些需要加钱的部分,来说一下比较有意思的 2+ 用户的在线视频,因为 WebRTC 是 P2P 的 一对一,并不是 一对多或者多对一,这种情况 2+ 用户视频的拓扑就像下图一样

而且时序上面要对,A、B、C 三个节点互联互通,不能出现两边都是主动节点的情况,那样无法建立连接。而是只要有一方主动连接另一方一次连接就建立了。

graph TD; A-->B; A-->C; B-->C;

做这部分的时候后端出现了一些小插曲,两个 Peer 连接的好好地时候,突然出现 Peer 3 的时候准有一个 Peer 要掉线或者连接不上,原因竟然是信令服务器只是做的傻瓜转发,将 signal 转发给了所有 Peer,这种情况下 Peer 3 分别发送给 Peer 1、2 的 signal 信息被 Peer 1、2 都收到了,这样可能就造成 Peer 1 会连接两次,而 Peer 2 因为收到信令、建立连接慢而一次都收不到导致建立连接混乱。

所以奶爸就在客户端包括服务端都 signal 信息做了点对点转发,Peer 3 发给 Peer 1 的 signal 绝对不会被 Peer 2 收到,这样就实现了 2+ Peer 的点对点连接。

* 小 tips:一个 MediaStream 对象不能被多个 WebRTC 连接使用,会导致黑屏或者视频失败的情况,要给每个连接获取一个单独的 MediaStream 对象。然后在网页被关闭的时候记得销毁对象,不然你的 Camera 可能会一直开着

this.peers[k].streams.forEach((stream: any) => {
  console.log("beforeDestroy", k, stream);
  stream.getTracks().forEach((track: any) => track.stop());
});

在线运行代码

这一个原本想使用 https://github.com/keller0/scr 已经做好的 Docker 运行代码的方案的,但是这方案也不能配置 Web 服务绑定的接口,用来运行代码的容器的 Dockerfile 也不是开源的,索性就自己再造个轮子了。

造轮子之前想过宿主机编译然后沙盒运行的方案,但是被 Compiler Bomb 教育了,怎么都逃不过编译,索性就编译也放在容器里面吧。然后做了基本的 超时限制、资源限制、执行后回收 之后,奶爸的在线执行代码 https://github.com/naiba/code-runner 就上线了,它有

  • 完全可定义的容器,都是以免维护为准绳,尽量使用公共镜像
  • 简单的身份验证
  • 简单的API

总结

image.png

好了 就是这样了,这里面很多小的细节摘出来都蛮有意思的。同时每个点摘出来 比如

  • 协同编辑的前端性能啊,后端数据持久化啊
  • WebRTC 的视频质量啊、NAT 连通性啊、音视频单独控制啊、会议室控麦之类的会议工具之类
  • 在线运行代码的稳定性啊、并发啊、垃圾回收啊
  • 当前的 WebSocket 的安全性和稳定性也是有待商榷

又还有很多想象空间,哈哈,不说了,牛逼 就完事了 🎉️

Comments

Ash ·v1 Reply

写的好棒,最近我也在复刻showmebug,文章里的一些经验也让我很受益。