TCP服务器

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net"
  6. )
  7. func main() {
  8. // net 包提供方便的工具用于 network I/O 开发,包括TCP/IP, UDP 协议等。
  9. // Listen 函数会监听来自 8080 端口的连接,返回一个 net.Listener 对象。
  10. li, err := net.Listen("tcp", ":8080")
  11. // 错误处理
  12. if err != nil {
  13. log.Panic(err)
  14. }
  15. // 释放连接,通过 defer 关键字可以让连接在函数结束前进行释放
  16. // 这样可以不关心释放资源的语句位置,增加代码可读性
  17. defer li.Close()
  18. // 不断循环,不断接收来自客户端的请求
  19. for {
  20. // Accept 函数会阻塞程序,直到接收到来自端口的连接
  21. // 每接收到一个链接,就会返回一个 net.Conn 对象表示这个连接
  22. conn, err := li.Accept()
  23. if err != nil {
  24. log.Println(err)
  25. }
  26. // 字符串写入到客户端
  27. fmt.Fprintln(conn, "Hello from TCP server")
  28. conn.Close()
  29. }
  30. }

在对应的文件夹下启动服务器

  1. $ go run main.go

模拟客户端程序发出的请求,这里使用netcat工具,也就是nc命令。

  1. $ nc localhost 8080
  2. Hello from TCP server


通过net包,我们可以很简单的去写一个TCP服务器,代码可读性强。

TCP 客户端

  1. package main
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "log"
  6. "net"
  7. )
  8. func main() {
  9. // net 包的 Dial 函数能创建一个 TCP 连接
  10. conn, err := net.Dial("tcp", ":8080")
  11. if err != nil {
  12. log.Fatal(err)
  13. }
  14. // 别忘了关闭连接
  15. defer conn.Close()
  16. // 通过 ioutil 来读取连接中的内容,返回一个 []byte 类型的对象
  17. byte, err := ioutil.ReadAll(conn)
  18. if err != nil {
  19. log.Println(err)
  20. }
  21. // []byte 类型的数据转成字符串型,再将其打印输出
  22. fmt.Println(string(byte))
  23. }

运行服务器后,再在所在的文件夹下启动客户端,会看到来自服务器的问候。

  1. $ go run main.go
  2. Hello from TCP server

TCP 协议模拟 HTTP 请求

我们知道TCP/IP协议是传输层协议,主要解决的是数据如何在网络中传输。而HTTP是应用层协议,主要解决的是如何包装这些数据。

下面的七层网络协议图也能看到HTTP协议是处于TCP的上层,也就是说,HTTP使用TCP来传输其报文数据。
Golang 网络编程 - 图1
现在我们写一个基于TCP协议的服务器,并能模拟。在这其中,我们需要模拟发送HTTP响应头信息,我们可以使用curl -i命令先来查看一下其他网络的响应头信息。

  1. $ curl -i "www.baidu.com"
  2. HTTP/1.1 200 OK # HTTP 协议及请求码
  3. Server: bfe/1.0.8.18 # 服务器使用的WEB软件名及版本
  4. Date: Sat, 29 Apr 2017 07:30:33 GMT # 发送时间
  5. Content-Type: text/html # MIME类型
  6. Content-Length: 277 # 内容长度
  7. Last-Modified: Mon, 13 Jun 2016 02:50:23 GMT
  8. ... # balabala
  9. Accept-Ranges: bytes
  10. <!DOCTYPE html> # 消息体
  11. <!--STATUS OK--><html>
  12. ...
  13. </body> </html>

接下来,我们尝试写出能输出对应格式响应内容的服务器。

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net"
  6. )
  7. func main() {
  8. li, err := net.Listen("tcp", ":8080")
  9. if err != nil {
  10. log.Fatalln(err.Error())
  11. }
  12. defer li.Close()
  13. for {
  14. conn, err := li.Accept()
  15. if err != nil {
  16. log.Fatalln(err.Error())
  17. continue
  18. }
  19. // 函数前添加 go 关键字,就能使其拥有 Go 语言的并发功能
  20. // 这样我们可以同时处理来自不同客户端的请求
  21. go handle(conn)
  22. }
  23. }
  24. func handle(conn net.Conn) {
  25. defer conn.Close()
  26. // 回应客户端的请求
  27. respond(conn)
  28. }
  29. func respond(conn net.Conn) {
  30. // 消息体
  31. body := `<!DOCTYPE html><html lang="en"><head><meta charet="UTF-8"><title>Go example</title></head><body><strong>Hello World</strong></body></html>`
  32. // HTTP 协议及请求码
  33. fmt.Fprint(conn, "HTTP/1.1 200 OK\r\n")
  34. // 内容长度
  35. fmt.Fprintf(conn, "Content-Length: %d\r\n", len(body))
  36. // MIME类型
  37. fmt.Fprint(conn, "Content-Type: text/html\r\n")
  38. fmt.Fprint(conn, "\r\n")
  39. fmt.Fprint(conn, body)
  40. }

go run main.go 启动服务器之后,跳转到 localhost:8080,就能看到网页内容,并且用开发者工具能看到其请求头。
Golang 网络编程 - 图2

最简单的HTTP服务器

  1. package main
  2. import "net/http"
  3. func main() {
  4. http.ListenAndServe(":8080", nil)
  5. }

ListenAndServe

Go 是通过一个函数 ListenAndServe 来处理这些事情的,这个底层其实这样处理的:初始化一个server 对象,然后调用了 net.Listen(“tcp”, addr),也就是底层用 TCP 协议搭建了一个服务,然后监控我们设置的端口。

http 包下的 ListenAndServe 函数第一个参数是地址,而第二个是 Handler 类型的参数,我们想要显示内容就要在第二个参数下功夫。

  1. func ListenAndServe(addr string, handler Handler) error
  1. Handler 是一个接口,也就是说只要我们给某一个类型创建 ServeHTTP(ResponseWriter, *Request) 方法,就能符合接口的要求,也就实现了接口。
  1. type Handler interface {
  2. ServeHTTP(ResponseWriter, *Request)
  3. }
  4. package main
  5. import (
  6. "fmt"
  7. "net/http"
  8. )
  9. // 创建一个 foo 类型
  10. type foo struct {}
  11. // 为 foo 类型创建 ServeHTTP 方法,以实现 Handle 接口
  12. func (f foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  13. fmt.Fprintln(w, "Implement the Handle interface.")
  14. }
  15. func main() {
  16. // 创建对象,类型名写后面..
  17. var f foo
  18. http.ListenAndServe(":8080",f)
  19. }

http.Request

上面我们实现的小服务器里,我们无论访问 localhost:8080 还是 localhost:8080/foo 都是一样的页面,这说明我们之前设定的是默认的页面,还没有为特定的路由(route)设置内容。

路由这些信息实际上就存在 ServeHTTP 函数的第二个参数 http.Request 中, http.Request 存放着客户端发送至服务器的请求信息,例如请求链接、请求方法、响应头、消息体等等。

