騰訊的校招面試也沒那么難嘛!
今天分享的是騰訊校招Golang后端面經(jīng),這位同學(xué)一面之后信心滿滿的來找我說:“騰訊的校招面試也沒那么難嘛,也可能只是一面,后面才會(huì)放大招?!?/p>
TA復(fù)盤的關(guān)鍵面試題如下:
圖片
我給大家整理一下考察的知識(shí):
- Go基礎(chǔ):數(shù)組和切片,結(jié)構(gòu)體,逃逸分析,GC
- 數(shù)據(jù)結(jié)構(gòu):B+樹和B樹
- 緩存:持久化策略,緩存穿透
- 計(jì)網(wǎng):TCP/UDP
- 算法:最長回文串
面試題詳解
slice和數(shù)組的區(qū)別
這是一個(gè)經(jīng)常問到的面試題。
slice 的底層數(shù)據(jù)是數(shù)組,它描述一個(gè)數(shù)組的片段。兩者都可以通過下標(biāo)來訪問單個(gè)元素。
數(shù)組是定長的,長度定義好之后,不能再更改。在 Go 中,數(shù)組是不常見的,因?yàn)槠溟L度是類型的一部分,限制了它的表達(dá)能力,比如 [3]int 和 [4]int 就是不同的類型。
而切片則非常靈活,它可以動(dòng)態(tài)地?cái)U(kuò)容。切片的類型和長度無關(guān)。
數(shù)組就是一片連續(xù)的內(nèi)存, slice 實(shí)際上是一個(gè)結(jié)構(gòu)體,包含三個(gè)字段:長度、容量、底層數(shù)組。
type slice struct {
array unsafe.Pointer // 元素指針
len int // 長度
cap int // 容量
}
slice的數(shù)據(jù)結(jié)構(gòu)如下:
圖片
(注意: 底層數(shù)組是可以被多個(gè) slice 同時(shí)指向的,因此對(duì)一個(gè) slice 的元素進(jìn)行操作是有可能影響到其他 slice 的。)
還有一個(gè)很重要的區(qū)別就是slice作為函數(shù)參數(shù)傳遞時(shí)為按引用傳遞的,函數(shù)內(nèi)對(duì)其元素的修改將導(dǎo)致函數(shù)外的值也發(fā)生改變,不過需要注意的是傳入的是一個(gè)指針的副本,如果對(duì)該指針進(jìn)行修改,不會(huì)導(dǎo)致原本的指針發(fā)生變化。而數(shù)組是值傳遞,函數(shù)內(nèi)對(duì)數(shù)組的值的改變不影響初始數(shù)組。
兩個(gè)結(jié)構(gòu)體可以進(jìn)行等值比較嗎?
可以試一下運(yùn)行下列代碼:
package main
import "fmt"
func main() {
sn1 := struct {
age int
name string
}{age: 11, name: "qq"}
sn2 := struct {
age int
name string
}{age: 11, name: "qq"}
if sn1 == sn2 {
fmt.Println("sn1 == sn2")
}
sm1 := struct {
age int
m map[string]string
}{age: 11, m: map[string]string{"a": "1"}}
sm2 := struct {
age int
m map[string]string
}{age: 11, m: map[string]string{"a": "1"}}
if sm1 == sm2 {
fmt.Println("sm1 == sm2")
}
}
參考答案:編譯不通過 invalid operation: sm1 == sm2
解析:
- 結(jié)構(gòu)體能比較是否相等,不能比較大小。
- 相同類型的結(jié)構(gòu)體才能夠進(jìn)行比較,結(jié)構(gòu)體是否相同不但與屬性類型有關(guān),還與屬性順序相關(guān),下面的sn3 與上面的 sn1 就是不同的結(jié)構(gòu)體
sn3:= struct {
name string
age int
}{age:11,name:"qq"}
- 如果 struct 的所有成員都可以?較,則該 struct 就可以通過 == 或 != 進(jìn)??較是否相等,?較時(shí)逐個(gè)項(xiàng)進(jìn)??較,如果每?項(xiàng)都相等,則兩個(gè)結(jié)構(gòu)體才相等,否則不相等;
那什么是可?較的呢,常?的有 bool、數(shù)值型、string、指針、數(shù)組等,像切?、map、函數(shù)等是不能 ?較的。
說說逃逸分析
要搞清楚GO的逃逸分析一定要先搞清楚內(nèi)存分配和堆棧:
內(nèi)存既可以分配到堆中,也可以分配到棧中。
GO語言是如何進(jìn)行內(nèi)存分配的呢?其設(shè)計(jì)初衷和實(shí)現(xiàn)原理是什么呢?
要搞清楚上面的問題,我們先來聊一下內(nèi)存管理和堆、棧的知識(shí)點(diǎn):
內(nèi)存管理
內(nèi)存管理主要包括兩個(gè)動(dòng)作:分配與釋放。逃逸分析就是服務(wù)于內(nèi)存分配的,而內(nèi)存的釋放由GC負(fù)責(zé)。
棧
在Go語言中,棧的內(nèi)存是由編譯器自動(dòng)進(jìn)行分配和釋放的,棧區(qū)往往存儲(chǔ)著函數(shù)參數(shù)、局部變量和調(diào)用函數(shù)幀,它們隨著函數(shù)的創(chuàng)建而分配,隨著函數(shù)的退出而銷毀。
Go應(yīng)用程序運(yùn)行時(shí),每個(gè) goroutine 都維護(hù)著一個(gè)自己的棧區(qū),這個(gè)棧區(qū)只能自己使用不能被其他 goroutine 使用。棧是調(diào)用棧(call stack)的簡稱。一個(gè)棧通常又包含了許多棧幀(stack frame),它描述的是函數(shù)之間的調(diào)用關(guān)系
堆
與棧不同的是,堆區(qū)的內(nèi)存一般由編譯器和工程師自己共同進(jìn)行管理分配,交給 Runtime GC 來釋放。在堆上分配時(shí),必須找到一塊足夠大的內(nèi)存來存放新的變量數(shù)據(jù)。后續(xù)釋放時(shí),垃圾回收器掃描堆空間尋找不再被使用的對(duì)象。
我們可以簡單理解為:我們用GO語言開發(fā)過程中,要考慮的內(nèi)存管理只是針對(duì)堆內(nèi)存而言的。
程序在運(yùn)行期間可以主動(dòng)從堆上申請(qǐng)內(nèi)存,這些內(nèi)存通過Go的內(nèi)存分配器分配,并由垃圾收集器回收。
為了方便大家理解,我們再從以下角度對(duì)比一下堆棧:
堆和棧的對(duì)比
加鎖
- 棧不需要加鎖:每個(gè)goroutine都獨(dú)享自己的棧空間,這就意味著棧上的內(nèi)存操作是不需要加鎖的。
- 堆有時(shí)需要加鎖:堆上的內(nèi)存,有時(shí)需要加鎖防止多線程沖突
延伸知識(shí)點(diǎn):為什么堆上的內(nèi)存有時(shí)需要加鎖?而不是一直需要加鎖呢?
因?yàn)镚o的內(nèi)存分配策略學(xué)習(xí)了TCMalloc的線程緩存思想,他為每個(gè)處理器分配了一個(gè)mcache,注意:從mcache分配內(nèi)存也是無鎖的。
性能
- 棧內(nèi)存管理 性能好:棧上的內(nèi)存,它的分配與釋放非常高效的。簡單地說,它只需要兩個(gè)CPU指令:一個(gè)是分配入棧,另外一個(gè)是棧內(nèi)釋放。只需要借助于棧相關(guān)寄存器即可完成。
- 堆內(nèi)存管理 性能差:對(duì)于程序堆上的內(nèi)存回收,還需要有標(biāo)記清除階段,例如Go采用的三色標(biāo)記法。
緩存策略
- 棧緩存性能更好
- 堆緩存性能較差原因是:棧內(nèi)存能更好地利用CPU的緩存策略,因?yàn)闂?臻g相較于堆來說是更連續(xù)的。
逃逸分析
上面說了這么多堆和棧的知識(shí)點(diǎn),目的是為了讓大家更好的理解逃逸分析。
正如上面講的,相比于把內(nèi)存分配到堆中,分配到棧中優(yōu)勢更明顯。
Go語言也是這么做的:Go編譯器會(huì)盡可能將變量分配到到棧上。
但是,在函數(shù)返回后無法證明變量未被引用,則該變量將被分配到堆上,該變量不隨函數(shù)棧的回收而回收。以此避免懸掛指針(dangling pointer)的問題。
另外,如果局部變量占用內(nèi)存非常大,也會(huì)將其分配在堆上。
Go是如何確定內(nèi)存是分配到棧上還是堆上的呢?
答案就是:逃逸分析。
編譯器通過逃逸分析技術(shù)去選擇堆或者棧,逃逸分析的基本思想如下:檢查變量的生命周期是否是完全可知的,如果通過檢查,則在棧上分配。否則,就是所謂的逃逸,必須在堆上進(jìn)行分配。
逃逸分析原則
Go語言雖然沒有明確說明逃逸分析原則,但是有以下幾點(diǎn)準(zhǔn)則,是可以參考的。
- 不同于JAVA JVM的運(yùn)行時(shí)逃逸分析,Go的逃逸分析是在編譯期完成的:編譯期無法確定的參數(shù)類型必定放到堆中;
- 如果變量在函數(shù)外部存在引用,則必定放在堆中;
- 如果變量占用內(nèi)存較大時(shí),則優(yōu)先放到堆中;
- 如果變量在函數(shù)外部沒有引用,則優(yōu)先放到棧中;
GC中的根節(jié)點(diǎn)是什么?
Go 基礎(chǔ)知識(shí)中 GC 肯定是比較重要的部分,然而平時(shí)我們在看八股文的時(shí)候總會(huì)對(duì)文中所提到的“根節(jié)點(diǎn)”產(chǎn)生疑惑,那么到底什么是根節(jié)點(diǎn)呢?
在垃圾回收的上下文中,“根節(jié)點(diǎn)”是指程序中被直接或間接引用的對(duì)象集合。
針對(duì) Go 語言,垃圾回收器會(huì)從程序的“根節(jié)點(diǎn)”開始遍歷,找出所有可以被訪問到的對(duì)象,并標(biāo)記它們?yōu)榭蛇_(dá)對(duì)象。根據(jù)上述“根節(jié)點(diǎn)”定義,Go 程序的根節(jié)點(diǎn)通常包括以下幾類對(duì)象:
- 程序的全局變量和靜態(tài)變量:這些變量在整個(gè)程序執(zhí)行過程中都可以被訪問到,因此垃圾回收器會(huì)將它們作為根節(jié)點(diǎn)。
- 程序的調(diào)用棧中的變量:這些變量在函數(shù)調(diào)用過程中被創(chuàng)建,并在函數(shù)返回時(shí)被銷毀。因此,在函數(shù)調(diào)用期間,它們被認(rèn)為是根節(jié)點(diǎn)。
- 當(dāng)前執(zhí)行的Goroutine:在 Go 語言中,Goroutine 是輕量級(jí)的線程,它們可以獨(dú)立地運(yùn)行,因此當(dāng)前執(zhí)行的Goroutine也被認(rèn)為是根節(jié)點(diǎn)。
垃圾回收器從這些根節(jié)點(diǎn)開始遍歷,查找所有可以被訪問到的對(duì)象,并標(biāo)記它們?yōu)榭蛇_(dá)對(duì)象。而沒有被標(biāo)記為可達(dá)對(duì)象的對(duì)象就是垃圾對(duì)象,可以被回收。這個(gè)過程被稱為可達(dá)性分析。
既然 GC 不在棧上起作用,那為什么根節(jié)點(diǎn)還包括程序的調(diào)用棧中的變量呢?
根節(jié)點(diǎn)是指程序中被直接或間接引用的對(duì)象集合,它們是垃圾回收器掃描堆中對(duì)象時(shí)的起點(diǎn)。程序的調(diào)用棧中的變量也可以被認(rèn)為是根節(jié)點(diǎn)之一,因?yàn)樗鼈兛梢员黄渌麑?duì)象引用。
調(diào)用棧是用于存儲(chǔ)函數(shù)調(diào)用信息的一種數(shù)據(jù)結(jié)構(gòu),它由多個(gè)幀組成,每個(gè)幀對(duì)應(yīng)一個(gè)函數(shù)調(diào)用。每當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),就會(huì)在調(diào)用棧中創(chuàng)建一個(gè)新的幀,并將該函數(shù)的參數(shù)、局部變量和返回地址等信息保存到幀中。當(dāng)函數(shù)返回時(shí),對(duì)應(yīng)的幀就會(huì)被銷毀,該函數(shù)的所有局部變量也隨之被釋放。雖然調(diào)用棧中的變量存儲(chǔ)在棧上,但它們也可以被其他對(duì)象引用,例如一個(gè)函數(shù)返回一個(gè)指向調(diào)用棧中局部變量的指針。因此,當(dāng)垃圾回收器掃描堆中對(duì)象時(shí),它也需要考慮調(diào)用棧中的變量是否被其他對(duì)象引用,以便正確地標(biāo)記和回收不再使用的對(duì)象。
綜上所述,調(diào)用棧中的變量雖然存儲(chǔ)在棧上,但它們也可以被認(rèn)為是根節(jié)點(diǎn)之一,因?yàn)樗鼈兛梢员黄渌麑?duì)象引用。因此,在Go語言中,垃圾回收器需要掃描調(diào)用棧中的變量,以確保不會(huì)回收被其他對(duì)象引用的變量。
b樹和b+樹的區(qū)別,為什么索引使用b+樹結(jié)構(gòu)?
- B樹的每個(gè)節(jié)點(diǎn)都存儲(chǔ)了key和data,而B+樹的data存儲(chǔ)在葉子節(jié)點(diǎn)上。
B+樹非葉子節(jié)點(diǎn)僅存儲(chǔ)key不存儲(chǔ)data,這樣一個(gè)節(jié)點(diǎn)就可以存儲(chǔ)更多的key??梢允沟肂+樹相對(duì)B樹來說更矮(IO次數(shù)就是樹的高度),所以與磁盤交換的IO操作次數(shù)更少。
- B+樹所有葉子節(jié)點(diǎn)構(gòu)成一個(gè)有序鏈表,按主鍵排序來遍歷全部記錄,能更好支持范圍查找。
由于數(shù)據(jù)順序排列并且相連,所以便于區(qū)間查找和搜索。而B樹則需要進(jìn)行每一層的遞歸遍歷,相鄰的元素可能在內(nèi)存中不相鄰,所以緩存命中性沒有B+樹好。
- B+樹所有的查詢都要從根節(jié)點(diǎn)查找到葉子節(jié)點(diǎn),查詢性能更穩(wěn)定;而B樹,每個(gè)節(jié)點(diǎn)都可能查找到數(shù)據(jù),需要在葉子節(jié)點(diǎn)和內(nèi)部節(jié)點(diǎn)不停的往返移動(dòng),所以不穩(wěn)定。
redis持久化策略?
RDB持久化(全量備份)
RDB持久化是指在指定時(shí)間間隔內(nèi)將內(nèi)存中的數(shù)據(jù)集快照寫入磁盤。實(shí)際上fork子線程,先將數(shù)據(jù)集寫入臨時(shí)文件,寫入成功后,在替換之前的文件,用二進(jìn)制壓縮文件,RDB是Redis默認(rèn)的持久化方式,會(huì)在對(duì)應(yīng)目錄下生產(chǎn)一個(gè)dump.rdb文件,重啟會(huì)通過加載dump.rdb文件恢復(fù)數(shù)據(jù)
RDB優(yōu)點(diǎn):
- 方便持久化:只有一個(gè)dump.rdb文件;
- 容災(zāi)性好:一個(gè)文件可以保存到安全的磁盤;
- 性能好:fork子線程來完成寫操作,主線程繼續(xù)處理命令;
- 效率高:如何數(shù)據(jù)集偏大,RDB啟動(dòng)效率比AOF高
RDB缺點(diǎn):
- 數(shù)據(jù)安全性低:因?yàn)镽DB是每隔一段時(shí)間進(jìn)行持久化,可能會(huì)造成數(shù)據(jù)丟失。
- 由于RDB是通過fork子線程協(xié)助完成數(shù)據(jù)持久化工作的,因此如果數(shù)據(jù)集較大時(shí),可能會(huì)導(dǎo)致整個(gè)服務(wù)停止服務(wù)幾百毫秒,甚至一分鐘。
AOF持久化(增量備份)
AOF持久化是以日志的形式記錄記錄每一個(gè)增刪操作然后追加到文件中。AOF的出現(xiàn)是為了彌補(bǔ)RDB備份的不足(數(shù)據(jù)不一致性)。
與RDB持久化相比,AOF的持久化實(shí)時(shí)性更好。
AOF的備份策略:Redis的配置文件中存在三種不同的AOF持久化方式:
- appendfsync always:每次有數(shù)據(jù)修改發(fā)生時(shí)都會(huì)同步。
- appendfsync everysec:每秒同步一次
- appendsync no:讓操作系統(tǒng)決定何時(shí)進(jìn)行同步。
AOF優(yōu)點(diǎn):
- AOF實(shí)時(shí)性哈好,數(shù)據(jù)安全性更高;
- AOF通過append模式寫文件,即使中途服務(wù)器宕機(jī),也可以通過redis-check-aof工具解決數(shù)據(jù)一致性問題。
- AOF機(jī)制的rewrite模式(文件過大會(huì)對(duì)命令進(jìn)行合并重寫),可以刪除其中某些命令(比如誤操作的命令)
AOF缺點(diǎn):
- AOF文件比RDB文件大,且恢復(fù)慢;
- 根據(jù)同步策略的不同,AOF在運(yùn)行效率上往往會(huì)慢于RDB。
兩者結(jié)合
將 RDB 和 AOF 合體使用,這個(gè)方法是在 Redis 4.0 提出的,該方法叫混合使用 AOF 日志和內(nèi)存快照,也叫混合持久化。
混合持久化工作在 AOF 日志重寫過程。
當(dāng)開啟了混合持久化時(shí),在 AOF 重寫日志時(shí),fork 出來的重寫子進(jìn)程會(huì)先將與主線程共享的內(nèi)存數(shù)據(jù)以 RDB 方式寫入到 AOF 文件,然后主線程處理的操作命令會(huì)被記錄在重寫緩沖區(qū)里,重寫緩沖區(qū)里的增量命令會(huì)以 AOF 方式寫入到 AOF 文件,寫入完成后通知主進(jìn)程將新的含有 RDB 格式和 AOF 格式的 AOF 文件替換舊的的 AOF 文件。
也就是說,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量數(shù)據(jù),后半部分是 AOF 格式的增量數(shù)據(jù)。
這樣的好處在于,重啟 Redis 加載數(shù)據(jù)的時(shí)候,由于前半部分是 RDB 內(nèi)容,這樣加載的時(shí)候速度會(huì)很快。
加載完 RDB 的內(nèi)容后,才會(huì)加載后半部分的 AOF 內(nèi)容,這里的內(nèi)容是 Redis 后臺(tái)子進(jìn)程重寫 AOF 期間,主線程處理的操作命令,可以使得數(shù)據(jù)更少的丟失。
緩存穿透,怎么解決?
當(dāng)用戶訪問的數(shù)據(jù),既不在緩存中,也不在數(shù)據(jù)庫中,導(dǎo)致請(qǐng)求在訪問緩存時(shí),發(fā)現(xiàn)緩存缺失,再去訪問數(shù)據(jù)庫時(shí),發(fā)現(xiàn)數(shù)據(jù)庫中也沒有要訪問的數(shù)據(jù),沒辦法構(gòu)建緩存數(shù)據(jù),來服務(wù)后續(xù)的請(qǐng)求。那么當(dāng)有大量這樣的請(qǐng)求到來時(shí),數(shù)據(jù)庫的壓力驟增,這就是緩存穿透的問題。
緩存穿透的發(fā)生一般有這兩種情況:
- 業(yè)務(wù)誤操作,緩存中的數(shù)據(jù)和數(shù)據(jù)庫中的數(shù)據(jù)都被誤刪除了,所以導(dǎo)致緩存和數(shù)據(jù)庫中都沒有數(shù)據(jù);
- 黑客惡意攻擊,故意大量訪問某些讀取不存在數(shù)據(jù)的業(yè)務(wù);
應(yīng)對(duì)緩存穿透的方案,常見的方案有三種。
- 第一種方案,非法請(qǐng)求的限制;
- 第二種方案,緩存空值或者默認(rèn)值;
- 第三種方案,使用布隆過濾器快速判斷數(shù)據(jù)是否存在,避免通過查詢數(shù)據(jù)庫來判斷數(shù)據(jù)是否存在;
第一種方案,非法請(qǐng)求的限制
當(dāng)有大量惡意請(qǐng)求訪問不存在的數(shù)據(jù)的時(shí)候,也會(huì)發(fā)生緩存穿透,因此在 API 入口處我們要判斷求請(qǐng)求參數(shù)是否合理,請(qǐng)求參數(shù)是否含有非法值、請(qǐng)求字段是否存在,如果判斷出是惡意請(qǐng)求就直接返回錯(cuò)誤,避免進(jìn)一步訪問緩存和數(shù)據(jù)庫。
第二種方案,緩存空值或者默認(rèn)值
當(dāng)我們線上業(yè)務(wù)發(fā)現(xiàn)緩存穿透的現(xiàn)象時(shí),可以針對(duì)查詢的數(shù)據(jù),在緩存中設(shè)置一個(gè)空值或者默認(rèn)值,這樣后續(xù)請(qǐng)求就可以從緩存中讀取到空值或者默認(rèn)值,返回給應(yīng)用,而不會(huì)繼續(xù)查詢數(shù)據(jù)庫。
第三種方案,使用布隆過濾器快速判斷數(shù)據(jù)是否存在,避免通過查詢數(shù)據(jù)庫來判斷數(shù)據(jù)是否存在。
我們可以在寫入數(shù)據(jù)庫數(shù)據(jù)時(shí),使用布隆過濾器做個(gè)標(biāo)記,然后在用戶請(qǐng)求到來時(shí),業(yè)務(wù)線程確認(rèn)緩存失效后,可以通過查詢布隆過濾器快速判斷數(shù)據(jù)是否存在,如果不存在,就不用通過查詢數(shù)據(jù)庫來判斷數(shù)據(jù)是否存在。
即使發(fā)生了緩存穿透,大量請(qǐng)求只會(huì)查詢 Redis 和布隆過濾器,而不會(huì)查詢數(shù)據(jù)庫,保證了數(shù)據(jù)庫能正常運(yùn)行,Redis 自身也是支持布隆過濾器的。
TCP/UDP 詳解,區(qū)別
作用
首先,tcp和udp都是工作再傳輸層,用于程序之間傳輸數(shù)據(jù)的。數(shù)一般包含:文件類型,視頻類型,jpg圖片等。
圖片
區(qū)別
TCP是基于連接的,而UDP是基于非連接的。
tcp傳輸數(shù)據(jù)穩(wěn)定可靠,適用于對(duì)網(wǎng)絡(luò)通訊質(zhì)量要求較高的場景,需要準(zhǔn)確無誤的傳輸給對(duì)方,比如,傳輸文件,發(fā)送郵件,瀏覽網(wǎng)頁等等。
udp的優(yōu)點(diǎn)是速度快,但是可能產(chǎn)生丟包,所以適用于對(duì)實(shí)時(shí)性要求較高但是對(duì)少量丟包并沒有太大要求的場景。比如:域名查詢,語音通話,視頻直播等。udp還有一個(gè)非常重要的應(yīng)用場景就是隧道網(wǎng)絡(luò),比如:vpn,VXLAN。
以人與人之間的通信為例:UDP協(xié)議就相當(dāng)于是寫信給對(duì)方,寄出去信件之后不能知道對(duì)方是否收到信件,信件內(nèi)容是否完整,也不能得到及時(shí)反饋,而TCP協(xié)議就像是打電話通信,在這一系列流程都能得到及時(shí)反饋,并能確保對(duì)方及時(shí)接收到。如下圖:
圖片
三次握手
當(dāng)客戶端向服務(wù)端發(fā)起連接時(shí),會(huì)先發(fā)一包連接請(qǐng)求數(shù)據(jù),過去詢問一下,能否與你建立連接?這包數(shù)據(jù)稱之為SYN包,如果對(duì)端同意連接,則回復(fù)一包SYN+ACK包,客戶端收到之后,發(fā)送一包ACK包,連接建立,因?yàn)檫@個(gè)過程中互相發(fā)送了三包數(shù)據(jù),所以稱之為三次握手。
圖片
為什么要三次握手而不是兩次握手?
這是為了防止,因?yàn)橐咽У恼?qǐng)求報(bào)文,突然又傳到服務(wù)器,引起錯(cuò)誤,這是什么意思?
假設(shè)采用兩次握手建立連接,客戶端向服務(wù)端發(fā)送一個(gè)syn包請(qǐng)求建立連接,因?yàn)槟承┪粗脑?,并沒有到達(dá)服務(wù)器,在中間某個(gè)網(wǎng)絡(luò)節(jié)點(diǎn)產(chǎn)生了滯留,為了建立連接,客戶端會(huì)重發(fā)syn包,這次的數(shù)據(jù)包正常送達(dá),服務(wù)端發(fā)送syn+ack之后就建立起了連接,但是第一包數(shù)據(jù)阻塞的網(wǎng)絡(luò)突然恢復(fù),第一包syn包又送達(dá)到服務(wù)端,這是服務(wù)端會(huì)認(rèn)為客戶端又發(fā)起了一個(gè)新的連接,從而在兩次握手之后進(jìn)入等待數(shù)據(jù)狀態(tài),服務(wù)端認(rèn)為是兩個(gè)連接,而客戶端認(rèn)為是一個(gè)連接,造成了狀態(tài)不一致,如果在三次握手的情況下,服務(wù)端收不到最后的ack包,自然不會(huì)認(rèn)為連接建立成功,所以三次握手本質(zhì)上來說就是為了解決網(wǎng)絡(luò)信道不可靠的問題,為了在不可靠的信道上建立起可靠的連接,經(jīng)過三次握手之后,客戶端和服務(wù)端都進(jìn)入了數(shù)據(jù)傳輸狀態(tài)。
四次揮手
圖片
處于連接狀態(tài)的客戶端和服務(wù)端,都可以發(fā)起關(guān)閉連接請(qǐng)求,此時(shí)需要四次揮手來進(jìn)行連接關(guān)閉。
假設(shè)客戶端主動(dòng)發(fā)起連接關(guān)閉請(qǐng)求,他給服務(wù)端發(fā)起一包FIN包,標(biāo)識(shí)要關(guān)閉連接,自己進(jìn)入終止等待1裝填,服務(wù)端收到FIN包,發(fā)送一包ACK包,標(biāo)識(shí)自己進(jìn)入了關(guān)閉等待狀態(tài),客戶端進(jìn)入終止等待2狀態(tài)。這是第二次揮手,服務(wù)端此時(shí)還可以發(fā)送未發(fā)送的數(shù)據(jù),而客戶端還可以接受數(shù)據(jù),待服務(wù)端發(fā)送完數(shù)據(jù)之后,發(fā)送一包FIN包,最后進(jìn)入確認(rèn)狀態(tài),這是第3次揮手,客戶端收到之后恢復(fù)ACK包,進(jìn)入超時(shí)等待狀態(tài),經(jīng)過超時(shí)時(shí)間后關(guān)閉連接,而服務(wù)端收到ACK包后,立即關(guān)閉連接,這是第四次揮手。
為什么客戶端要等待超時(shí)時(shí)間?
這是為了保證對(duì)方已經(jīng)收到ACK包,因?yàn)榧僭O(shè)客戶端發(fā)送完最后一包ACK包后釋放了連接,一旦ACK包在網(wǎng)絡(luò)中丟失,服務(wù)端將一直停留在 最后確認(rèn)狀態(tài),如果等待一段時(shí)間,這時(shí)服務(wù)端會(huì)因?yàn)闆]有收到ack包重發(fā)FIN包,客戶端會(huì)響應(yīng) 這個(gè)FIN包進(jìn)行重發(fā)ack包,并刷新超時(shí)時(shí)間,這個(gè)機(jī)制跟第三次握手一樣。也是為了保證在不可靠的網(wǎng)絡(luò)鏈路中進(jìn)行可靠的連接斷開確認(rèn)。
本文轉(zhuǎn)載自微信公眾號(hào)「 程序員升級(jí)打怪之旅」,作者「小韜&王中陽」,可以通過以下二維碼關(guān)注。
轉(zhuǎn)載本文請(qǐng)聯(lián)系「 程序員升級(jí)打怪之旅」公眾號(hào)。