Go语言的网络编程简介

本文通过 Go 语言写几个简单的通信示例,从 TCP 服务器过渡到 HTTP 开发,从而简单介绍 net 包的运用。

TCP 服务器

首先来看一个 TCP 服务器例子

package main

import (
    "fmt"
    "log"
    "net"
)

func main() {
    // net 包提供方便的工具用于 network I/O 开发,包括TCP/IP, UDP 协议等。
    // Listen 函数会监听来自 8080 端口的连接,返回一个 net.Listener 对象。
    li, err := net.Listen("tcp", ":8080")
    // 错误处理
    if err != nil {
        log.Panic(err)
    }
    // 释放连接,通过 defer 关键字可以让连接在函数结束前进行释放
    // 这样可以不关心释放资源的语句位置,增加代码可读性
    defer li.Close()

    // 不断循环,不断接收来自客户端的请求
    for {
        // Accept 函数会阻塞程序,直到接收到来自端口的连接
        // 每接收到一个链接,就会返回一个 net.Conn 对象表示这个连接
        conn, err := li.Accept()

        if err != nil {
            log.Println(err)
        }
        // 字符串写入到客户端
        fmt.Fprintln(conn, "Hello from TCP server")

        conn.Close()
    }
}

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

$ go run main.go

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

$ nc localhost 8080
Hello from TCP server

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

TCP 客户端

那么我们能不能用 Go 语言来模拟客户端,从而连接前面的服务器呢?答案是肯定的。

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net"
)

func main() {
    // net 包的 Dial 函数能创建一个 TCP 连接
    conn, err := net.Dial("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }
    // 别忘了关闭连接
    defer conn.Close()
    // 通过 ioutil 来读取连接中的内容,返回一个 []byte 类型的对象
    byte, err := ioutil.ReadAll(conn)
    if err != nil {
        log.Println(err)
    }
    // []byte 类型的数据转成字符串型,再将其打印输出
    fmt.Println(string(byte))
}

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

$ go run main.go
Hello from TCP server

TCP 协议模拟 HTTP 请求

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

下面的七层网络协议图也能看到 HTTP 协议是处于 TCP 的上层,也就是说,HTTP 使用 TCP 来传输其报文数据。

七层网络协议图

现在我们写一个基于 TCP 协议的服务器,并能模拟。在这其中,我们需要模拟发送 HTTP 响应头信息,我们可以用 curl -i 命令先来查看一下其他网站的响应头信息。

$ curl -i "www.baidu.com"
HTTP/1.1 200 OK  # HTTP 协议及请求码
Server: bfe/1.0.8.18    # 服务器使用的WEB软件名及版本
Date: Sat, 29 Apr 2017 07:30:33 GMT  # 发送时间
Content-Type: text/html   # MIME类型
Content-Length: 277            # 内容长度
Last-Modified: Mon, 13 Jun 2016 02:50:23 GMT
...  # balabala
Accept-Ranges: bytes

<!DOCTYPE html>  # 消息体
<!--STATUS OK--><html>
...
</body> </html>

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

package main

import (
    "fmt"
    "log"
    "net"
)

func main() {
    li, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatalln(err.Error())
    }
    defer li.Close()

    for {
        conn, err := li.Accept()
        if err != nil {
            log.Fatalln(err.Error())
            continue
        }
        // 函数前添加 go 关键字,就能使其拥有 Go 语言的并发功能
        // 这样我们可以同时处理来自不同客户端的请求
        go handle(conn)
    }
}

func handle(conn net.Conn) {
    defer conn.Close()
    // 回应客户端的请求
    respond(conn)
}

func respond(conn net.Conn) {
    // 消息体
    body := `<!DOCTYPE html><html lang="en"><head><meta charet="UTF-8"><title>Go example</title></head><body><strong>Hello World</strong></body></html>`
    // HTTP 协议及请求码
    fmt.Fprint(conn, "HTTP/1.1 200 OK\r\n")
    // 内容长度
    fmt.Fprintf(conn, "Content-Length: %d\r\n", len(body)) 
    // MIME类型
    fmt.Fprint(conn, "Content-Type: text/html\r\n")
    fmt.Fprint(conn, "\r\n")
    fmt.Fprint(conn, body)
}

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

