您好,登錄后才能下訂單哦!
這篇開始講網絡編程。不過網絡編程的內容過于龐大,這里主要講socket。而socket可以講的東西也太多了,因此,這里只圍繞Go語言介紹一些它的基礎知識。
所謂socket,是一種IPC(Inter-Process Communication)方法,可以被翻譯為進程間通信。顧名思義,IPC這個概念(或者說規范)主要定義的是多個進程之間,相互通信的方法。這些方法主要包括:
現存的主要操作系統大都對IPC提供了強有力的支持,尤其是socket。
socket,常被稱作套接字,它是網絡編程世界中最為核心的知識之一。
毫不夸張的說,在眾多IPC方法中,socket是最為通用和靈活的一種。與其他的IPC方法不同,利用socket進行通信的進程,可以不局限在同一臺計算機當中。通信雙方只要能夠通過網絡進行互聯,就可以使用socket。
支持socket的操作系統一般都會對外提供一套API。跑在它們之上的應用程序,利用這套API就可以與互聯網上的另一臺計算機中的程序、同一臺計算機中的其他程序,甚至同一個程序中的其他線程進行通信。例如,在Linux操作系統中,用于創建socket實例的API,就是由一個名為socket的系統調用代表的。這個系統調用是Linux內核的一部分。所謂的系統調用,你可以理解為特殊的C語言函數。它們是連接應用程序和操作系統內核的橋梁,也是應用程序使用操作系統功能的唯一渠道。
在Go語言標準庫的syscall包中,有一個與這個socket系統調用相對應的函數。這兩者的函數簽名是基本一致的,它們都會接受三個int類型的參數,并會返回一個可以代表文件描述符的結果。但不同的是,syscall包中的Socket函數本身是平臺不相關的。在其底層,Go語言為它支持的每個操作系統都做了適配,這樣這個函數無論在哪個平臺上,總是有效的。
在syscall.Socket函數中的三個參數分別是:
下面,通過這3個參數來了解一下socket的基礎知識。
Socket的通信域主要有3種,分別對應syscall包中的一個常量:
關于IPv4和IPv6就不講了,Unix域簡單提一下。
Unix域,指的是一種類Unix操作系統中特有的通信域。在裝有此類操作系統的同一臺計算機中,應用程序可以基于此域建立socket連接。
Socket的類型一個有4種,在syscall包中有同名的常量對應:
上面的4種類型,前兩個更加常用。
UDP
SOCK_DGRA中的DGRAM就是datagram,即數據報文。它是一種有消息邊界但沒有邏輯連接的非可靠socket類型,UDP協議的網絡通信就是這類。
有消息邊界的意思是,與socket相關的操作系統內核中的程序,即內核程序,在發送或接收數據的時候是以消息為單位的。這里可以把消息理解為帶有固定邊界的一段數據。內核程序可以自動的識別和維護這種邊界。在必要的時候,把數據切割成一個一個的消息,或者把多個消息串接成連續的數據。這樣,應用程序值需要面向消息進行處理就可以了。
只要應用程序指定好對方的網絡地址,內核程序就可以立即把數據報文發送出去。這有優勢也有劣勢。優勢是,發送速度快,不長期占用網絡資源,并且每次發送都可以指定不同的網絡地址。最后一條既是優勢也是劣勢,因為這會使數據報文更長。其他劣勢還有,無法保證傳輸的可靠性,不能實現數據的有序性,以及數據只能單向進行傳輸。
TCP
SOCK_STREAM類型,是沒有消息邊界但有邏輯連接,能夠保證傳輸的可靠性和數據的有序性,同時還可以實現數據的雙向傳輸。TCP協議的網絡通信就是這類。
有邏輯連接是指,通信雙方在收發數據之前必須先建立網絡連接。等連接建立好之后,雙方就可以一對一的進行數據傳輸了。
這樣的網絡通信傳輸數據的形式是字節流,而不是數據報文。字節流是以字節為單位的。內核程序無法感知一段字節流中包含了多少個消息,以及這些消息是否完整,這完全需要應用程序自己來把控。不過,此類網絡通信中的一段,總會忠實的按照另一端發送數據是的字節排列順序,接收和緩存它們。所以,應用程序需要根據雙方的約定去數據中查找消息邊界,并按照邊界切割數據。
通常只要明確指定了前兩個參數值,就無需在去確定這里的使用協議了,一般把它置為0就可以了。這時,內核程序會自行選擇最合適的協議。
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
return
}
defer syscall.Close(fd)
fmt.Println("socket的文件描述符:", fd)
// 之后就省略了,要使用syscall包來建立網絡連接,過程太繁瑣
}
這個代碼包的使用太底層,通常也不需要我們直接使用。Go語言的net包中的很多程序實體,都會直接或間接的使用到syscall.Socket函數,并且無需給定細致的參數。但是,在使用這些API的時候,現在我們就應該知道上面這些基礎知識了。
net.Dial函數會接受兩個參數,network和address,具體看下面:
func Dial(network, address string) (Conn, error) {
var d Dialer
return d.Dial(network, address)
}
參數network常用的可選值一共有9個,這些值分別代表了程序底層創建的socket實例可使用的不同通信協議:
對于http請求,在標準庫里還有更高級的封裝,不過http本質上也是socket,這里展示用net包發送請求的示例:
package main
import (
"fmt"
"io"
"net"
"os"
)
func main() {
conn, err := net.Dial("tcp", "baidu.com:80")
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
return
}
defer conn.Close()
reqStr := "HEAD / HTTP/1.1\r\n" + // HEAD請求,只返回請求頭
"Host: baidu.com\r\n" +
"Connection: close\r\n" + // 返回后,服務器會斷開連接,默認是keep-alive
"\r\n" // 請求頭結束
_, err = io.WriteString(conn, reqStr)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
return
}
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
fmt.Println(string(buf[:n]))
if err != nil {
if err == io.EOF {
fmt.Println("END")
break
} else {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
}
}
}
}
如果是https的請求,還需要借助crypto/tls包,而調用起來基本是一樣的:
package main
import (
"crypto/tls"
"fmt"
"io"
"os"
)
func main() {
tlsConf := &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS10,
}
conn, err := tls.Dial("tcp", "gitee.com:443", tlsConf)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
return
}
defer conn.Close()
reqStr := "HEAD / HTTP/1.1\r\n" + // HEAD請求,只返回請求頭
"Host: gitee.com\r\n" +
"Connection: close\r\n" + // 返回后,服務器會斷開連接,默認是keep-alive
"\r\n" // 請求頭結束
_, err = io.WriteString(conn, reqStr)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
return
}
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
fmt.Println(string(buf[:n]))
if err != nil {
if err == io.EOF {
fmt.Println("END")
break
} else {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
}
}
}
}
net.DialTimeout函數和net.Dial函數相比,多接受了一個參數timeout。而底層實現可以看到是一樣的,只是對Dialer結構體的Timeout字段進行了設置,而在net.Dial函數里結構體都是默認值:
func DialTimeout(network, address string, timeout time.Duration) (Conn, error) {
d := Dialer{Timeout: timeout}
return d.Dial(network, address)
}
這里的超時時間,出函數為網絡連接建立完成而等待的最長時間。
開始的時間點幾乎是調用net.DialTimeout函數的那一刻。在這之后,時間會主要花費在解析參數的network值和address值,以及創建socket實例并建立網絡連接這兩件事情上。如果超時了而網絡連接還沒有建立完成,該函數就會返回一個I/O操作超時的錯誤值。
在解析address的值的時候,函數會確定網絡服務的IP地址、端口號等必要信息,并在需要的時候訪問DNS服務。另外,如果解析出的IP地址有多個,函數會串行或并行的嘗試建立連接。無論用什么方式嘗試,函數總會以最先建立成功的那個連接為準。同時還會根據超時時間的剩余時間去設定對每次連接嘗試的超時時間。
找一個國外的網站,或者干脆找一個連不上的地址,看下超時時間的作用:
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
tStart := time.Now()
conn, err := net.DialTimeout("tcp", "godoc.org:80", time.Second * 10)
tEnd := time.Now()
fmt.Println("連接持續時間:", time.Duration(tEnd.Sub(tStart)))
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
return
}
defer conn.Close()
fmt.Println("本地連接地址:", conn.LocalAddr())
fmt.Println("對端連接地址:", conn.RemoteAddr())
}
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。