Golang Socket編程

Socket編程

在不少底層網絡應用開發者的眼裏一切編程都是Socket,話雖然有點誇張,但卻也幾乎如此了,如今的網絡編程幾乎都是用Socket來編程。你想過這些情景麼?咱們天天打開瀏覽器瀏覽網頁時,瀏覽器進程怎麼和Web服務器進行通訊的呢?當你用QQ聊天時,QQ進程怎麼和服務器或者是你的好友所在的QQ進程進行通訊的呢?當你打開PPstream觀看視頻時,PPstream進程如何與視頻服務器進行通訊的呢? 如此種種,都是靠Socket來進行通訊的,以一斑窺全豹,可見Socket編程在現代編程中佔據了多麼重要的地位,這一節咱們將介紹Go語言中如何進行Socket編程。git

什麼是Socket?

Socket起源於Unix,而Unix基本哲學之一就是「一切皆文件」,均可以用「打開open –> 讀寫write/read –> 關閉close」模式來操做。Socket就是該模式的一個實現,網絡的Socket數據傳輸是一種特殊的I/O,Socket也是一種文件描述符。Socket也具備一個相似於打開文件的函數調用:Socket(),該函數返回一個整型的Socket描述符,隨後的鏈接創建、數據傳輸等操做都是經過該Socket實現的。github

經常使用的Socket類型有兩種:流式Socket(SOCK_STREAM)和數據報式Socket(SOCK_DGRAM)。流式是一種面向鏈接的Socket,針對於面向鏈接的TCP服務應用;數據報式Socket是一種無鏈接的Socket,對應於無鏈接的UDP服務應用。golang

Socket如何通訊

網絡中的進程之間如何經過Socket通訊呢?首要解決的問題是如何惟一標識一個進程,不然通訊無從談起!在本地能夠經過進程PID來惟一標識一個進程,可是在網絡中這是行不通的。其實TCP/IP協議族已經幫咱們解決了這個問題,網絡層的「ip地址」能夠惟一標識網絡中的主機,而傳輸層的「協議+端口」能夠惟一標識主機中的應用程序(進程)。這樣利用三元組(ip地址,協議,端口)就能夠標識網絡的進程了,網絡中須要互相通訊的進程,就能夠利用這個標誌在他們之間進行交互。請看下面這個TCP/IP協議結構圖web

使用TCP/IP協議的應用程序一般採用應用編程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已經被淘汰),來實現網絡進程之間的通訊。就目前而言,幾乎全部的應用程序都是採用socket,而如今又是網絡時代,網絡中進程通訊是無處不在,這就是爲何說「一切皆Socket」。編程

Socket基礎知識

經過上面的介紹咱們知道Socket有兩種:TCP Socket和UDP Socket,TCP和UDP是協議,而要肯定一個進程的須要三元組,須要IP地址和端口。windows

IPv4地址

目前的全球因特網所採用的協議族是TCP/IP協議。IP是TCP/IP協議中網絡層的協議,是TCP/IP協議族的核心協議。目前主要採用的IP協議的版本號是4(簡稱爲IPv4),發展至今已經使用了30多年。瀏覽器

IPv4的地址位數爲32位,也就是最多有2的32次方的網絡設備能夠聯到Internet上。近十年來因爲互聯網的蓬勃發展,IP位址的需求量越來越大,使得IP位址的發放愈趨緊張,前一段時間,據報道IPV4的地址已經發放完畢,咱們公司目前不少服務器的IP都是一個寶貴的資源。安全

地址格式相似這樣:127.0.0.1 172.122.121.111服務器

IPv6地址

IPv6是下一版本的互聯網協議,也能夠說是下一代互聯網的協議,它是爲了解決IPv4在實施過程當中遇到的各類問題而被提出的,IPv6採用128位地址長度,幾乎能夠不受限制地提供地址。按保守方法估算IPv6實際可分配的地址,整個地球的每平方米麪積上仍可分配1000多個地址。在IPv6的設計過程當中除了一勞永逸地解決了地址短缺問題之外,還考慮了在IPv4中解決很差的其它問題,主要有端到端IP鏈接、服務質量(QoS)、安全性、多播、移動性、即插即用等。網絡

地址格式相似這樣:2002:c0e8:82e7:0:0:0:c0e8:82e7

