Go語言的網(wǎng)絡編程簡介

本文通過 Go 語言寫幾個簡單的通信示例暂氯,從 TCP 服務器過渡到 HTTP 開發(fā)痴施,從而簡單介紹 net 包的運用。

TCP 服務器

首先來看一個 TCP 服務器例子

package main

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

func main() {
    // net 包提供方便的工具用于 network I/O 開發(fā)动遭,包括TCP/IP, UDP 協(xié)議等厘惦。
    // Listen 函數(shù)會監(jiān)聽來自 8080 端口的連接哩簿,返回一個 net.Listener 對象节榜。
    li, err := net.Listen("tcp", ":8080")
    // 錯誤處理
    if err != nil {
        log.Panic(err)
    }
    // 釋放連接,通過 defer 關鍵字可以讓連接在函數(shù)結(jié)束前進行釋放
    // 這樣可以不關心釋放資源的語句位置缝左,增加代碼可讀性
    defer li.Close()

    // 不斷循環(huán)浓若,不斷接收來自客戶端的請求
    for {
        // Accept 函數(shù)會阻塞程序挪钓,直到接收到來自端口的連接
        // 每接收到一個鏈接,就會返回一個 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

模擬客戶端程序發(fā)出請求倚评,這里使用 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 函數(shù)能創(chuàng)建一個 TCP 連接
    conn, err := net.Dial("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }
    // 別忘了關閉連接
    defer conn.Close()
    // 通過 ioutil 來讀取連接中的內(nèi)容,返回一個 []byte 類型的對象
    byte, err := ioutil.ReadAll(conn)
    if err != nil {
        log.Println(err)
    }
    // []byte 類型的數(shù)據(jù)轉(zhuǎn)成字符串型葱弟,再將其打印輸出
    fmt.Println(string(byte))
}

運行服務器后壹店,再在所在的文件夾下啟動客戶端,會看到來自服務器的問候翘悉。

$ go run main.go
Hello from TCP server

TCP 協(xié)議模擬 HTTP 請求

我們知道 TCP/IP 協(xié)議是傳輸層協(xié)議茫打,主要解決的是數(shù)據(jù)如何在網(wǎng)絡中傳輸。而 HTTP 是應用層協(xié)議妖混,主要解決的是如何包裝這些數(shù)據(jù)。

下面的七層網(wǎng)絡協(xié)議圖也能看到 HTTP 協(xié)議是處于 TCP 的上層制市,也就是說抬旺,HTTP 使用 TCP 來傳輸其報文數(shù)據(jù)。

七層網(wǎng)絡協(xié)議圖
七層網(wǎng)絡協(xié)議圖

現(xiàn)在我們寫一個基于 TCP 協(xié)議的服務器祥楣,并能模擬开财。在這其中,我們需要模擬發(fā)送 HTTP 響應頭信息误褪,我們可以用 curl -i 命令先來查看一下其他網(wǎng)站的響應頭信息责鳍。

$ curl -i "www.baidu.com"
HTTP/1.1 200 OK  # HTTP 協(xié)議及請求碼
Server: bfe/1.0.8.18    # 服務器使用的WEB軟件名及版本
Date: Sat, 29 Apr 2017 07:30:33 GMT  # 發(fā)送時間
Content-Type: text/html   # MIME類型
Content-Length: 277         # 內(nèi)容長度
Last-Modified: Mon, 13 Jun 2016 02:50:23 GMT
...  # balabala
Accept-Ranges: bytes

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

接下來,我們嘗試寫出能輸出對應格式響應內(nèi)容的服務器兽间。

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
        }
        // 函數(shù)前添加 go 關鍵字历葛,就能使其擁有 Go 語言的并發(fā)功能
        // 這樣我們可以同時處理來自不同客戶端的請求
        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 協(xié)議及請求碼
    fmt.Fprint(conn, "HTTP/1.1 200 OK\r\n")
    // 內(nèi)容長度
    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 啟動服務器之后,跳轉(zhuǎn)到 localhost:8080嘀略,就能看到網(wǎng)頁內(nèi)容恤溶,并且用開發(fā)者工具能看到其請求頭乓诽。

最簡單的 HTTP 服務器

幾行代碼就能實現(xiàn)一個最簡單的 HTTP 服務器。

package main

import "net/http"

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

打開后會發(fā)現(xiàn)顯示「404 page not found」咒程,這說明 HTTP 已經(jīng)開始服務了鸠天!

ListenAndServe

Go 是通過一個函數(shù) ListenAndServe 來處理這些事情的,這個底層其實這樣處理的:初始化一個server 對象帐姻,然后調(diào)用了 net.Listen("tcp", addr)稠集,也就是底層用 TCP 協(xié)議搭建了一個服務,然后監(jiān)控我們設置的端口卖宠。
《Build web application with golang》, astaxie

前面我們已經(jīng)對 TCP 服務器有點熟悉了巍杈,而 HTTP 使用 TCP 來傳輸其報文數(shù)據(jù)忧饭,接下來看看如何用 net/http 包來實現(xiàn)在其上的 HTTP 層扛伍。

查文檔可以發(fā)現(xiàn) http 包下的 ListenAndServe 函數(shù)第一個參數(shù)是地址,而第二個是 Handler 類型的參數(shù)词裤,我們想要顯示內(nèi)容就要在第二個參數(shù)下功夫刺洒。

func ListenAndServe(addr string, handler Handler) error

再次查文檔,得知 Handler 是一個接口吼砂,也就是說只要我們給某一個類型創(chuàng)建 ServeHTTP(ResponseWriter, *Request) 方法逆航,就能符合接口的要求,也就實現(xiàn)了接口渔肩。

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

import (
    "fmt"
    "net/http"
)
// 創(chuàng)建一個 foo 類型
type foo struct {}
// 為 foo 類型創(chuàng)建 ServeHTTP 方法因俐,以實現(xiàn) Handle 接口
func (f foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Implement the Handle interface.")
}

func main() {
    // 創(chuàng)建對象,類型名寫后面..
    var f foo
    http.ListenAndServe(":8080",f)
}

運行代碼后打開能看到輸出的字符串周偎。

*http.Request

上面我們實現(xiàn)的小服務器里抹剩,我們無論訪問 localhost:8080 還是 localhost:8080/foo 都是一樣的頁面,這說明我們之前設定的是默認的頁面蓉坎,還沒有為特定的路由(route)設置內(nèi)容澳眷。

路由這些信息實際上就存在 ServeHTTP 函數(shù)的第二個參數(shù) *http.Request 中, *http.Request 存放著客戶端發(fā)送至服務器的請求信息蛉艾,例如請求鏈接钳踊、請求方法、響應頭勿侯、消息體等等拓瞪。

現(xiàn)在我們可以把上面的代碼改造一下。

package main

import (
    "fmt"
    "net/http"
)
// 創(chuàng)建一個 foo 類型
type foo struct {}
// 為 foo 類型創(chuàng)建 ServeHTTP 方法助琐,以實現(xiàn) Handle 接口
func (f foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 根據(jù) URL 的相對路徑來設置網(wǎng)頁內(nèi)容(不優(yōu)雅)
    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() {
    // 創(chuàng)建對象祭埂,類型名寫后面..
    var f foo
    http.ListenAndServe(":8080",f)
}

再優(yōu)雅一點

我們可以用 HTTP 請求多路復用器(HTTP request multiplexer) 來實現(xiàn)分發(fā)路由,而http.NewServeMux() 返回的 *ServeMux 對象就能實現(xiàn)這樣的功能弓柱。下面是 *ServeMux 的部分源碼沟堡,能看到通過 *ServeMux 就能為每一個路由設置單獨的一個 handler 了侧但,簡單地說就是不同的內(nèi)容。

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)
}

