在很多底層網(wǎng)絡(luò)應(yīng)用開(kāi)發(fā)者的眼里一切編程都是Socket,話雖然有點(diǎn)夸張,但卻也幾乎如此了,現(xiàn)在的網(wǎng)絡(luò)編程幾乎都是用Socket來(lái)編程。你想過(guò)這些情景么?我們每天打開(kāi)瀏覽器瀏覽網(wǎng)頁(yè)時(shí),瀏覽器進(jìn)程怎么和Web服務(wù)器進(jìn)行通信的呢?當(dāng)你用QQ聊天時(shí),QQ進(jìn)程怎么和服務(wù)器或者是你的好友所在的QQ進(jìn)程進(jìn)行通信的呢?當(dāng)你打開(kāi)PPstream觀看視頻時(shí),PPstream進(jìn)程如何與視頻服務(wù)器進(jìn)行通信的呢? 如此種種,都是靠Socket來(lái)進(jìn)行通信的,以一斑窺全豹,可見(jiàn)Socket編程在現(xiàn)代編程中占據(jù)了多么重要的地位,這一節(jié)我們將介紹Go語(yǔ)言中如何進(jìn)行Socket編程。
Socket起源于Unix,而Unix基本哲學(xué)之一就是“一切皆文件”,都可以用“打開(kāi)open –> 讀寫write/read –> 關(guān)閉close”模式來(lái)操作。Socket就是該模式的一個(gè)實(shí)現(xiàn),網(wǎng)絡(luò)的Socket數(shù)據(jù)傳輸是一種特殊的I/O,Socket也是一種文件描述符。Socket也具有一個(gè)類似于打開(kāi)文件的函數(shù)調(diào)用:Socket(),該函數(shù)返回一個(gè)整型的Socket描述符,隨后的連接建立、數(shù)據(jù)傳輸?shù)炔僮鞫际峭ㄟ^(guò)該Socket實(shí)現(xiàn)的。
常用的Socket類型有兩種:流式Socket(SOCK_STREAM)和數(shù)據(jù)報(bào)式Socket(SOCK_DGRAM)。流式是一種面向連接的Socket,針對(duì)于面向連接的TCP服務(wù)應(yīng)用;數(shù)據(jù)報(bào)式Socket是一種無(wú)連接的Socket,對(duì)應(yīng)于無(wú)連接的UDP服務(wù)應(yīng)用。
網(wǎng)絡(luò)中的進(jìn)程之間如何通過(guò)Socket通信呢?首要解決的問(wèn)題是如何唯一標(biāo)識(shí)一個(gè)進(jìn)程,否則通信無(wú)從談起!在本地可以通過(guò)進(jìn)程PID來(lái)唯一標(biāo)識(shí)一個(gè)進(jìn)程,但是在網(wǎng)絡(luò)中這是行不通的。其實(shí)TCP/IP協(xié)議族已經(jīng)幫我們解決了這個(gè)問(wèn)題,網(wǎng)絡(luò)層的“ip地址”可以唯一標(biāo)識(shí)網(wǎng)絡(luò)中的主機(jī),而傳輸層的“協(xié)議+端口”可以唯一標(biāo)識(shí)主機(jī)中的應(yīng)用程序(進(jìn)程)。這樣利用三元組(ip地址,協(xié)議,端口)就可以標(biāo)識(shí)網(wǎng)絡(luò)的進(jìn)程了,網(wǎng)絡(luò)中需要互相通信的進(jìn)程,就可以利用這個(gè)標(biāo)志在他們之間進(jìn)行交互。請(qǐng)看下面這個(gè)TCP/IP協(xié)議結(jié)構(gòu)圖
http://wiki.jikexueyuan.com/project/go-web-programming/images/8.1.socket.png" alt="" />
圖8.1 七層網(wǎng)絡(luò)協(xié)議圖
使用TCP/IP協(xié)議的應(yīng)用程序通常采用應(yīng)用編程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已經(jīng)被淘汰),來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)進(jìn)程之間的通信。就目前而言,幾乎所有的應(yīng)用程序都是采用socket,而現(xiàn)在又是網(wǎng)絡(luò)時(shí)代,網(wǎng)絡(luò)中進(jìn)程通信是無(wú)處不在,這就是為什么說(shuō)“一切皆Socket”。
通過(guò)上面的介紹我們知道Socket有兩種:TCP Socket和UDP Socket,TCP和UDP是協(xié)議,而要確定一個(gè)進(jìn)程的需要三元組,需要IP地址和端口。
目前的全球因特網(wǎng)所采用的協(xié)議族是TCP/IP協(xié)議。IP是TCP/IP協(xié)議中網(wǎng)絡(luò)層的協(xié)議,是TCP/IP協(xié)議族的核心協(xié)議。目前主要采用的IP協(xié)議的版本號(hào)是4(簡(jiǎn)稱為IPv4),發(fā)展至今已經(jīng)使用了30多年。
IPv4的地址位數(shù)為32位,也就是最多有2的32次方的網(wǎng)絡(luò)設(shè)備可以聯(lián)到Internet上。近十年來(lái)由于互聯(lián)網(wǎng)的蓬勃發(fā)展,IP位址的需求量愈來(lái)愈大,使得IP位址的發(fā)放愈趨緊張,前一段時(shí)間,據(jù)報(bào)道IPV4的地址已經(jīng)發(fā)放完畢,我們公司目前很多服務(wù)器的IP都是一個(gè)寶貴的資源。
地址格式類似這樣:127.0.0.1 172.122.121.111
IPv6是下一版本的互聯(lián)網(wǎng)協(xié)議,也可以說(shuō)是下一代互聯(lián)網(wǎng)的協(xié)議,它是為了解決IPv4在實(shí)施過(guò)程中遇到的各種問(wèn)題而被提出的,IPv6采用128位地址長(zhǎng)度,幾乎可以不受限制地提供地址。按保守方法估算IPv6實(shí)際可分配的地址,整個(gè)地球的每平方米面積上仍可分配1000多個(gè)地址。在IPv6的設(shè)計(jì)過(guò)程中除了一勞永逸地解決了地址短缺問(wèn)題以外,還考慮了在IPv4中解決不好的其它問(wèn)題,主要有端到端IP連接、服務(wù)質(zhì)量(QoS)、安全性、多播、移動(dòng)性、即插即用等。
地址格式類似這樣:2002:c0e8:82e7:0:0:0:c0e8:82e7
在Go的net包中定義了很多類型、函數(shù)和方法用來(lái)網(wǎng)絡(luò)編程,其中IP的定義如下:
type IP []byte
在net包中有很多函數(shù)來(lái)操作IP,但是其中比較有用的也就幾個(gè),其中ParseIP(s string) IP函數(shù)會(huì)把一個(gè)IPv4或者IPv6的地址轉(zhuǎn)化成IP類型,請(qǐng)看下面的例子:
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í)行之后你就會(huì)發(fā)現(xiàn)只要你輸入一個(gè)IP地址就會(huì)給出相應(yīng)的IP格式
當(dāng)我們知道如何通過(guò)網(wǎng)絡(luò)端口訪問(wèn)一個(gè)服務(wù)時(shí),那么我們能夠做什么呢?作為客戶端來(lái)說(shuō),我們可以通過(guò)向遠(yuǎn)端某臺(tái)機(jī)器的的某個(gè)網(wǎng)絡(luò)端口發(fā)送一個(gè)請(qǐng)求,然后得到在機(jī)器的此端口上監(jiān)聽(tīng)的服務(wù)反饋的信息。作為服務(wù)端,我們需要把服務(wù)綁定到某個(gè)指定端口,并且在此端口上監(jiān)聽(tīng),當(dāng)有客戶端來(lái)訪問(wèn)時(shí)能夠讀取信息并且寫入反饋信息。
在Go語(yǔ)言的net包中有一個(gè)類型TCPConn,這個(gè)類型可以用來(lái)作為客戶端和服務(wù)器端交互的通道,他有兩個(gè)主要的函數(shù):
func (c *TCPConn) Write(b []byte) (n int, err os.Error)
func (c *TCPConn) Read(b []byte) (n int, err os.Error)
TCPConn可以用在客戶端和服務(wù)器端來(lái)讀寫數(shù)據(jù)。
還有我們需要知道一個(gè)TCPAddr類型,他表示一個(gè)TCP的地址信息,他的定義如下:
type TCPAddr struct {
IP IP
Port int
}
在Go語(yǔ)言中通過(guò)ResolveTCPAddr獲取一個(gè)TCPAddr
func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
Go語(yǔ)言中通過(guò)net包中的DialTCP函數(shù)來(lái)建立一個(gè)TCP連接,并返回一個(gè)TCPConn類型的對(duì)象,當(dāng)連接建立時(shí)服務(wù)器端也創(chuàng)建一個(gè)同類型的對(duì)象,此時(shí)客戶端和服務(wù)器段通過(guò)各自擁有的TCPConn對(duì)象來(lái)進(jìn)行數(shù)據(jù)交換。一般而言,客戶端通過(guò)TCPConn對(duì)象將請(qǐng)求信息發(fā)送到服務(wù)器端,讀取服務(wù)器端響應(yīng)的信息。服務(wù)器端讀取并解析來(lái)自客戶端的請(qǐng)求,并返回應(yīng)答信息,這個(gè)連接只有當(dāng)任一端關(guān)閉了連接之后才失效,不然這連接可以一直在使用。建立連接的函數(shù)定義如下:
func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error)
接下來(lái)我們寫一個(gè)簡(jiǎn)單的例子,模擬一個(gè)基于HTTP協(xié)議的客戶端請(qǐng)求去連接一個(gè)Web服務(wù)端。我們要寫一個(gè)簡(jiǎn)單的http請(qǐng)求頭,格式類似如下:
"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)
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)
}
}
通過(guò)上面的代碼我們可以看出:首先程序?qū)⒂脩舻妮斎胱鳛閰?shù)service傳入net.ResolveTCPAddr獲取一個(gè)tcpAddr,然后把tcpAddr傳入DialTCP后創(chuàng)建了一個(gè)TCP連接conn,通過(guò)conn來(lái)發(fā)送請(qǐng)求信息,最后通過(guò)ioutil.ReadAll從conn中讀取全部的文本,也就是服務(wù)端響應(yīng)反饋的信息。
上面我們編寫了一個(gè)TCP的客戶端程序,也可以通過(guò)net包來(lái)創(chuàng)建一個(gè)服務(wù)器端程序,在服務(wù)器端我們需要綁定服務(wù)到指定的非激活端口,并監(jiān)聽(tīng)此端口,當(dāng)有客戶端請(qǐng)求到達(dá)的時(shí)候可以接收到來(lái)自客戶端連接的請(qǐng)求。net包中有相應(yīng)功能的函數(shù),函數(shù)定義如下:
func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
func (l *TCPListener) Accept() (c Conn, err os.Error)
參數(shù)說(shuō)明同DialTCP的參數(shù)一樣。下面我們實(shí)現(xiàn)一個(gè)簡(jiǎn)單的時(shí)間同步服務(wù),監(jiān)聽(tīng)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ù)跑起來(lái)之后,它將會(huì)一直在那里等待,直到有新的客戶端請(qǐng)求到達(dá)。當(dāng)有新的客戶端請(qǐng)求到達(dá)并同意接受Accept該請(qǐng)求的時(shí)候他會(huì)反饋當(dāng)前的時(shí)間信息。值得注意的是,在代碼中for循環(huán)里,當(dāng)有錯(cuò)誤發(fā)生時(shí),直接continue而不是退出,是因?yàn)樵诜?wù)器端跑代碼的時(shí)候,當(dāng)有錯(cuò)誤發(fā)生的情況下最好是由服務(wù)端記錄錯(cuò)誤,然后當(dāng)前連接的客戶端直接報(bào)錯(cuò)而退出,從而不會(huì)影響到當(dāng)前服務(wù)端運(yùn)行的整個(gè)服務(wù)。
上面的代碼有個(gè)缺點(diǎn),執(zhí)行的時(shí)候是單任務(wù)的,不能同時(shí)接收多個(gè)請(qǐng)求,那么該如何改造以使它支持多并發(fā)呢?Go里面有一個(gè)goroutine機(jī)制,請(qǐng)看下面改造后的代碼
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)
}
}
通過(guò)把業(yè)務(wù)處理分離到函數(shù)handleClient,我們就可以進(jìn)一步地實(shí)現(xiàn)多并發(fā)執(zhí)行了??瓷先ナ遣皇呛軒?,增加go關(guān)鍵詞就實(shí)現(xiàn)了服務(wù)端的多并發(fā),從這個(gè)小例子也可以看出goroutine的強(qiáng)大之處。
有的朋友可能要問(wèn):這個(gè)服務(wù)端沒(méi)有處理客戶端實(shí)際請(qǐng)求的內(nèi)容。如果我們需要通過(guò)從客戶端發(fā)送不同的請(qǐng)求來(lái)獲取不同的時(shí)間格式,而且需要一個(gè)長(zhǎng)連接,該怎么做呢?請(qǐng)看:
package main
import (
"fmt"
"net"
"os"
"time"
"strconv"
)
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 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
}
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
在上面這個(gè)例子中,我們使用conn.Read()不斷讀取客戶端發(fā)來(lái)的請(qǐng)求。由于我們需要保持與客戶端的長(zhǎng)連接,所以不能在讀取完一次請(qǐng)求后就關(guān)閉連接。由于conn.SetReadDeadline()設(shè)置了超時(shí),當(dāng)一定時(shí)間內(nèi)客戶端無(wú)請(qǐng)求發(fā)送,conn便會(huì)自動(dòng)關(guān)閉,下面的for循環(huán)即會(huì)因?yàn)檫B接已關(guān)閉而跳出。需要注意的是,request在創(chuàng)建時(shí)需要指定一個(gè)最大長(zhǎng)度以防止flood attack;每次讀取到請(qǐng)求處理完畢后,需要清理request,因?yàn)?code>conn.Read()會(huì)將新讀取到的內(nèi)容append到原內(nèi)容之后。
TCP有很多連接控制函數(shù),我們平常用到比較多的有如下幾個(gè)函數(shù):
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)
設(shè)置建立連接的超時(shí)時(shí)間,客戶端和服務(wù)器端都適用,當(dāng)超過(guò)設(shè)置時(shí)間時(shí),連接自動(dòng)關(guān)閉。
func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error
用來(lái)設(shè)置寫入/讀取一個(gè)連接的超時(shí)時(shí)間。當(dāng)超過(guò)設(shè)置時(shí)間時(shí),連接自動(dòng)關(guān)閉。
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error
設(shè)置客戶端是否和服務(wù)器端保持長(zhǎng)連接,可以降低建立TCP連接時(shí)的握手開(kāi)銷,對(duì)于一些需要頻繁交換數(shù)據(jù)的應(yīng)用場(chǎng)景比較適用。
更多的內(nèi)容請(qǐng)查看net包的文檔。
Go語(yǔ)言包中處理UDP Socket和TCP Socket不同的地方就是在服務(wù)器端處理多個(gè)客戶端請(qǐng)求數(shù)據(jù)包的方式不同,UDP缺少了對(duì)客戶端連接請(qǐng)求的Accept函數(shù)。其他基本幾乎一模一樣,只有TCP換成了UDP而已。UDP的幾個(gè)主要函數(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)
一個(gè)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)
}
}
我們來(lái)看一下UDP服務(wù)器端如何來(lái)處理:
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)
}
}
通過(guò)對(duì)TCP和UDP Socket編程的描述和實(shí)現(xiàn),可見(jiàn)Go已經(jīng)完備地支持了Socket編程,而且使用起來(lái)相當(dāng)?shù)姆奖?,Go提供了很多函數(shù),通過(guò)這些函數(shù)可以很容易就編寫出高性能的Socket應(yīng)用。