本文通過 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ù)。
現(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)秀的語言。