Go Socket編程

2022-05-13 17:42 更新

在很多底層網(wǎng)絡(luò)應(yīng)用開發(fā)者的眼里一切編程都是Socket,話雖然有點(diǎn)夸張,但卻也幾乎如此了,現(xiàn)在的網(wǎng)絡(luò)編程幾乎都是用Socket來編程。你想過這些情景么?我們每天打開瀏覽器瀏覽網(wǎng)頁時,瀏覽器進(jìn)程怎么和Web服務(wù)器進(jìn)行通信的呢?當(dāng)你用QQ聊天時,QQ進(jìn)程怎么和服務(wù)器或者是你的好友所在的QQ進(jìn)程進(jìn)行通信的呢?當(dāng)你打開PPstream觀看視頻時,PPstream進(jìn)程如何與視頻服務(wù)器進(jìn)行通信的呢? 如此種種,都是靠Socket來進(jìn)行通信的,以一斑窺全豹,可見Socket編程在現(xiàn)代編程中占據(jù)了多么重要的地位,這一節(jié)我們將介紹Go語言中如何進(jìn)行Socket編程。

什么是Socket?

Socket起源于Unix,而Unix基本哲學(xué)之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關(guān)閉close”模式來操作。Socket就是該模式的一個實(shí)現(xiàn),網(wǎng)絡(luò)的Socket數(shù)據(jù)傳輸是一種特殊的I/O,Socket也是一種文件描述符。Socket也具有一個類似于打開文件的函數(shù)調(diào)用:Socket(),該函數(shù)返回一個整型的Socket描述符,隨后的連接建立、數(shù)據(jù)傳輸?shù)炔僮鞫际峭ㄟ^該Socket實(shí)現(xiàn)的。

常用的Socket類型有兩種:流式Socket(SOCK_STREAM)和數(shù)據(jù)報式Socket(SOCK_DGRAM)。流式是一種面向連接的Socket,針對于面向連接的TCP服務(wù)應(yīng)用;數(shù)據(jù)報式Socket是一種無連接的Socket,對應(yīng)于無連接的UDP服務(wù)應(yīng)用。

Socket如何通信

網(wǎng)絡(luò)中的進(jìn)程之間如何通過Socket通信呢?首要解決的問題是如何唯一標(biāo)識一個進(jìn)程,否則通信無從談起!在本地可以通過進(jìn)程PID來唯一標(biāo)識一個進(jìn)程,但是在網(wǎng)絡(luò)中這是行不通的。其實(shí)TCP/IP協(xié)議族已經(jīng)幫我們解決了這個問題,網(wǎng)絡(luò)層的“ip地址”可以唯一標(biāo)識網(wǎng)絡(luò)中的主機(jī),而傳輸層的“協(xié)議+端口”可以唯一標(biāo)識主機(jī)中的應(yīng)用程序(進(jìn)程)。這樣利用三元組(ip地址,協(xié)議,端口)就可以標(biāo)識網(wǎng)絡(luò)的進(jìn)程了,網(wǎng)絡(luò)中需要互相通信的進(jìn)程,就可以利用這個標(biāo)志在他們之間進(jìn)行交互。請看下面這個TCP/IP協(xié)議結(jié)構(gòu)圖:


使用TCP/IP協(xié)議的應(yīng)用程序通常采用應(yīng)用編程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已經(jīng)被淘汰),來實(shí)現(xiàn)網(wǎng)絡(luò)進(jìn)程之間的通信。就目前而言,幾乎所有的應(yīng)用程序都是采用socket,而現(xiàn)在又是網(wǎng)絡(luò)時代,網(wǎng)絡(luò)中進(jìn)程通信是無處不在,這就是為什么說“一切皆Socket”。

Socket基礎(chǔ)知識

通過上面的介紹我們知道Socket有兩種:TCP Socket和UDP Socket,TCP和UDP是協(xié)議,而要確定一個進(jìn)程的需要三元組,需要IP地址和端口。

IPv4地址

目前的全球因特網(wǎng)所采用的協(xié)議族是TCP/IP協(xié)議。IP是TCP/IP協(xié)議中網(wǎng)絡(luò)層的協(xié)議,是TCP/IP協(xié)議族的核心協(xié)議。目前主要采用的IP協(xié)議的版本號是4(簡稱為IPv4),發(fā)展至今已經(jīng)使用了30多年。

