64行代碼實現(xiàn)零拷貝go的TCP拆包粘包

64行代碼實現(xiàn)零拷貝go的TCP拆包粘包

前言

這段時間想用go寫一個簡單IM系統(tǒng)户魏,就思考了一下go語言TCP的拆包粘包蔬蕊。TCP的拆包粘包有一般有三種解決方案绅作。

使用定長字節(jié)

實際使用中,少于固定字長的囊扳,要用字符去填充烁挟,空間使用率不夠高婴洼。

使用分隔符

一般用文本傳輸?shù)模褂梅指舴成ぃ琁M系統(tǒng)一般對性能要求高柬采,不推薦使用文本傳輸欢唾。

用消息的頭字節(jié)標識消息內(nèi)容的長度

可以使用二進制傳輸,效率高警没,推薦匈辱。下面看看怎么實現(xiàn)。

嘗試使用系統(tǒng)庫自帶的bytes.Buffer實現(xiàn)

代碼實現(xiàn):

package tcp

import (
    "fmt"
    "net"
    "log"
    "bytes"
    "encoding/binary"
)

const (
    BYTES_SIZE uint16 = 1024
    HEAD_SIZE  int    = 2
)

func StartServer(address string) {
    listener, err := net.Listen("tcp", address)
    if err != nil {
        log.Println("Error listening", err.Error())
        return
    }
    for {
        conn, err := listener.Accept()
        fmt.Println(conn.RemoteAddr())
        if err != nil {
            fmt.Println("Error accepting", err.Error())
            return // 終止程序
        }
        go doConn(conn)
    }
}

func doConn(conn net.Conn) {
    var (
        buffer           = bytes.NewBuffer(make([]byte, 0, BYTES_SIZE))
        bytes            = make([]byte, BYTES_SIZE);
        isHead      bool = true
        contentSize int
        head        = make([]byte, HEAD_SIZE)
        content     = make([]byte, BYTES_SIZE)
    )
    for {
        readLen, err := conn.Read(bytes);
        if err != nil {
            log.Println("Error reading", err.Error())
            return
        }
        _, err = buffer.Write(bytes[0:readLen])
        if err != nil {
            log.Println("Error writing to buffer", err.Error())
            return
        }

        for {
            if isHead {
                if buffer.Len() >= HEAD_SIZE {
                    _, err := buffer.Read(head)
                    if err != nil {
                        fmt.Println("Error reading", err.Error())
                        return
                    }
                    contentSize = int(binary.BigEndian.Uint16(head))
                    isHead = false
                } else {
                    break
                }
            }
            if !isHead {
                if buffer.Len() >= contentSize {
                    _, err := buffer.Read(content[:contentSize])
                    if err != nil {
                        fmt.Println("Error reading", err.Error())
                        return
                    }
                    fmt.Println(string(content[:contentSize]))
                    isHead = true
                } else {
                    break
                }
            }
        }
    }
}

測試用例:

package tcp

import (
    "testing"
    "net"
    "fmt"
    "encoding/binary"
)

func TestStartServer(t *testing.T) {
    StartServer("localhost:50002")
}

func TestClient(t *testing.T) {
    conn, err := net.Dial("tcp", "localhost:50002")
    if err != nil {
        fmt.Println("Error dialing", err.Error())
        return // 終止程序
    }
    var headSize int
    var headBytes = make([]byte, 2)
    s := "hello world"
    content := []byte(s)
    headSize = len(content)
    binary.BigEndian.PutUint16(headBytes, uint16(headSize))
    conn.Write(headBytes)
    conn.Write(content)

    s = "hello go"
    content = []byte(s)
    headSize = len(content)
    binary.BigEndian.PutUint16(headBytes, uint16(headSize))
    conn.Write(headBytes)
    conn.Write(content)

    s = "hello tcp"
    content = []byte(s)
    headSize = len(content)
    binary.BigEndian.PutUint16(headBytes, uint16(headSize))
    conn.Write(headBytes)
    conn.Write(content)
}

執(zhí)行結果

127.0.0.1:51062
hello world
hello go
hello tcp

用go系統(tǒng)庫的buffer杀迹,是不是感覺代碼特別別扭,兩大缺點

1.要寫大量的邏輯代碼押搪,來彌補buffer對這個場景的不適用树酪。

2.性能不高,有三次次內(nèi)存拷貝大州,coon->[]byte->Buffer->[]byte续语。

自己實現(xiàn)

既然輪子不合適,就自己造輪子厦画,首先實現(xiàn)一個自己的Buffer,很簡單疮茄,只有六十幾行代碼,所有過程只有一次byte數(shù)組的拷貝根暑,conn->buffer,剩下的全部操作都在原buffer的字節(jié)數(shù)組里面操作

package tcp

import (
    "errors"
    "io"
)

type buffer struct {
    reader io.Reader
    buf    []byte
    start  int
    end    int
}

func newBuffer(reader io.Reader, len int) buffer {
    buf := make([]byte, len)
    return buffer{reader, buf, 0, 0}
}

func (b *buffer) Len() int {
    return b.end - b.start
}

//將有用的字節(jié)前移
func (b *buffer) grow() {
    if b.start == 0 {
        return
    }
    copy(b.buf, b.buf[b.start:b.end])
    b.end -= b.start
    b.start = 0;
}

//從reader里面讀取數(shù)據(jù)力试,如果reader阻塞,會發(fā)生阻塞
func (b *buffer) readFromReader() (int, error) {
    b.grow()
    n, err := b.reader.Read(b.buf[b.end:])
    if (err != nil) {
        return n, err
    }
    b.end += n
    return n, nil
}

//返回n個字節(jié)排嫌,而不產(chǎn)生移位
func (b *buffer) seek(n int) ([]byte, error) {
    if b.end-b.start >= n {
        buf := b.buf[b.start:b.start+n]
        return buf, nil
    }
    return nil, errors.New("not enough")
}

//舍棄offset個字段畸裳,讀取n個字段
func (b *buffer) read(offset, n int) ([]byte) {
    b.start += offset
    buf := b.buf[b.start:b.start+n]
    b.start += n
    return buf
}

再看看怎樣使用它,將上面的doConn函數(shù)改成這樣就行了。

func doConn(conn net.Conn) {
    var (
        buffer      = newBuffer(conn, 16)
        headBuf     []byte
        contentSize int
        contentBuf  []byte
    )
    for {
        _, err := buffer.readFromReader()
        if err != nil {
            fmt.Println(err)
            return
        }
        for {
            headBuf, err = buffer.seek(HEAD_SIZE);
            if err != nil {
                break
            }
            contentSize = int(binary.BigEndian.Uint16(headBuf))
            if (buffer.Len() >= contentSize-HEAD_SIZE) {
                contentBuf = buffer.read(HEAD_SIZE, contentSize)
                fmt.Println(string(contentBuf))
                continue
            }
            break
        }
    }
}

跑下測試用例淳地,看下結果

127.0.0.1:51062
hello world
hello go
hello tcp

源碼地址:https://github.com/alberliu/goim

你有更好的方式怖糊,可以郵箱我,alber_liu@qq.com,讓我學習一下

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颇象,一起剝皮案震驚了整個濱河市伍伤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌遣钳,老刑警劉巖扰魂,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異耍贾,居然都是意外死亡阅爽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門荐开,熙熙樓的掌柜王于貴愁眉苦臉地迎上來付翁,“玉大人,你說我怎么就攤上這事晃听“俨啵” “怎么了砰识?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長佣渴。 經(jīng)常有香客問我辫狼,道長,這世上最難降的妖魔是什么辛润? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任膨处,我火速辦了婚禮,結果婚禮上砂竖,老公的妹妹穿的比我還像新娘真椿。我一直安慰自己,他們只是感情好乎澄,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布突硝。 她就那樣靜靜地躺著,像睡著了一般置济。 火紅的嫁衣襯著肌膚如雪解恰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天浙于,我揣著相機與錄音护盈,去河邊找鬼。 笑死路媚,一個胖子當著我的面吹牛黄琼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播整慎,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼脏款,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了裤园?” 一聲冷哼從身側響起撤师,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拧揽,沒想到半個月后剃盾,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡淤袜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年痒谴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铡羡。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡积蔚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出烦周,到底是詐尸還是另有隱情尽爆,我是刑警寧澤怎顾,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站漱贱,受9級特大地震影響槐雾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜幅狮,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一募强、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧彪笼,春花似錦钻注、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽杏死。三九已至泵肄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間淑翼,已是汗流浹背腐巢。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留玄括,地道東北人冯丙。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像遭京,于是被迫代替她去往敵國和親胃惜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,846評論 25 707
  • 從三月份找實習到現(xiàn)在哪雕,面了一些公司船殉,掛了不少,但最終還是拿到小米斯嚎、百度利虫、阿里、京東堡僻、新浪糠惫、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,218評論 11 349
  • Java NIO(New IO)是從Java 1.4版本開始引入的一個新的IO API钉疫,可以替代標準的Java I...
    JackChen1024閱讀 7,549評論 1 143
  • 夜間散步的收獲硼讽!人間的,自然的陌选,融為一體理郑,成為有機風景蹄溉。
    退休人老高閱讀 240評論 0 0
  • 需甲方確認的內(nèi)容 1.建筑 a.建筑層高及總高度控制 b.人防部門的設計要點 c.空調(diào)設備形式 d.綠色植被種類是...
    青石js閱讀 116評論 0 0