Go支持的IP類型

在Go的net包中定義了不少類型、函數和方法用來網絡編程,其中IP的定義以下:

type IP []byte

net包中有不少函數來操做IP,可是其中比較有用的也就幾個,其中ParseIP(s string) IP函數會把一個IPv4或者IPv6的地址轉化成IP類型,請看下面的例子:

package main
import (
    "net"
    "os"
    "fmt"
)
func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
        os.Exit(1)
    }
    name := os.Args[1]
    addr := net.ParseIP(name)
    if addr == nil {
        fmt.Println("Invalid address")
    } else {
        fmt.Println("The address is ", addr.String())
    }
    os.Exit(0)
}

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

TCP Socket

當咱們知道如何經過網絡端口訪問一個服務時,那麼咱們可以作什麼呢?做爲客戶端來講,咱們能夠經過向遠端某臺機器的的某個網絡端口發送一個請求,而後獲得在機器的此端口上監聽的服務反饋的信息。做爲服務端,咱們須要把服務綁定到某個指定端口,而且在此端口上監聽,當有客戶端來訪問時可以讀取信息而且寫入反饋信息。

在Go語言的net包中有一個類型TCPConn,這個類型能夠用來做爲客戶端和服務器端交互的通道,他有兩個主要的函數:

func (c *TCPConn) Write(b []byte) (int, error)
func (c *TCPConn) Read(b []byte) (int, error)

TCPConn能夠用在客戶端和服務器端來讀寫數據。

還有咱們須要知道一個TCPAddr類型,他表示一個TCP的地址信息,他的定義以下:

type TCPAddr struct {
    IP IP
    Port int
    Zone string // IPv6 scoped addressing zone
}

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

func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
  • net參數是"tcp4"、"tcp6"、"tcp"中的任意一個,分別表示TCP(IPv4-only), TCP(IPv6-only)或者TCP(IPv4, IPv6的任意一個)。
  • addr表示域名或者IP地址,例如"www.google.com:80" 或者"127.0.0.1:22"。

TCP client

Go語言中經過net包中的DialTCP函數來創建一個TCP鏈接,並返回一個TCPConn類型的對象,當鏈接創建時服務器端也建立一個同類型的對象,此時客戶端和服務器段經過各自擁有的TCPConn對象來進行數據交換。通常而言,客戶端經過TCPConn對象將請求信息發送到服務器端,讀取服務器端響應的信息。服務器端讀取並解析來自客戶端的請求,並返回應答信息,這個鏈接只有當任一端關閉了鏈接以後才失效,否則這鏈接能夠一直在使用。創建鏈接的函數定義以下:

func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
  • net參數是"tcp4"、"tcp6"、"tcp"中的任意一個,分別表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一個)
  • laddr表示本機地址,通常設置爲nil
  • raddr表示遠程的服務地址

接下來咱們寫一個簡單的例子,模擬一個基於HTTP協議的客戶端請求去鏈接一個Web服務端。咱們要寫一個簡單的http請求頭,格式相似以下:

"HEAD / HTTP/1.0\r\n\r\n"

從服務端接收到的響應信息格式可能以下:

HTTP/1.0 200 OK
ETag: "-9985996"
Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT
Content-Length: 18074
Connection: close
Date: Sat, 28 Aug 2010 00:43:48 GMT
Server: lighttpd/1.4.23

咱們的客戶端代碼以下所示:

package main

