iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 29
0

這天小櫻收集到了最後一張Golang牌,正當小櫻把名字寫上Golang牌時,天色一瞬間變暗了。最後的審判登場!

小櫻是否能真的成為Golang魔法使呢?我們下集待續!!

網路架構

網際網路可以分為五層架構。

  1. Application layer (應用層)
  2. Transport layer (傳輸層)
  3. Network layer (網路層)
  4. Link layer (連接層)
  5. Physical layer (物理層)

Application layer 指的是網路的應用服務,比如瀏覽網頁所使用的 http (Hypertext transfer protocol, 超文本傳輸協定) 和 https (http + secure, 超文本傳輸安全協定),中文名稱聽起來有點中二。只要遵守這些協定後,全世界的人都能正常的使用網頁服務。

因為用 Golang 開 http, https 伺服器已經有太多教學了,所以想直接講實戰 TCP 的部分

什麼是 TCP 呢?

一提到 TCP (Transmission Control Protocol, 傳輸控制協定) 就一定得提一下 UDP,這兩個東西稱霸了 Transport layer。TCP 可以保證資料傳輸的正確性、順序性...等,意思就是說透過 TCP 傳送的資訊(比如 http, https, ftp,...),並不需要考慮資料有錯誤發生,我透過 TCP 傳送 A,那麼接收到的就是 A,先傳送 A 再傳送 B,那麼就是先接收到 A 再接收到 B。而 UDP 則跟 TCP 完全不一樣,你有可能透過 UDP 傳送了 A,但接收者就接收到 C,有可能你先傳了 A 再傳 B,接收者卻先接收到 B 才接收到 A。 UDP 不但不能保證資料傳輸正確,甚至資料在中途就丟失了,還有可能資料傳輸時順序倒轉的。

看這一張圖很快就能理解了。因此,只要是注重正確性的網路服務都會透過 TCP 實作,如剛剛提到的網頁 http, https 就算是一個字錯了也不允許,再來還有如郵件、FTP...等。UDP 好像被講的一無事處,但其實並不然,因為 TCP 太小心了,所以會有延遲,因為只要有一點錯就會重傳,而且使用前還要花時間建立連線。UDP 就沒這個問題,他就像圖片底下那個拋嬰兒一樣非常隨便,也不管接收端有沒有收到,這有一個好處,比如講求即時性的應用,如影片、直播、網路電話,即使錯了幾個畫面也無傷大雅,然而如果是延遲則會很嚴重影響體驗,這時就會優先考慮使用 UDP 實作。

Go 提供的 net 套件

Go 主攻網路服務,net 套件相當豐富,Go 的官方網站在 net 套件的 Overview 寫了一段:

The Dial function connects to a server:

conn, err := net.Dial("tcp", "golang.org:80")
if err != nil {
	// handle error
}
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
status, err := > bufio.NewReader(conn).ReadString('\n')
// ...

這段程式碼是什麼意思呢?

首先 Dial() 可以理解成撥號,有點像初始化的意思,可以對 TCP, UDP 這兩個傳輸層的協定進行撥號,也可以對 Network layer 的 IP4, IP6 撥號,但是 ip 又更底層了,只能等下一季才能教

那這個 Dial() 要怎麼用

func Dial(network, address string) (Conn, error)

func Dial(network, address string) (Conn, error)
Dial connects to the address on the named network.
Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only), "udp", "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only), "ip6" (IPv6-only), "unix", "unixgram" and "unixpacket".

For TCP and UDP networks, the address has the form "host:port". The host must be a literal IP address, or a host name that can be resolved to IP addresses. The port must be a literal port number or a service name. If the host is a literal IPv6 address it must be enclosed in square brackets, as in "[2001:db8::1]:80" or "[fe80::1%zone]:80". The zone specifies the scope of the literal IPv6 address as defined in RFC 4007. The functions JoinHostPort and SplitHostPort manipulate a pair of host and port in this form. When using TCP, and the host resolves to multiple IP addresses, Dial will try each IP address in order until one succeeds.

其中第一個參數可以擺放連線方式,有的透過 tcp、有的透過 udp,有的則是由 ip 做連線

而第二個參數則是放 IP 地址,或者,可以是一串網址,網址中可以包含 port,至於什麼是 port 呢?假如現在有一台伺服器使用了 140.120.1.20 這個 IP (每次上網時,網路提供商會提供一組 ip,可以要求網路提供商固定這個位址) ,別人可以連到這台伺服器上,但是如果一台伺服器只想要提供不同的服務要怎麼辦呢?這時 port 的概念就出現了。這個伺服器可以

  • 用 port 443 開啟一個 https 的伺服器。

https://140.120.1.20 = 140.120.1.20:443

  • 用 port80 開啟 http 伺服器

http://140.120.1.20 = 140.120.1.20:80

  • 用 3306 開啟 MySQL 的資料庫伺服器

140.120.1.20:3306

  • 也可以將 ip 地址註冊成一串有意義的網址

140.120.1.20 = nchu.edu.tw

透過這些 port ,伺服器可以知道接收著想要使用的是哪個服務,比如這個使用者要用 MySQL 的服務,你確不知道,以為他要用 http

使用 TCP 連線,連入 Go 官方網站

知道 port 的概念後我們馬上來試試看利用 tcp 連線來連上 golang.org 這個網站

package main

import(
    "fmt"
    "net"
)