IPv4的地址位數(shù)為32位,也就是最多有2的32次方的網(wǎng)絡(luò)設(shè)備可以聯(lián)到Internet上。近十年來由于互聯(lián)網(wǎng)的蓬勃發(fā)展,IP位址的需求量愈來愈大,使得IP位址的發(fā)放愈趨緊張,前一段時間,據(jù)報道IPV4的地址已經(jīng)發(fā)放完畢,我們公司目前很多服務(wù)器的IP都是一個寶貴的資源。

地址格式類似這樣:127.0.0.1 172.122.121.111

IPv6地址

IPv6是下一版本的互聯(lián)網(wǎng)協(xié)議,也可以說是下一代互聯(lián)網(wǎng)的協(xié)議,它是為了解決IPv4在實(shí)施過程中遇到的各種問題而被提出的,IPv6采用128位地址長度,幾乎可以不受限制地提供地址。按保守方法估算IPv6實(shí)際可分配的地址,整個地球的每平方米面積上仍可分配1000多個地址。在IPv6的設(shè)計過程中除了一勞永逸地解決了地址短缺問題以外,還考慮了在IPv4中解決不好的其它問題,主要有端到端IP連接、服務(wù)質(zhì)量(QoS)、安全性、多播、移動性、即插即用等。

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

Go支持的IP類型

在Go的?net?包中定義了很多類型、函數(shù)和方法用來網(wǎng)絡(luò)編程,其中IP的定義如下:

type IP []byte

net包中有很多函數(shù)來操作IP,但是其中比較有用的也就幾個,其中ParseIP(s string) IP函數(shù)會把一個IPv4或者IPv6的地址轉(zhuǎn)化成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)
}

執(zhí)行之后你就會發(fā)現(xiàn)只要你輸入一個IP地址就會給出相應(yīng)的IP格式

TCP Socket

當(dāng)我們知道如何通過網(wǎng)絡(luò)端口訪問一個服務(wù)時,那么我們能夠做什么呢?作為客戶端來說,我們可以通過向遠(yuǎn)端某臺機(jī)器的的某個網(wǎng)絡(luò)端口發(fā)送一個請求,然后得到在機(jī)器的此端口上監(jiān)聽的服務(wù)反饋的信息。作為服務(wù)端,我們需要把服務(wù)綁定到某個指定端口,并且在此端口上監(jiān)聽,當(dāng)有客戶端來訪問時能夠讀取信息并且寫入反饋信息。

在Go語言的net包中有一個類型TCPConn,這個類型可以用來作為客戶端和服務(wù)器端交互的通道,他有兩個主要的函數(shù):

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

?TCPConn?可以用在客戶端和服務(wù)器端來讀寫數(shù)據(jù)。

還有我們需要知道一個?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參數(shù)是"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函數(shù)來建立一個TCP連接,并返回一個TCPConn類型的對象,當(dāng)連接建立時服務(wù)器端也創(chuàng)建一個同類型的對象,此時客戶端和服務(wù)器端通過各自擁有的TCPConn對象來進(jìn)行數(shù)據(jù)交換。一般而言,客戶端通過TCPConn對象將請求信息發(fā)送到服務(wù)器端,讀取服務(wù)器端響應(yīng)的信息。服務(wù)器端讀取并解析來自客戶端的請求,并返回應(yīng)答信息,這個連接只有當(dāng)任一端關(guān)閉了連接之后才失效,不然這連接可以一直在使用。建立連接的函數(shù)定義如下:

func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
    
  • network參數(shù)是"tcp4"、"tcp6"、"tcp"中的任意一個,分別表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一個)
  • laddr表示本機(jī)地址,一般設(shè)置為nil
  • raddr表示遠(yuǎn)程的服務(wù)地址

接下來我們寫一個簡單的例子,模擬一個基于HTTP協(xié)議的客戶端請求去連接一個Web服務(wù)端。我們要寫一個簡單的http請求頭,格式類似如下:

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

從服務(wù)端接收到的響應(yīng)信息格式可能如下:

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)
	result := make([]byte, 256)
	_, err = conn.Read(result)
	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)
	}
}

通過上面的代碼我們可以看出:首先程序?qū)⒂脩舻妮斎胱鳛閰?shù)service傳入net.ResolveTCPAddr獲取一個tcpAddr,然后把tcpAddr傳入DialTCP后創(chuàng)建了一個TCP連接conn,通過conn來發(fā)送請求信息,最后通過ioutil.ReadAllconn中讀取全部的文本,也就是服務(wù)端響應(yīng)反饋的信息。

TCP server

