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

Original: <https://lifelonglearn.ing/Implement-an-online-interview-tool-from-scratch>

Author: [奶爸](https://lifelonglearn.ing/users/3b04650c-8231-499b-8ef0-381fc4d7efb0)

Published: 2020-10-25T14:20:20Z

Updated: 2026-09-27T06:57:43Z

Version: 1

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

![image.png](https://lifelonglearn.ing/upload/1603630899509973826.png)

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

![image.png](https://lifelonglearn.ing/upload/1604061985740288750.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 前端文件，它的路由看起来是这样的 😄

```go
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](https://lifelonglearn.ing/r/go?url=aHR0cHM6Ly9naXRodWIuY29tL1ZhbmVzc2EyMTkvdmRpdG9yL2lzc3Vlcy82OTk=) 学到了 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](https://lifelonglearn.ing/r/go?url=aHR0cHM6Ly9naXRodWIuY29tL25haWJhL2F4b2xvdGwvY29tbWl0LzYzMGYxNjFhOGQyMGU2ODU3MGQxMTQ1OTZiODUzNDZhNjY0MGJiMDE=)

## WebRTC

这个工具的在线视频会话部分使用 WebRTC 实现的，因为奶爸不太喜欢使用免费的云服务额度，以开源技术方案为先。从一个 [WebRTC 聊天的 demo](https://lifelonglearn.ing/r/go?url=aHR0cHM6Ly9naXRodWIuY29tL2lnb3JiZXpzbWVydG55aS93ZWJydGMtdmlkZW8tY2hhdA==) 开始，奶爸从浏览器调用音视频设备，到通过信令服务器建立起连接，一点一点的把在线视频做起来了。

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

![](https://lifelonglearn.ing/upload/1603634292575959596.png)

而且时序上面要对，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 可能会一直开着

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

## 在线运行代码

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

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

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

## 总结

![image.png](https://lifelonglearn.ing/upload/1603635546687851797.png)

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

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

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