Go http.Server處理長連接

如果讓我們自己來實(shí)現(xiàn)一個http server,大部分同學(xué)都可以寫出以下實(shí)現(xiàn):

import ( 
"fmt"
"net"
"os" 
)
func main() {
  tcpAddr, err := net.ResolveTcpAddr("tcp", "localhost:6666")
  checkError(err)
  fd, err := net.ListenTcp("tcp", tcpAddr)
  checkError(err)
  for {
    conn, err := fd.Accept()
    if err != nil {
      continue
    }
    go Handle(conn) 
  }
}

主要步驟就是:

  • 監(jiān)聽一個端口
  • 一個大循環(huán)不斷地Accept連接
  • 每個連接開一個goroutine去處理業(yè)務(wù)邏輯

以上的步驟也很直觀猾浦,非常好理解紧卒。如果你粗略地掃一眼標(biāo)準(zhǔn)庫httpServer的實(shí)現(xiàn)肮塞,也能看到大致是一個路子。以下是標(biāo)準(zhǔn)庫的關(guān)鍵部分:

    for {
        rw, e := l.Accept()
        if e != nil {
            //處理錯誤 ...
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }

看起來幾乎差不多也是一個套路,accept一個連接嗽桩,然后起一個goroutine去處理兔乞。不過最近在做流量錄制時遇到一個奇怪的現(xiàn)象汇鞭。

我起了一個非常簡單的echo server,然后使用strace查看它的系統(tǒng)調(diào)用庸追。同時我有一個client每隔10s curl一次霍骄。奇怪的事情來了,client發(fā)了無數(shù)次請求淡溯,一直到我關(guān)閉了server读整,strace都顯示server只進(jìn)行過兩次accept,而且這兩次accept都是在第一個請求到來之前咱娶。

這里就比較困惑了米间,前面httpserver的源碼我們也看了煎楣,確實(shí)是accept一個連接,再go 一個handler车伞,怎么會出現(xiàn)沒有accept依然能走到handler呢择懂?

尋找問題

我第一時間想到的是,會不會是Go把Accept包了一層另玖,在內(nèi)部復(fù)用了連接困曙,因此我嘗試去讀這塊的源碼實(shí)現(xiàn)。這里還是要吐槽一下這塊兒Go的代碼實(shí)現(xiàn)得還是不怎么好谦去,看著比較亂慷丽。同時vscode的代碼跳轉(zhuǎn)也問題很多,經(jīng)常跳到錯誤的地方去了鳄哭,很多platform related的代碼實(shí)現(xiàn)都跳不對要糊。

這里總結(jié)的第一條經(jīng)驗(yàn)就是,看標(biāo)準(zhǔn)庫的源碼妆丘,一定要用delve進(jìn)行單步锄俄,用IDE跳轉(zhuǎn)就廢了。當(dāng)然你也應(yīng)該要理解IDE勺拣,畢竟很多代碼都是compiler自動生成的奶赠,或者有些函數(shù)需要ld的幫助才能找到對應(yīng)實(shí)現(xiàn)。所以一定要用dlv药有!

同步與異步

要理解這個現(xiàn)象的根本原因毅戈,我們需要先理解Go提供給我們的同步調(diào)用的抽象。首先愤惰,Go標(biāo)準(zhǔn)庫中所有API都是“同步”的苇经,并不是異步的。那么問題就來了宦言,如果都是同步調(diào)用扇单,那么和多線程還有啥區(qū)別,syscall始終要有一個線程阻塞在那里等待返回蜡励,實(shí)現(xiàn)一個N*M的goroutine還有啥意義令花?
上面的“同步”我是打了引號的,如果真的完全是同步的凉倚,那么goroutine確實(shí)就廢了兼都,這說明它并沒有表面上那么簡單。我們在進(jìn)行網(wǎng)絡(luò)IO操作時稽寒,代碼都是這樣的:

err := conn.Write(buffer)
data,err := conn.ReadAll()

然而實(shí)際上內(nèi)部實(shí)現(xiàn)做了很多事情扮碧,最關(guān)鍵的一點(diǎn)是,最底層的IO操作其實(shí)是異步的。異步就需要等待event fire慎王,但是我的代碼并沒有這么做膀就痢?——這便是Go提供的抽象所在赖淤。
底層其實(shí)把每個socket都加入到了一個全局的epollfd中進(jìn)行監(jiān)聽蜀漆,當(dāng)我們在socket上發(fā)生讀寫操作時,標(biāo)準(zhǔn)庫都會進(jìn)行異步操作咱旱,也就是說底層其實(shí)立刻就返回了确丢。但是如果事件并沒有完成,那么它會配合runtime park當(dāng)前的goroutine吐限,也就是說會把當(dāng)前這個goroutine掛起鲜侥。這里要注意,由于是異步操作诸典,因此真正的線程是不阻塞而是立刻返回的描函,因此scheduler可以調(diào)度當(dāng)前線程執(zhí)行其它goroutine。當(dāng)epoll收到event fire時狐粱,scheduler再把該goroutine喚醒舀寓,然后讓對應(yīng)線程去執(zhí)行那個goroutine。這樣脑奠,我們編寫代碼時就可以以同步的方式基公,寫出基于異步回調(diào)的高性能的代碼了。
這里的關(guān)鍵是宋欺,IO相關(guān)的系統(tǒng)調(diào)用是異步的,操作系統(tǒng)線程不會阻塞胰伍。因此配合scheduler正確的調(diào)度齿诞,才能實(shí)現(xiàn)這種抽象。同步的代碼骂租,異步的性能祷杈。如果系統(tǒng)調(diào)用會阻塞,那scheduler也沒轍渗饮。

問題所在

回到之前的問題但汞,由于底層會把每個socket都加入到epoll中進(jìn)行監(jiān)聽,因此主循環(huán)每accept到一個連接互站,就會加入到epoll中私蕾。如果你對網(wǎng)絡(luò)編程的概念不是很熟悉,你可能會問胡桃,如果把某個socket加入epoll之后就無法accept了嗎踩叭?如果你問出這個問題,至少說明你沒有理解accept的語義∪荼矗看一個accept的manpage自脯,一開始是這么說的:

The accept() system call is used with connection-based socket types(SOCK_STREAM, SOCK_SEQPACKET). It extracts the first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket. The newly created socket is not in the listening state. The original socket sockfd is unaffected by this call.

當(dāng)client和server通過三次握手建立了連接之后(也就是實(shí)例化了一個socket結(jié)構(gòu)體),它會被放到內(nèi)核的一個緩存隊(duì)列里斤富。accept就是從這個隊(duì)列的隊(duì)首取出一個socket膏潮,然后返回一個fd指向該socket。換句話說满力,accept消費(fèi)的是三次握手新建的連接焕参!對于建立好的連接,后續(xù)發(fā)送的數(shù)據(jù)脚囊,只需要對該fd調(diào)用read方法即可龟糕。當(dāng)然,這里也很復(fù)雜悔耘,因?yàn)槟悴恢肋@個fd對應(yīng)的socket什么時候有數(shù)據(jù)讲岁,然后你要么輪詢地試要么select要么就epoll,反正這里就有很多方法了衬以。
因此你應(yīng)該理解缓艳,accept是讀取新連接,而epoll等各種方法實(shí)際上針對的是已建立的連接看峻,對其后續(xù)的數(shù)據(jù)流入流出事件進(jìn)行通知阶淘。
但是還是很奇怪啊,據(jù)說HTTP不是短連接嗎互妓?NoNoNo溪窒,這個觀點(diǎn)已經(jīng)過時了。在HTTP/1.0時代冯勉,確實(shí)是短連接澈蚌。雖然TCP是長連接,但是基于TCP的HTTP/1.0協(xié)議要求:如果client沒有明確告之keepalive灼狰,server在發(fā)送完response后就要主動關(guān)閉連接宛瞄。那時keepalive還是一個試驗(yàn)特性,不過現(xiàn)在使用的http/1.1協(xié)議中交胚,keepalive已經(jīng)是默認(rèn)行為了份汗。因此大部分http請求也是長連接,也就是說socket并不會主動被server關(guān)掉蝴簇。
一般來說杯活,如果要復(fù)用連接,我們需要保存對應(yīng)的數(shù)據(jù)結(jié)構(gòu)军熏,比如:

cli := newClient(host)
cli.Post()
cli.Get()
cli.Post()

但是轩猩,我們大多數(shù)時候直接使用了靜態(tài)方法,并沒有保存句柄:

http.Get()
http.Post()

不過go的http包底層幫我們做了連接池,我們的TCP連接都會被放到一個map中均践,后續(xù)建立連接前先檢查map中有沒有對應(yīng)的連接晤锹,如果沒有才會進(jìn)行新建連接。

// getConn dials and creates a new persistConn to the target as
// specified in the connectMethod. This includes doing a proxy CONNECT
// and/or setting up TLS.  If this doesn't return an error, the persistConn
// is ready to write requests to.
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistConn, error) {
    req := treq.Request
    trace := treq.trace
    ctx := req.Context()
    if trace != nil && trace.GetConn != nil {
        trace.GetConn(cm.addr())
    }
    if pc, idleSince := t.getIdleConn(cm); pc != nil {
        if trace != nil && trace.GotConn != nil {
            trace.GotConn(pc.gotIdleConnTrace(idleSince))
        }
        // set request canceler to some non-nil function so we
        // can detect whether it was cleared between now and when
        // we enter roundTrip
        t.setReqCanceler(req, func(error) {})
        return pc, nil
    }
    type dialRes struct {
        pc  *persistConn
        err error
    }
    dialc := make(chan dialRes)
//...后面省略

你可以看到彤委,獲取連接時會先getIdleConn鞭铆,如果沒有IdleConn再去dial。具體的代碼就不細(xì)講了焦影,因?yàn)閷?shí)現(xiàn)一個連接池簡單车遂,但是要實(shí)現(xiàn)一個高性能的連接池還是挺麻煩的,感興趣可以具體學(xué)習(xí)下為什么有兩個map斯辰。