最简单的 HTTP 服务器

几行代码就能实现一个最简单的 HTTP 服务器。

package main

import "net/http"

func main() {
    http.ListenAndServe(":8080", nil)
}

打开后会发现显示「404 page not found」,这说明 HTTP 已经开始服务了!

ListenAndServe

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

前面我们已经对 TCP 服务器有点熟悉了,而 HTTP 使用 TCP 来传输其报文数据,接下来看看如何用 net/http 包来实现在其上的 HTTP 层。

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

func ListenAndServe(addr string, handler Handler) error

再次查文档,得知 Handler 是一个接口,也就是说只要我们给某一个类型创建 ServeHTTP(ResponseWriter, *Request) 方法,就能符合接口的要求,也就实现了接口。

type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}
package main

import (
    "fmt"
    "net/http"
)
// 创建一个 foo 类型
type foo struct {}
// 为 foo 类型创建 ServeHTTP 方法,以实现 Handle 接口
func (f foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Implement the Handle interface.")
}

func main() {
    // 创建对象,类型名写后面..
    var f foo
    http.ListenAndServe(":8080",f)
}

运行代码后打开能看到输出的字符串。

*http.Request

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

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

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

package main

import (
    "fmt"
    "net/http"
)
// 创建一个 foo 类型
type foo struct {}
// 为 foo 类型创建 ServeHTTP 方法,以实现 Handle 接口
func (f foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 根据 URL 的相对路径来设置网页内容(不优雅)
    switch r.URL.Path {
    case "/boy":
        fmt.Fprintln(w, "I love you!!!")
    case "/girl":
        fmt.Fprintln(w, "hehe.")
    default:
        fmt.Fprintln(w, "Men would stop talking and women would shed tears when they see this.")
}

func main() {
    // 创建对象,类型名写后面..
    var f foo
    http.ListenAndServe(":8080",f)
}

再优雅一点

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

type ServeMux struct {
    mu    sync.RWMutex         // 读写锁
    m     map[string]muxEntry  // 路由信息(键值对)
    hosts bool                 // 是否包含 hostnames
}

type muxEntry struct {
    explicit bool     // 是否精确匹配
    h        Handler  // muxEntry.Handler 是接口
    pattern  string   // 路由
}

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

*ServeMux 来写一个例子。

package main

import (
    "fmt"
    "net/http"
)

type boy struct{}

func (b boy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "I love you!!!")
}

type girl struct{}

func (g girl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hehe.")
}

type foo struct{}

func (f foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Men would stop talking and women would shed tears when they see this.")
}

func main() {
    var b boy
    var g girl
    var f foo

    // 返回一个 *ServeMux 对象
    mux := http.NewServeMux()  
    mux.Handle("/boy/", b)
    mux.Handle("/girl/", g)
    mux.Handle("/", f)
    http.ListenAndServe(":8080", mux)
}

这样就能为每一个路由设置单独的页面了。

再再优雅一点

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

package main

import (
    "fmt"
    "net/http"
)

type boy struct{}

func (b boy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "I love you!!!")
}

type girl struct{}

func (g girl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hehe.")
}

type foo struct{}

func (f foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Men would stop talking and women would shed tears when they see this.")
}

func main() {
    var b boy
    var g girl
    var f foo

    http.Handle("/boy/", b)
    http.Handle("/girl/", g)
    http.Handle("/", f)
    http.ListenAndServe(":8080", nil)
}

再再再优雅一点

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

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    // 同样利用 DefaultServeMux 来存路由信息
    DefaultServeMux.HandleFunc(pattern, handler)
}

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    // 是不是似曾相识?
    mux.Handle(pattern, HandlerFunc(handler))
}

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

