在posix標準推出後,socket在各大主流OS平臺上都得到了很好的支持。而Golang是自帶runtime的跨平臺編程語言,Go中提供給開發者的socket API是建立在操作系統原生socket介面之上的。但golang 中的socket介面在行為特點與操作系統原生介面有一些不同。本文將對結合 ...
在posix標準推出後,socket在各大主流OS平臺上都得到了很好的支持。而Golang是自帶runtime的跨平臺編程語言,Go中提供給開發者的socket API是建立在操作系統原生socket介面之上的。但golang 中的socket介面在行為特點與操作系統原生介面有一些不同。本文將對結合一個簡單的hello/hi的網路聊天程式加以分析。
一、socket簡介
首先進程之間可以進行通信的前提是進程可以被唯一標識,在本地通信時可以使用PID唯一標識,而在網路中這種方法不可行,我們可以通過IP地址+協議+埠號來唯一標識一個進程,然後利用socket進行通信。
socket是位於應用層和傳輸層中的抽象層,它是不屬於七層架構中的:
而socket通信流程如下:
1.服務端創建socket
2.服務端綁定socket和埠號
3.服務端監聽該埠號
4.服務端啟動accept()用來接收來自客戶端的連接請求,此時如果有連接則繼續執行,否則將阻塞在這裡。
5.客戶端創建socket
6.客戶端通過IP地址和埠號連接服務端,即tcp中的三次握手
7.如果連接成功,客戶端可以向服務端發送數據
8.服務端讀取客戶端發來的數據
9.任何一端均可主動斷開連接
二、socket編程
有了抽象的socket後,當使用TCP或UDP協議進行web編程時,可以通過以下的方式進行
服務端偽代碼:
listenfd = socket(……)
bind(listenfd, ServerIp:Port, ……)
listen(listenfd, ……)
while(true) {
conn = accept(listenfd, ……)
receive(conn, ……)
send(conn, ……)
}
客戶端偽代碼:
clientfd = socket(……)
connect(clientfd, serverIp:Port, ……)
send(clientfd, data)
receive(clientfd, ……)
close(clientfd)
上述偽代碼中,listenfd就是為了實現服務端監聽創建的socket描述符,而bind方法就是服務端進程占用埠,避免其它埠被其它進程使用,listen方法開始對埠進行監聽。下麵的while迴圈用來處理客戶端源源不斷的請求,accept方法返回一個conn,用來區分各個客戶端的連接的,之後的接受和發送動作都是基於這個conn來實現的。其實accept就是和客戶端的connect一起完成了TCP的三次握手。
三、golang中的socket
golang中提供了一些網路編程的API,包括Dial,Listen,Accept,Read,Write,Close等.
3.1 Listen()
首先使用服務端net.Listen()方法創建套接字,綁定埠和監聽埠。
1 func Listen(network, address string) (Listener, error) { 2 var lc ListenConfig 3 return lc.Listen(context.Background(), network, address) 4 }
以上是golang提供的Listen函數源碼,其中network表示網路協議,如tcp,tcp4,tcp6,udp,udp4,udp6等。address為綁定的地址,返回的Listener實際上是一個套接字描述符,error中保存錯誤信息。
而在Linuxsocket中使用socket,bind和listen函數來完成同樣功能
// socket(協議域,套接字類型,協議) int socket(int domain, int type, int protocol); int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); int listen(int sockfd, int backlog);
3.2 Dial()
當客戶端想要發起莫個連接時,就會使用net.Dial()方法來發起連接
func Dial(network, address string) (Conn, error) { var d Dialer return d.Dial(network, address) }
其中network表示網路協議,address為要建立連接的地址,返回的Conn實際是標識每一個客戶端的,在golang中定義了一個Conn的介面:
type Conn interface { Read(b []byte) (n int, err error) Write(b []byte) (n int, err error) Close() error LocalAddr() Addr RemoteAddr() Addr SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error SetWriteDeadline(t time.Time) error }type conn struct { fd *netFD }
其中netFD是golang網路庫里最核心的數據結構,貫穿了golang網路庫所有的API,對底層的socket進行封裝,屏蔽了不同操作系統的網路實現,這樣通過返回的Conn,我們就可以使用golang提供的socket底層函數了。
在Linuxsocket中使用connect函數來創建連接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
3.3 Accept()
當服務端調用net.Listen()後會開始監聽指定地址,而客戶端調用net.Dial()後發起連接請求,然後服務端調用net.Accept()接收請求,這裡端與端的連接就建立好了,實際上到這一步也就完成了TCP中的三次握手。
Accept() (Conn, error)
golang的socket實際上是非阻塞的,但golang本身對socket做了一定處理,使其看起來是阻塞的。
在Linuxsocket中使用accept函數來實現同樣功能
//sockfd是伺服器套接字描述符,sockaddr返回客戶端協議地址,socklen_t是協議地址長度。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
3.4 Write()
端與端的連接已經建立了,接下來開始進行讀寫操作,conn.Write()向socket寫數據
Write(b []byte) (n int, err error)func (c *conn) Write(b []byte) (int, error) { if !c.ok() { return 0, syscall.EINVAL } n, err := c.fd.Write(b) if err != nil { err = &OpError{Op: "write", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return n, err }
其中寫入的數據是一個二進位位元組流,n返回的數據的長度,err保存錯誤信息
Linuxsocket中對應的則是send函數
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
3.5 Read()
客戶端發送完數據以後,服務端可以接收數據,golang中調用conn.Read()讀取數據,源碼如下:
Read(b []byte) (n int, err error)func (c *conn) Read(b []byte) (int, error) { if !c.ok() { return 0, syscall.EINVAL } n, err := c.fd.Read(b) if err != nil && err != io.EOF { err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return n, err }
其參數與Write()中的含義一樣,在Linuxsocket中使用recv函數完成此功能
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
3.6 Close()
當服務端或者客戶端想要關閉套接字時,調用Close()方法關閉連接。
Close() error
func (c *conn) Close() error { if !c.ok() { return syscall.EINVAL } err := c.fd.Close() if err != nil { err = &OpError{Op: "close", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return err }
在Linuxsocket中使用close函數
int close(int socketfd)
四、golang實現Hello/hi網路聊天程式
4.1 server.go
package main import ( "fmt" "net" "strings" ) //UserMap保存的是當前聊天室所有用戶id的集合 var UserMap map[string]net.Conn = make(map[string]net.Conn) func main() { //監聽本地所有ip的8000埠 listen_socket, err := net.Listen("tcp", "127.0.0.1:8000") if err != nil { fmt.Println("服務啟動失敗") } //關閉監聽的埠 defer listen_socket.Close() fmt.Println("等待用戶加入聊天室") for { //用於conn接收鏈接 conn, err := listen_socket.Accept() if err != nil { fmt.Println("連接失敗") } //列印加入聊天室的公網IP地址 fmt.Println(conn.RemoteAddr(), "連接成功") //定義一個goroutine,這裡主要是為了併發運行 go DataProcessing(conn) } } func DataProcessing(conn net.Conn) { for { //定義一個長度為255的切片 data := make([]byte, 255) //讀取客戶端傳來的數據,msg_length保存長度,err保存錯誤信息 msg_length, err := conn.Read(data) if msg_length == 0 || err != nil { continue } //解析協議,通過分隔符"|"獲取需要的數據,msg_str[0]存放操作類別 //msg_str[1]存放用戶名,msg_str[2]如果有就存放發送的消息 msg_str := strings.Split(string(data[0:msg_length]), "|") switch msg_str[0] { case "nick": fmt.Println(conn.RemoteAddr(), "的用戶名是", msg_str[1]) for user, message := range UserMap { //向除自己之外的用戶發送加入聊天室的消息 if user != msg_str[1] { message.Write([]byte("用戶" + msg_str[1] + "加入聊天室")) } } //將該用戶加入用戶id的集合 UserMap[msg_str[1]] = conn case "send": for user, message := range UserMap { if user != msg_str[1] { fmt.Println("Send "+msg_str[2]+" to ", user) //向除自己之外的用戶發送聊天消息 message.Write([]byte(" 用戶" + msg_str[1] + ": " + msg_str[2])) } } case "quit": for user, message := range UserMap { if user != msg_str[1] { //向除自己之外的用戶發送退出聊天室的消失 message.Write([]byte("用戶" + msg_str[1] + "退出聊天室")) } } fmt.Println("用戶 " + msg_str[1] + "退出聊天室") //將該用戶名從用戶id的集合中刪除 delete(UserMap, msg_str[1]) } } }
5.2 client.go
package main import ( "bufio" "fmt" "net" "os" ) var nick string = "" func main() { //撥號操作 conn, err := net.Dial("tcp", "127.0.0.1:8000") if err != nil { fmt.Println("連接失敗") } defer conn.Close() fmt.Println("連接服務成功 \n") //創建用戶名 fmt.Printf("在進入聊天室之前給自己取個名字吧:") fmt.Scanf("%s", &nick) fmt.Println("用戶" + nick + "歡迎進入聊天室") //向伺服器發送數據 conn.Write([]byte("nick|" + nick)) //定義一個goroutine,這裡主要是為了併發運行 go SendMessage(conn) var msg string for { msg = "" //由於golangz的fmt包輸入字元串不能讀取空格,所以此處重寫了一個Scanf函數 Scanf(&msg) if msg == "quit" { //這裡的quit,send,以及上面的nick是為了識別客戶端做的是設置用戶名,發消息還是退出 conn.Write([]byte("quit|" + nick)) break } if msg != "" { conn.Write([]byte("send|" + nick + "|" + msg)) } } } func SendMessage(conn net.Conn) { for { //定義一個長度為255的切片 data := make([]byte, 255) //讀取伺服器傳來的數據,msg_length保存長度,err保存錯誤信息 msg_length, err := conn.Read(data) if msg_length == 0 || err != nil { break } fmt.Println(string(data[0:msg_length])) } } //重寫的Scanf函數 func Scanf(a *string) { reader := bufio.NewReader(os.Stdin) data, _, _ := reader.ReadLine() *a = string(data) }
golang中使用goroutine實現併發
5.3 運行截圖
多人聊天截圖(左上角為服務端)
用戶退出聊天室(左上角為服務端)
參考資料:
https://tonybai.com/2015/11/17/tcp-programming-in-golang/
https://www.jianshu.com/p/325ac02fc31c
https://blog.csdn.net/dyd961121/article/details/81252920