在写 HTTP 服务的时候,我时常感觉 Go 能够对 HTTP 事件注册回调是一件很神奇的事情。来自网络的 HTTP 请求是如何映射到回调函数的参数的?昨天晚上我本来打算 10 点睡觉,看《杀戮尖塔》铁甲战士分析看到 11 点,突然想到这个问题,于是想看看在 LLVM 语言里面是如何实现 HTTP 服务的。
经过一段时间的资料搜寻,我发现核心在于对 Socket 的处理。据我过去对 Socket 的理解,它应该是个和文件很像的东西,但是有一些神秘的事件函数。这引出一个问题:这些函数是如何实现的?是基于文件系统实现的吗?如果是这样的话,感觉两边都要在同一个文件里塞一堆奇奇怪怪的东西,似乎不那么线程安全。今天我想到可以看 Go 的源码,所以准备尝试一下。
从 http.ListenAndServe 开始
在 Socket 之前,大部分其实都是一些封装和判断:
1 | func ListenAndServe(addr string, handler Handler) error { |
到这一层开始略有抽象:
1 | func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) { |
可以看到 Listen 函数返回的都是 Listener 对象,但这个 Listener 是否带有回调函数呢?我们不妨看一下 TCPListener 的定义:
1 | // TCPListener is a TCP network listener. Clients should typically |
可以看到最后 FD 收敛成了一个 int,和 Linux API 里面一样。
在 fd_unix.go 里面可以看到有 Recv 函数:
1 | // ReadFromInet4 wraps the recvfrom network call for IPv4. |
那么回调机制是如何实现的呢?回调意味着没有请求的时候不执行函数,有请求了再执行,但由于这种底层操作并没有异步机制,所以理论上需要疯狂重试。实际上看上面的代码确实有一个 for loop,同时接受 syscall.EAGAIN 错误,这个错误正是表示「现在没有资源可以获取,请重试」。
但同步变异步的机制真的是这样的吗?可以看到还有一个有趣的 fd.pd.waitRead 函数,首先 pd 的类型是:
1 | type pollDesc struct { |
Golang 官方没有给出注释,继续看 wait 函数:
1 | func (pd *pollDesc) waitRead(isFile bool) error { |
可以看到最后也变成了一个 unimplemented 的函数,这个函数是否会延时还是未知数……
但经过一段时间的查找,我发现这玩意实际上在 runtime 包里实现了:
1 | func poll_runtime_pollWait(pd *pollDesc, mode int) int { |
这里面有个非常明显的 netpollblock 轮询,所以并不是靠前面的 EAGAIN 实现轮询,而是 runtime_pollWait 本身有延时功能。
Socket
刚刚 listenTCP 里面的 internetSocket 函数还没看,但可以发现 Socket 最终收敛成了一个 RawSyscall:
1 | func internetSocket(ctx context.Context, net string, laddr, raddr sockaddr, sotype, proto int, mode string, ctrlCtxFn func(context.Context, string, string, syscall.RawConn) error) (fd *netFD, err error) { |
但是在上面的代码中我们没有看到任何回调,只有 netpollwait 这种同步变异步的东西,所以……
看 srv.Serve!
首先需要明确回调函数在 Server 结构体里面,Socket 数据结构在 ln 里面,接下来看看能不能找到对 socket 含有 netpollwait 函数的直接调用:
1 | func (srv *Server) Serve(l net.Listener) error { |
对 Socket Accept 函数的调用并没有直接导致 srv 回调函数的调用,因为这里有一个「建立连接」的概念,只有连接建立之后才能继续收信息传给 srv. 同时,这里连接建立之后 Golang 选择开一个 goroutine 去处理这个连接。因此我们看一看 newConn 的 serve 函数:
1 | // Serve a new connection. |
函数有删减,原本里面一大堆判断,但我们关注的只是回调函数的调用。基本上可以看出一个读取请求到调用回调函数的结构。不过对 readRequest 进行溯源最多到一个 Reader,至于如何和 Socket 关联,还得回去看 rw 的构造(这个是 l.Accept() 出来的东西)以及 srv.ConnContext 到底干了什么(它怎么是个函数?)
不对,connCtx 和 rw 只是为了更新 Context,实际上 rw 真正生效是在 srv.newConn(rw) 里面。
1 | // Create new connection from rwc. |
可以看到 newConn 只是一个简单封装,同时 net.Conn 溯源其实最后也是到一些 Reader:
1 | // Conn is a generic stream-oriented network connection. |
同时注意 conn.serve 里面对 conn.rwc 的封装,所以最后对 socket 溯源还得看 rw 的生成。然而,net.Listener 只是一个接口,我们需要想办法获取它在我的应用场景下的实现。在前面的小节里,我们知道 TCPListener 是这样的一个实现,因此看其 Listen 函数:
1 | // Accept implements the Accept method in the Listener interface; it |
可以看到最终到了 unimplemented 的函数,同时也看到了熟悉的 syscall.EAGAIN
总结
Go 对 HTTP 回调函数的处理机制就是先尝试形成连接(采用了指数增加的等待时间),然后对于每个连接去开一个 goroutine 去 listen,listen 里面最终的实现应该基于轮询,最终直接调用回调函数。