package main

import (
    "fmt"
    "net/http"
)

func boy(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "I love you!!!")
}

func girl(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hehe.")
}

func foo(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Men would stop talking and women would shed tears when they see this.")
}

func main() {
    http.HandleFunc("/boy/", boy)
    http.HandleFunc("/girl/", girl)
    http.HandleFunc("/", foo)
    http.ListenAndServe(":8080", nil)
}

HandlerFunc

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

type HandlerFunc func(ResponseWriter, *Request)

// 实现 Handler 接口
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
package main

import (
    "fmt"
    "net/http"
)

func boy(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "I love you!!!")
}

func girl(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hehe.")
}

func foo(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Men would stop talking and women would shed tears when they see this.")
}

func main() {
    // http.Handler() 的第二个参数是要实现了 Handler 接口的类型
    // 可以通过类型强转来重新使用该函数来实现
    http.Handle("/boy/", http.HandlerFunc(boy))
    http.Handle("/girl/", http.HandlerFunc(girl))
    http.Handle("/", http.HandlerFunc(foo))
    http.ListenAndServe(":8080", nil)
}

结尾

本文从搭建 TCP 服务器一步步到搭建 HTTP 服务器,展示了 Go 语言网络库的强大,我认为 Go 语言是熟悉网络协议的一个很好的工具。自己从熟悉了拥有各种 feature 的 Swift 语言之后再入门到看似平凡无奇的 Go 语言,经历了从为语言的平庸感到惊讶不解到为其遵循规范和良好的工业语言设计而感到惊叹和兴奋的转变。

最后希望本文能为有基础的同学理清思路,也能吸引更多同学来学习这门优秀的语言。

原文发布于微信公众号 - Golang语言社区(Golangweb)

原文发表时间:2017-10-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

go http 服务器编程(1)

1. 初识 http 是典型的 C/S 架构,客户端向服务端发送请求(request),服务端做出应答(response)。 golang 的标准库 net/h...

2.8K70
来自专栏风中追风

分布式基础__TCP IP 里的网络请求 到底是什么样的?

当我输入 一个url 在浏览器的地址栏按下回车 这个以前也写过博客了:一次URL输入域名按下回车到底发生了什么?

412160
来自专栏Golang语言社区

Go语言的网络编程简介

本文通过 Go 语言写几个简单的通信示例,从 TCP 服务器过渡到 HTTP 开发,从而简单介绍 net 包的运用。 TCP 服务器 首先来看一个 TCP 服务...

47070
来自专栏技术博客

MVC项目开发中那些用到的知识点(Ajax.BeginForm)

 AjaxFormPost为Action,Home为控制器,new {ID=“11”,ClassName="FirstClass"}为路由参数即Url参数

10920
来自专栏Java3y

【Java】留下没有基础眼泪的面试题

使用多线程时,不是多线程能提升程序的执行速度,使用多线程是为了更好地利用CPU资源!

18920
来自专栏lgp20151222

Git 的 .gitignore 配置

.gitignore 配置文件用于配置不需要加入版本管理的文件,配置好该文件可以为我们的版本管理带来很大的便利,以下是个人对于配置 .gitignore 的一些...

11430
来自专栏nummy

Grunt快速入门

Grunt是基于JavaScript的命令行构建工具,它可以帮助开发者们自动化重复性的工作。你可以把它看成是JavaScript下的Make或者Ant。它可以完...

9520
来自专栏Java与Android技术栈

android studio加速编译

1 修改项目的gradle.properties文件 将#org.gradle.parallel=true 去掉注释,增加org.gradle.daemon=...

10620
来自专栏前端大白专栏

关于ant-design表单问题

34840
来自专栏Golang语言社区

【转】Go语言Http Server源码阅读

目录(?)[-] 前言 几个重要概念 具体分析 几个接口 Handler ResponseWriter Flusher Hijacker response H...

29940

扫码关注云+社区

领取腾讯云代金券