socket簡單介紹—套接字編程
什么是Socket
Socket,英文含義是【插座、插孔】,一般稱之為套接字,用于描述IP地址和端口??梢詫?shí)現(xiàn)不同程序間的數(shù)據(jù)通信。
Socket起源于Unix,而Unix基本哲學(xué)之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關(guān)閉close”模式來操作。Socket就是該模式的一個(gè)實(shí)現(xiàn),網(wǎng)絡(luò)的Socket數(shù)據(jù)傳輸是一種特殊的I/O,Socket也是一種文件描述符。Socket也具有一個(gè)類似于打開文件的函數(shù)調(diào)用:Socket(),該函數(shù)返回一個(gè)整型的Socket描述符,隨后的連接建立、數(shù)據(jù)傳輸?shù)炔僮鞫际峭ㄟ^該Socket實(shí)現(xiàn)的。
在TCP/IP協(xié)議中,“IP地址+TCP或UDP端口號”唯一標(biāo)識網(wǎng)絡(luò)通訊中的一個(gè)進(jìn)程?!癐P地址+端口號”就對應(yīng)一個(gè)socket。欲建立連接的兩個(gè)進(jìn)程各自有一個(gè)socket來標(biāo)識,那么這兩個(gè)socket組成的socket pair(套接字對)就唯一標(biāo)識一個(gè)連接。因此可以用Socket來描述網(wǎng)絡(luò)連接的一對一關(guān)系。
常用的Socket類型有兩種:流式Socket(SOCK_STREAM)和數(shù)據(jù)報(bào)式Socket(SOCK_DGRAM)。流式是一種面向連接的Socket,針對于面向連接的TCP服務(wù)應(yīng)用;數(shù)據(jù)報(bào)式Socket是一種無連接的Socket,對應(yīng)于無連接的UDP服務(wù)應(yīng)用。
網(wǎng)絡(luò)應(yīng)用程序設(shè)計(jì)模式
C/S模式
傳統(tǒng)的網(wǎng)絡(luò)應(yīng)用設(shè)計(jì)模式,客戶機(jī)(client)/服務(wù)器(server)模式。需要在通訊兩端各自部署客戶機(jī)和服務(wù)器來完成數(shù)據(jù)通信。
B/S模式
瀏覽器(Browser)/服務(wù)器(Server)模式。只需在一端部署服務(wù)器,而另外一端使用每臺PC都默認(rèn)配置的瀏覽器即可完成數(shù)據(jù)的傳輸。
優(yōu)缺點(diǎn)
對于C/S模式來說,其優(yōu)點(diǎn)明顯??蛻舳宋挥谀繕?biāo)主機(jī)上可以保證性能,將數(shù)據(jù)緩存至客戶端本地,從而提高數(shù)據(jù)傳輸效率。且,一般來說客戶端和服務(wù)器程序由一個(gè)開發(fā)團(tuán)隊(duì)創(chuàng)作,所以他們之間所采用的協(xié)議相對靈活。可以在標(biāo)準(zhǔn)協(xié)議的基礎(chǔ)上根據(jù)需求裁剪及定制。例如,騰訊所采用的通信協(xié)議,即為ftp協(xié)議的修改剪裁版。
因此,傳統(tǒng)的網(wǎng)絡(luò)應(yīng)用程序及較大型的網(wǎng)絡(luò)應(yīng)用程序都首選C/S模式進(jìn)行開發(fā)。如,知名的網(wǎng)絡(luò)游戲魔獸世界。3D畫面,數(shù)據(jù)量龐大,使用C/S模式可以提前在本地進(jìn)行大量數(shù)據(jù)的緩存處理,從而提高觀感。
C/S模式的缺點(diǎn)也較突出。由于客戶端和服務(wù)器都需要有一個(gè)開發(fā)團(tuán)隊(duì)來完成開發(fā)。工作量將成倍提升,開發(fā)周期較長。另外,從用戶角度出發(fā),需要將客戶端安插至用戶主機(jī)上,對用戶主機(jī)的安全性構(gòu)成威脅。這也是很多用戶不愿使用C/S模式應(yīng)用程序的重要原因。
B/S模式相比C/S模式而言,由于它沒有獨(dú)立的客戶端,使用標(biāo)準(zhǔn)瀏覽器作為客戶端,其工作開發(fā)量較小。只需開發(fā)服務(wù)器端即可。另外由于其采用瀏覽器顯示數(shù)據(jù),因此移植性非常好,不受平臺限制。如早期的偷菜游戲,在各個(gè)平臺上都可以完美運(yùn)行。
B/S模式的缺點(diǎn)也較明顯。由于使用第三方瀏覽器,因此網(wǎng)絡(luò)應(yīng)用支持受限。另外,沒有客戶端放到對方主機(jī)上,緩存數(shù)據(jù)不盡如人意,從而傳輸數(shù)據(jù)量受到限制。應(yīng)用的觀感大打折扣。第三,必須與瀏覽器一樣,采用標(biāo)準(zhǔn)http協(xié)議進(jìn)行通信,協(xié)議選擇不靈活。
因此在開發(fā)過程中,模式的選擇由上述各自的特點(diǎn)決定。根據(jù)實(shí)際需求選擇應(yīng)用程序設(shè)計(jì)模式。
TCP的C/S架構(gòu)
TCP服務(wù)器代碼編寫
package main
import (
"fmt"
"net"
)
func main() {
//監(jiān)聽nerwork為tcp和udp,address為ip:端口,本地ip地址可以不寫
listener, err1 := net.Listen("tcp", "127.0.0.1:8080")
if err1 != nil {
fmt.Println("err =", err1)
return
}
defer listener.Close() //監(jiān)聽關(guān)閉
//阻塞等待用戶連接
conn, err := listener.Accept()
if err != nil {
fmt.Println("阻塞:err = ", err)
return
}
//接收用戶請求
buf := make([]byte, 1024) //1024大小的緩沖區(qū)
n, err2 := conn.Read(buf)
if err2 != nil {
fmt.Println("接收: err2 = ", err2)
return
}
fmt.Println("buf = ", string(buf[:n])) //指定處理的讀多少
defer conn.Close() //關(guān)閉當(dāng)前用戶連接
}
TCP客戶端代碼編寫
package main
import (
"fmt"
"net"
)
func main() {
//主動連接服務(wù)器
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("err = ", err)
return
}
defer conn.Close() //關(guān)閉
//發(fā)送數(shù)據(jù)
conn.Write([]byte("are you ok?"))
}
并發(fā)的C/S模型通信
并發(fā)Server
現(xiàn)在已經(jīng)完成了客戶端與服務(wù)端的通信,但是服務(wù)端只能接收一個(gè)用戶發(fā)送過來的數(shù)據(jù),怎樣接收多個(gè)客戶端發(fā)送過來的數(shù)據(jù),實(shí)現(xiàn)一個(gè)高效的并發(fā)服務(wù)器呢?
Accept()函數(shù)的作用是等待客戶端的鏈接,如果客戶端沒有鏈接,該方法會阻塞。如果有客戶端鏈接,那么該方法返回一個(gè)Socket負(fù)責(zé)與客戶端進(jìn)行通信。所以,每來一個(gè)客戶端,該方法就應(yīng)該返回一個(gè)Socket與其通信,因此,可以使用一個(gè)死循環(huán),將Accept()調(diào)用過程包裹起來。
需要注意的是,實(shí)現(xiàn)并發(fā)處理多個(gè)客戶端數(shù)據(jù)的服務(wù)器,就需要針對每一個(gè)客戶端連接,單獨(dú)產(chǎn)生一個(gè)Socket,并創(chuàng)建一個(gè)單獨(dú)的goroutine與之完成通信。
簡單版并發(fā)服務(wù)器
package main
import (
"fmt"
"net"
"strings"
)
// 處理用戶請求
func HandleConn(conn net.Conn) {
//函數(shù)調(diào)用完畢,自動關(guān)閉conn
defer conn.Close()
//獲取客戶端的網(wǎng)絡(luò)地址信息
addr := conn.RemoteAddr().String()
fmt.Println(addr, "addr conncet sucessful") //連接成功
//讀取用戶數(shù)據(jù)
buf := make([]byte, 2048)
for true {
//讀取用戶數(shù)據(jù)
read, err := conn.Read(buf)
if err != nil {
//read tcp 127.0.0.1:8080->127.0.0.1:56933:
//wsarecv: An existing connection was forcibly closed by the remote host.
fmt.Println("err = ", err)
return
}
fmt.Printf("[%s]: = %s\n", addr, string(buf[:read])) //read為讀取數(shù)據(jù)的個(gè)數(shù)
fmt.Println("len = ", len(string(buf[:read])))
if "exit" == string(buf[:read-1]) {
fmt.Println(addr, "exit")
return
}
//把數(shù)據(jù)轉(zhuǎn)化為大寫,再給用戶發(fā)送
conn.Write([]byte(strings.ToUpper(string(buf[:read]))))
}
}
func main() {
//監(jiān)聽nerwork為tcp和udp,address為ip:端口,本地ip地址可以不寫
listener, err1 := net.Listen("tcp", "127.0.0.1:8080")
if err1 != nil {
fmt.Println("err =", err1)
return
}
defer listener.Close() //監(jiān)聽關(guān)閉
//接收多個(gè)用戶
for true {
conn, err := listener.Accept() //等待客戶端的鏈接,如果客戶端沒有鏈接,該方法會阻塞
if err != nil {
fmt.Println("err = ", err)
return
}
//處理用戶請求,新建一個(gè)協(xié)程,每來一個(gè)就單獨(dú)為它獲取
go HandleConn(conn)
}
}
并發(fā)Client
客戶端不僅需要持續(xù)的向服務(wù)端發(fā)送數(shù)據(jù),同時(shí)也要接收從服務(wù)端返回的數(shù)據(jù)。因此可將發(fā)送和接收放到不同的協(xié)程中。
主協(xié)程循環(huán)接收服務(wù)器回發(fā)的數(shù)據(jù)(該數(shù)據(jù)應(yīng)已轉(zhuǎn)換為大寫),并打印至屏幕;子協(xié)程循環(huán)從鍵盤讀取用戶輸入數(shù)據(jù),寫給服務(wù)器。讀取鍵盤輸入可使用 os.Stdin.Read(str)。定義切片str,將讀到的數(shù)據(jù)保存至str中。
這樣,客戶端也實(shí)現(xiàn)了多任務(wù)。
客戶端即可輸入也可接收服務(wù)器回復(fù)
package main
import (
"fmt"
"net"
"os"
)
func main() {
//主動連接服務(wù)器
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("net Dial err = ", err)
return
}
//main調(diào)用完畢,關(guān)閉連接
defer conn.Close() //關(guān)閉
//接收服務(wù)器回復(fù)的數(shù)據(jù),新任務(wù)
go func() {
//從鍵盤輸入內(nèi)容,給服務(wù)器發(fā)送內(nèi)容
str := make([]byte, 1024)
for true {
n, err2 := os.Stdin.Read(str) //從鍵盤讀取內(nèi)容,放在str
if err2 != nil {
fmt.Println("os.Stdin.err = ", err)
return
}
//把輸入的內(nèi)容給服務(wù)器發(fā)送
conn.Write(str[:n])
}
}()
//切片緩存
buf := make([]byte, 1024)
//不停地接收
for true {
n, err := conn.Read(buf) //接收服務(wù)器的請求
if err != nil {
fmt.Println("conn.Read err = ", err)
return
}
fmt.Println(string(buf[:n])) //打印接收到的內(nèi)容,轉(zhuǎn)換為字符串再打印
}
}
TCP通信
下圖是一次TCP通訊的時(shí)序圖。TCP連接建立斷開。包含大家熟知的三次握手和四次握手。
三次握手:
所謂三次握手(Three-Way Handshake)即建立TCP連接,就是指建立一個(gè)TCP連接時(shí),需要客戶端和服務(wù)端總共發(fā)送3個(gè)包以確認(rèn)連接的建立。好比兩個(gè)人在打電話:
Client:“喂,你聽得到嗎?”
Server:“我聽得到,你聽得到我嗎?”
Client:“我能聽到你,今天balabala…”
建立連接(三次握手)的過程:
1.客戶端發(fā)送一個(gè)帶SYN標(biāo)志的TCP報(bào)文到服務(wù)器。這是上圖中三次握手過程中的段1??蛻舳税l(fā)出SYN位表示連接請求。序號是1000,這個(gè)序號在網(wǎng)絡(luò)通訊中用作臨時(shí)的地址,每發(fā)一個(gè)數(shù)據(jù)字節(jié),這個(gè)序號要加1,這樣在接收端可以根據(jù)序號排出數(shù)據(jù)包的正確順序,也可以發(fā)現(xiàn)丟包的情況。
另外,規(guī)定SYN位和FIN位也要占一個(gè)序號,這次雖然沒發(fā)數(shù)據(jù),但是由于發(fā)了SYN位,因此下次再發(fā)送應(yīng)該用序號1001。
mss表示最大段尺寸,如果一個(gè)段太大,封裝成幀后超過了鏈路層的最大長度,就必須在IP層分片,為了避免這種情況,客戶端聲明自己的最大段尺寸,建議服務(wù)器端發(fā)來的段不要超過這個(gè)長度。
2.服務(wù)器端回應(yīng)客戶端,是三次握手中的第2個(gè)報(bào)文段,同時(shí)帶ACK標(biāo)志和SYN標(biāo)志。表示對剛才客戶端SYN的回應(yīng);同時(shí)又發(fā)送SYN給客戶端,詢問客戶端是否準(zhǔn)備好進(jìn)行數(shù)據(jù)通訊。
服務(wù)器發(fā)出段2,也帶有SYN位,同時(shí)置ACK位表示確認(rèn),確認(rèn)序號是1001,表示“我接收到序號1000及其以前所有的段,請你下次發(fā)送序號為1001的段”,也就是應(yīng)答了客戶端的連接請求,同時(shí)也給客戶端發(fā)出一個(gè)連接請求,同時(shí)聲明最大尺寸為1024。
3.客戶必須再次回應(yīng)服務(wù)器端一個(gè)ACK報(bào)文,這是報(bào)文段3。
客戶端發(fā)出段3,對服務(wù)器的連接請求進(jìn)行應(yīng)答,確認(rèn)序號是8001。在這個(gè)過程中,客戶端和服務(wù)器分別給對方發(fā)了連接請求,也應(yīng)答了對方的連接請求,其中服務(wù)器的請求和應(yīng)答在一個(gè)段中發(fā)出。
因此一共有三個(gè)段用于建立連接,稱為“三方握手”。在建立連接的同時(shí),雙方協(xié)商了一些信息,例如,雙方發(fā)送序號的初始值、最大段尺寸等。
數(shù)據(jù)傳輸?shù)倪^程:
1.客戶端發(fā)出段4,包含從序號1001開始的20個(gè)字節(jié)數(shù)據(jù)。
2.服務(wù)器發(fā)出段5,確認(rèn)序號為1021,對序號為1001-1020的數(shù)據(jù)表示確認(rèn)收到,同時(shí)請求發(fā)送序號1021開始的數(shù)據(jù),服務(wù)器在應(yīng)答的同時(shí)也向客戶端發(fā)送從序號8001開始的10個(gè)字節(jié)數(shù)據(jù)。
3.客戶端發(fā)出段6,對服務(wù)器發(fā)來的序號為8001-8010的數(shù)據(jù)表示確認(rèn)收到,請求發(fā)送序號8011開始的數(shù)據(jù)。
在數(shù)據(jù)傳輸過程中,ACK和確認(rèn)序號是非常重要的,應(yīng)用程序交給TCP協(xié)議發(fā)送的數(shù)據(jù)會暫存在TCP層的發(fā)送緩沖區(qū)中,發(fā)出數(shù)據(jù)包給對方之后,只有收到對方應(yīng)答的ACK段才知道該數(shù)據(jù)包確實(shí)發(fā)到了對方,可以從發(fā)送緩沖區(qū)中釋放掉了,如果因?yàn)榫W(wǎng)絡(luò)故障丟失了數(shù)據(jù)包或者丟失了對方發(fā)回的ACK段,經(jīng)過等待超時(shí)后TCP協(xié)議自動將發(fā)送緩沖區(qū)中的數(shù)據(jù)包重發(fā)。
四次揮手:
所謂四次揮手(Four-Way-Wavehand)即終止TCP連接,就是指斷開一個(gè)TCP連接時(shí),需要客戶端和服務(wù)端總共發(fā)送4個(gè)包以確認(rèn)連接的斷開。在socket編程中,這一過程由客戶端或服務(wù)器任一方執(zhí)行close來觸發(fā)。好比兩個(gè)人打完電話要掛斷:
Client:“我要說的事情都說完了,我沒事了。掛啦?”
Server:“等下,我還有一個(gè)事兒。Balabala…”
Server:“好了,我沒事兒了。掛了啊。”
Client:“ok!拜拜”
關(guān)閉連接(四次握手)的過程:
由于TCP連接是全雙工的,因此每個(gè)方向都必須單獨(dú)進(jìn)行關(guān)閉。這原則是當(dāng)一方完成它的數(shù)據(jù)發(fā)送任務(wù)后就能發(fā)送一個(gè)FIN來終止這個(gè)方向的連接。收到一個(gè) FIN只意味著這一方向上沒有數(shù)據(jù)流動,一個(gè)TCP連接在收到一個(gè)FIN后仍能發(fā)送數(shù)據(jù)。首先進(jìn)行關(guān)閉的一方將執(zhí)行主動關(guān)閉,而另一方執(zhí)行被動關(guān)閉。
1.客戶端發(fā)出段7,F(xiàn)IN位表示關(guān)閉連接的請求。
2.服務(wù)器發(fā)出段8,應(yīng)答客戶端的關(guān)閉連接請求。
3.服務(wù)器發(fā)出段9,其中也包含F(xiàn)IN位,向客戶端發(fā)送關(guān)閉連接請求。
4.客戶端發(fā)出段10,應(yīng)答服務(wù)器的關(guān)閉連接請求。
建立連接的過程是三次握手,而關(guān)閉連接通常需要4個(gè)段,服務(wù)器的應(yīng)答和關(guān)閉連接請求通常不合并在一個(gè)段中,因?yàn)橛羞B接半關(guān)閉的情況,這種情況下客戶端關(guān)閉連接之后就不能再發(fā)送數(shù)據(jù)給服務(wù)器了,但是服務(wù)器還可以發(fā)送數(shù)據(jù)給客戶端,直到服務(wù)器也關(guān)閉連接為止。
UDP通信
在之前的案例中,我們一直使用的是TCP協(xié)議來編寫Socket的客戶端與服務(wù)端。其實(shí)也可以使用UDP協(xié)議來編寫Socket的客戶端與服務(wù)端。
UDP服務(wù)器
由于UDP是“無連接”的,所以,服務(wù)器端不需要額外創(chuàng)建監(jiān)聽套接字,只需要指定好IP和port,然后監(jiān)聽該地址,等待客戶端與之建立連接,即可通信。
創(chuàng)建監(jiān)聽地址:
func ResolveUDPAddr(network, address string) (*UDPAddr, error)
創(chuàng)建監(jiān)聽連接:
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
接收udp數(shù)據(jù):
func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)
寫出數(shù)據(jù)到udp:
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
文件傳輸原理
UDP與TCP的差異
文章來源:http://www.zghlxwxcb.cn/news/detail-709294.html
os.Stat的使用
package main
import (
"fmt"
"os"
)
func main() {
list := os.Args
fmt.Println(len(list))
if len(list) != 2 {
fmt.Println("useage :xxx file")
return
}
for i, s := range list {
fmt.Println(i, s)
}
fileName := list[1]
fmt.Printf("fileName = %s\n", fileName)
//Stat返回一個(gè)描述name指定的文件對象的FileInfo。如果指定的文件對象是一個(gè)符號鏈接,
//返回的FileInfo描述該符號鏈接指向的文件的信息,本函數(shù)會嘗試跳轉(zhuǎn)該鏈接。如果出錯(cuò),返回的錯(cuò)誤值為*PathError類型。
//過濾路徑
info, err := os.Stat(fileName)
if err != nil {
fmt.Println("err = ", err)
}
fmt.Println("name = ", info.Name()) //name = 01_昨日回顧.mp4
fmt.Println("size = ", info.Size()) //size = 67010611
}
文章來源地址http://www.zghlxwxcb.cn/news/detail-709294.html
傳輸文件:發(fā)送方
package main
import (
"fmt"
"io"
"net"
"os"
)
func sendFile(path string, conn net.Conn) {
//以只讀方式打開文件
file, err := os.Open(path)
if err != nil {
fmt.Println("send Open err = ", err)
return
}
defer file.Close()
//讀文件內(nèi)容,讀多少發(fā)多少,一點(diǎn)不差
buf := make([]byte, 1024*4)
for true {
read, err := file.Read(buf) //從文件讀取內(nèi)容
if err != nil {
if err == io.EOF {
fmt.Println("文件發(fā)送完畢")
} else {
fmt.Println("send Rend err = ", err)
}
return
}
//發(fā)送內(nèi)容
conn.Write(buf[:read]) //給服務(wù)器發(fā)送內(nèi)容
}
}
func main() {
//提示輸入文件
fmt.Println("請輸入需要傳輸?shù)奈募? ")
var path string
_, err := fmt.Scan(&path)
if err != nil {
fmt.Println("Scan open err = ", err)
return
}
//獲取文件名
fileInfo, err := os.Stat(path)
if err != nil {
fmt.Println("os.Stat err = ", err)
return
}
//主動連接服務(wù)器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("net.Dial err = ", err)
return
}
defer conn.Close()
//給接收方,先發(fā)送文件名
_, err = conn.Write([]byte(fileInfo.Name()))
if err != nil {
fmt.Println("conn.write err = ", err)
return
}
//接收對方的回復(fù),如果回復(fù)ok,說明對方準(zhǔn)備好,可以發(fā)文件
buf := make([]byte, 1024)
readSize, err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err = ", err)
return
}
if "ok" == string(buf[:readSize]) {
//發(fā)送文件內(nèi)容
sendFile(path, conn)
}
}
傳輸文件:接收方
package main
import (
"fmt"
"io"
"net"
"os"
)
// RecvFile 接收文件內(nèi)容
func RecvFile(fileName string, conn net.Conn) {
//新建文件
file, err := os.Create(fileName)
if err != nil {
fmt.Println("os create err =", err)
return
}
buf := make([]byte, 1024*4)
//接收多少,寫多少,一點(diǎn)不差
for true {
readSize, err := conn.Read(buf) //接收對方發(fā)過來的文件內(nèi)容
if err != nil {
if err == io.EOF {
fmt.Println("文件接收完畢")
} else {
fmt.Println("conn read err =", err)
}
return
}
if readSize == 0 {
fmt.Println("n == 0 文件接收完畢")
return
}
file.Write(buf[:readSize])
}
}
func main() {
//監(jiān)聽
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("net listener err = ", err)
return
}
defer listener.Close()
//阻塞等待用戶連接
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener accept err =", err)
return
}
defer conn.Close()
buf := make([]byte, 1024)
//讀取對方發(fā)送的文件名
readSize, err := conn.Read(buf)
if err != nil {
fmt.Println("conn read err =", err)
return
}
fileName := string(buf[:readSize])
//回復(fù)"ok"
_, err1 := conn.Write([]byte("ok"))
if err1 != nil {
return
}
//接收文件內(nèi)容
RecvFile(fileName, conn)
}
到了這里,關(guān)于go語言基礎(chǔ)操作---七的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!