1. 程式人生 > >Go36-46-訪問網絡服務(socket)

Go36-46-訪問網絡服務(socket)

代碼 tcp 形式 套接字 clas erro 版本 c語言 使用

訪問網絡服務

這篇開始講網絡編程。不過網絡編程的內容過於龐大,這裏主要講socket。而socket可以講的東西也太多了,因此,這裏只圍繞Go語言介紹一些它的基礎知識。

IPC方法

所謂socket,是一種IPC(Inter-Process Communication)方法,可以被翻譯為進程間通信。顧名思義,IPC這個概念(或者說規範)主要定義的是多個進程之間,相互通信的方法。這些方法主要包括:

  • 系統信號(signal),os包和os/signal包有針對系統信號的API
  • 管道(pipe),os.Pipe函數可以創建命名管道,os/exec包支持另一類管道:匿名管道
  • 套接字(socket),net包中提供支持
  • 文件鎖(file lock)
  • 消息隊列(message queue)
  • 信號燈(semaphore),也稱為信號量

現存的主要操作系統大都對IPC提供了強有力的支持,尤其是socket。

socket

socket,常被稱作套接字,它是網絡編程世界中最為核心的知識之一。
毫不誇張的說,在眾多IPC方法中,socket是最為通用和靈活的一種。與其他的IPC方法不同,利用socket進行通信的進程,可以不局限在同一臺計算機當中。通信雙方只要能夠通過網絡進行互聯,就可以使用socket。
支持socket的操作系統一般都會對外提供一套API。跑在它們之上的應用程序,利用這套API就可以與互聯網上的另一臺計算機中的程序、同一臺計算機中的其他程序,甚至同一個程序中的其他線程進行通信。例如,在Linux操作系統中,用於創建socket實例的API,就是由一個名為socket的系統調用代表的。這個系統調用是Linux內核的一部分。所謂的系統調用,你可以理解為特殊的C語言函數。它們是連接應用程序和操作系統內核的橋梁,也是應用程序使用操作系統功能的唯一渠道。

syscall包

在Go語言標準庫的syscall包中,有一個與這個socket系統調用相對應的函數。這兩者的函數簽名是基本一致的,它們都會接受三個int類型的參數,並會返回一個可以代表文件描述符的結果。但不同的是,syscall包中的Socket函數本身是平臺不相關的。在其底層,Go語言為它支持的每個操作系統都做了適配,這樣這個函數無論在哪個平臺上,總是有效的。
在syscall.Socket函數中的三個參數分別是:

  • socket實例的通信域
  • socket實例的類型
  • socket實例的使用協議

下面,通過這3個參數來了解一下socket的基礎知識。

通信域

Socket的通信域主要有3種,分別對應syscall包中的一個常量:

  1. AF_INET : IPv4域
  2. AF_INET6 : IPv6域
  3. AF_UNIX : Unix域

關於IPv4和IPv6就不講了,Unix域簡單提一下。
Unix域,指的是一種類Unix操作系統中特有的通信域。在裝有此類操作系統的同一臺計算機中,應用程序可以基於此域建立socket連接。

類型

Socket的類型一個有4種,在syscall包中有同名的常量對應:

  1. SOCK_DGRAM
  2. SOCK_STREAM
  3. SOCK_SEQPACKET
  4. SOCK_RAW

上面的4種類型,前兩個更加常用。

UDP
SOCK_DGRA中的DGRAM就是datagram,即數據報文。它是一種有消息邊界但沒有邏輯連接的非可靠socket類型,UDP協議的網絡通信就是這類。
有消息邊界的意思是,與socket相關的操作系統內核中的程序,即內核程序,在發送或接收數據的時候是以消息為單位的。這裏可以把消息理解為帶有固定邊界的一段數據。內核程序可以自動的識別和維護這種邊界。在必要的時候,把數據切割成一個一個的消息,或者把多個消息串接成連續的數據。這樣,應用程序值需要面向消息進行處理就可以了。
只要應用程序指定好對方的網絡地址,內核程序就可以立即把數據報文發送出去。這有優勢也有劣勢。優勢是,發送速度快,不長期占用網絡資源,並且每次發送都可以指定不同的網絡地址。最後一條既是優勢也是劣勢,因為這會使數據報文更長。其他劣勢還有,無法保證傳輸的可靠性,不能實現數據的有序性,以及數據只能單向進行傳輸。

TCP
SOCK_STREAM類型,是沒有消息邊界但有邏輯連接,能夠保證傳輸的可靠性和數據的有序性,同時還可以實現數據的雙向傳輸。TCP協議的網絡通信就是這類。
有邏輯連接是指,通信雙方在收發數據之前必須先建立網絡連接。等連接建立好之後,雙方就可以一對一的進行數據傳輸了。
這樣的網絡通信傳輸數據的形式是字節流,而不是數據報文。字節流是以字節為單位的。內核程序無法感知一段字節流中包含了多少個消息,以及這些消息是否完整,這完全需要應用程序自己來把控。不過,此類網絡通信中的一段,總會忠實的按照另一端發送數據是的字節排列順序,接收和緩存它們。所以,應用程序需要根據雙方的約定去數據中查找消息邊界,並按照邊界切割數據。

使用協議

通常只要明確指定了前兩個參數值,就無需在去確定這裏的使用協議了,一般把它置為0就可以了。這時,內核程序會自行選擇最合適的協議。

不完整的示例

package main

import (
    "fmt"
    "os"
    "syscall"
)