上面我們編寫了一個TCP的客戶端程序,也可以通過net包來創(chuàng)建一個服務(wù)器端程序,在服務(wù)器端我們需要綁定服務(wù)到指定的非激活端口,并監(jiān)聽此端口,當(dāng)有客戶端請求到達(dá)的時候可以接收到來自客戶端連接的請求。net包中有相應(yīng)功能的函數(shù),函數(shù)定義如下:

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

參數(shù)說明同DialTCP的參數(shù)一樣。下面我們實(shí)現(xiàn)一個簡單的時間同步服務(wù),監(jiān)聽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)
	}
}

上面的服務(wù)跑起來之后,它將會一直在那里等待,直到有新的客戶端請求到達(dá)。當(dāng)有新的客戶端請求到達(dá)并同意接受?Accept?該請求的時候他會反饋當(dāng)前的時間信息。值得注意的是,在代碼中?for?循環(huán)里,當(dāng)有錯誤發(fā)生時,直接continue而不是退出,是因?yàn)樵诜?wù)器端跑代碼的時候,當(dāng)有錯誤發(fā)生的情況下最好是由服務(wù)端記錄錯誤,然后當(dāng)前連接的客戶端直接報錯而退出,從而不會影響到當(dāng)前服務(wù)端運(yùn)行的整個服務(wù)。

上面的代碼有個缺點(diǎn),執(zhí)行的時候是單任務(wù)的,不能同時接收多個請求,那么該如何改造以使它支持多并發(fā)呢?Go里面有一個goroutine機(jī)制,請看下面改造后的代碼

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

通過把業(yè)務(wù)處理分離到函數(shù)?handleClient?,我們就可以進(jìn)一步地實(shí)現(xiàn)多并發(fā)執(zhí)行了??瓷先ナ遣皇呛軒?,增加?go?關(guān)鍵詞就實(shí)現(xiàn)了服務(wù)端的多并發(fā),從這個小例子也可以看出goroutine的強(qiáng)大之處。

有的朋友可能要問:這個服務(wù)端沒有處理客戶端實(shí)際請求的內(nèi)容。如果我們需要通過從客戶端發(fā)送不同的請求來獲取不同的時間格式,而且需要一個長連接,該怎么做呢?請看:

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()不斷讀取客戶端發(fā)來的請求。由于我們需要保持與客戶端的長連接,所以不能在讀取完一次請求后就關(guān)閉連接。由于conn.SetReadDeadline()設(shè)置了超時,當(dāng)一定時間內(nèi)客戶端無請求發(fā)送,conn便會自動關(guān)閉,下面的for循環(huán)即會因?yàn)檫B接已關(guān)閉而跳出。需要注意的是,request在創(chuàng)建時需要指定一個最大長度以防止flood attack;每次讀取到請求處理完畢后,需要清理request,因?yàn)?code>conn.Read()會將新讀取到的內(nèi)容append到原內(nèi)容之后。

控制TCP連接

TCP有很多連接控制函數(shù),我們平常用到比較多的有如下幾個函數(shù):

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

設(shè)置建立連接的超時時間,客戶端和服務(wù)器端都適用,當(dāng)超過設(shè)置時間時,連接自動關(guān)閉。

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

用來設(shè)置寫入/讀取一個連接的超時時間。當(dāng)超過設(shè)置時間時,連接自動關(guān)閉。

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

設(shè)置keepAlive屬性。操作系統(tǒng)層在tcp上沒有數(shù)據(jù)和ACK的時候,會間隔性的發(fā)送keepalive包,操作系統(tǒng)可以通過該包來判斷一個tcp連接是否已經(jīng)斷開,在windows上默認(rèn)2個小時沒有收到數(shù)據(jù)和keepalive包的時候認(rèn)為tcp連接已經(jīng)斷開,這個功能和我們通常在應(yīng)用層加的心跳包的功能類似。

更多的內(nèi)容請查看?net?包的文檔。

UDP Socket

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

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 %s", err.Error())
		os.Exit(1)
	}
}

我們來看一下UDP服務(wù)器端如何來處理:

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 %s", err.Error())
		os.Exit(1)
	}
}

總結(jié)

通過對TCP和UDP Socket編程的描述和實(shí)現(xiàn),可見Go已經(jīng)完備地支持了Socket編程,而且使用起來相當(dāng)?shù)姆奖?,Go提供了很多函數(shù),通過這些函數(shù)可以很容易就編寫出高性能的Socket應(yīng)用。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號