Skip to content

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

奶爸在一次偶然的机会中看到了 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:Issue #699 · Vanessa219/vditor 学到了 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

奶爸

奶爸 Guest ·v1

@Jake 推测是 浏览器 wasm 离线 interceptor + webrtc

Jake

Jake Guest ·v1

@奶爸 谢谢!

Commenting as a guest. A nickname is required; email is optional. Only new comments posted after verifying your email and signing in will appear on your profile. Existing guest comments will not be linked automatically.

Share article

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

Email X

View original ↗