func main() {
    fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
        return
    }
    defer syscall.Close(fd)
    fmt.Println("socket的文件描述符:", fd)
    // 之後就省略了,要使用syscall包來建立網絡連接,過程太繁瑣
}

這個代碼包的使用太底層,通常也不需要我們直接使用。Go語言的net包中的很多程序實體,都會直接或間接的使用到syscall.Socket函數,並且無需給定細致的參數。但是,在使用這些API的時候,現在我們就應該知道上面這些基礎知識了。

net.Dial函數

net.Dial函數會接受兩個參數,network和address,具體看下面:

func Dial(network, address string) (Conn, error) {
    var d Dialer
    return d.Dial(network, address)
}

network參數

參數network常用的可選值一共有9個,這些值分別代表了程序底層創建的socket實例可使用的不同通信協議:

  1. "tcp" : 代表TCP協議,其基於的IP協議的版本根據參數address的值自適應
  2. "tcp4" : 代表基於IPv4協議的TCP協議
  3. "tcp6" : 代表基於IPv6協議的TCP協議
  4. "udp" : 代表UDP協議,其基於的IP協議的版本根據address的值自適應
  5. "udp4" : 代表基於IPv4協議的UDP協議
  6. "udp6" : 代表基於IPv6協議的UDP協議
  7. "unix" : 代表Unix通信域下的一種內部socket協議,以SOCK_STREAM為socket類型
  8. "unixgram" : 代表Unix通信域下的一種內部socket協議,以SOCK_DGRAM為socket類型
  9. "unixpacket" : 代表Unix通信域下的一種內部socket協議,以SOCK_SEQPACKET為socket類型

net包發送http請求

對於http請求,在標準庫裏還有更高級的封裝,不過http本質上也是socket,這裏展示用net包發送請求的示例:

package main

import (
    "fmt"
    "io"
    "net"
    "os"
)

func main() {
    conn, err := net.Dial("tcp", "baidu.com:80")
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
        return
    }
    defer conn.Close()

    reqStr := "HEAD / HTTP/1.1\r\n" + // HEAD請求,只返回請求頭
        "Host: baidu.com\r\n" + 
        "Connection: close\r\n" + // 返回後,服務器會斷開連接,默認是keep-alive
        "\r\n"  // 請求頭結束
    _, err = io.WriteString(conn, reqStr)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
        return
    }

    buf := make([]byte, 1024)
    for {
        n, err := conn.Read(buf)
        fmt.Println(string(buf[:n]))
        if err != nil {
            if err == io.EOF {
                fmt.Println("END")
                break
            } else {
                fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
            }
        }
    }
}

如果是https的請求,還需要借助crypto/tls包,而調用起來基本是一樣的:

package main

import (
    "crypto/tls"
    "fmt"
    "io"
    "os"
)

func main() {
    tlsConf := &tls.Config{
        InsecureSkipVerify: true,
        MinVersion:         tls.VersionTLS10,
    }

    conn, err := tls.Dial("tcp", "gitee.com:443", tlsConf)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
        return
    }
    defer conn.Close()

    reqStr := "HEAD / HTTP/1.1\r\n" + // HEAD請求,只返回請求頭
        "Host: gitee.com\r\n" +
        "Connection: close\r\n" + // 返回後,服務器會斷開連接,默認是keep-alive
        "\r\n" // 請求頭結束
    _, err = io.WriteString(conn, reqStr)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
        return
    }

    buf := make([]byte, 1024)
    for {
        n, err := conn.Read(buf)
        fmt.Println(string(buf[:n]))
        if err != nil {
            if err == io.EOF {
                fmt.Println("END")
                break
            } else {
                fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
            }
        }
    }
}

net.DialTimeout函數

net.DialTimeout函數和net.Dial函數相比,多接受了一個參數timeout。而底層實現可以看到是一樣的,只是對Dialer結構體的Timeout字段進行了設置,而在net.Dial函數裏結構體都是默認值:

func DialTimeout(network, address string, timeout time.Duration) (Conn, error) {
    d := Dialer{Timeout: timeout}
    return d.Dial(network, address)
}

超時時間

這裏的超時時間,出函數為網絡連接建立完成而等待的最長時間。
開始的時間點幾乎是調用net.DialTimeout函數的那一刻。在這之後,時間會主要花費在解析參數的network值和address值,以及創建socket實例並建立網絡連接這兩件事情上。如果超時了而網絡連接還沒有建立完成,該函數就會返回一個I/O操作超時的錯誤值。
在解析address的值的時候,函數會確定網絡服務的IP地址、端口號等必要信息,並在需要的時候訪問DNS服務。另外,如果解析出的IP地址有多個,函數會串行或並行的嘗試建立連接。無論用什麽方式嘗試,函數總會以最先建立成功的那個連接為準。同時還會根據超時時間的剩余時間去設定對每次連接嘗試的超時時間。
找一個國外的網站,或者幹脆找一個連不上的地址,看下超時時間的作用:

package main

import (
    "fmt"
    "net"
    "os"
    "time"
)

func main() {
    tStart := time.Now()
    conn, err := net.DialTimeout("tcp", "godoc.org:80", time.Second * 10)
    tEnd := time.Now()
    fmt.Println("連接持續時間:", time.Duration(tEnd.Sub(tStart)))
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
        return
    }
    defer conn.Close()
    fmt.Println("本地連接地址:", conn.LocalAddr())
    fmt.Println("對端連接地址:", conn.RemoteAddr())
}

Go36-46-訪問網絡服務(socket)