1. 程式人生 > >Go學習(16):網路程式設計

Go學習(16):網路程式設計

網路程式設計

一、網路程式設計基礎

go 的網路程式設計模組主要支援兩種Internet協議: TCP 和 UDP.

1.1通訊協議

通訊協議也叫網路傳輸協議或簡稱為傳送協議(Communications Protocol),是指計算機通訊或網路裝置的共同語言。

現在最普及的計算機通訊為網路通訊,所以“傳送協議”一般都指計算機通訊的傳送協議,如:TCP/IP、NetBEUI、HTTP、FTP等。

然而,傳送協議也存在於計算機的其他形式通訊,例如:面向物件程式設計裡面物件之間的通訊;作業系統內不同程式之間的訊息,都需要有一個傳送協議,以確保傳信雙方能夠溝通無間。


1.2TCP/IP協議

在Internet中TCP/IP協議是使用最為廣泛的通訊協議(網際網路上的一種事實的標準)。TCP/IP是英文Transmission Control Protocol/Internet Protocol的縮寫,意思是“傳輸控制協議/網際協議”

TCP/IP 協議是一個工業標準協議套件,專為跨廣域網(WAN)的大型網際網路絡而設計。

TCP/IP 網路體系結構模型就是遵循TCP/IP 協議進行通訊的一種分層體系,現今,Internet和Intranet所使用的協議一般都為TCP/IP 協議。

在瞭解該協議之前,我們必須掌握基於該協議的體系結構層次,而TCP/IP體系結構分為四層。

第 1 層 網路介面層
包括用於協作IP資料在已有網路介質上傳輸的協議,提供TCP/IP協議的資料結構和實際物理硬體之間的介面。比如地址解析協議(Address Resolution Protocol, ARP )等。

第 2 層 網路層
對應於OSI模型的網路層,主要包含了IP、RIP等相關協議,負責資料的打包、定址及路由。還包括網間控制報文協議(ICMP)來提供網路診斷資訊。

第 3 層 傳輸層
對應於OSI的傳輸層,提供了兩種端到端的通訊服務,分別是TCP和UDP協議。

第 4 層 應用層
對應於OSI的應用層、表達層和會話層,提供了網路與應用之間的對話介面。包含了各種網路應用層協議,比如Http、FTP等應用協議。


附錄:OSI 七層參考模型