现在我们可以把上面的代码改造一下。

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. // 创建一个 foo 类型
  7. type foo struct {}
  8. // 为 foo 类型创建 ServeHTTP 方法,以实现 Handle 接口
  9. func (f foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  10. // 根据 URL 的相对路径来设置网页内容(不优雅)
  11. switch r.URL.Path {
  12. case "/boy":
  13. fmt.Fprintln(w, "I love you!!!")
  14. case "/girl":
  15. fmt.Fprintln(w, "hehe.")
  16. default:
  17. fmt.Fprintln(w, "Men would stop talking and women would shed tears when they see this.")
  18. }
  19. func main() {
  20. // 创建对象,类型名写后面..
  21. var f foo
  22. http.ListenAndServe(":8080",f)
  23. }

我们可以用 HTTP 请求多路复用器(HTTP request multiplexer) 来实现分发路由,而http.NewServeMux() 返回的 ServeMux 对象就能实现这样的功能。下面是 ServeMux 的部分源码,能看到通过 *ServeMux 就能为每一个路由设置单独的一个 handler 了,简单地说就是不同的内容。

  1. type ServeMux struct {
  2. mu sync.RWMutex // 读写锁
  3. m map[string]muxEntry // 路由信息(键值对)
  4. hosts bool // 是否包含 hostnames
  5. }
  6. type muxEntry struct {
  7. explicit bool // 是否精确匹配
  8. h Handler // muxEntry.Handler 是接口
  9. pattern string // 路由
  10. }
  11. type Handler interface {
  12. ServeHTTP(ResponseWriter, *Request)
  13. }

用 *ServeMux 来写一个例子。

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. type boy struct{}
  7. func (b boy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  8. fmt.Fprintln(w, "I love you!!!")
  9. }
  10. type girl struct{}
  11. func (g girl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  12. fmt.Fprintln(w, "hehe.")
  13. }
  14. type foo struct{}
  15. func (f foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  16. fmt.Fprintln(w, "Men would stop talking and women would shed tears when they see this.")
  17. }
  18. func main() {
  19. var b boy
  20. var g girl
  21. var f foo
  22. // 返回一个 *ServeMux 对象
  23. mux := http.NewServeMux()
  24. mux.Handle("/boy/", b)
  25. mux.Handle("/girl/", g)
  26. mux.Handle("/", f)
  27. http.ListenAndServe(":8080", mux)
  28. }

http.Handle(pattern string, handler Handler) 还能帮我们简化代码,它默认创建一个 DefaultServeMux,也就是默认的 ServeMux 来存 handler 信息,这样就不需要 http.NewServeMux() 函数了。这看起来虽然没有什么少写多少代码,但是这是下一个更加优雅方法的转折点。

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. type boy struct{}
  7. func (b boy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  8. fmt.Fprintln(w, "I love you!!!")
  9. }
  10. type girl struct{}
  11. func (g girl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  12. fmt.Fprintln(w, "hehe.")
  13. }
  14. type foo struct{}
  15. func (f foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  16. fmt.Fprintln(w, "Men would stop talking and women would shed tears when they see this.")
  17. }
  18. func main() {
  19. var b boy
  20. var g girl
  21. var f foo
  22. http.Handle("/boy/", b)
  23. http.Handle("/girl/", g)
  24. http.Handle("/", f)
  25. http.ListenAndServe(":8080", nil)
  26. }

http.HandleFunc(pattern string, handler func(ResponseWriter, *Request)) 可以看做 http.Handle(pattern string, handler Handler) 的一种包装。前者的第二个参数变成了一个函数,这样我们就不用多次新建对象,再为对象实现 ServeHTTP() 方法来实现不同的 handler 了。下面是 http.HandleFun() 的部分源码。

  1. func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  2. // 同样利用 DefaultServeMux 来存路由信息
  3. DefaultServeMux.HandleFunc(pattern, handler)
  4. }
  5. func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  6. // 是不是似曾相识?
  7. mux.Handle(pattern, HandlerFunc(handler))
  8. }

用 http.HandleFun() 来重写之前的例子。

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. func boy(w http.ResponseWriter, r *http.Request) {
  7. fmt.Fprintln(w, "I love you!!!")
  8. }
  9. func girl(w http.ResponseWriter, r *http.Request) {
  10. fmt.Fprintln(w, "hehe.")
  11. }
  12. func foo(w http.ResponseWriter, r *http.Request) {
  13. fmt.Fprintln(w, "Men would stop talking and women would shed tears when they see this.")
  14. }
  15. func main() {
  16. http.HandleFunc("/boy/", boy)
  17. http.HandleFunc("/girl/", girl)
  18. http.HandleFunc("/", foo)
  19. http.ListenAndServe(":8080", nil)
  20. }

HandlerFunc

另外,http 包里面还定义了一个类型 http.HandlerFunc,该类型默认实现 Handler 接口,我们可以通过 HandlerFunc(foo) 的方式来实现类型强转,使 foo 也实现了 Handler 接口。

  1. type HandlerFunc func(ResponseWriter, *Request)
  2. // 实现 Handler 接口
  3. func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  4. f(w, r)
  5. }
  6. package main
  7. import (
  8. "fmt"
  9. "net/http"
  10. )
  11. func boy(w http.ResponseWriter, r *http.Request) {
  12. fmt.Fprintln(w, "I love you!!!")
  13. }
  14. func girl(w http.ResponseWriter, r *http.Request) {
  15. fmt.Fprintln(w, "hehe.")
  16. }
  17. func foo(w http.ResponseWriter, r *http.Request) {
  18. fmt.Fprintln(w, "Men would stop talking and women would shed tears when they see this.")
  19. }
  20. func main() {
  21. // http.Handler() 的第二个参数是要实现了 Handler 接口的类型
  22. // 可以通过类型强转来重新使用该函数来实现
  23. http.Handle("/boy/", http.HandlerFunc(boy))
  24. http.Handle("/girl/", http.HandlerFunc(girl))
  25. http.Handle("/", http.HandlerFunc(foo))
  26. http.ListenAndServe(":8080", nil)
  27. }

Golang 网络编程 - 图3