golang socket與Linux socket比較分析

       在posix標準推出後,socket在各大主流OS平臺上都獲得了很好的支持。而Golang是自帶runtime的跨平臺編程語言,Go中提供給開發者的socket API是創建在操做系統原生socket接口之上的。但golang 中的socket接口在行爲特色與操做系統原生接口有一些不一樣。本文將對結合一個簡單的hello/hi的網絡聊天程序加以分析。golang

    1、socket簡介web

       首先進程之間能夠進行通訊的前提是進程能夠被惟一標識,在本地通訊時可使用PID惟一標識,而在網絡中這種方法不可行,咱們能夠經過IP地址+協議+端口號來惟一標識一個進程,而後利用socket進行通訊。編程

       socket是位於應用層和傳輸層中的抽象層,它是不屬於七層架構中的:服務器

                                                     

     而socket通訊流程以下:網絡

1.服務端建立socket數據結構

2.服務端綁定socket和端口號架構

3.服務端監聽該端口號併發

4.服務端啓動accept()用來接收來自客戶端的鏈接請求,此時若是有鏈接則繼續執行,不然將阻塞在這裏。dom

5.客戶端建立socketsocket

6.客戶端經過IP地址和端口號鏈接服務端,即tcp中的三次握手

7.若是鏈接成功,客戶端能夠向服務端發送數據

8.服務端讀取客戶端發來的數據

9.任何一端都可主動斷開鏈接

                                                     

 2、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的三次握手。

3、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)

4、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

     http://www.javashuo.com/article/p-oentjnxr-eq.html

相關文章
相關標籤/搜索