常見面試題整理,金三銀四全靠它了

x1aoda1發表於2021-03-02

目錄

基礎面試題

1 GO

1.1 如何防止goroutin洩露

其實無論是死迴圈、channel 阻塞、鎖等待,只要是會造成阻塞的寫法都可能產生洩露。因而,如何防止 goroutine 洩露就變成了如何防止發生阻塞。為進一步防止洩露,有些實現中會加入超時處理,主動釋放處理時間太長的 goroutine。

1.2 Go語言的鎖

Go語言存在兩種鎖,排他鎖和共享鎖。

var (
    lock sync.Mutex // 排他鎖又叫互斥鎖
    rwlock sync.RWMutex // 讀寫鎖又叫共享鎖
    )

1.3 Go的IO

1、常規讀寫

  • os.Mkdir(name string, perm FileMode) error // 僅建立一層
  • os.MkdirAll(path string, perm FileMode) error // 建立多層
  • os.Create(name string) (file *File, err error) // 存在則覆蓋
  • os.Open(name string) (file *File, err error) // 只讀方式開啟檔案
  • os.OpenFile(name string, flag int, perm FileMode) (file *File, err error) // parm控制許可權,例如0066、0777
  • file.Close() error // 關閉檔案,斷開程式與檔案的連線
  • os.Remove(name string) error // 刪除檔案一層
  • os.RemoveAll(path string) error // 級聯刪除

2、帶緩衝讀寫bufio

io.Reader和io.Writer

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

1.4 new和make的區別

new(T) 和 make(T,args) 是 Go 語言內建函式,用來分配記憶體,但適用的型別不同。

new(T) 會為 T 型別的新值分配已置零的記憶體空間,並返回地址(指標),即型別為 *T的值。換句話說就是,返回一個指標,該指標指向新分配的、型別為 T 的零值。適用於值型別,如陣列、結構體等。

make(T,args) 返回初始化之後的 T 型別的值,這個值並不是 T 型別的零值,也不是指標 *T,是經過初始化之後的 T 的引用。make() 只適用於 slice、map 和 channel.

1.5 go中init函式

一個包中,可以包含多個 init 函式;

程式編譯時,先執行依賴包的 init 函式,再執行 main 包內的 init 函式;可以用_來忽略匯入包,但是執行該包的init函式

main包也可以包含不止一個Init函式,且init函式不能被其他函式顯示呼叫,否則編譯錯誤

1.6 golang中引用型別和值型別及記憶體分配

1.值型別:變數直接儲存值,記憶體通常在棧中分配。

值型別:基本資料型別int、float、bool、string以及陣列和struct

2.引用型別:變數儲存的是一個地址,這個地址儲存最終的值。記憶體通常在 堆上分配。通過GC回收。

引用型別:指標、slice、map、chan等都是引用型別。

1.7 接受者和方法引數何時使用指標型別,何時使用值型別,區別?

方法接受推薦使用指標型別。

  • 推薦在例項方法上使用指標(前提是這個型別不是一個自定義的 map、slice 等引用型別)
  • 當結構體較大的時候使用指標會更高效。一般結構體超過五個欄位,用指標
  • 如果要修改結構內部的資料或狀態必須使用指標
  • 當結構型別包含 sync.Mutex 或者同步這種欄位時,必須使用指標以避免成員拷貝
  • 如果你不知道該不該使用指標,使用指標!

方法引數該使用什麼型別?

  • map、slice 等型別不需要使用指標(自帶 buf)
  • 指標可以避免記憶體拷貝,結構大的時候不要使用值型別
  • 值型別和指標型別在方法內部都會產生一份拷貝,指向不同
  • 小資料型別如 bool、int 等沒必要使用指標傳遞
  • 初始化一個新型別時(像 NewEngine() *Engine)使用指標
  • 變數的生命週期越長則使用指標,否則使用值型別

1.8 值接受者和指標接受者互相呼叫問題

1、值型別可以呼叫值接受者方法,也能呼叫指標接收者方法,當值型別呼叫指標接受者方法是,編譯器底層會先取地址&,再呼叫

2、指標型別也已呼叫指標接受者方法,也可以呼叫值接收方法,當指標型別呼叫值接受者方法是,編譯器底層會先解引用*,再呼叫

特殊情況需要注意:

1、當值型別不能被定址的時候,該值不能呼叫對應的指標接受者方法