func main(){
    // 藉由 tcp 對 golang.org 建立連線
    // 採用 port 80 (http)
    // 好比在瀏覽器中打下 http://golang.org
    conn, err := net.Dial("tcp", "golang.org:80")
    if err != nil {
    	panic(err)
    }
    
    // conn 的型態屬於 net.Conn
    // 但 conn 也同時滿足 io.Writer 這個 interface{}

    // func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
    // 利用 fmt.Fprintf 可以對 conn 寫入額外的資訊
    // 這會使 tcp 封包內加入一個請求 http 連線時的資訊
    fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")

    // 發送連線後,golang.org 的伺服器也會反回一個封包
    // conn 同時也滿足 io.Reader 這個 interface{}

    // 利用 .Read() 的方式可以讀取 conn 中的訊息

    // 新建一個 byte slice 長度 64
    // 每次從 conn 緩充 64 個 bytes 下來
    res := make([]byte, 64)
    // num 代表讀入的字數,直到讀到零為止
    for num, _ := conn.Read(res); num!=0; num, _ = conn.Read(res){
        // 將傳來的 []byte 透過 string() 轉型成字串印出
        fmt.Print(string(res))
    }
}

執行結果:
HTTP/1.0 200 OK
Date: Tue, 29 Sep 2020 18:26:16 GMT
Expires: -1
...略...

conn, err := net.Dial("tcp", "golang.org:80")

利用這個函式可以透過 tcp 和 golang.org:80 做連線

回傳的 conn 是 connection 的縮寫,型態為 net.Conn。而 net.Conn 有什麼方法可以來使用呢?

因為說明太長了我直接給連結

https://golang.org/pkg/net/#Conn

這些印出來的資訊就是一個 TCP 封包所含有的資訊,其中最底下的部份是 http 封包的內容。以伺服器端來說, golang.org 的伺服器會先產生一份 http 封包(Application layer),該封包會被封裝進 TCP (Transport layer)的封包裡,接著又會往下封裝。送出封包後,接收端再一一拆包。透過 net/tcp 套件可以從 ip 的封包中拆出 tcp 封包使用

開設一個 tcp 伺服器

剛剛介紹的方法是利用 Dial 的方式與伺服器做連線。現在我們希望可以將手上的電腦變成一個可以監聽某個 port 的伺服器

至於要選哪一個 port 可以上 維基百科查,因為 port 使用上有潛規則,盡量使用沒有被使用的 port 來練習

TCP/UDP端口列表 - 維基百科,自由的百科全書

監聽 port: 1450

查尋了一下,發現 1450 這個通訊埠沒有人使用。那麼就拿這個台灣人無人不知無人不曉的數字來練習吧!

server (伺服器端)

伺服器端的部份我們會以 net.Listen() 來實作,相關的用法請參考 net.Listener - Go

package main

import(
    "fmt"
    "net"
)

func main(){
    // 新增一個監聽
    // 監聽 tcp port 1450
    ln, err := net.Listen("tcp", ":1450")
    // defer 是一個特殊用法,會延遲函式執行
    // 也就是說 ln.Close() 會在 main() 即將結束時才執行
    defer ln.Close()
    
    if err != nil {
    	panic("監聽 port 1450 失敗")
    }
    
    // 印出這個是 server (debug 方便)
    fmt.Println("SERVER")
    
    // server 是不能停止的,必需無時無刻監聽 port: 1450
    for {
        // Accept() 在沒有接到封包時會暫停,直到有接收到才會往下繼續執行
    	conn, err := ln.Accept()
    	if err != nil {
            fmt.Println("ln.Accept() 失敗")
            continue
    	}
        // 處理 conn
        // 因為處理 conn 時會沒辦法繼續監聽,所以要另開一條執行緒來處理,這樣若有其他用戶同時需要伺服器的服務時才不會塞車
    	go func (conn net.Conn){
            // 處理完後記得關閉 conn
            // 不然客戶端會不知道訊息傳完了沒
            defer conn.Close()
            req := make([]byte, 64)
            conn.Read(req)

            // 回傳說已接收到並且關閉連線
            fmt.Fprintf(conn, "伺服器端回傳伺服器端已接收到 %s", string(req))

            // 印出接收到的訊息
            fmt.Println("伺服器已接收到", string(req))
        }(conn)
    }
}

client (用戶端)

package main

import(
    "fmt"
    "net"
)

func main(){

    fmt.Println("CLIENT")

    // 如果要在相同的裝置上開用戶端,可以使用 localhost,用來代表現在這台主機
    conn, err := net.Dial("tcp", "localhost:1450")
    defer conn.Close()
    if err != nil {
        panic(err)
    }

    //發送訊息
    fmt.Fprintf(conn, "封印解除!")

    //接收伺服器回傳的訊息
    res := make([]byte, 64)
    conn.Read(res)
    fmt.Print(string(res))
}

執行

執行時先啟動 server,再啟動 client

client 執行結果:
CLIENT
伺服器端回傳伺服器端已接收到 封印解除!

server 執行結果:
SERVER
伺服器已接收到 封印解除!

...沒有結束執行...
...因為伺服器終其一生都要等待被客戶觸發...

後記

9/29 剛接一份新作,然後進度就亂掉了,而且這部份我很不熟,所以一直 9/30 才弄好。重點是我第 30 篇還沒動筆哦 9/30 22:40

圖片大多來自:庫洛魔法使第二季第十集


上一篇
#28 優先佇列 Priority Queue 實戰 container/heap 套件 | Golang 魔法使
下一篇
#30 UDP ── 用戶資料報協定,嘗試使用 UDP 傳送圖片 | Golang魔法使
系列文
Golang魔法使 ─ 30天從零開始學習Go語言 | 比Python還簡單 | 理科生一定學得會 | 文科生不好說30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言