Go筆記-socket

SOCKET

基礎知識

Socket有兩種:TCP Socket和UDP Socket,TCP和UDP是協議。肯定一個進程須要三元組,IP地址、協議和端口。html

IPv4的地址位數爲32位,地址格式相似這樣:127.0.0.1 172.122.121.111。nginx

IPv6採用128位地址長度,地址格式相似這樣:2002:c0e8:82e7:0:0:0:c0e8:82e7。程序員

IP IN GO

IP類型的定義在net包中。golang

使用下面的語句獲取一個ip類型:web

var ip net.IP = net.ParseIP("127.0.0.1")

TCP SOCKET

net.TCPConn可做爲客戶端和服務器端交互的通道,利用它的Read()和Write()能夠讀寫數據。算法

net.TCPAddr表示一個TCP的地址信息:瀏覽器

type TCPAddr struct {
    IP IP
    Port int
  }

得到TCPAddr:安全

func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)

TCP client

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

func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error)
  • net參數是"tcp4"、"tcp6"、"tcp"中的任意一個,分別表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一個)websocket

  • laddr表示本機地址,通常設置爲nil

    • raddr表示遠程的服務地址

    示例:

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)
    }
}

從conn中讀取數據時最好不要直接用 conn.Read(),建議用:

reader := bufio.NewReader(sender.tcpConn)
respStr, err := reader.ReadString('\n')

TCP server

在服務器端咱們須要綁定服務到指定的非激活端口,並監聽此端口,當有客戶端請求到達的時候能夠接收到來自客戶端鏈接的請求。net包中有相應功能的函數,函數定義以下:

func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
func (l *TCPListener) Accept() (c Conn, err os.Error)
  • 示例:下面實現一個簡單的時間同步服務,監聽7777端口
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(),其餘代碼參考上面

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 128KB 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 string(request) == "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
    }
}

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

設置客戶端是否和服務器端保持長鏈接,能夠下降創建TCP鏈接時的握手開銷,對於一些須要頻繁交換數據的應用場景比較適用。

tcp設置超時:

conn.SetDeadline(time.Now().Add(timeout))
	defer func() {
		var zero time.Time
		conn.SetDeadline(zero) // 重置
	}()

tcp超時檢測:

if neterr, ok := err.(net.Error); ok && neterr.Timeout()

測試連接是否能連通:

if _, err := sender.tcpConn.Read(empty); err == io.EOF {
		fmt.Println("%s closed, reconnection now.")
		sender.Close()
		sender.Init()
	}

http://stackoverflow.com/questions/12741386/how-to-know-tcp-connection-is-closed-in-golang-net-package

TCP鏈接網站示例

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

func Tcptest() {
	service := "www.csdn.net:80" // 後面要跟端口號
	conn, err := net.Dial("tcp", service)
	checkError2(err)

	_, err = conn.Write([]byte("HEAR / HTTP/1.0\r\n\r\n"))
	checkError2(err)

	result, err := readFully2(conn)
	checkError2(err)

	fmt.Println(string(result))
	os.Exit(0)
}

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

func readFully2(conn net.Conn) ([]byte, error) {
	defer conn.Close()

	result := bytes.NewBuffer(nil)
	var buf [512]byte
	for {
		n, err := conn.Read(buf[0:])
		result.Write(buf[0:n])
		if err != nil {
			if err == io.EOF {
				break
			}
			return nil, err
		}
	}
	return result.Bytes(), nil
}

結果示例:

HTTP/1.1 404 Not Found
Server: ngx_openresty
Date: Sat, 20 Sep 2014 07:49:31 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 162
Connection: close

<html>
<head><title>404 Not Found</title></head>
<body bgcolor="white">
\<center><h1>404 Not Found</h1></center>
\<hr><center>nginx</center>
</body>
</html>

上面的兩個斜槓只是爲了能夠在頁面上顯示。

UDP SOCKET

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)
    }
}

咱們來看一下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)
    }
}

DIAL

Dial()函數的原型以下:

func Dial(net, addr string) (Conn, error)

其中net參數是網絡協議的名字,addr參數是IP地址或域名,而端口號以「:」的形式跟隨在地址或域名的後面,端口號可選。若是鏈接成功,返回鏈接對象,不然返回error。 咱們來看一下幾種常見協議的調用方式。 TCP連接:

conn, err := net.Dial("tcp", "192.168.0.10:2100")

UDP連接:

conn, err := net.Dial("udp", "192.168.0.12:975")

ICMP連接(使用協議名稱):

conn, err := net.Dial("ip4:icmp", "www.baidu.com")

ICMP連接(使用協議編號):

conn, err := net.Dial("ip4:1", "10.0.0.3")

目前,Dial()函數支持以下幾種網絡協議:"tcp"、"tcp4"(僅限IPv4)、"tcp6"(僅限IPv6)、"udp"、"udp4"(僅限IPv4)、"udp6"(僅限IPv6)、"ip"、"ip4"(僅限IPv4)和"ip6" (僅限IPv6)。