2、當指標接受者方法是為了實現某個介面的時候,那麼只有指標型別的實體實現了介面。用值型別,不算實現介面。如果是值接收者,實體型別的值和指標都可以實現對應的介面;

1.9 Go的排程

G: 協程,對應的資料結構為runtime.G。全域性變數allgs記錄著所有的g

P: 資料結構為runtime.P。擁有本地runq變數,和全域性變數sched,sched對應的結構是runtime.schedt代表排程器,P會和一個M繫結,M可以直接從P這裡獲取待執行的G

M: 工作執行緒,對應的資料結構為runtime.M。全域性變數allm記錄著所有的m

如果P的本地佇列已滿,那麼等待執行的G就會被放到全域性佇列中,M會先從關聯P所持有的本地runq中獲取待執行的G,如果沒有的話再去全域性佇列中領取一些G來執行,如果全域性佇列中也沒有多餘的G,那就去別的P那裡領取一些G。

1.10 go中的slice擴容規則

規則1:需要增長到的容量cap是原始容量的兩倍還多,則擴容到cap

規則2.1:需要增長的容量小於原容量2倍,但是原用量小於1024,則2倍擴容

規則2.2:需要增長的容量小於原容量2倍,但是原容量大於1024,則1.25倍擴容

擴容後需要分配的記憶體,並不是擴容後的容量乘以資料型別位元組。而是根據擴容後的容量去記憶體管理模組申請最匹配且覆蓋的容量,根據這個容量乘以資料型別,才是最終需要分配的記憶體空間

1.11 go中的map擴容規則

  • go語言map的預設負載因子是6.5

規則1:count / (2 ^ B) > 6.5 時,翻倍擴容;

規則2:負載因子沒超標,但是溢位桶使用較多,觸發等量擴容。所謂等量擴容,就是建立和舊桶數目一樣多的新桶,把舊桶中的值遷移到新桶中。

注意:等量擴容有什麼用,如果負載因子沒超,但是用了很多的溢位桶。那麼只能說明存在很多的刪除的鍵值對。擴容後更加緊湊,減少了溢位桶的使用

超過負載因子,翻倍擴容;溢位桶較多,等量擴容

1.12 Go的反射包怎麼找到對應的方法

反射主要兩個函式:

func TypeOf(i interface{}) Type  // 用來提取一個介面中值的型別資訊。
func ValueOf(i interface{}) Value

返回的Type是介面型別,Type型別包含很多個方法,可以呼叫:

type Type interface {
    // 所有的型別都可以呼叫下面這些函式。下面簡單列舉,函式比較多

    // 此型別的變數對齊後所佔用的位元組數
    Align() int

    // 如果是 struct 的欄位,對齊後佔用的位元組數
    FieldAlign() int

    // 返回型別方法集裡的第 `i` (傳入的引數)個方法
    Method(int) Method

    // 通過名稱獲取方法
    MethodByName(string) (Method, bool)

    // 獲取型別方法集裡匯出的方法個數
    NumMethod() int

    // 型別名稱
    Name() string

    // 返回型別所在的路徑,如:encoding/base64
    PkgPath() string

    // 返回型別的大小,和 unsafe.Sizeof 功能類似
    Size() uintptr

    // 返回型別的字串表示形式
    String() string

    // 返回型別的型別值
    Kind() Kind

    // 型別是否實現了介面 u
    Implements(u Type) bool

    // 是否可以賦值給 u
    AssignableTo(u Type) bool

    // 是否可以型別轉換成 u
    ConvertibleTo(u Type) bool

    // 型別是否可以比較
    Comparable() bool
    
    // ......

}

Value是結構體,它包含型別結構體指標、真實資料的地址、標誌位。Value 結構體定義了很多方法,通過這些方法可以直接操作 Value 欄位 ptr 所指向的實際資料:

// 設定切片的 len 欄位,如果型別不是切片,就會panic
 func (v Value) SetLen(n int)

 // 設定切片的 cap 欄位
 func (v Value) SetCap(n int)

 // 設定字典的 kv
 func (v Value) SetMapIndex(key, val Value)

 // 返回切片、字串、陣列的索引 i 處的值
 func (v Value) Index(i int) Value

 // 根據名稱獲取結構體的內部欄位值
 func (v Value) FieldByName(name string) Value

 // ……

Value 欄位還有很多其他的方法。例如:

// 用來獲取 int 型別的值
func (v Value) Int() int64

// 用來獲取結構體欄位(成員)數量
func (v Value) NumField() int

// 嘗試向通道傳送資料(不會阻塞)
func (v Value) TrySend(x reflect.Value) bool

// 通過引數列表 in 呼叫 v 值所代表的函式(或方法
func (v Value) Call(in []Value) (r []Value) 

// 呼叫變參長度可變的函式
func (v Value) CallSlice(in []Value) []Value 

// 等等....

1.13 Go的channel(有緩衝和無緩衝)

1、ch := make(chan int) 無緩衝的channel由於沒有緩衝傳送和接收需要同步.

2、ch := make(chan int, 2) 有緩衝channel不要求傳送和接收操作同步.

3、channel無緩衝時,傳送阻塞直到資料被接收,接收阻塞直到讀到資料。

4、channel有緩衝時,當緩衝滿時傳送阻塞,當緩衝空時接收阻塞

1.14 Gin的context和Go的context

Context是由Golang官方開發的併發控制包,一方面可以用於當請求超時或者取消時候,相關的goroutine馬上退出釋放資源,另一方面Context本身含義就是上下文,其可以在多個goroutine或者多個處理函式之間傳遞共享的資訊。

建立一個新的context,必須基於一個父context,新的context又可以作為其他context的父context。所有context在一起構造一個context樹。

1、Context用途:

  • Context一大用處就是超時控制。
  • Context另外一個用途就是傳遞上下文資訊。

2、Context介面一共包含四個方法:

  • Deadline:返回繫結該context任務的執行超時時間,若未設定,則ok等於false
  • Done:返回一個只讀通道,當繫結該context的任務執行完成並呼叫cancel方法或者任務執行超時時候,該通道會被關閉
  • Err:返回一個錯誤,如果Done返回的通道未關閉則返回nil,如果context如果被取消,返回Canceled錯誤,如果超時則會返回DeadlineExceeded錯誤
  • Value:根據key返回,儲存在context中k-v資料

3、使用Context注意事項:

  • 不要將Context作為結構體的一個欄位儲存,相反而應該顯示傳遞Context給每一個需要它的函式,Context應該作為函式的第一個引數,並命名為ctx
  • 不要傳遞一個nil Context給一個函式,即使該函式能夠接受它。如果你不確定使用哪一個Context,那你就傳遞context.TODO
  • context是併發安全的,相同的Context能夠傳遞給執行在不同goroutine的函式

Gin的Context是Go中Context介面的一個實現。

1.15 go的垃圾回收

常用垃圾回收原則有引用計數和標記清掃。Go採用的是標記清掃。

Go語言中的垃圾回收採用標記清掃演算法,支援主體併發增量式回收。使用插入和刪除兩種寫屏障的混合寫屏障,併發是使用者程式和垃圾回收可以併發執行,增量式回收保證一次垃圾回收的STW分攤到多次。

標記清掃-三色標記:

要識別存活物件,可以把棧,資料段上的資料物件作為根root,基於他們進一步追蹤,把能追蹤到的資料都進行標記。剩下的追蹤不到的就是垃圾了。

  • 垃圾回收開始時,所有資料都為白色,然後把直接追蹤到的root節點標記為灰色,灰色代表基於當前節點展開的追蹤還未完成。
  • 當基於某個root節點的追蹤任務完成後,便會把該root節點標記為黑色,黑色表示它是存活資料,而且無需基於它再次追蹤了。
  • 基於黑色節點找到的所有節點都被標記為灰色,表示還要基於它們進一步展開追蹤
    。當沒有灰色節點時,意味著標記工作可以結束了。此時有用資料都為黑色,無用資料都為白色,接下來回收這些白色物件的記憶體地址即可

1.16 golang中開協程的限制,底層排程?8C8G的伺服器最多開多少協程?

1、計算機資源肯定是有限的,所以goroutine肯定也是有限制的,單純的goroutine,一開始每個佔用4K記憶體,所以這裡會受到記憶體使用量的限制,還有goroutine是通過系統執行緒來執行的,golang預設最大的執行緒數是10000個。可以通過https://gowalker.org/runtime/debug#SetMaxThreads來修改。

2、要注意執行緒和goroutine不是一一對應關係,理論上記憶體足夠大,而且goroutine不是計算密集型的話,可以開啟無限個goroutine。

2 Redis

2.1 redis中快取穿透和快取雪崩的概念?

