golang socket與Linux socket比較分析

来源:https://www.cnblogs.com/ustc-kunkun/archive/2019/12/05/11990285.html
-Advertisement-
Play Games

在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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Spring Cloud,它將幫我們填平橫跨在應用開發與微服務、DevOps、雲計算之間的溝壑,讓我們輕鬆擁抱雲上微服務,但你知道它是如何做到的嗎?你對它有全面的瞭解嗎?你知道如何正確使用它嗎?新概念新技術層出不窮,讓人雲里霧裡,你是否想撥開雲霧對它們有更清晰的認知? ...
  • 類的變數分成2種: 1、成員變數 概念:在構造方法中的變數,前面帶有self 作用:可以在類中不同的方法間使用 2、類變數-靜態變數 概念:在類中,構造方法和普通方法之外,定義的變數 作用: 1、調用 1、類名可以調用 類名.類變數 2、對象名也可以調用 對象名.類變數 ... ...
  • 原鏈接:https://zhuanlan.zhihu.com/p/73001806 在使用PC時與PC交互的主要途徑是看屏幕顯示、聽聲音,點擊滑鼠和敲鍵盤等等。在自動化辦公的趨勢下,繁瑣的工作可以讓程式自動完成。比如自動化測試、自動下單交易等。很多軟體除了可以GUI方式操作外還可以用CLI介面操作, ...
  • IO概述 當我們在生活中把電腦上的數據拷貝到U盤或者硬碟上時,就是進行數據傳輸,按照數據的流動方向,我們分為輸入(input)和輸出(output),即就是所謂IO流 Java中I/O操作主要是指使用 java.io 包下的內容,進行輸入、輸出操作。輸入也叫做讀取數據,輸出也叫做作寫出數據 IO的分 ...
  • ——日拱一卒,不期而至! 你好,我是彤哥,本篇是netty系列的第七篇。 簡介 上一章我們一起學習了Java NIO的核心組件Buffer,它通常跟Channel一起使用,但是它們在網路IO中又該如何使用呢,今天我們將一起學習另一個NIO核心組件—— Selector ,沒有它可以說就乾不起來網路I ...
  • 在控制面板的程式與功能里啟用和關閉windows功能打開,適用於linux的windows子系統 在微軟商店裡搜索ubuntu,直接點擊安裝就可以了 安裝完成後的windows與linux的磁碟映射見下圖 配置開發環境,下載和安裝vscode,打開後直接讓安裝一個擴展可以連接子系統的目錄 windo ...
  • Java編程思想總結(一)對象導論 1.1 抽象過程 萬物皆對象。 程式是對象的集合(即:類),他們通過發送消息(調用方法)來告知彼此要做的。 每個對象都有自己的由其他對象所構成的存儲(引用其他對象或基本類型,即組合)。 每個對象都擁有其類型。每個類最重要的區別於其他類的特征就是“可以發送什麼樣的消 ...
  • 一、基礎 ArrayList不是線程安全的,多線程建議使用Vector或者CopyOnWriteArrayList; 底層實現是Object數組。預設容量DEFAULT_CAPACITY為10,最大數組大小MAX_ARRAY_SIZE為Integer.MAX_VALUE-8; 實現了RandomAc ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...