(圖片來自:http://www.cnblogs.com/qishui/p/5428938.html)
在這裡插入圖片描述


1.3 IP 地址和埠號

1.3.1 IP 地址

網際網路協議地址(英語:Internet Protocol Address,又譯為網際協議地址),縮寫為IP地址(英語:IP Address)

IP 地址是分配給網路上使用網際協議(英語:Internet Protocol, IP)的裝置的數字標籤。常見的IP地址分為IPv4與IPv6兩大類。


IPV4地址

IP地址由32位二進位制陣列成,為便於使用,常以XXX.XXX.XXX.XXX形式表現,每組XXX代表小於或等於255的10進位制數。例如維基媒體的一個IP地址是208.80.152.2。

地址可分為A、B、C、D、E五大類,其中E類屬於特殊保留地址。
IP地址是唯一的。目前IP技術可能使用的IP地址最多可有4,294,967,296個(即2的32次方)。驟看可能覺得很難會用盡,但由於早期編碼和分配上的問題,使很多區域的編碼實際上被空出或不能使用。加上網際網路的普及,使大部分家庭都至少有一部電腦,連同公司的電腦,以及連線網路的各種裝置都消耗了大量IPv4地址資源。

隨著網際網路的快速成長,IPv4的42億個地址的分配最終於2011年2月3日用盡[1][2]。相應的科研組織已研究出128位的IPv6,其IP地址數量最高可達3.402823669 × 1038個,屆時每個人家居中的每件電器,每件物件,甚至地球上每一粒沙子都可以擁有自己的IP地址。

在A類、B類、C類IP地址中,如果主機號是全1,那麼這個地址為直接廣播地址,它是用來使路由器將一個分組以廣播形式傳送給特定網路上的所有主機。32位全為1的IP地址“255.255.255.255”為受限廣播地址(“limited broadcast” destination address),用來將一個分組以廣播方式傳送給本網路中的所有主機,路由器則阻擋該分組通過,將其廣播功能限制在本網內部。


IPV6地址

IPv6地址為128位長但通常寫作8組每組四個十六進位制數的形式。例如:
2001:0db8:85a3:08d3:1319:8a2e:0370:7344
是一個合法的IPv6地址。

IPv4地址可以很容易的轉化為IPv6格式。舉例來說,如果IPv4的一個地址為135.75.43.52(十六進位制為0x874B2B34),它可以被轉化為0000:0000:0000:0000:0000:0000:874B:2B34或者::874B:2B34。同時,還可以使用混合符號(IPv4-compatible address),則地址可以為::135.75.43.52。

GO支援IP的型別

在Go的net包中定義了很多型別、函式和方法用來網路程式設計,其中IP的定義如下:

type IP []byte

在net包中有很多函式來操作IP,但是其中比較有用的也就幾個,其中ParseIP(s string) IP函式會把一個IPv4或者IPv6的地址轉化成IP型別,請看下面的例子:

name := "192.168.10.105"
addr := net.ParseIP(name)
if addr == nil {
   fmt.Println("Invalid address")
} else {
   fmt.Println("The address is ", addr.String())
}

執行之後你就會發現只要你輸入一個IP地址就會給出相應的IP格式


1.3.2埠

在網路技術中,埠(Port)包括邏輯埠和物理埠兩種型別。

物理埠指的是物理存在的埠,如ADSL Modem、集線器、交換機、路由器上用 於連線其他網路裝置的介面,如RJ-45埠、SC埠等等。

邏輯埠是指邏輯意義上用於區分服務的埠,如TCP/IP協議中的服務埠,埠號的範圍從0到65535,比如用於瀏覽網頁服務的80埠,用於FTP服務的21埠等。由於物理埠和邏輯埠數量較多,為了對埠進行區分,將每個埠進行了編號,這就是埠號。

我們主要研究的是邏輯埠號.我們平時所說的埠號也是指的邏輯埠號

埠是一個軟體結構,被客戶程式或服務程式用來發送和接收資料,一臺伺服器有 256*256個埠。 埠號範圍: 0 - 65535

0-1023是公認埠號,即已經公認定義或為將要公認定義的軟體保留的

1024-65535是並沒有公共定義的埠號,使用者可以自己定義這些埠的作用。

埠與協議有關:TCP和UDP的埠互不相干

二、TCP程式設計

什麼是 TCP 協議

TCP(Transmission Control Protocol 傳輸控制協議)是一種面向連線(連線導向)的、可靠的、基於IP的傳輸層協議。

彌補了IP協議的不足,屬於一種較高階的協議,它實現了資料包的有力捆綁,通過排序和重傳來確保資料傳輸的可靠(即資料的準確傳輸以及完整性)。

排序可以保證資料的讀取是按照正確的格式進行,重傳則保證了資料能夠準確傳送到目的地!

使用 TCP 協議通訊是, 首先建立 TCP 連線, 主動發起連線的叫客戶端, 被動響應連線的叫伺服器

比如:
當我們在瀏覽器中訪問新浪主頁時,我們自己的計算機就是客戶端,瀏覽器會主動向新浪的伺服器發起連線。如果一切順利,新浪的伺服器接受了我們的連線,一個TCP連線就建立起來的,後面的通訊就是傳送網頁內容了。

2.1什麼是Socket

Socket又稱"套接字",應用程式通常通過"套接字"向網路發出請求或者應答網路請求,使主機間或者一臺計算機上的程序間可以通訊。

可以把Socket理解成類似插座的東西, 通過Socket就可以傳送和接受資料了, 就像插座插上電器之後就可以向外提供電能了.

TCP程式設計的客戶端和伺服器端都是通過Socket來完成的.

其實UDP協議通訊也是使用的套接字, 和TCP協議稍有差別. TCP是面向連線的套接字, 而UDP是面向無連線的套接字.


套接字的起源可以追溯到20世紀70年底, 他是加利福尼亞大學的伯克利版本 Unix(也成 BSD Unix) 的一部分. 因此, 有時你可能會聽過將套接字稱為伯克利套接字或 BSD 套接字.

套接字最初是為同一主機上的應用程式鎖建立, 使得主機上一個程式(也叫一個程序)與另一個允許的程式進行通訊. 這就是所謂的程序間通訊(Inter Process Communication IPC)


當我們知道如何通過網路埠訪問到一個服務時,那麼我們能夠做什麼呢?其實作為客戶端來說,我們可以通過網路埠傳送一個請求,然後得到伺服器端反饋的資訊。作為服務端,我們需要把我們的服務繫結到埠,並且在相應的埠上監聽,當有客戶端來訪問時能夠讀取資訊並且寫入反饋資訊。

在Go語言的net包中有一個型別TCPConn,這個型別可以用來作為客戶端和伺服器端互動的通道,他有兩個主要的函式:

func (c *TCPConn) Write(b []byte) (n int, err os.Error)
func (c *TCPConn) Read(b []byte) (n int, err os.Error)

TCPConn可以用在客戶端和伺服器端來讀寫資料。

還有我們需要知道一個TCPAddr型別,他表示一個TCP的地址資訊,他的定義如下:

type TCPAddr struct {
    IP IP
    Port int
}

在Go語言中通過ResolveTCPAddr獲取一個TCPAddr

func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
  • net引數是"tcp4"、“tcp6”、"tcp"中的任意一個,分別表示TCPv4、TCPv6或者任意
  • addr表示域名或者IP地址,例如"www.google.com:80" 或者"127.0.0.1:22".

TCP client
Go語言中通過net包中的DialTCP函式建議一個TCP連線,返回一個TCPConn型別,客戶端和伺服器段通過這個型別來進行資料交換。一般而言,客戶端通過TCPConn寫入請求資訊傳送到伺服器端,讀取伺服器端反饋的資訊。這個連結只有當任意一遍關閉了連線之後才失效,不然我們都可以一直使用。函式的定義如下:

func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error)
  • net引數是"tcp4"、“tcp6”、"tcp"中的任意一個,分別表示TCPv4、TCPv6或者任意
  • laddr表示本機地址,一般設定為nil
  • raddr表示遠端的服務地址