快取穿透:就是查詢一個資料庫不存在的key,先查快取,快取沒有再查資料庫,資料庫也沒有也就不會快取。那麼不停的查這個不存在的key就屬於惡意攻擊,存在快取擊穿的概念

快取雪崩:就是一批商品的快取時間是一樣的,如果遇到雙十一,一批商品同時快取失效,查詢都會落到DB的頭像。所以設定快取儘量分散快取過期時間,熱門類目的商品快取時間長一些,冷門類目的商品快取時間短一些,也能節省快取服務的資源

快取擊穿:是指一個key非常熱點,在不停的扛著大併發,大併發集中對這一個點進行訪問,當這個key在失效的瞬間,持續的大併發就穿破快取,直接請求資料庫,就像在一個屏障上鑿開了一個洞。

2.2 redis持久化?兩種持久化的方式?

RDB(Redis DataBase)和AOF(Append Only File)。

RDB其實就是把資料以快照的形式儲存在磁碟上。什麼是快照呢,你可以理解成把當前時刻的資料拍成一張照片儲存下來。

全量備份總是耗時的,有時候我們提供一種更加高效的方式AOF,工作機制很簡單,redis會將每一個收到的寫命令都通過write函式追加到檔案中。通俗的理解就是日誌記錄。

3 Mysql

3.1 Mysql事務ACID特性

  • 原子性(Atomicity):事務開始後所有操作,要麼全部做完,要麼全部不做,不可能停滯在中間環節。事務執行過程中出錯,會回滾到事務開始前的狀態,所有的操作就像沒有發生一樣。也就是說事務是一個不可分割的整體,就像化學中學過的原子,是物質構成的基本單位。
  • 一致性(Consistency):事務開始前和結束後,資料庫的完整性約束沒有被破壞 。比如A向B轉賬,不可能A扣了錢,B卻沒收到。
  • 隔離性(Isolation):同一時間,只允許一個事務請求同一資料,不同的事務之間彼此沒有任何干擾。比如A正在從一張銀行卡中取錢,在A取錢的過程結束前,B不能向這張卡轉賬。
  • 永續性(Durability):事務完成後,事務對資料庫的所有更新將被儲存到資料庫,不能回滾。

3.2 事務的隔離級別

  • 髒讀:讀取未提交資料、常發生在轉賬與取款操作中
  • 不可重複讀:前後多次讀取,資料內容不一致。
  • 幻讀:前後多次讀取,資料總量不一致

讀未提交:在這種隔離級別下,所有事務能夠讀取其他事務未提交的資料。讀取其他事務未提交的資料,會造成髒讀。因此在該種隔離級別下,不能解決髒讀、不可重複讀和幻讀。

讀已提交:在這種隔離級別下,所有事務只能讀取其他事務已經提交的內容。能夠徹底解決髒讀的現象。但在這種隔離級別下,會出現一個事務的前後多次的查詢中卻返回了不同內容的資料的現象,也就是出現了不可重複讀。這是大多數資料庫系統預設的隔離級別,例如Oracle和SQL Server,但mysql不是。

可重複讀:在這種隔離級別下,所有事務前後多次的讀取到的資料內容是不變的。也就是某個事務在執行的過程中,不允許其他事務進行update操作,但允許其他事務進行add操作,造成某個事務前後多次讀取到的資料總量不一致的現象,從而產生幻讀。mysql的預設事務隔離級別

可序列化:在這種隔離級別下,所有的事務順序執行,所以他們之間不存在衝突,從而能有效地解決髒讀、不可重複讀和幻讀的現象。但是安全和效率不能兼得,這樣事務隔離級別,會導致大量的操作超時和鎖競爭,從而大大降低資料庫的效能,一般不使用這樣事務隔離級別。

隔離級別 髒讀 不可重複讀 幻讀
read uncommitted(未提交讀) T T T
read committed(提交讀) F T T
repeatable read(可重複讀) F F T
serializable (可序列化) F F F

3.3 InnoDb是表鎖還是行鎖,為什麼

3.4 Mysql索引的底層資料結構是什麼?為什麼要使用B+樹?B+樹為什麼要比B樹穩定?

Mysql底層資料結構是B+樹,B+樹的查詢效率更高,B+樹中間層級的節點不儲存資料,只儲存索引,所以整體層數回更少,範圍查詢上B+樹優勢更大,原因是B+樹資料節點都在最下層,且節點與節點之間有引用指向

  • 單一節點儲存更多的元素,使得查詢的IO次數更少;
  • 所有查詢都要查詢到葉子節點,查詢效能穩定;
  • 所有葉子節點形成有序連結串列,便於範圍查詢。