而具體從conn中通過epoll或者kqueue讀取數(shù)據(jù)然后執(zhí)行ServeHTTP的邏輯舶担,都在go c.serve(ctx)中感興趣可以看看

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市彬呻,隨后出現(xiàn)的幾起案子衣陶,更是在濱河造成了極大的恐慌,老刑警劉巖闸氮,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剪况,死亡現(xiàn)場離奇詭異,居然都是意外死亡蒲跨,警方通過查閱死者的電腦和手機(jī)译断,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來或悲,“玉大人孙咪,你說我怎么就攤上這事⊙灿铮” “怎么了该贾?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捌臊。 經(jīng)常有香客問我,道長兜材,這世上最難降的妖魔是什么理澎? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮曙寡,結(jié)果婚禮上糠爬,老公的妹妹穿的比我還像新娘。我一直安慰自己举庶,他們只是感情好执隧,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般镀琉。 火紅的嫁衣襯著肌膚如雪峦嗤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天屋摔,我揣著相機(jī)與錄音烁设,去河邊找鬼。 笑死钓试,一個胖子當(dāng)著我的面吹牛装黑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播弓熏,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼恋谭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了挽鞠?” 一聲冷哼從身側(cè)響起疚颊,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎滞谢,沒想到半個月后串稀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狮杨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年母截,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片橄教。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡清寇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出护蝶,到底是詐尸還是另有隱情华烟,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布持灰,位于F島的核電站盔夜,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏堤魁。R本人自食惡果不足惜喂链,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望妥泉。 院中可真熱鬧椭微,春花似錦、人聲如沸盲链。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至本慕,卻和暖如春排拷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背间狂。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工攻泼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鉴象。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓忙菠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纺弊。 傳聞我的和親對象是個殘疾皇子牛欢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348

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