2.2 TCP 服務端程式設計

建立伺服器端程式,我們知道伺服器端我們需要繫結服務到對應的埠,然後監聽埠,當有客戶端請求到達的時候接收客戶端連線。net包中有相應的函式,函式定義如下:

func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
func (l *TCPListener) Accept() (c Conn, err os.Error)

示例程式碼:

// TCPServer project main.go
package main

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

func main() {
    service := ":5000"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service) //在Go語言中通過ResolveTCPAddr獲取一個TCPAddr
    checkErr(err)
    listener, err := net.ListenTCP("tcp", tcpAddr) // 繫結伺服器埠,進行監聽
    checkErr(err)
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        go handleClient(conn)
    }
}

func checkErr(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

func handleClient(conn net.Conn) {
    defer conn.Close()
    var buf [512]byte
    for {
        n, err := conn.Read(buf[0:])
        if err != nil {
            return
        }
        rAddr := conn.RemoteAddr()
        fmt.Println("Receive from client", rAddr.String(), string(buf[0:n]))
        _, err2 := conn.Write([]byte("Welcome client!"))
        if err2 != nil {
            return
        }
    }
}

2.3 TCP 客戶端程式設計

客戶端例項程式碼:

// TCPClient project main.go
package main

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

func main() {
    var buf [512]byte
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s host:port ", os.Args[0])
        os.Exit(1)
    }
    service := os.Args[1]
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkErr(err)
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    defer conn.Close()
    checkErr(err)
    rAddr := conn.RemoteAddr()
    n, err := conn.Write([]byte("Hello server!"))
    checkErr(err)
    n, err = conn.Read(buf[0:])
    checkErr(err)
    fmt.Println("Reply from server ", rAddr.String(), string(buf[0:n]))
    os.Exit(0)
}