3.5 Mysql的執行計劃?執行計劃中,哪些語句可以看出來該語句走了全表掃描?

其中最重要的欄位為:id、type、key、rows、Extra

  • id : id相同:執行順序由上至下 ;id不同:如果是子查詢,id的序號會遞增,id值越大優先順序越高,越先被執行
  • select_type:查詢的型別,主要是用於區分普通查詢、聯合查詢、子查詢等複雜的查詢
  • type:訪問型別,sql查詢優化中一個很重要的指標,結果值從好到壞依次是:system > const > eq_ref > ref > range > index > ALL;一般來說,好的sql查詢至少達到range級別,最好能達到ref

4 網路

4.1 time-wait和close-wait

4.2 TCP三次握手

4.3TCP的擁塞控制?擁塞控制做什麼用的,通過哪種方式控制網路的傳輸速度?

1、慢啟動階段思路是不要一開始就傳送大量的資料,先探測一下網路的擁塞程度,也就是說由小到大逐漸增加擁塞視窗的大小,在沒有出現丟包時每收到一個 ACK 就將擁塞視窗大小加一(單位是 MSS,最大單個報文段長度),每輪次傳送視窗增加一倍,呈指數增長,若出現丟包,則將擁塞視窗減半,進入擁塞避免階段;

2、當視窗達到慢啟動閾值或出現丟包時,進入擁塞避免階段,視窗每輪次加一,呈線性增長;當收到對一個報文的三個重複的 ACK 時,認為這個報文的下一個報文丟失了,進入快重傳階段,要求接收方在收到一個失序的報文段後就立即發出重複確認(為的是使傳送方及早知道有報文段沒有到達對方,可提高網路吞吐量約20%)而不要等到自己傳送資料時捎帶確認;

5 Linux

5.1 孤兒程式,殭屍程式

孤兒程式:一個父程式退出,而它的一個或多個子程式還在執行,那麼那些子程式將成為孤兒程式。孤兒程式將被init程式(程式號為1)所收養,並由init程式對它們完成狀態收集工作。

殭屍程式:一個程式使用fork建立子程式,如果子程式退出,而父程式並沒有呼叫wait或waitpid獲取子程式的狀態資訊,那麼子程式的程式控制塊(PCB)仍然儲存在系統中。這種程式稱之為僵死程式

危害:

殭屍程式危害,大量的產生僵死程式,將因為沒有可用的程式號而導致系統不能產生新的程式. 此即為殭屍程式的危害,應當避免。

孤兒程式是沒有父程式的程式,孤兒程式這個重任就落到了init程式身上,除了init程式會忙一些,並沒有什麼危害

5.2 死鎖條件,如何避免

死鎖產生的四個必要條件:

1.互斥性:執行緒對資源的佔有是排他性的,一個資源只能被一個執行緒佔有,直到釋放。

2.請求和保持條件:一個執行緒對請求被佔有資源發生阻塞時,對已經獲得的資源不釋放。

3.不剝奪:一個執行緒在釋放資源之前,其他的執行緒無法剝奪佔用。

4.迴圈等待:發生死鎖時,執行緒進入死迴圈,永久阻塞。

避免死鎖的方法為破壞後三個必要條件:

1.破壞“請求和保持”條件:想辦法,讓程式不要那麼貪心,自己已經有了資源就不要去競爭那些不可搶佔的資源。比如,讓程式在申請資源時,一次性申請所有需要用到的資源,不要一次一次來申請,當申請的資源有一些沒空,那就讓執行緒等待。不過這個方法比較浪費資源,程式可能經常處於飢餓狀態。還有一種方法是,要求程式在申請資源前,要釋放自己擁有的資源。

2.破壞“不可搶佔”條件:允許程式進行搶佔,方法一:如果去搶資源,被拒絕,就釋放自己的資源。方法二:作業系統允許搶,只要你優先順序大,可以搶到。

3.破壞“迴圈等待”條件:將系統中的所有資源統一編號,程式可在任何時刻提出資源申請,但所有申請必須按照資源的編號順序(升序)提出

6 併發

6.1 三個協程ABC同時啟動,不停迴圈列印ABC。如何實現?

  • 方法1:協程通知與切換方式
