⚠ 转载请注明出处:作者:ZobinHuang,更新日期:Aug.6 2021
本作品由 ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。
目录
有特定需要的内容直接跳转到相关章节查看即可。
Section 1. Golang 网络标准库:介绍了 Golang 网络标准库 的概况,列出了各个 package 大概的功能。
Section 2. package net:介绍了 package net 的基本功能和使用方法;
2.1 回顾 Socket:回顾了 Socket 的种类、作用域的内容;
2.2 package net 基本结构:分析了 package net 的基本结构;
2.3 基于流的协议:分析了基于流的协议的编程框架;
2.4 基于包的协议:分析了基于包的协议的编程框架;
2.5 Dial 函数:分析了用于在客户端创建连接使用的 Dial 函数;
2.6 Listen 函数:分析了用于在服务端监听连接使用的 Listen 函数;
Section 3. package net/http:详细且清楚地分析了 package net/http.
3.1 关键数据结构:列举了 package net/http 中的关键数据结构;
3.2 HTTP 请求和响应:分析了 HTTP 处理的基本数据结构:http.Request 和 http.ResponseWriter;
3.3 路由注册实现细节:详细分析了 package net/http 是如何将 Handler 注册到 HTTP Server 中的,以及 HTTP Server 是如何实现重定向的功能的;
3.4 HTTP 请求处理实现细节:详细分析了 package net/http 是如何创建 HTTP Server 并且处理输入的 HTTP 请求的;
3.5 实践:搭建 HTTP 服务器:回到用户接口,作为总结,展示了创建 HTTP Server 的方法;
1. Golang 网络标准库
Golang 在标准库中提供了对程序网络功能的很好的支持。与网络相关的包如下所示:
包名 | 功能 |
---|---|
net | net 包提供了可移植的网络I/O接口,包括TCP/IP、UDP、域名解析和 Unix 域 socket |
net/http | net/http 包提供了 HTTP 客户端和服务端的实现 |
net/mail | net/mail 包实现了邮件的解析 |
net/rpc | rpc 包提供了通过网络或其他 I/O 连接对一个对象的导出方法的访问。服务端注册一个对象,使它作为一个服务被暴露,服务的名字是该对象的类型名。注册之后,对象的导出方法就可以被远程访问。 |
net/smtp | net/smtp 包实现了简单邮件传输协议(SMTP) |
net/textproto | net/textproto 实现了对基于文本的请求/回复协议的一般性支持,包括 HTTP、NNTP 和 SMTP |
net/url | net/url 包可以用于解析 URL 并实现了查询的逸码 |
下面我们会对重要的一些标准库网络 package 进行分析。
2. package net
2.1 回顾 Socket
package net 提供了让程序访问底层网络原语的能力,能够让我们在 Golang 中实现 socket 的编程。在开始讲解如何在 Golang 中进行套接字编程之前,我们必须梳理一下套接字都有哪些类型。
打破次元壁,我们来看一下 Linux 提供给我们的基于 C 的创建 socket 的接口是什么样的:
1
int socket(int domain, int type, int protocol);
在功能维度上,通常也称为按 域 (Domain) 划分,对应于 socket 创建接口的第一个参数 domain,我们可以把套接字分为两种:TCP/IP domain socket 和 UNIX domain socket,前者用于主机间的网络通信,后者用于进程间通信。二者由于作用对象的不同,因此在数据传输的实现细节上也有所不同:前者的数据传输需要经过内核协议栈,而后者的数据传输仅需要在内核 buffer 中进行拷贝即可,但是它们呈现给用户态程序的接口是一样的,因此在编程框架上基本无区别。这里,当我们指定 domain=AF_INET / AF_INET6 / AF_PACKET 时,我们就是在 TCP/IP domain 下;当我们指定 domain=AF_UNIX 时,我们就是在 UNIX domain 下。
顺便提一句,有时候我们还会看见以 "PF_" 为前缀的域,读者只需要简单的把 "PF_XXXX" 等价为 "AF_XXXX" 就可以了,这种区分是有历史原因的,但是实际上如今已经不需要区分,具体原因见于 Beej's Guide to Network Programming,下面摘取相关内容:
In some documentation, you'll see mention of a mystical "PF_INET". This is a weird etherial beast that is rarely seen in nature, but I might as well clarify it a bit here. Once a long time ago, it was thought that maybe a address family (what the "AF" in "AF_INET" stands for) might support several protocols that were referenced by their protocol family (what the "PF" in "PF_INET" stands for) [很久以前,人们认为也许一个地址族可能支持其协议族引用的几种协议]. That didn't happen. Oh well. So the correct thing to do is to use AF_INET in your struct sockaddr_in and PF_INET in your call to socket(). But practically speaking, you can use AF_INET everywhere. And, since that's what W. Richard Stevens does in his book, that's what I'll do here.
在数据传输维度上,我们又可以把套接字分为三种:面向流的 socket,面向数据报 (datagram) 的 socket 和 原始 (raw) socket,对应于 socket 创建接口的第二个参数 type。面向流的 socket 提供了顶层的有序的、可靠的,面向连接的通信;面向数据报 (datagram) 的 socket 提供了顶层的无连接的,固定大小传输的、不可靠的通信。这两种类型的套接字在 TCP/IP domain socket 和 UNIX domain socket 上都被支持。而最后一种类型 —— 原始 (raw) socket 则仅在 TCP/IP domain socket 上被讨论才有意义,它允许我们获取传输层以下的网络数据。这里,当我们指定 type=SOCK_STREAM 时,我们就是在创建面向流的 socket;当我们指定 type=SOCK_DGRAM 时,我们就是在创建面向数据报的 socket;当我们指定 type=SOCK_RAW 时,我们就是在创建原始 (raw) socket。
值得一提的是,当我们在创建原始 (raw) socket时,domain 字段似乎已经失去了意义。其实不然,当我们指定 domain=AF_INET 和 type=SOCK_RAW 时,我们创建的原始 (raw) socket可用于收发 IP 数据包 (网络层);当我们指定 domain=AF_PACKET 和 type=SOCK_RAW 时,我们创建的原始 (raw) socket可用于收发包含链路层协议头的以太网数据帧 (数据链路层);当我们指定 domain=AF_PACKET 和 type=SOCK_DGRAM 时,我们创建的原始 (raw) socket可用于收发不包含链路层协议头的以太网数据帧 (数据链路层)。
我们使用下表进行总结:
Domain | Type | 功能 |
---|---|---|
TCP/IP: AF_INET/AF_INET6 | SOCK_STREAM | TCP socket |
TCP/IP: AF_INET/AF_INET6 | SOCK_DGRAM | UDP socket |
TCP/IP: AF_INET/AF_INET6 | SOCK_RAW | IP socket |
TCP/IP: AF_PACKET | SOCK_RAW | Eth socket (with eth header) |
TCP/IP: AF_PACKET | SOCK_DGRAM | Eth socket (without eth header) |
UNIX: AF_UNIX | SOCK_STREAM | UNIX stream socket |
UNIX: AF_UNIX | SOCK_DGRAM | UNIX datagram socket |
对于 socket 创建接口的第三个参数 protocol,这个适用于指定具体协议的。常见的用法如下:
1
2
3
4
5
6
7
8// Case 1: 发送接收 ip 数据包
socket(PF_INET, SOCK_RAW, IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP);
// Case 2: 发送接收以太网数据帧
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL));
// Case 3: 发送接收以太网数据帧(不包括以太网头部)
socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL));
下面我们来看一下一个数据包从被内核接收上来,到抵达用户态套接字缓冲区,期间发生了什么事情。
首先,网卡对该数据帧进行硬过滤 (根据网卡的模式不同会有不同的动作,如果设置了 promisc 混杂模式的话,则不做任何过滤直接交给下一层输入例程,否则非本机 mac 或者广播 mac 会被直接丢弃)。如果成功的话,会进入 ip 输入例程,但是在进入 ip 输入例程之前,系统会检查系统中是否有通过 socket(AF_PACKET, SOCK_RAW/SOCK_DGRAM, ..) 创建的套接字,如果有的话并且协议相符 (即参数 protocol 相匹配),系统就给每个这样的 socket 接收缓冲区发送一个数据帧拷贝,然后进入下一步。
随后数据包就进入了 ip 输入例程 (ip 层会对该数据包进行软过滤,就是检查校验或者丢弃非本机 ip 或者广播 ip 的数据包等,具体要参考源代码),如果成功的话会进入 TCP/UDP 输入例程.但是在交给 TCP/UDP 输入例程之前,系统会检查系统中是否有通过 socket(AF_INET, SOCK_RAW, ..) 创建的套接字,如果有的话并且协议相符,系统就给每个这样的 socket 接收缓冲区发送一个数据帧拷贝.然后就进入 我们熟悉的 TCP/UDP 输入例程。
2.2 package net 基本结构
回顾完 socket 的内容后,我希望读者朋友没有被绕晕。如果你已经晕了也别担心,Golang 标准库在 package net 中为我们屏蔽了这些繁琐的细节,提供了十分友好的编程接口,实现了对底层 socket 细节的屏蔽。