這樣就能為每一個路由設置單獨的頁面了禀横。

再再優(yōu)雅一點

http.Handle(pattern string, handler Handler) 還能幫我們簡化代碼,它默認創(chuàng)建一個 DefaultServeMux粥血,也就是默認的 ServeMux 來存 handler 信息柏锄,這樣就不需要 http.NewServeMux() 函數(shù)了。這看起來雖然沒有什么少寫多少代碼复亏,但是這是下一個更加優(yōu)雅方法的轉(zhuǎn)折點趾娃。

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)
}

再再再優(yōu)雅一點

http.HandleFunc(pattern string, handler func(ResponseWriter, *Request)) 可以看做 http.Handle(pattern string, handler Handler) 的一種包裝。前者的第二個參數(shù)變成了一個函數(shù)缔御,這樣我們就不用多次新建對象抬闷,再為對象實現(xiàn) ServeHTTP() 方法來實現(xiàn)不同的 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炕泳,該類型默認實現(xiàn) Handler 接口,我們可以通過 HandlerFunc(foo) 的方式來實現(xiàn)類型強轉(zhuǎn)上祈,使 foo 也實現(xiàn)了 Handler 接口培遵。

type HandlerFunc func(ResponseWriter, *Request)

// 實現(xiàn) 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() 的第二個參數(shù)是要實現(xiàn)了 Handler 接口的類型
    // 可以通過類型強轉(zhuǎn)來重新使用該函數(shù)來實現(xiàn)
    http.Handle("/boy/", http.HandlerFunc(boy))
    http.Handle("/girl/", http.HandlerFunc(girl))
    http.Handle("/", http.HandlerFunc(foo))
    http.ListenAndServe(":8080", nil)
}