func main() {
	chA, chB, chC := make(chan string), make(chan string), make(chan string)

	go func() {
		for i := 1; ; {
			select {
			case <-chA:
				fmt.Println("[A]:", "A")
				chB <- "B"
				i += 3
			}
		}
	}()

	go func() {
		for i := 2; ; {
			select {
			case <-chB:
				fmt.Println("[B]:", "B")
				chC <- "C"
				i += 3
			}
		}
	}()

	go func() {
		for i := 3; ; {
			if i == 3 {
				chA <- "A"
			}
			select {
			case <-chC:
				fmt.Println("[C]:", "C")
				chA <- "A"
				i += 3
			}
		}
	}()

	for !false {
		fmt.Print()
	}
}
  • 方法2:協程寫入,主協程列印的方式

7 演算法

7.1 LRU演算法

LRU 是 Least Recently Used 的縮寫,這種演算法認為最近使用的資料是熱門資料,下一次很大概率將會再次被使用。而最近很少被使用的資料,很大概率下一次不再用到。當快取容量滿的時候,優先淘汰最近很少使用的資料。

思路:

準備兩張表,一個雜湊表,一個雙向連結串列。假設A,3存入表中,則map中key還是原始的key,key=A,value加工一下,包括A和3.
對於雙向連結串列從尾部加,從頭部出。如果需要將某個元素從拿出,則此時將需要拿出的元素拿出,放到連結串列的最後,此時,該元素優先順序最高。

7.2 TopK問題。記憶體2G, 檔案10G,按行讀取檔案,求任意兩行互為逆序的字串,儲存起來

Hash大法好!

8 Web

8.1 Cookie和Session

  • Cookie

Cookie通過在客戶端記錄資訊確定使用者身份,Session通過在伺服器端記錄資訊確定使用者身份。

由於HTTP是一種無狀態的協議,伺服器單從網路連線上無從知道客戶身份。怎麼辦呢?就給客戶端們頒發一個通行證吧,每人一個,無論誰訪問都必須攜帶自己通行證。這樣伺服器就能從通行證上確認客戶身份了。這就是Cookie的工作原理。

Cookie具有不可跨域名性。根據Cookie規範,瀏覽器訪問Google只會攜帶Google的Cookie,而不會攜帶Baidu的Cookie。Google也只能操作Google的Cookie,而不能操作Baidu的Cookie。

  • Session

Session是伺服器端使用的一種記錄客戶端狀態的機制,使用上比Cookie簡單一些,相應的也增加了伺服器的儲存壓力。

  • 對比

1、cookie資料存放在客戶的瀏覽器上,session資料放在伺服器上.

2、cookie不是很安全,別人可以分析存放在本地的COOKIE並進行COOKIE欺騙考慮到安全應當使用session。

4、session會在一定時間內儲存在伺服器上。當訪問增多,會比較佔用你伺服器的效能考慮到減輕伺服器效能方面,應當使用cookie。

8.2 http,https

https是基於安全套接字的http協議,也可以理解為是http+ssl/tls(數字證照)的組合

8.3 單點登入,tcp粘包

單點登入,門戶鑑權後,分發token,使用者請求在cookie中帶上token資訊,就可以訪問門戶下的所有子系統了。

tcp粘包:

傳送端為了將多個發往接收端的包,更加高效的的發給接收端,於是採用了優化演算法(Nagle演算法),將多次間隔較小、資料量較小的資料,合併成一個資料量大的資料塊,然後進行封包。那麼這樣一來,接收端就必須使用高效科學的拆包機制來分辨這些資料。

TCP粘包就是指傳送方傳送的若干包資料到達接收方時粘成了一包,從接收緩衝區來看,後一包資料的頭緊接著前一包資料的尾,出現粘包的原因是多方面的,可能是來自傳送方,也可能是來自接收方。

解決方法:

1、傳送方:對於傳送方造成的粘包問題,可以通過關閉Nagle演算法來解決,使用TCP_NODELAY選項來關閉演算法。

2、接收方:接收方沒有辦法來處理粘包現象,只能將問題交給應用層來處理。

3、應用層:

格式化資料:每條資料有固定的格式(開始符,結束符),這種方法簡單易行,但是選擇開始符和結束符時一定要確保每條資料的內部不包含開始符和結束符。

傳送長度:傳送每條資料時,將資料的長度一併傳送,例如規定資料的前4位是資料的長度,應用層在處理時可以根據長度來判斷每個分組的開始和結束位置。

相關文章