go 网络编程

本篇主要内容是 Go HTTP 原理和 RPC 相关知识。 在进行原理讲解之前我觉得有必要熟悉一下 TCP 传输原理C Socket 编程,因为 Go 是在 C 的基础上进行封装的,这样对哪些属于 C,哪些属于 Go 有一个清晰的了解,也更能避繁就简。

1   Go HTTP 原理

1.1   服务端

1.1.1   三层逻辑

先说明一下,这种分类仁者见仁,智者见智,能说清楚脉络就是好方法。

  • ServeMux 类型:有两大类方法: (1) HandleFunc()、Handle() 创建其属性,m: map[string]muxEntry 路由和函数执行体; (2) ServeHTTP()、Handler()、handler() 接收到请求后对 m 的路由查找,找到函数执行体;
  • Server 类型:包含了 ServeMux 这个 Handler 接口类型,ListenAndServe() 和 Serve() 方法主要负责监听和接口请求,并启用 goroutine 调用下面 conn 类型的 serve();
  • conn 类型:包含了 Server 类型,serve() 方法,先通过 serverHandler 类型的 ServeHTTP() 方法 查找到 ServeMux,调用 ServeMux 的(2)相关方法找到函数执行体(也是一个 ServeHTTP() 方法)并执行。 这一下就提到三个 ServeHTTP() 方法,功能都是不一样的,第一个是用于查找 ServeMux,第二个是用于查找函数执行体,第三个是要执行的函数执行体,注意不要混淆了。

一言以蔽之,先保存路由和函数执行体映射(ServeMux 准备阶段);开启监听和等待连接,接受连接请求并新开一个 goroutine(Server 等待连接阻塞,承上启下);通过查找映射找到函数执行体(conn 执行阶段)。 其中的监听、等待连接、接受连接是 Go 语言的实现,底层调用了 C 的接口。

1.1.2   细节剖析

上面的三个类型中其实就是一层一层包含,里面通过一些重点内核函数(如监听连接等)或者辅助函数(如 serverHandler 等)实现该层的功能或逻辑。 在调用 func ListenAndServe(addr string, handler Handler) 函数的的时候,第二个就是可以自定义 Handler,也可以传 nil 表示使用 http.DefaultServeMux 进行处理,而下面是就设置 http.DefaultServeMux 中的 m 属性的方式:

//句柄pattern1  通过一个结构体
type Mux struct{}

func (Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "welcome to china")
}

//句柄pattern2 通过一个函数
type HandlerFunc func(http.ResponseWriter, *http.Request)

func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	f(w, r)
}

//句柄pattern3 这其实和pattern2一样,只是调用的不是自定义的HandlerFunc,而是http.HandlerFunc
func IndexHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello world")
}

func httpServe(){
	http.Handle("/pattern1", new(Mux))
	http.Handle("/pattern2", HandlerFunc(IndexHandler))
	http.Handle("pattern3", http.HandlerFunc(IndexHandler))
	http.HandleFunc("/pattern4", IndexHandler)
	http.ListenAndServe(":8000", nil)
}

1.2   客户端

客户端的实现方式有很多种,下面我只实现两种。另外,理解 go 的 HTTP 最佳的方式是理解原代码,理解原代码的方式是进行服务端调试,要进行服务端的调度需要从客户端请求开始。

1.2.1   基于 TCP 的 HTTP 客户端

tcpAddr, err := net.ResolveTCPAddr("tcp4", "localhost:8000")
conn, err := net.DialTCP("tcp", nil, tcpAddr)
_, err = conn.Write([]byte("GET /pattern2 HTTP/1.1\r\nHost: localhost:8000\r\n\r\n"))
_ = conn.CloseWrite() //发送 FIN
result, err := ioutil.ReadAll(conn)

1.2.2   基于 http.Client 的 HTTP 客户端

req, _ := http.NewRequest("GET", "http://localhost:8000/pattern2", nil)
client := &http.Client{}
resp, err := client.Do(req)
result, err := ioutil.ReadAll(resp.Body)

2   RPC

2.1 概述

RPC 是远程过程调用,或者说是远程方法调用、远程函数调用,多了一个“远程”,区别于同一进程空间内的方法或者函数调用,服务调用方须以网络的形式进行远程调用,是服务化框架的核心之一。 TCP/IP 和 HTTP 我把他们定位为实现数据传输,是协议设计。 RPC 和 Restful 我把他们定位为在数据传输基础上提供服务,是框架设计。 Restful 是基于 HTTP 的,但 RPC 可以基于 HTTP,也可以基于 TCP/IP。换句话说,HTTP 是面向“事先没有沟通”的普通用户,Restful 一般是面向“事先有简单沟通”的程序员(有后端的,但更多是前端),相对来说还是存在通用性设计;而 RPC 是面向“事先有良好沟通”的程序员(后端),所以可以去掉一些通用设计而采用精准功能设计,从而提高效率。 打个比喻,Restful 或者 HTTP 犹如有界面操作系统,事先用户简单学习甚至完全不需要学习其使用方式,用户自己摸索就能使用它;RPC 犹如命令操作系统,直接提供命令给你操作,前提是你得来学习命令或者函数操作方式,去掉“中间商赚差价”,所以效率提高了。

Go 语言标准包中已经提供了对 RPC 的支持,而且支持三个级别的 RPC:TCP(使用的标准包 net/rpc)、HTTP(使用的标准包 net/http、net/rpc)、JSONRPC(使用的标准包 net/rpc、net/rpc/jsonrpc)。但 Go 语言的 RPC 包是独一无二的 RPC,只支持 Go 语言开发的服务器与客户端之间的交互,因为在内部,它们采用了 Gob 来编码。所以如果是跨语言的,可以采用开源的 RPC,比如 gRPC,基于 HTTP,它采用 protocol 编码,支持很多语言,如 Java、Go、C++ 和 PHP 等。 下面我只讲解实现基于 TCP 的 Go 标准包 RPC(说明这个 RPC 是用 TCP 传输,且用了 Gob 编码),服务端和客户端都使用标准包 net/rpc。

2.2   基于 TCP 的标准包 RPC