結(jié)尾

本文從搭建 TCP 服務器一步步到搭建 HTTP 服務器,展示了 Go 語言網(wǎng)絡庫的強大登刺,我認為 Go 語言是熟悉網(wǎng)絡協(xié)議的一個很好的工具籽腕。自己從熟悉了擁有各種 feature 的 Swift 語言之后再入門到看似平凡無奇的 Go 語言,經(jīng)歷了從為語言的平庸感到驚訝不解到為其遵循規(guī)范和良好的工業(yè)語言設計而感到驚嘆和興奮的轉(zhuǎn)變塘砸。

最后希望本文能為有基礎的同學理清思路节仿,也能吸引更多同學來學習這門優(yōu)秀的語言。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掉蔬,一起剝皮案震驚了整個濱河市廊宪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌女轿,老刑警劉巖箭启,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蛉迹,居然都是意外死亡傅寡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來荐操,“玉大人芜抒,你說我怎么就攤上這事⊥衅簦” “怎么了宅倒?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長屯耸。 經(jīng)常有香客問我拐迁,道長,這世上最難降的妖魔是什么疗绣? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任线召,我火速辦了婚禮,結(jié)果婚禮上多矮,老公的妹妹穿的比我還像新娘缓淹。我一直安慰自己,他們只是感情好工窍,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布割卖。 她就那樣靜靜地躺著前酿,像睡著了一般患雏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上罢维,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天淹仑,我揣著相機與錄音,去河邊找鬼肺孵。 笑死匀借,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的平窘。 我是一名探鬼主播吓肋,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼瑰艘!你這毒婦竟也來了是鬼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤紫新,失蹤者是張志新(化名)和其女友劉穎均蜜,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芒率,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡囤耳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片充择。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡德玫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出椎麦,到底是詐尸還是另有隱情化焕,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布铃剔,位于F島的核電站撒桨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏键兜。R本人自食惡果不足惜凤类,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望普气。 院中可真熱鬧谜疤,春花似錦、人聲如沸现诀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仔沿。三九已至坐桩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間封锉,已是汗流浹背绵跷。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留成福,地道東北人碾局。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像奴艾,于是被迫代替她去往敵國和親净当。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容