import (
    "fmt"
    "io/ioutil"
    "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]
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    checkError(err)
    _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
    checkError(err)
    result, err := ioutil.ReadAll(conn)
    checkError(err)
    fmt.Println(string(result))
    os.Exit(0)
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

經過上面的代碼咱們能夠看出:首先程序將用戶的輸入做爲參數service傳入net.ResolveTCPAddr獲取一個tcpAddr,而後把tcpAddr傳入DialTCP後建立了一個TCP鏈接conn,經過conn來發送請求信息,最後經過ioutil.ReadAllconn中讀取所有的文本,也就是服務端響應反饋的信息。

TCP server

上面咱們編寫了一個TCP的客戶端程序,也能夠經過net包來建立一個服務器端程序,在服務器端咱們須要綁定服務到指定的非激活端口,並監聽此端口,當有客戶端請求到達的時候能夠接收到來自客戶端鏈接的請求。net包中有相應功能的函數,函數定義以下:

func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
func (l *TCPListener) Accept() (Conn, error)

參數說明同DialTCP的參數同樣。下面咱們實現一個簡單的時間同步服務,監聽7777端口

package main

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

func main() {
    service := ":7777"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        daytime := time.Now().String()
        conn.Write([]byte(daytime)) // don't care about return value
        conn.Close()                // we're finished with this client
    }
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

上面的服務跑起來以後,它將會一直在那裏等待,直到有新的客戶端請求到達。當有新的客戶端請求到達並贊成接受Accept該請求的時候他會反饋當前的時間信息。值得注意的是,在代碼中for循環裏,當有錯誤發生時,直接continue而不是退出,是由於在服務器端跑代碼的時候,當有錯誤發生的狀況下最好是由服務端記錄錯誤,而後當前鏈接的客戶端直接報錯而退出,從而不會影響到當前服務端運行的整個服務。

上面的代碼有個缺點,執行的時候是單任務的,不能同時接收多個請求,那麼該如何改造以使它支持多併發呢?Go裏面有一個goroutine機制,請看下面改造後的代碼

package main

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

func main() {
    service := ":1200"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        go handleClient(conn)
    }
}

func handleClient(conn net.Conn) {
    defer conn.Close()
    daytime := time.Now().String()
    conn.Write([]byte(daytime)) // don't care about return value
    // we're finished with this client
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

經過把業務處理分離到函數handleClient,咱們就能夠進一步地實現多併發執行了。看上去是否是很帥,增長go關鍵詞就實現了服務端的多併發,從這個小例子也能夠看出goroutine的強大之處。

有的朋友可能要問:這個服務端沒有處理客戶端實際請求的內容。若是咱們須要經過從客戶端發送不一樣的請求來獲取不一樣的時間格式,並且須要一個長鏈接,該怎麼作呢?請看:

package main

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

func main() {
    service := ":1200"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        go handleClient(conn)
    }
}

func handleClient(conn net.Conn) {
    conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout
    request := make([]byte, 128) // set maxium request length to 128B to prevent flood attack
    defer conn.Close()  // close connection before exit
    for {
        read_len, err := conn.Read(request)

        if err != nil {
            fmt.Println(err)
            break
        }

            if read_len == 0 {
                break // connection already closed by client
            } else if strings.TrimSpace(string(request[:read_len])) == "timestamp" {
                daytime := strconv.FormatInt(time.Now().Unix(), 10)
                conn.Write([]byte(daytime))
            } else {
                daytime := time.Now().String()
                conn.Write([]byte(daytime))
            }

            request = make([]byte, 128) // clear last read content
    }
}

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

在上面這個例子中,咱們使用conn.Read()不斷讀取客戶端發來的請求。因爲咱們須要保持與客戶端的長鏈接,因此不能在讀取完一次請求後就關閉鏈接。因爲conn.SetReadDeadline()設置了超時,當必定時間內客戶端無請求發送,conn便會自動關閉,下面的for循環即會由於鏈接已關閉而跳出。須要注意的是,request在建立時須要指定一個最大長度以防止flood attack;每次讀取到請求處理完畢後,須要清理request,由於conn.Read()會將新讀取到的內容append到原內容以後。

控制TCP鏈接

TCP有不少鏈接控制函數,咱們日常用到比較多的有以下幾個函數:

func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)

設置創建鏈接的超時時間,客戶端和服務器端都適用,當超過設置時間時,鏈接自動關閉。

func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error

用來設置寫入/讀取一個鏈接的超時時間。當超過設置時間時,鏈接自動關閉。

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

設置keepAlive屬性,是操做系統層在tcp上沒有數據和ACK的時候,會間隔性的發送keepalive包,操做系統能夠經過該包來判斷一個tcp鏈接是否已經斷開,在windows上默認2個小時沒有收到數據和keepalive包的時候人爲tcp鏈接已經斷開,這個功能和咱們一般在應用層加的心跳包的功能相似。

更多的內容請查看net包的文檔。

 

 

 

 

 

本文轉自:https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/08.1.md

相關文章
相關標籤/搜索