亚洲国产日韩欧美在线a乱码,国产精品路线1路线2路线,亚洲视频一区,精品国产自,www狠狠,国产情侣激情在线视频免费看,亚洲成年网站在线观看

TCP/IP網(wǎng)絡(luò)編程中socket的行為

時(shí)間:2020-10-06 15:44:26 TCP/IP 我要投稿

TCP/IP網(wǎng)絡(luò)編程中socket的行為

  想要熟練掌握Linux下的TCP/IP網(wǎng)絡(luò)編程,至少有三個(gè)層面的知識(shí)需要熟悉:

  1. TCP/IP協(xié)議(如連接的建立和終止、重傳和確認(rèn)、滑動(dòng)窗口和擁塞控制等等)

  2. Socket I/O系統(tǒng)調(diào)用(重點(diǎn)如read/write),這是TCP/IP協(xié)議在應(yīng)用層表現(xiàn)出來(lái)的行為。

  3. 編寫(xiě)Performant, Scalable的服務(wù)器程序。包括多線程、IO Multiplexing、非阻塞、異步等各種技術(shù)。

  關(guān)于TCP/IP協(xié)議,建議參考Richard Stevens的《TCP/IP Illustrated,vol1》(TCP/IP詳解卷1)。

  關(guān)于第二層面,依然建議Richard Stevens的《Unix network proggramming,vol1》(Unix網(wǎng)絡(luò)編程卷1),這兩本書(shū)公認(rèn)是Unix網(wǎng)絡(luò)編程的圣經(jīng)。

  至于第三個(gè)層面,UNP的書(shū)中有所提及,也有著名的C10K問(wèn)題,業(yè)界也有各種各樣的框架和解決方案,本人才疏學(xué)淺,在這里就不一一敷述。

  本文的重點(diǎn)在于第二個(gè)層面,主要總結(jié)一下Linux下TCP/IP網(wǎng)絡(luò)編程中的read/write系統(tǒng)調(diào)用的行為,知識(shí)來(lái)源于自己網(wǎng)絡(luò)編程的粗淺經(jīng)驗(yàn)和對(duì)《Unix網(wǎng)絡(luò)編程卷1》相關(guān)章節(jié)的總結(jié)。由于本人接觸Linux下網(wǎng)絡(luò)編程時(shí)間不長(zhǎng),錯(cuò)誤和疏漏再所難免,望看官不吝賜教。

  一. read/write 的語(yǔ)義:為什么會(huì)阻塞?

  先從write說(shuō)起:

  #include <unistd.h>

  ssize_t write(int fd, const void *buf, size_t count);

  首先,write成功返回,只是buf中的數(shù)據(jù)被復(fù)制到了kernel中的TCP發(fā)送緩沖區(qū)。至于數(shù)據(jù)什么時(shí)候被發(fā)往網(wǎng)絡(luò),什么時(shí)候被對(duì)方主機(jī)接收,什么時(shí)候被對(duì)方進(jìn)程讀取,系統(tǒng)調(diào)用層面不會(huì)給予任何保證和通知。

  write在什么情況下會(huì)阻塞?當(dāng)kernel的該socket的發(fā)送緩沖區(qū)已滿時(shí)。對(duì)于每個(gè)socket,擁有自己的send buffer和receive buffer。從Linux 2.6開(kāi)始,兩個(gè)緩沖區(qū)大小都由系統(tǒng)來(lái)自動(dòng)調(diào)節(jié)(autotuning),但一般在default和max之間浮動(dòng)。

  # 獲取socket的發(fā)送/接受緩沖區(qū)的大。海ê竺娴闹凳窃谖以贚inux 2.6.38 x86_64上測(cè)試的結(jié)果)

  sysctl net.core.wmem_default       #126976

  sysctl net.core.wmem_max        #131071

  sysctl net.core.wmem_default       #126976

  sysctl net.core.wmem_max           #131071

  已經(jīng)發(fā)送到網(wǎng)絡(luò)的數(shù)據(jù)依然需要暫存在send buffer中,只有收到對(duì)方的ack后,kernel才從buffer中清除這一部分?jǐn)?shù)據(jù),為后續(xù)發(fā)送數(shù)據(jù)騰出空間。接收端將收到的數(shù)據(jù)暫存在receive buffer中,自動(dòng)進(jìn)行確認(rèn)。但如果socket所在的進(jìn)程不及時(shí)將數(shù)據(jù)從receive buffer中取出,最終導(dǎo)致receive buffer填滿,由于TCP的滑動(dòng)窗口和擁塞控制,接收端會(huì)阻止發(fā)送端向其發(fā)送數(shù)據(jù)。這些控制皆發(fā)生在TCP/IP棧中,對(duì)應(yīng)用程序是透明的,應(yīng)用程序繼續(xù)發(fā)送數(shù)據(jù),最終導(dǎo)致send buffer填滿,write調(diào)用阻塞。

  一般來(lái)說(shuō),由于接收端進(jìn)程從socket讀數(shù)據(jù)的速度跟不上發(fā)送端進(jìn)程向socket寫(xiě)數(shù)據(jù)的速度,最終導(dǎo)致發(fā)送端write調(diào)用阻塞。

  而read調(diào)用的行為相對(duì)容易理解,從socket的receive buffer中拷貝數(shù)據(jù)到應(yīng)用程序的buffer中。read調(diào)用阻塞,通常是發(fā)送端的數(shù)據(jù)沒(méi)有到達(dá)。

  二. blocking(默認(rèn))和nonblock模式下 read/write 行為的區(qū)別:

  將socket fd設(shè)置為nonblock(非阻塞)是在服務(wù)器編程中常見(jiàn)的做法,采用blocking IO并為每一個(gè)client創(chuàng)建一個(gè)線程的模式開(kāi)銷(xiāo)巨大且可擴(kuò)展性不佳(帶來(lái)大量的切換開(kāi)銷(xiāo)),更為通用的做法是采用線程池+Nonblock I/O+Multiplexing(select/poll,以及Linux上特有的epoll)。

  // 設(shè)置一個(gè)文件描述符為nonblock

  int set_nonblocking(int fd)

  {

  int flags;

  if ((flags = fcntl(fd, F_GETFL, 0)) == -1)

  flags = 0;

  return fcntl(fd, F_SETFL, flags | O_NONBLOCK);

  }

  幾個(gè)重要的結(jié)論:

  1. read總是在接收緩沖區(qū)有數(shù)據(jù)時(shí)立即返回,而不是等到給定的read buffer填滿時(shí)返回。

  只有當(dāng)receive buffer為空時(shí),blocking模式才會(huì)等待,而nonblock模式下會(huì)立即返回-1(errno = EAGAIN或EWOULDBLOCK)

  2. blocking的write只有在緩沖區(qū)足以放下整個(gè)buffer時(shí)才返回(與blocking read并不相同)

  nonblock write則是返回能夠放下的字節(jié)數(shù),之后調(diào)用則返回-1(errno = EAGAIN或EWOULDBLOCK)

  對(duì)于blocking的write有個(gè)特例:當(dāng)write正阻塞等待時(shí)對(duì)面關(guān)閉了socket,則write則會(huì)立即將剩余緩沖區(qū)填滿并返回所寫(xiě)的字節(jié)數(shù),再次調(diào)用則write失。╟onnection reset by peer),這正是下個(gè)小節(jié)要提到的:

  三. read/write對(duì)連接異常的反饋行為:

  對(duì)應(yīng)用程序來(lái)說(shuō),與另一進(jìn)程的TCP通信其實(shí)是完全異步的過(guò)程:

  1. 我并不知道對(duì)面什么時(shí)候、能否收到我的數(shù)據(jù)

  2. 我不知道什么時(shí)候能夠收到對(duì)面的數(shù)據(jù)

  3. 我不知道什么時(shí)候通信結(jié)束(主動(dòng)退出或是異常退出、機(jī)器故障、網(wǎng)絡(luò)故障等等)

  對(duì)于1和2,采用write() -> read() -> write() -> read() ->…的序列,通過(guò)blocking read或者nonblock read+輪詢的方式,應(yīng)用程序基于可以保證正確的處理流程。

  對(duì)于3,kernel將這些事件的“通知”通過(guò)read/write的結(jié)果返回給應(yīng)用層。

  假設(shè)A機(jī)器上的一個(gè)進(jìn)程a正在和B機(jī)器上的進(jìn)程b通信:某一時(shí)刻a正阻塞在socket的read調(diào)用上(或者在nonblock下輪詢socket)

  當(dāng)b進(jìn)程終止時(shí),無(wú)論應(yīng)用程序是否顯式關(guān)閉了socket(OS會(huì)負(fù)責(zé)在進(jìn)程結(jié)束時(shí)關(guān)閉所有的文件描述符,對(duì)于socket,則會(huì)發(fā)送一個(gè)FIN包到對(duì)面)。

  ”同步通知“:進(jìn)程a對(duì)已經(jīng)收到FIN的socket調(diào)用read,如果已經(jīng)讀完了receive buffer的剩余字節(jié),則會(huì)返回EOF:0

  ”異步通知“:如果進(jìn)程a正阻塞在read調(diào)用上(前面已經(jīng)提到,此時(shí)receive buffer一定為空,因?yàn)閞ead在receive buffer有內(nèi)容時(shí)就會(huì)返回),則read調(diào)用立即返回EOF,進(jìn)程a被喚醒。

  socket在收到FIN后,雖然調(diào)用read會(huì)返回EOF,但進(jìn)程a依然可以其調(diào)用write,因?yàn)楦鶕?jù)TCP協(xié)議,收到對(duì)方的FIN包只意味著對(duì)方不會(huì)再發(fā)送任何消息。 在一個(gè)雙方正常關(guān)閉的流程中,收到FIN包的一端將剩余數(shù)據(jù)發(fā)送給對(duì)面(通過(guò)一次或多次write),然后關(guān)閉socket。

  但是事情遠(yuǎn)遠(yuǎn)沒(méi)有想象中簡(jiǎn)單。優(yōu)雅地(gracefully)關(guān)閉一個(gè)TCP連接,不僅僅需要雙方的應(yīng)用程序遵守約定,中間還不能出任何差錯(cuò)。

  假如b進(jìn)程是異常終止的,發(fā)送FIN包是OS代勞的`,b進(jìn)程已經(jīng)不復(fù)存在,當(dāng)機(jī)器再次收到該socket的消息時(shí),會(huì)回應(yīng)RST(因?yàn)閾碛性搒ocket的進(jìn)程已經(jīng)終止)。a進(jìn)程對(duì)收到RST的socket調(diào)用write時(shí),操作系統(tǒng)會(huì)給a進(jìn)程發(fā)送SIGPIPE,默認(rèn)處理動(dòng)作是終止進(jìn)程,知道你的進(jìn)程為什么毫無(wú)征兆地死亡了吧:)

  from 《Unix Network programming, vol1》 3rd Edition:

  “It is okay to write to a socket that has received a FIN, but it is an error to write to a socket that has received an RST.”

  通過(guò)以上的敘述,內(nèi)核通過(guò)socket的read/write將雙方的連接異常通知到應(yīng)用層,雖然很不直觀,似乎也夠用。

  這里說(shuō)一句題外話:

  不知道有沒(méi)有同學(xué)會(huì)和我有一樣的感慨:在寫(xiě)TCP/IP通信時(shí),似乎沒(méi)怎么考慮連接的終止或錯(cuò)誤,只是在read/write錯(cuò)誤返回時(shí)關(guān)閉socket,程序似乎也能正常運(yùn)行,但某些情況下總是會(huì)出奇怪的問(wèn)題。想完美處理各種錯(cuò)誤,卻發(fā)現(xiàn)怎么也做不對(duì)。

  原因之一是:socket(或者說(shuō)TCP/IP棧本身)對(duì)錯(cuò)誤的反饋能力是有限的。

  考慮這樣的錯(cuò)誤情況:

  不同于b進(jìn)程退出(此時(shí)OS會(huì)負(fù)責(zé)為所有打開(kāi)的socket發(fā)送FIN包),當(dāng)B機(jī)器的OS崩潰(注意不同于人為關(guān)機(jī),因?yàn)殛P(guān)機(jī)時(shí)所有進(jìn)程的退出動(dòng)作依然能夠得到保證)/主機(jī)斷電/網(wǎng)絡(luò)不可達(dá)時(shí),a進(jìn)程根本不會(huì)收到FIN包作為連接終止的提示。

  如果a進(jìn)程阻塞在read上,那么結(jié)果只能是永遠(yuǎn)的等待。

  如果a進(jìn)程先write然后阻塞在read,由于收不到B機(jī)器TCP/IP棧的ack,TCP會(huì)持續(xù)重傳12次(時(shí)間跨度大約為9分鐘),然后在阻塞的read調(diào)用上返回錯(cuò)誤:ETIMEDOUT/EHOSTUNREACH/ENETUNREACH

  假如B機(jī)器恰好在某個(gè)時(shí)候恢復(fù)和A機(jī)器的通路,并收到a某個(gè)重傳的pack,因?yàn)椴荒茏R(shí)別所以會(huì)返回一個(gè)RST,此時(shí)a進(jìn)程上阻塞的read調(diào)用會(huì)返回錯(cuò)誤ECONNREST

  恩,socket對(duì)這些錯(cuò)誤還是有一定的反饋能力的,前提是在對(duì)面不可達(dá)時(shí)你依然做了一次write調(diào)用,而不是輪詢或是阻塞在read上,那么總是會(huì)在重傳的周期內(nèi)檢測(cè)出錯(cuò)誤。如果沒(méi)有那次write調(diào)用,應(yīng)用層永遠(yuǎn)不會(huì)收到連接錯(cuò)誤的通知。

  write的錯(cuò)誤最終通過(guò)read來(lái)通知應(yīng)用層,有點(diǎn)陰差陽(yáng)錯(cuò)?

  四. 還需要做什么?

  至此,我們知道了僅僅通過(guò)read/write來(lái)檢測(cè)異常情況是不靠譜的,還需要一些額外的工作:

  1. 使用TCP的KEEPALIVE功能?

  cat /proc/sys/net/ipv4/tcp_keepalive_time

  7200

  cat /proc/sys/net/ipv4/tcp_keepalive_intvl

  75

  cat /proc/sys/net/ipv4/tcp_keepalive_probes

  9

  以上參數(shù)的大致意思是:keepalive routine每2小時(shí)(7200秒)啟動(dòng)一次,發(fā)送第一個(gè)probe(探測(cè)包),如果在75秒內(nèi)沒(méi)有收到對(duì)方應(yīng)答則重發(fā)probe,當(dāng)連續(xù)9個(gè)probe沒(méi)有被應(yīng)答時(shí),認(rèn)為連接已斷。(此時(shí)read調(diào)用應(yīng)該能夠返回錯(cuò)誤,待測(cè)試)

  但在我印象中keepalive不太好用,默認(rèn)的時(shí)間間隔太長(zhǎng),又是整個(gè)TCP/IP棧的全局參數(shù):修改會(huì)影響其他進(jìn)程,Linux的下似乎可以修改per socket的keepalive參數(shù)?(希望有使用經(jīng)驗(yàn)的人能夠指點(diǎn)一下),但是這些方法不是portable的。

  2. 進(jìn)行應(yīng)用層的心跳

  嚴(yán)格的網(wǎng)絡(luò)程序中,應(yīng)用層的心跳協(xié)議是必不可少的。雖然比TCP自帶的keep alive要麻煩不少,但有其最大的優(yōu)點(diǎn):可控。

【TCP/IP網(wǎng)絡(luò)編程中socket的行為】相關(guān)文章:

TCP/IP網(wǎng)絡(luò)協(xié)議簡(jiǎn)介12-11

TCP/IP傳輸層12-11

TCP/IP、Http的區(qū)別10-04

TCP/IP協(xié)議棧網(wǎng)絡(luò)層常見(jiàn)協(xié)議匯總12-11

TCP/IP協(xié)議是什么10-11

對(duì)TCP/IP網(wǎng)絡(luò)協(xié)議的深入淺出歸納10-04

OSI七層與TCP/IP五層網(wǎng)絡(luò)架構(gòu)詳解10-04

查找本地IP/網(wǎng)絡(luò)IP/對(duì)方IP地址圖文教程11-20

TCP/IP三次握手四次揮手過(guò)程11-13