在成功創建鏈接後,咱們就能夠進行數據的發送和接收。發送數據時,使用conn的Write()成員方法,接收數據時使用Read()方法

WebSocket

WebSocket採用了一些特殊的報頭,使得瀏覽器和服務器只須要作一個握手的動做,就能夠在瀏覽器和服務器之間創建一條鏈接通道。且此鏈接會保持在活動狀態,你可使用JavaScript來向鏈接寫入或從中接收數據,就像在使用一個常規的TCP Socket同樣。它解決了Web實時化的問題,相比傳統HTTP有以下好處:

  • 一個Web客戶端只創建一個TCP鏈接
  • Websocket服務端能夠推送(push)數據到web客戶端.
  • 有更加輕量級的頭,減小數據傳送量

WebSocket URL的起始輸入是ws://或是wss://(在SSL上)。

WebSocket原理

WebSocket的協議頗爲簡單,在第一次handshake經過之後,鏈接便創建成功,其後的通信數據都是以」\x00″開頭,以」\xFF」結尾。在客戶端,這個是透明的,WebSocket組件會自動將原始數據「掐頭去尾」。

瀏覽器發出WebSocket鏈接請求,而後服務器發出迴應,而後鏈接創建成功,這個過程一般稱爲「握手」 (handshaking)。請看下面的請求和反饋信息:

websocket請求

在請求中的"Sec-WebSocket-Key"是隨機的,對於成天跟編碼打交到的程序員,一眼就能夠看出來:這個是一個通過base64編碼後的數據。服務器端接收到這個請求以後須要把這個字符串鏈接上一個固定的字符串:

258EAFA5-E914-47DA-95CA-C5AB0DC85B11

即:f7cb4ezEAl6C3wRaU6JORA==鏈接上那一串固定字符串,生成一個這樣的字符串:

f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

對該字符串先用 sha1安全散列算法計算出二進制的值,而後用base64對其進行編碼,便可以獲得握手後的字符串:

rE91AJhfC+6JdVcVXOGJEADEJdQ=

將之做爲響應頭Sec-WebSocket-Accept的值反饋給客戶端。

GO中的WebSocket

Go語言標準包裏面沒有提供對WebSocket的支持,可是在由官方維護的go.net子包中有對這個的支持,你能夠經過以下的命令獲取該包:

go get code.google.com/p/go.net/websocket

更豐富的網絡通訊

實際上,Dial()函數是對DialTCP()、DialUDP()、DialIP()和DialUnix()的封裝。咱們也能夠直接調用這些函數,它們的功能是一致的。這些函數的原型以下:

func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err error) 
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err error) 
func DialIP(netProto string, laddr, raddr *IPAddr) (*IPConn, error) 
func DialUnix(net string, laddr, raddr *UnixAddr) (c *UnixConn, err error)

上面TCP示例也可使用下面的代碼來實現:

import (
	"fmt"
	"io/ioutil"
	"net"
)
	service := "www.csdn.net:80"
	tcpAddr, err := net.ResolveTCPAddr("tcp4", service) // 解析地址和端口
	checkError2(err)

	conn, err := net.DialTCP("tcp", nil, tcpAddr) // 創建鏈接,參2表示本地地址,參3表示遠程地址
	checkError2(err)

	_, err = conn.Write([]byte("HEAR / HTTP/1.0\r\n\r\n")) // 寫數據
	checkError2(err)

	result, err := ioutil.ReadAll(conn) // 調用工具類,用Buffer來讀
	checkError2(err)

	fmt.Println(string(result))

工具函數

驗證IP地址有效性

import (
	"fmt"
	"net"
)

func Othertest_main() {
	ip := net.ParseIP("127.0.0.1")
	fmt.Println(ip.String()) // 127.0.0.1

	ip = net.ParseIP("123")
	fmt.Println(ip) // <nil>
}

建立子網掩碼

mask := net.IPv4Mask(255, 255, 0, 0)
fmt.Println(mask) // ffff0000

IP地址的默認子網掩碼

ip := net.ParseIP("127.0.0.1")
mask := ip.DefaultMask()
fmt.Println(mask) // ff000000

根據域名得到IP地址

addr := "www.baidu.com"
ipaddr, err := net.ResolveIPAddr("ip", addr)
if err != nil {
	fmt.Println(err)
} else {
	fmt.Println(ipaddr) // 61.135.169.121
}

addrs, err := net.LookupHost("www.baidu.com")
if err != nil {
	fmt.Println(err)
	return
}
for i, v := range addrs {
	fmt.Println(i, "=>", v)
}
//0 => 61.135.169.121
//1 => 61.135.169.125

獲取本機地址

conn, err := net.Dial("udp", "baidu.com:80")
	if err != nil {
		fmt.Println(err)
		return ""
	}
	defer conn.Close()
	ipPort := conn.LocalAddr().String()
	return strings.Split(ipPort, ":")[0]
相關文章
相關標籤/搜索