func checkErr(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

三、UDP程式設計

UDP簡介

UDP也叫使用者資料報協議

UDP程式設計相比TCP程式設計簡單了很多.

因為UDP不是面向連線的, 而是面向無連線的.

TCP是面向連線的, 客戶端和服務端必須連線之後才能通訊, 就像打電話, 必須先接通才能通話.

UDP是面向無連線的, 一方負責傳送資料(客戶端), 只要知道對方(接受資料:伺服器) 的地址就可以直接發資料了, 但是能不能達到就沒有辦法保證了.

雖然用UDP傳輸面向無連線, 資料不可靠,但它的優點是和TCP比,速度快,對於不要求可靠到達的資料,就可以使用UDP協議。 比如區域網的視訊同步, 使用 udp 是比較合適的:快, 延遲越小越好

Go語言包中處理UDP Socket和TCP Socket不同的地方就是在伺服器端處理多個客戶端請求資料包的方式不同,UDP缺少了對客戶端連線請求的Accept函式。其他基本幾乎一模一樣,只有TCP換成了UDP而已。UDP的幾個主要函式如下所示:

	func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
    func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
    func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
    func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error)
    func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)

一個UDP的客戶端程式碼如下所示,我們可以看到不同的就是TCP換成了UDP而已:

package main

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

    func main() {
        if len(os.Args) != 2 {
            fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
            os.Exit(1)
        }
        service := os.Args[1]
        udpAddr, err := net.ResolveUDPAddr("udp4", service)
        checkError(err)
        conn, err := net.DialUDP("udp", nil, udpAddr)
        checkError(err)
        _, err = conn.Write([]byte("anything"))
        checkError(err)
        var buf [512]byte
        n, err := conn.Read(buf[0:])
        checkError(err)
        fmt.Println(string(buf[0:n]))
        os.Exit(0)
    }
    func checkError(err error) {
        if err != nil {
            fmt.Fprintf(os.Stderr, "Fatal error ", err.Error())
            os.Exit(1)
        }
    }

3.2 UDP伺服器程式設計

我們來看一下UDP伺服器端如何來處理:

package main

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

    func main() {
        service := ":1200"
        udpAddr, err := net.ResolveUDPAddr("udp4", service)
        checkError(err)
        conn, err := net.ListenUDP("udp", udpAddr)
        checkError(err)
        for {
            handleClient(conn)
        }
    }
    func handleClient(conn *net.UDPConn) {
        var buf [512]byte
        _, addr, err := conn.ReadFromUDP(buf[0:])
        if err != nil {
            return
        }
        daytime := time.Now().String()
        conn.WriteToUDP([]byte(daytime), addr)
    }
    func checkError(err error) {
        if err != nil {
            fmt.Fprintf(os.Stderr, "Fatal error ", err.Error())
            os.Exit(1)
        }
    }

四、net模組其他屬性和函式

控制TCP連線

TCP連線有很多控制函式,我們平常用到比較多的有如下幾個函式:

func (c *TCPConn) SetTimeout(nsec int64) os.Error
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error

第一個函式用來設定超時時間,客戶端和伺服器端都適用,當超過設定的時間時那麼該連結就失效。

第二個函式用來設定客戶端是否和伺服器端一直保持著連線,即使沒有任何的資料傳送

更多的內容請檢視net包的文件。

阻塞Dial:

conn, err := net.Dial("tcp", "google.com:80")
if err != nil {
    //handle error
}
// read or write on conn

或是帶上超時機制的Dial:

conn, err := net.DialTimeout("tcp", ":8080", 2 * time.Second)
if err != nil {
    //handle error
}
// read or write on conn