上图所示是 package net 中的关系,灰色方框代表了结构体,黄色方框代表了接口,蓝色线代表了结构体对接口的实现。
在上图最下面的结构体中,我们可以发现所有我们在 回顾 socket 中描述到的 socket 类型,包括以下结构体:
- 对应 TCP socket 的 TCPConn 和 TCPListener
- 对应 UDP socket 的 UDPConn
- 对应 UNIX stream socket 的 UnixConn 以及 UnixListener
- 对应 UNIX datagram socket 的 UnixConn
- 对应 IP Raw socket 的 IPConn
当我们在进行网络编程的时候,实际上操作的就是上面的结构体。比如,我们在服务端会使用 net.Listen() 来新建一个 Listener,我们在客户端会使用 net.Dial() 来获取一个 Conn,实际上这些函数就是在操作上面的这些结构体,再往底层走就是在操作 socket。net 包已经给我们提供了很好的封装,因此我们一般不会直接与结构体进行交互。下面我们就来理清楚这些封装函数的结构以及它们所提供的功能。在看这些函数之前,我们首先再回忆一下 socket 编程的一些流程框架。
2.3 基于流的协议
基于流的协议,net 包中支持了常见的 TCP,Unix(Stream 方式)两种。基于流的协议需要先于对端建立链接,然后再发送消息。下面是基于流的编程的流程:

首先,服务端需要绑定并监听端口,然后等待客户端与其建立链接,通过 Accept 接收到客户端的连接后,开始读写消息。最后,当服务端收到 EOF 标识后,关闭链接即可。 HTTP, SMTP 等应用层协议都是使用的 TCP 传输层协议。
2.4 基于包的协议
基于包的协议,net 包中支持了常见的 UDP,Unix(DGRAM 包方式,PacketConn 方式),IP (网络层协议,支持了icmp, igmp) 几种。基于包的协议在 bind 端口后,无需建立连接,是一种即发即收的模式。
基于包的协议,有例如基于 UDP 的 DNS解析, 文件传输(TFTP协议)等协议。 下面是基于包请求的 Server 端和 Client 端:

可以看到,在 Socket 编程里, 基于包的协议是不需要 Listen 和 Accept 的。在 net 包中,使用 ListenPacket,实际上仅是构造了一个UDP Socket,做了端口绑定而已。端口绑定后,Server 端开始阻塞读取包数据,之后二者开始通信。由于基于包协议,因此,我们也可以采用 PacketConn 接口(看第一个实现接口的图)构造UDP包。
2.5 Dial 函数
在 Golang 中,Dial 用于在客户端创建 socket 和一些相关的初始化操作,可以参考上面 2.3 基于流的协议。Golang 大致提供了 5 种 dial,包括:
- Dial(network, address string) (Conn, error)
- DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
- DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)
- DialIP(network string, laddr, raddr *IPAddr) (*IPConn, error)
- DialUnix(network string, laddr, raddr *UnixAddr) (*UnixConn, error)
其中 Dial 是对其他几个的封装,输入参数 network 用于指定要创建的套接字所在的网络的类型,可以是如下类型:
tcp | tcp4 | tcp6 | udp | udp4 | udp6 | ip | ip4(IPv4-only) | ip6(IPv6-only) | unix | unixgram | unixpacket |
下面我们使用 Dial 和 TCPDial 为例来看它们的使用方法。首先是 Dial:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 //创建连接
conn,err := net.Dial("tcp","192.168.1.254:4001")
//发送数据
conn.Write([]byte{0x00,0x07,0x00,0x00,0x00,0x06,0x01,0x03,0x00,0x00,0x00,0x0A})
defer conn.Close()
buffer := make([]byte,32)
//读取返回数据
result,err := conn.Read(buffer)
if err != nil {
fmt.Println(err)
}
fmt.Println(result,buffer)
首先,我们使用 net.Dial("tcp","192.168.1.254:4001") 返回的是一个 DialTCP 结构体,也就是说 net.Dial 会根据我们输入的 network 类型来返回相应的结构。其次,我们注意到收发数据使用的是 Write 和 Read 函数,也就是说 TCPConn 等结构体实际上实现了 io.Reader 和 io.Writer 接口,我们可以使用它们实现的 Read 和 Write 函数来获取数据,也可以使用 bufio.Reader 和 bufio.Writer 来创建带有缓存的 io 结构来读取数据。
下面我们来看 DialTCP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 //进行地址化转换
tcpAddr,err:=net.ResolveTCPAddr("tcp","192.168.1.254:4001")
//创建一个连接
conn,err:=net.DialTCP("tcp",nil,tcpAddr)
if err != nil {
fmt.Println(err)
}
str := []byte{32, 0, 0, 0, 7, 85, 35, 160, 176, 7, 226, 12, 18, 15, 45, 0}
//向连接端发送一个数据
_,err=conn.Write(str)
if err != nil {
fmt.Println(err)
}
//读取连接端返回来的数据,
result,err:=ioutil.ReadAll(conn)
if err != nil {
fmt.Println(err)
}
fmt.Println(string(result))
可以发现,在使用 DialTCP 的时候,需要使用 net.ResolveTCPAddr 来获得用于描述 TCP 地址的 TCPAddr 结构。其实在 Dial 中,连接会根据你填入的连接类型,自动把地址转换为对应类型地址。
另外,在 Dial 的例子中,我们是把 buffer 填充完后就直接执行后面的代码了,但是 buffer 的你需要事先定义大小。在 TCPDial 的例子中, ioutil.ReadAll() 要是拿不到结束符,会一直等到连接超时才执行后面代码。这样就会导致阻塞很长时间。
2.6 Listen 函数
看完了 Dial 函数,我们现在来看一下 Listen 函数。Listen 函数用于在服务端创建监听方法,在 golang 中监听方法大致有:
- net.Listen()
- net.ListenPacket()
- net.ListenTcp()
- net.ListenUdp()
- net.ListenUnix()
- net.ListenIp()
- net.ListenUnixgram()
- net.ListenMulticastUDP()
- http.ListenAndServe()
- http.ListenAndServeTLS()
它们的关系如下图所示:

http 有关的函数在 package net/http 中被提供。由上图可以看出,http 的监听是对 net.Listen() 的封装,而 net.Listen() 是对 TCP 和 Unix 的封装。而 http 的监听在调用 net.Listen() 传入的都是 tcp。即 http 的监听最终都是实现的 net.ListenTcp()。
当我们调用 net.DialTCP() 时,实际上 net 包内部会去调用 doDialTCP();当我们调用 net.ListenTCP() 时,实际上 net 包内部会去调用 listenTCP()。下面我们对比一下这两个内部函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, "listen", sl.ListenConfig.Control)
if err != nil {
return nil, err
}
return &TCPListener{fd}, nil
}
func (sd *sysDialer) doDialTCP(ctx context.Context, laddr, raddr *TCPAddr) (*TCPConn, error) {
fd, err := internetSocket(ctx, sd.network, laddr, raddr, syscall.SOCK_STREAM, 0, "dial", sd.Dialer.Control)
for i := 0; i < 2 && (laddr == nil || laddr.Port == 0) && (selfConnect(fd, err) || spuriousENOTAVAIL(err)); i++ {
if err == nil {
fd.Close()
}
fd, err = internetSocket(ctx, sd.network, laddr, raddr, syscall.SOCK_STREAM, 0, "dial", sd.Dialer.Control)
}
if err != nil {
return nil, err
}
return newTCPConn(fd), nil
}
实际上它们都调用了 internetSocket(),传入的倒数第二个参数指示了创建的 socket 是客户端的还是服务器的。在服务端创建完 socket 之后,会直接使用 socket 的文件句柄创建一个 TCPListener 结构;而在客户端创建玩 socket 之后,会调用 newTCPConn(td) 使用文件句柄来实例化一个 TCPConn 结构。
同样地,当我们调用 net.DialUDP() 时,实际上 net 包内部会去调用 doDialUDP();当我们调用 net.ListenUDP() 时,实际上 net 包内部会去调用 listenUDP()。下面我们对比一下这两个内部函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15func (sl *sysListener) listenUDP(ctx context.Context, laddr *UDPAddr) (*UDPConn, error) {
fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_DGRAM, 0, "listen", sl.ListenConfig.Control)
if err != nil {
return nil, err
}
return newUDPConn(fd), nil
}
func (sd *sysDialer) dialUDP(ctx context.Context, laddr, raddr *UDPAddr) (*UDPConn, error) {
fd, err := internetSocket(ctx, sd.network, laddr, raddr, syscall.SOCK_DGRAM, 0, "dial", sd.Dialer.Control)
if err != nil {
return nil, err
}
return newUDPConn(fd), nil
}
我们会发现,相比于 TCP,UDP 在服务器和客户端两边的 socket 创建和结构体初始化逻辑是完全一样的。
下面的这段代码展示了基本的 TCP Server 的初始化流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35func chkError(err error) {
if err != nil {
log.Fatal(err);
}
}
//单独处理客户端的请求
func clientHandle(conn net.Conn) {
defer conn.Close();
conn.Write([]byte("hello " + time.Now().String()));
}
func main() {
//创建一个TCP服务端
tcpaddr, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:8080");
chkError(err);
//监听端口
tcplisten, err2 := net.ListenTCP("tcp", tcpaddr);
chkError(err2);
//死循环的处理客户端请求
for {
//等待客户的连接
conn, err3 := tcplisten.Accept();
//如果有错误直接跳过
if err3 != nil {
continue;
}
//通过goroutine来处理用户的请求
go clientHandle(conn);
}
}
下面的这段代码展示了基本的 HTTP Server 的初始化流程:
1
2
3
4
5
6
7
8
9
10
11// 普通 HTTP Server
http.HandleFunc("/", HelloServer)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
// TLS HTTP Server
http.HandleFunc("/", handler)
http.ListenAndServeTLS(":8081", "server.crt",
"server.key", nil)
我们在 package net/http 中将看到和 HTTP 服务器有关的实现细节。
3. package net/http
3.1 关键数据结构
在 Golang 中,通过 net/http 标准库, 我们可以快速实现一个 HTTP 服务器,然后让这个服务器接受请求并返回响应。
packge net/http 中关键的类型如下所示:
在后面的几个小节中,我们将看到这些类型的大部分细节。这张图片作为地图使用,读者被绕晕的时候可以回来寻找所处的位置。
3.2 HTTP 请求和响应
首先我们来理清楚与 HTTP 请求和响应处理相关的结构/类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50type Request struct {
Method string // HTTP 方法 (GET, POST, PUT, etc.)
URL *url.URL // HTTP 请求中的 URL
// HTTP 版本
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
// 用于描述 HTTP Request Header,是一个 map[string][]string 的结构
// Header = map[string][]string{
// "Accept-Encoding": {"gzip, deflate"},
// "Accept-Language": {"en-us"},
// "Foo": {"Bar", "two"},
// }
Header Header
// 用于描述 HTTP Request body
Body io.ReadCloser
// 用于获取 HTTP Request body 的一份拷贝
GetBody func() (io.ReadCloser, error)
// 用于获取 HTTP Request body 的长度
ContentLength int64
TransferEncoding []string
// 用于指示在回复完该 HTTP Request 后是否关闭 TCP 连接
Close bool
// HTTP Reqeust Header 中的 Host 字段
Host string
...
// 记录发送 HTTP Request 的主机地址,通常用于记录日志
RemoteAddr string
// RFC 规定的 HTTP Header 中的一个字段
RequestURI string
...
// Response 用于重定向时使用
Response *Response
...
ctx context.Context
}
第一个是 http.Request,它是一个结构体,如上所示。可以看到它用于描述服务器收到的 HTTP Request,具体字段的含义已经在上面的注释中给出,这里不再赘述。
1
2
3
4
5
6
7type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
然后是 ResponseWriter,它是一个接口,用于 HTTP 服务端构建 HTTP Response 使用。Header 用于返回当前正在处理的 Reponse 的头部。Reponse 头部和上面看到的 Request 头部的格式是一样的,都是一个 map[string][]string 的结构;WriteHeader 用于向 HTTP Response Header 中添加 HTTP 状态码;Write 用于向 HTTP Response 中添加正文内容。
以上这两种类型分别对应 HTTP 服务器的输入和输出的形态,是整个 HTTP 处理流程的基础。基于它们,我们下面可以开始来看 Golang 标准库是如何处理 HTTP 请求的。
我们首先展示整个调用堆栈,如下图所示,我们在后续的所有描述都围绕这张图展开:
再次说明,这张图片作为地图使用,读者被绕晕的时候可以回来寻找所处的位置。
3.3 路由注册实现细节
我们在本文中将讲解调用堆栈图中背景为黄色的部分。
这里我们来看我们是如何实现 HTTP 路由注册的。所谓路由注册就是 将 URL 和相应的处理程序绑定起来。下面我们首先来看 Golang 中对于这里的 "处理程序" 的定义。
1
2
3type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
来看 Handler,它是一个接口,我们最终在实现某个 API 上的业务逻辑的时候,所编写的函数就需要去满足这个接口,即实现 ServeHTTP(ResponseWriter, *Request) 函数,其中,正如我们在上面描述过的,参数 ResponseWriter 是一个接口,表示服务端的输出,Request 表示来自客户端的请求。
在 package net/http 中,有一个名为 ServeMux 的结构体,它实现了 Handler 接口,也就是说 ServeMux 实现了 ServeHTTP(ResponseWriter, *Request) 函数。ServeMux 是一个特殊的 "处理程序",它的功能是可以根据 URL 来判断应该把请求转发到哪个 Handler,这里的 Handler 也是 http.Handler,而 ServeMux 本身也是 http.Handler。也就是说 Golang 使用 http.Handler 来管理 http.Handler,这是一个非常优雅的设计。
下面是 ServeMux.ServeHTTP(ResponseWriter, *Request) 的源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
可以看到,在 ServeMux.ServeHTTP 中实际上是调用了 ServeMux.Handler 来进行重定向,ServeMux.Handler 的函数签名如下:
1
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
它实现的内部逻辑是去 ServeMux 结构体的成员 m 中寻找有没有对应 pattern 的 Handler,有的话就返回相应的 Handler,没有的话返回一个回送 404 error 的 Handler。这里提到的 ServeMux 结构体的成员 m 是一个 map[string]muxEntry,用于存储 Handler 和 pattern 的映射关系。
以上就是 ServeMux 结构提供的重定向功能的内部细节。
那么,Handler 是如何被注册到 ServeMux 结构体中去的呢?在 package net/http 中实际上初始化了一个 ServeMux 的实例 defaultServeMux,并以 DefaultServeMux 的名称将其导出。我们在创建 HTTP Server 的时候,会调用 package net/http 提供的方法 http.HandleFunc(pattern string, handler func(ResponseWriter, *Request)) 来注册 Handler,实际上 http.HandleFunc 内部调用的是 DefaultServeMux.HandleFunc(pattern string, handler func(ResponseWriter, *Request))。由于 http.HandleFunc(pattern string, handler func(ResponseWriter, *Request)) 接收的是一个匿名函数,它会在内部将其转化为 "实现了 Handle 接口的函数对象",然后再调用 DefaultServeMux.Handle(pattern string, handler Handler) 方法来将 Handler 注册到我们上文讲过的 ServeMux 结构体的成员 m 中。具体代码如下所示:
1
2
3func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
okay,现在我们有了一个 ServerMux 的实例 DefaultServeMux,它下面的 ServeMux.ServeHTTP(ResponseWriter, *Request) 函数可以用来帮助我们重定向 HTTP 请求。那么 HTTP 请求是怎么被引导到这个 Handler 去的呢?下面我们来看看 HTTP Server 是如何被创建并且处理 HTTP 请求的。
3.4 HTTP 请求处理实现细节
我们在本文中将讲解调用堆栈图中背景为灰色的部分。
为了创建一个 HTTP Server,我们需要调用 http.ListenAndServe(addr string, handler Handler) 函数,函数定义如下:
1
2
3
4func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
这个函数会创建一个 http.Server 结构体,它可以用于描述我们创建的 HTTP Server 的基本信息,其定义如下:
1
2
3
4
5
6
7
8
9
10type Server struct {
// 用于指示 HTTP Server 所使用的 TCP 地址,默认为 ":http" (port 80)
Addr string
// 相应 HTTP 请求的 handler,默认为 http.DefaultServeMux
Handler Handler
...
}
注意到,当我们在创建 http.Server 的时候,如果没有指定 http.Server.Handler 成员的值的话,那么默认将为 http.DefaultServeMux,这一点我们在后面将会看到具体的实现方式。因此,我们基本不会去手动设置 http.Server.Handler 的值,因为我们想创建一个具有多路复用功能的 HTTP Server。
回到 http.ListenAndServe(addr string, handler Handler) 函数,我们注意到它调用了刚创建好的 http.Server 结构体 的 server.ListenAndServe() 函数,我们来看它的定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
可以看到,在 server.ListenAndServe() 中,我们创建了一个 TCP 服务端的 socket 并且启动了监听。socket 创建和初始化完成之后,我们把 socket 对应的文件句柄 ln 传给了 func (srv *Server) Serve(l net.Listener) 函数,我们来看它的定义:
1
2
3
4
5
6
7
8
9
10
11
12
13func (srv *Server) Serve(l net.Listener) error {
...
for {
rw, err := l.Accept()
...
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
}
实际上这里面就是在实现 HTTP Server 的外层逻辑:接收请求,使用 http.Server.newConn 函数将 net.Conn 封装为 http.conn,然后创建处理请求的 goroutine。这里的 http.conn 是一个 package net/http 的内部结构体,实现了对 net.Conn 的包装,在承载 net.Conn 的基础上,增加了一些属性用于描述 HTTP 连接。我们可以看到上面代码的最后,goroutine 起的是 http.conn 下的方法 http.conn.serve(ctx context.Context),它的定义大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
13func (c *conn) serve(ctx context.Context) {
...
for{
...
w, err := c.readRequest(ctx)
serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
...
}
...
}
可以看见它将我们的 http.Server 结构体封装为了 http.serverHandler,然后调用了 http.serverHandler 下的方法 http.ServeHTTP(rw ResponseWriter, req *Request) 进行对 HTTP 请求的处理。代码如下:
1
2
3
4
5
6
7
8
9
10func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
我们看到它回去判断我们的 http.Server 结构体有没有绑定 Handler,如果没有的话,就指定 DefaultServeMux 为我们服务。如果有的话,就使用指定的 Handler。这与我们上面所描述的是一致的。
这样一来,HTTP Server 处理 HTTP 请求的这条路我们也理清楚了。
3.5 实践:搭建 HTTP 服务器
讲了这么多底层的东西,当我们回过头来尝试去使用 Golang 标准库留给我们的用户接口的时候,一切就很简单了。
我们现在创建一个指定有 Handler 的 HTTP 服务器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package main
import (
"net/http"
"fmt"
)
type WorldHandler struct {
}
func (h *WorldHandler) (w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "World!n")
}
func main() {
world := WorldHandler{}
http.ListenAndServe(": 8080", &world)
}
我们现在对它发起 HTTP 请求。可以发现,不论我们的 URL 长什么样子,HTTP 都是 invoke 我们指定的那个 Handler:
1
2
3
4
5
6curlsdeMacBook-Pro:src curls$ curl localhost:8080
World!
curlsdeMacBook-Pro:src curls$ curl localhost:8080/hello
World!
curlsdeMacBook-Pro:src curls$ curl localhost:8080/welcom
World!
下面我们使用多路复用器的形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package main
import (
"net/http"
"fmt"
)
func handler(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, "Hello, %s!n", request.URL.Path[1:])
}
type WorldHandler struct {
}
func (h *WorldHandler) (w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "World!n")
}
func main() {
world := &WorldHandler{}
http.HandleFunc("/", handler)
http.Handle("/world", world)
http.ListenAndServe(": 8080", nil)
}
注意,我们这里使用了两种方法来注册路由:http.HandleFunc 和 http.Handle。回顾我们上文所讲的,http.HandleFunc 传入的是一个匿名函数,http.HandleFunc 内部会将其转化为 "实现了 Handle 接口的函数对象" 后,再调用 http.Handle。
我们可以看到多路服用的效果如下所示:
1
2
3
4
5
6
7
8curlsdeMacBook-Pro:src curls$ curl localhost:8080/world
World!
curlsdeMacBook-Pro:src curls$ curl localhost:8080/welcome
Hello, welcome!
curlsdeMacBook-Pro:src curls$ curl localhost:8080/curls
Hello, curls!
附录:参考源
- segmentfault, Golang net 包学习和实战
- CSDN, AF_INET 域与 AF_UNIX 域 socket 通信原理对比
- IBM, socket() — Create a socket
- 博客园, Linux下PF_PACKET的使用
- CSDN, net包 dial - golang
- CSDN, net包 listen - golang
- 曝晒三十分, 深入学习golang中的net/http包