在很多底層網(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起源于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)用。
網(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有兩種:TCP Socket和UDP Socket,TCP和UDP是協(xié)議,而要確定一個進(jìn)程的需要三元組,需要IP地址和端口。
目前的全球因特網(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是下一版本的互聯(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的?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格式
當(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)
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)
接下來我們寫一個簡單的例子,模擬一個基于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.ReadAll
從conn
中讀取全部的文本,也就是服務(wù)端響應(yīng)反饋的信息。
上面我們編寫了一個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有很多連接控制函數(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
?包的文檔。
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)
}
}
通過對TCP和UDP Socket編程的描述和實(shí)現(xiàn),可見Go已經(jīng)完備地支持了Socket編程,而且使用起來相當(dāng)?shù)姆奖?,Go提供了很多函數(shù),通過這些函數(shù)可以很容易就編寫出高性能的Socket應(yīng)用。
更多建議: