最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Golang?編寫(xiě)Tcp服務(wù)器的解決方案

 更新時(shí)間:2022年10月25日 10:59:39   作者:Finley  
Golang?作為廣泛用于服務(wù)端和云計(jì)算領(lǐng)域的編程語(yǔ)言,tcp?socket?是其中至關(guān)重要的功能,這篇文章給大家介紹Golang?開(kāi)發(fā)?Tcp?服務(wù)器及拆包粘包、優(yōu)雅關(guān)閉的解決方案,感興趣的朋友一起看看吧

Golang 開(kāi)發(fā) Tcp 服務(wù)器及拆包粘包、優(yōu)雅關(guān)閉的解決方案

Golang 作為廣泛用于服務(wù)端和云計(jì)算領(lǐng)域的編程語(yǔ)言,tcp socket 是其中至關(guān)重要的功能。您可以在 github.com/hdt3213/godis/tcp 中看到本文所述 TCP 服務(wù)器的完整代碼及其應(yīng)用。

早期的 Tomcat/Apache 服務(wù)器使用的是阻塞 IO 模型。它使用一個(gè)線程處理一個(gè)連接,在沒(méi)有收到新數(shù)據(jù)時(shí)監(jiān)聽(tīng)線程處于阻塞狀態(tài),直到數(shù)據(jù)就緒后線程被喚醒。因?yàn)樽枞?IO 模型需要開(kāi)啟大量線程并且頻繁地進(jìn)行上下文切換,所以效率很差。

IO 多路復(fù)用技術(shù)為了解決上述問(wèn)題采用了一個(gè)線程監(jiān)聽(tīng)多路連接的方案。一個(gè)線程持有多個(gè)連接并阻塞等待,當(dāng)其中某個(gè)連接可讀寫(xiě)時(shí)線程被喚醒進(jìn)行處理。因?yàn)槎鄠€(gè)連接復(fù)用了一個(gè)線程所以 IO 多路復(fù)用需要的線程數(shù)少很多。

主流操作系統(tǒng)都提供了IO多路復(fù)用技術(shù)的實(shí)現(xiàn),比如 Linux上的 epoll,freeBSD 上的 kqueue 以及 Windows 平臺(tái)上的 iocp。有得必有失,因?yàn)?epoll 等技術(shù)提供的接口面向 IO 事件而非面向連接,所以需要編寫(xiě)復(fù)雜的異步代碼,開(kāi)發(fā)難度很大。

Golang 的 netpoller 基于IO多路復(fù)用和 goroutine scheduler 構(gòu)建了一個(gè)簡(jiǎn)潔高性能的網(wǎng)絡(luò)模型,并給開(kāi)發(fā)者提供了 goroutine-per-connection 風(fēng)格的極簡(jiǎn)接口。

更多關(guān)于 netpoller 的剖析可以參考Golang實(shí)現(xiàn)四種負(fù)載均衡的算法(隨機(jī),輪詢等), 接下來(lái)我們嘗試用 netpoller 編寫(xiě)我們的服務(wù)器。

Echo 服務(wù)器

作為開(kāi)始我們來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Echo 服務(wù)器。它會(huì)接受客戶端連接并將客戶端發(fā)送的內(nèi)容原樣傳回客戶端。

package main

import (
    "fmt"
    "net"
    "io"
    "log"
    "bufio"
)

func ListenAndServe(address string) {
    // 綁定監(jiān)聽(tīng)地址
    listener, err := net.Listen("tcp", address)
    if err != nil {
        log.Fatal(fmt.Sprintf("listen err: %v", err))
    }
    defer listener.Close()
    log.Println(fmt.Sprintf("bind: %s, start listening...", address))

    for {
        // Accept 會(huì)一直阻塞直到有新的連接建立或者listen中斷才會(huì)返回
        conn, err := listener.Accept()
        if err != nil {
            // 通常是由于listener被關(guān)閉無(wú)法繼續(xù)監(jiān)聽(tīng)導(dǎo)致的錯(cuò)誤
            log.Fatal(fmt.Sprintf("accept err: %v", err))
        }
        // 開(kāi)啟新的 goroutine 處理該連接
        go Handle(conn)
    }
}

func Handle(conn net.Conn) {
    // 使用 bufio 標(biāo)準(zhǔn)庫(kù)提供的緩沖區(qū)功能
    reader := bufio.NewReader(conn)
    for {
        // ReadString 會(huì)一直阻塞直到遇到分隔符 '\n'
        // 遇到分隔符后會(huì)返回上次遇到分隔符或連接建立后收到的所有數(shù)據(jù), 包括分隔符本身
        // 若在遇到分隔符之前遇到異常, ReadString 會(huì)返回已收到的數(shù)據(jù)和錯(cuò)誤信息
        msg, err := reader.ReadString('\n')
        if err != nil {
            // 通常遇到的錯(cuò)誤是連接中斷或被關(guān)閉,用io.EOF表示
            if err == io.EOF {
                log.Println("connection close")
            } else {
                log.Println(err)
            }
            return
        }
        b := []byte(msg)
        // 將收到的信息發(fā)送給客戶端
        conn.Write(b)
    }
}

func main() {
    ListenAndServe(":8000")
}

使用 telnet 工具測(cè)試我們編寫(xiě)的 Echo 服務(wù)器:

$ telnet 127.0.0.1 8000
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
> a
a
> b
b
Connection closed by foreign host.

拆包與粘包問(wèn)題

某些朋友可能看到"拆包與粘包"后表示極度震驚,并再三強(qiáng)調(diào): TCP是個(gè)字節(jié)流協(xié)議,不存在粘包問(wèn)題。

我們常說(shuō)的 TCP 服務(wù)器并非「實(shí)現(xiàn) TCP 協(xié)議的服務(wù)器」而是「基于TCP協(xié)議的應(yīng)用層服務(wù)器」。TCP 是面向字節(jié)流的協(xié)議,而應(yīng)用層協(xié)議大多是面向消息的,比如 HTTP 協(xié)議的請(qǐng)求/響應(yīng),Redis 協(xié)議的指令/回復(fù)都是以消息為單位進(jìn)行通信的。

作為應(yīng)用層服務(wù)器我們有責(zé)任從 TCP 提供的字節(jié)流中正確地解析出應(yīng)用層消息,在這一步驟中我們會(huì)遇到「拆包/粘包」問(wèn)題。

socket 允許我們通過(guò) read 函數(shù)讀取新收到的一段數(shù)據(jù)(當(dāng)然這段數(shù)據(jù)并不對(duì)應(yīng)一個(gè) TCP 包)。在上文的 Echo 服務(wù)器示例中我們用\n表示消息結(jié)束,從 read 函數(shù)讀取的數(shù)據(jù)可能存在下列幾種情況:

  • 收到兩段數(shù)據(jù): "abc", "def\n" 它們屬于一條消息 "abcdef\n" 這是拆包的情況
  • 收到一段數(shù)據(jù): "abc\ndef\n" 它們屬于兩條消息 "abc\n", "def\n" 這是粘包的情況

應(yīng)用層協(xié)議通常采用下列幾種思路之一來(lái)定義消息,以保證完整地進(jìn)行讀取:

  • 定長(zhǎng)消息
  • 在消息尾部添加特殊分隔符,如示例中的Echo協(xié)議和FTP控制協(xié)議。bufio 標(biāo)準(zhǔn)庫(kù)會(huì)緩存收到的數(shù)據(jù)直到遇到分隔符才會(huì)返回,它可以幫助我們正確地分割字節(jié)流。
  • 將消息分為 header 和 body, 并在 header 中提供 body 總長(zhǎng)度,這種分包方式被稱為 LTV(length,type,value) 包。這是應(yīng)用最廣泛的策略,如HTTP協(xié)議。當(dāng)從 header 中獲得 body 長(zhǎng)度后, io.ReadFull 函數(shù)會(huì)讀取指定長(zhǎng)度字節(jié)流,從而解析應(yīng)用層消息。

在沒(méi)有具體應(yīng)用層協(xié)議的情況下,我們很難詳細(xì)地討論拆包與粘包問(wèn)題。在本系列的第二篇文章: 實(shí)現(xiàn) Redis 協(xié)議解析器 中我們可以看到 Redis 序列化協(xié)議(RESP)對(duì)分隔符和 LTV 包的結(jié)合應(yīng)用,以及兩種分包方式的具體解析代碼。

優(yōu)雅關(guān)閉

在生產(chǎn)環(huán)境下需要保證TCP服務(wù)器關(guān)閉前完成必要的清理工作,包括將完成正在進(jìn)行的數(shù)據(jù)傳輸,關(guān)閉TCP連接等。這種關(guān)閉模式稱為優(yōu)雅關(guān)閉,可以避免資源泄露以及客戶端未收到完整數(shù)據(jù)導(dǎo)致故障。

TCP 服務(wù)器的優(yōu)雅關(guān)閉模式通常為: 先關(guān)閉listener阻止新連接進(jìn)入,然后遍歷所有連接逐個(gè)進(jìn)行關(guān)閉。首先修改一下TCP服務(wù)器:

// handler 是應(yīng)用層服務(wù)器的抽象
type Handler interface {
    Handle(ctx context.Context, conn net.Conn)
    Close()error
}

// 監(jiān)聽(tīng)并提供服務(wù),并在收到 closeChan 發(fā)來(lái)的關(guān)閉通知后關(guān)閉
func ListenAndServe(listener net.Listener, handler tcp.Handler, closeChan <-chan struct{}) {
    // 監(jiān)聽(tīng)關(guān)閉通知
    go func() {
        <-closeChan
        logger.Info("shutting down...")
        // 停止監(jiān)聽(tīng),listener.Accept()會(huì)立即返回 io.EOF
        _ = listener.Close()
        // 關(guān)閉應(yīng)用層服務(wù)器 
        _ = handler.Close()
    }()

    // 在異常退出后釋放資源
    defer func() {
        // close during unexpected error
        _ = listener.Close()
        _ = handler.Close()
    }()
    ctx := context.Background()
    var waitDone sync.WaitGroup
    for {
        // 監(jiān)聽(tīng)端口, 阻塞直到收到新連接或者出現(xiàn)錯(cuò)誤
        conn, err := listener.Accept()
        if err != nil {
            break
        }
        // 開(kāi)啟 goroutine 來(lái)處理新連接
        logger.Info("accept link")
        waitDone.Add(1)
        go func() {
            defer func() {
                waitDone.Done()
            }()
            handler.Handle(ctx, conn)
        }()
    }
    waitDone.Wait()
}

// ListenAndServeWithSignal 監(jiān)聽(tīng)中斷信號(hào)并通過(guò) closeChan 通知服務(wù)器關(guān)閉
func ListenAndServeWithSignal(cfg *Config, handler tcp.Handler) error {
    closeChan := make(chan struct{})
    sigCh := make(chan os.Signal)
    signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
    go func() {
        sig := <-sigCh
        switch sig {
        case syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
            closeChan <- struct{}{}
        }
    }()
    listener, err := net.Listen("tcp", cfg.Address)
    if err != nil {
        return err
    }
    logger.Info(fmt.Sprintf("bind: %s, start listening...", cfg.Address))
    ListenAndServe(listener, handler, closeChan)
    return nil
}

接下來(lái)修改應(yīng)用層服務(wù)器:

// 客戶端連接的抽象
type Client struct {
    // tcp 連接
    Conn net.Conn
    // 當(dāng)服務(wù)端開(kāi)始發(fā)送數(shù)據(jù)時(shí)進(jìn)入waiting, 阻止其它goroutine關(guān)閉連接
    // wait.Wait是作者編寫(xiě)的帶有最大等待時(shí)間的封裝: 
    // https://github.com/HDT3213/godis/blob/master/src/lib/sync/wait/wait.go
    Waiting wait.Wait
}

type EchoHandler struct {
    
    // 保存所有工作狀態(tài)client的集合(把map當(dāng)set用)
    // 需使用并發(fā)安全的容器
    activeConn sync.Map 

    // 關(guān)閉狀態(tài)標(biāo)識(shí)位
    closing atomic.AtomicBool
}

func MakeEchoHandler()(*EchoHandler) {
    return &EchoHandler{}
}

func (h *EchoHandler)Handle(ctx context.Context, conn net.Conn) {
    // 關(guān)閉中的 handler 不會(huì)處理新連接
    if h.closing.Get() {
        conn.Close()
        return 
    }

    client := &Client {
        Conn: conn,
    }
    h.activeConn.Store(client, struct{}{}) // 記住仍然存活的連接

    reader := bufio.NewReader(conn)
    for {
        msg, err := reader.ReadString('\n')
        if err != nil {
            if err == io.EOF {
                logger.Info("connection close")
                h.activeConn.Delete(client)
            } else {
                logger.Warn(err)
            }
            return
        }
        // 發(fā)送數(shù)據(jù)前先置為waiting狀態(tài),阻止連接被關(guān)閉
        client.Waiting.Add(1)

        // 模擬關(guān)閉時(shí)未完成發(fā)送的情況
        //logger.Info("sleeping")
        //time.Sleep(10 * time.Second)

        b := []byte(msg)
        conn.Write(b)
        // 發(fā)送完畢, 結(jié)束waiting
        client.Waiting.Done()
    }
}

// 關(guān)閉客戶端連接
func (c *Client)Close()error {
    // 等待數(shù)據(jù)發(fā)送完成或超時(shí)
    c.Waiting.WaitWithTimeout(10 * time.Second)
    c.Conn.Close()
    return nil
}

// 關(guān)閉服務(wù)器
func (h *EchoHandler)Close()error {
    logger.Info("handler shutting down...")
    h.closing.Set(true)
    // 逐個(gè)關(guān)閉連接
    h.activeConn.Range(func(key interface{}, val interface{})bool {
        client := key.(*Client)
        client.Close()
        return true
    })
    return nil
}

到此這篇關(guān)于Golang 編寫(xiě)Tcp服務(wù)器的解決方案的文章就介紹到這了,更多相關(guān)go tcp服務(wù)器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 一文搞懂Go?Exec?僵尸與孤兒進(jìn)程

    一文搞懂Go?Exec?僵尸與孤兒進(jìn)程

    本文主要介紹了Go?Exec?僵尸與孤兒進(jìn)程,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-02-02
  • Go語(yǔ)言實(shí)現(xiàn)超時(shí)的三種方法實(shí)例

    Go語(yǔ)言實(shí)現(xiàn)超時(shí)的三種方法實(shí)例

    超時(shí)在一些業(yè)務(wù)場(chǎng)景里非常普遍,下面這篇文章主要給大家介紹了關(guān)于Go語(yǔ)言實(shí)現(xiàn)超時(shí)的三種方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Go語(yǔ)言具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2022-07-07
  • Go語(yǔ)言編譯程序從后臺(tái)運(yùn)行,不出現(xiàn)dos窗口的操作

    Go語(yǔ)言編譯程序從后臺(tái)運(yùn)行,不出現(xiàn)dos窗口的操作

    這篇文章主要介紹了Go語(yǔ)言編譯程序從后臺(tái)運(yùn)行,不出現(xiàn)dos窗口的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2021-04-04
  • golang gin ShouldBind的介紹和使用示例詳解

    golang gin ShouldBind的介紹和使用示例詳解

    在 Go 語(yǔ)言的 Gin 框架中,ShouldBind 是用于將請(qǐng)求中的數(shù)據(jù)綁定到結(jié)構(gòu)體的一個(gè)方法,它簡(jiǎn)化了從請(qǐng)求中提取參數(shù)的過(guò)程,支持多種數(shù)據(jù)格式,下面給大家分享golang gin ShouldBind的介紹和使用示例,感興趣的朋友一起看看吧
    2024-10-10
  • golang如何實(shí)現(xiàn)抓取IP地址的蜘蛛程序詳解

    golang如何實(shí)現(xiàn)抓取IP地址的蜘蛛程序詳解

    這篇文章主要給大家介紹了關(guān)于利用golang如何實(shí)現(xiàn)抓取IP地址的蜘蛛程序的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2018-07-07
  • Golang關(guān)鍵字defer的用法詳解

    Golang關(guān)鍵字defer的用法詳解

    defer是Go里面的一個(gè)關(guān)鍵字,用在方法或函數(shù)前面,作為方法或函數(shù)的延遲調(diào)用。這篇文章主要為大家介紹了defer的簡(jiǎn)單使用,需要的可以參考一下
    2023-05-05
  • Go語(yǔ)言Zap庫(kù)Logger的定制化和封裝使用詳解

    Go語(yǔ)言Zap庫(kù)Logger的定制化和封裝使用詳解

    這篇文章主要介紹了Go語(yǔ)言Zap庫(kù)Logger的定制化和封裝使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • golang 如何替換掉字符串里面的換行符\n

    golang 如何替換掉字符串里面的換行符\n

    這篇文章主要介紹了golang 替換掉字符串里面的換行符\n操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2021-03-03
  • 詳解Go語(yǔ)言中for range的

    詳解Go語(yǔ)言中for range的"坑"

    這篇文章主要介紹了詳解Go語(yǔ)言中for range的"坑",文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-09-09
  • Go語(yǔ)言Zap日志庫(kù)使用教程

    Go語(yǔ)言Zap日志庫(kù)使用教程

    在項(xiàng)目開(kāi)發(fā)中,經(jīng)常需要把程序運(yùn)行過(guò)程中各種信息記錄下來(lái),有了詳細(xì)的日志有助于問(wèn)題排查和功能優(yōu)化;但如何選擇和使用性能好功能強(qiáng)大的日志庫(kù),這個(gè)就需要我們從多角度考慮
    2023-02-02

最新評(píng)論

确山县| 徐闻县| 太原市| 大余县| 南部县| 洛隆县| 锦州市| 罗甸县| 锡林浩特市| 翼城县| 谷城县| 乐业县| 盐边县| 文昌市| 林周县| 北辰区| 宜兰县| 岐山县| 巴楚县| 浪卡子县| 呼图壁县| 土默特左旗| 琼海市| 汉中市| 余干县| 聊城市| 沂源县| 长葛市| 辛集市| 青阳县| 鄂托克旗| 陕西省| 嘉义县| 广德县| 丹巴县| 福建省| 师宗县| 乌什县| 河东区| 卓资县| 大宁县|