Socket原理
1纲缓、什么是Socket
在計算機通信領(lǐng)域,socket 被翻譯為“套接字”喊废,它是計算機之間進行通信的一種約定或一種方式祝高。通過 socket 這種約定,一臺計算機可以接收其他計算機的數(shù)據(jù)污筷,也可以向其他計算機發(fā)送數(shù)據(jù)
socket起源于Unix工闺,而Unix/Linux基本哲學(xué)之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關(guān)閉close”模式來操作。
我的理解就是Socket就是該模式的一個實現(xiàn):即socket是一種特殊的文件陆蟆,一些socket函數(shù)就是對其進行的操作(讀/寫IO雷厂、打開、關(guān)閉)叠殷。
Socket()函數(shù)返回一個整型的Socket描述符改鲫,隨后的連接建立、數(shù)據(jù)傳輸?shù)炔僮鞫际峭ㄟ^該Socket實現(xiàn)的林束。
2钩杰、網(wǎng)絡(luò)中進程如何通信
既然Socket主要是用來解決網(wǎng)絡(luò)通信的,那么我們就來理解網(wǎng)絡(luò)中進程是如何通信的诊县。
2.1、本地進程間通信
a措左、消息傳遞(管道依痊、消息隊列、FIFO)
b怎披、同步(互斥量胸嘁、條件變量、讀寫鎖凉逛、文件和寫記錄鎖性宏、信號量)
c、共享內(nèi)存(匿名的和具名的状飞,eg:channel)
d毫胜、遠程過程調(diào)用(RPC)
2.2、網(wǎng)絡(luò)中進程如何通信
我們要理解網(wǎng)絡(luò)中進程如何通信诬辈,得解決兩個問題:
〗褪埂a、我們要如何標識一臺主機焙糟,即怎樣確定我們將要通信的進程是在那一臺主機上運行口渔。
b穿撮、我們要如何標識唯一進程缺脉,本地通過pid標識,網(wǎng)絡(luò)中應(yīng)該怎樣標識悦穿?
解決辦法:
」ダ瘛a、TCP/IP協(xié)議族已經(jīng)幫我們解決了這個問題咧党,網(wǎng)絡(luò)層的“ip地址”可以唯一標識網(wǎng)絡(luò)中的主機
∶鼗住b、傳輸層的“協(xié)議+端口”可以唯一標識主機中的應(yīng)用程序(進程),因此深员,我們利用三元組(ip地址负蠕,協(xié)議,端口)就可以標識網(wǎng)絡(luò)的進程了倦畅,網(wǎng)絡(luò)中的進程通信就可以利用這個標志與其它進程進行交互
3遮糖、Socket怎么通信
現(xiàn)在,我們知道了網(wǎng)絡(luò)中進程間如何通信叠赐,即利用三元組【ip地址欲账,協(xié)議,端口】可以進行網(wǎng)絡(luò)間通信了芭概,那我們應(yīng)該怎么實現(xiàn)了赛不,因此,我們socket應(yīng)運而生罢洲,它就是利用三元組解決網(wǎng)絡(luò)通信的一個中間件工具踢故,就目前而言,幾乎所有的應(yīng)用程序都是采用socket惹苗,如UNIX BSD的套接字(socket)和UNIX System V的TLI(已經(jīng)被淘汰)殿较。
Socket通信的數(shù)據(jù)傳輸方式,常用的有兩種:
∽亍a淋纲、SOCK_STREAM:表示面向連接的數(shù)據(jù)傳輸方式。數(shù)據(jù)可以準確無誤地到達另一臺計算機院究,如果損壞或丟失洽瞬,可以重新發(fā)送,但效率相對較慢业汰。常見的 http 協(xié)議就使用 SOCK_STREAM 傳輸數(shù)據(jù)片任,因為要確保數(shù)據(jù)的正確性,否則網(wǎng)頁不能正常解析蔬胯。
《怨b、SOCK_DGRAM:表示無連接的數(shù)據(jù)傳輸方式氛濒。計算機只管傳輸數(shù)據(jù)产场,不作數(shù)據(jù)校驗,如果數(shù)據(jù)在傳輸中損壞舞竿,或者沒有到達另一臺計算機京景,是沒有辦法補救的。也就是說骗奖,數(shù)據(jù)錯了就錯了确徙,無法重傳醒串。因為 SOCK_DGRAM 所做的校驗工作少,所以效率比 SOCK_STREAM 高鄙皇。
例如:QQ 視頻聊天和語音聊天就使用 SOCK_DGRAM 傳輸數(shù)據(jù)芜赌,因為首先要保證通信的效率,盡量減小延遲伴逸,而數(shù)據(jù)的正確性是次要的缠沈,即使丟失很小的一部分數(shù)據(jù),視頻和音頻也可以正常解析错蝴,最多出現(xiàn)噪點或雜音洲愤,不會對通信質(zhì)量有實質(zhì)的影響
4、TCP/IP協(xié)議
4.1顷锰、概念
TCP/IP【TCP(傳輸控制協(xié)議)和IP(網(wǎng)際協(xié)議)】提供點對點的鏈接機制柬赐,將數(shù)據(jù)應(yīng)該如何封裝、定址官紫、傳輸躺率、路由以及在目的地如何接收,都加以標準化万矾。它將軟件通信過程抽象化為四個抽象層,采取協(xié)議堆棧的方式慎框,分別實現(xiàn)出不同通信協(xié)議良狈。協(xié)議族下的各種協(xié)議,依其功能不同笨枯,被分別歸屬到這四個層次結(jié)構(gòu)之中薪丁,常被視為是簡化的七層OSI模型。
它們之間好比送信的線路和驛站的作用馅精,比如要建議送信驛站严嗜,必須得了解送信的各個細節(jié)。
TCP(Transmission Control Protocol洲敢,傳輸控制協(xié)議)是一種面向連接的漫玄、可靠的、基于字節(jié)流的通信協(xié)議压彭,數(shù)據(jù)在傳輸前要建立連接睦优,傳輸完畢后還要斷開連接,客戶端在收發(fā)數(shù)據(jù)前要使用 connect() 函數(shù)和服務(wù)器建立連接壮不。建立連接的目的是保證IP地址汗盘、端口、物理鏈路等正確無誤询一,為數(shù)據(jù)的傳輸開辟通道隐孽。
TCP建立連接時要傳輸三個數(shù)據(jù)包癌椿,俗稱三次握手(Three-way Handshaking)×庹螅可以形象的比喻為下面的對話:
[Shake 1] 套接字A:“你好踢俄,套接字B,我這里有數(shù)據(jù)要傳送給你送粱,建立連接吧褪贵。”
[Shake 2] 套接字B:“好的抗俄,我這邊已準備就緒脆丁。”
[Shake 3] 套接字A:“謝謝你受理我的請求动雹。
4.2槽卫、TCP的粘包問題以及數(shù)據(jù)的無邊界性: https://blog.csdn.net/m0_37947204/article/details/80490512
4.4、TCP數(shù)據(jù)報結(jié)構(gòu):
帶陰影的幾個字段需要重點說明一下:
(1) 序號:Seq(Sequence Number)序號占32位胰蝠,用來標識從計算機A發(fā)送到計算機B的數(shù)據(jù)包的序號歼培,計算機發(fā)送數(shù)據(jù)時對此進行標記。
(2) 確認號:Ack(Acknowledge Number)確認號占32位茸塞,客戶端和服務(wù)器端都可以發(fā)送躲庄,Ack = Seq + 1。
(3) 標志位:每個標志位占用1Bit钾虐,共有6個噪窘,分別為 URG、ACK效扫、PSH倔监、RST、SYN菌仁、FIN浩习,具體含義如下:
(1)URG:緊急指針(urgent pointer)有效。
(2)ACK:確認序號有效济丘。
(3)PSH:接收方應(yīng)該盡快將這個報文交給應(yīng)用層谱秽。
(4)RST:重置連接。
(5)SYN:建立一個新連接摹迷。
(6)FIN:斷開一個連接弯院。
4.5、連接的建立(三次握手):
使用 connect() 建立連接時泪掀,客戶端和服務(wù)器端會相互發(fā)送三個數(shù)據(jù)包听绳,請看下圖:
客戶端調(diào)用 socket() 函數(shù)創(chuàng)建套接字后,因為沒有建立連接异赫,所以套接字處于CLOSED狀態(tài)椅挣;服務(wù)器端調(diào)用 listen() 函數(shù)后头岔,套接字進入LISTEN狀態(tài),開始監(jiān)聽客戶端請求
這時客戶端發(fā)起請求:
1) 當(dāng)客戶端調(diào)用 connect() 函數(shù)后鼠证,TCP協(xié)議會組建一個數(shù)據(jù)包峡竣,并設(shè)置 SYN 標志位,表示該數(shù)據(jù)包是用來建立同步連接的量九。同時生成一個隨機數(shù)字 1000适掰,填充“序號(Seq)”字段,表示該數(shù)據(jù)包的序號荠列。完成這些工作类浪,開始向服務(wù)器端發(fā)送數(shù)據(jù)包,客戶端就進入了SYN-SEND狀態(tài)肌似。
2) 服務(wù)器端收到數(shù)據(jù)包费就,檢測到已經(jīng)設(shè)置了 SYN 標志位,就知道這是客戶端發(fā)來的建立連接的“請求包”川队。服務(wù)器端也會組建一個數(shù)據(jù)包力细,并設(shè)置 SYN 和 ACK 標志位,SYN 表示該數(shù)據(jù)包用來建立連接固额,ACK 用來確認收到了剛才客戶端發(fā)送的數(shù)據(jù)包
服務(wù)器生成一個隨機數(shù) 2000眠蚂,填充“序號(Seq)”字段。2000 和客戶端數(shù)據(jù)包沒有關(guān)系斗躏。
服務(wù)器將客戶端數(shù)據(jù)包序號(1000)加1逝慧,得到1001,并用這個數(shù)字填充“確認號(Ack)”字段瑟捣。
服務(wù)器將數(shù)據(jù)包發(fā)出,進入SYN-RECV狀態(tài)
3) 客戶端收到數(shù)據(jù)包栅干,檢測到已經(jīng)設(shè)置了 SYN 和 ACK 標志位迈套,就知道這是服務(wù)器發(fā)來的“確認包”〖盍郏客戶端會檢測“確認號(Ack)”字段桑李,看它的值是否為 1000+1,如果是就說明連接建立成功窿给。
接下來贵白,客戶端會繼續(xù)組建數(shù)據(jù)包,并設(shè)置 ACK 標志位崩泡,表示客戶端正確接收了服務(wù)器發(fā)來的“確認包”禁荒。同時,將剛才服務(wù)器發(fā)來的數(shù)據(jù)包序號(2000)加1角撞,得到 2001呛伴,并用這個數(shù)字來填充“確認號(Ack)”字段勃痴。
客戶端將數(shù)據(jù)包發(fā)出,進入ESTABLISED狀態(tài)热康,表示連接已經(jīng)成功建立沛申。
4) 服務(wù)器端收到數(shù)據(jù)包,檢測到已經(jīng)設(shè)置了 ACK 標志位姐军,就知道這是客戶端發(fā)來的“確認包”铁材。服務(wù)器會檢測“確認號(Ack)”字段,看它的值是否為 2000+1奕锌,如果是就說明連接建立成功著觉,服務(wù)器進入ESTABLISED狀態(tài)。
至此歇攻,客戶端和服務(wù)器都進入了ESTABLISED狀態(tài)固惯,連接建立成功,接下來就可以收發(fā)數(shù)據(jù)了缴守。
4.6葬毫、TCP四次握手斷開連接
建立連接非常重要,它是數(shù)據(jù)正確傳輸?shù)那疤崧潘耄粩嚅_連接同樣重要贴捡,它讓計算機釋放不再使用的資源。如果連接不能正常斷開村砂,不僅會造成數(shù)據(jù)傳輸錯誤烂斋,還會導(dǎo)致套接字不能關(guān)閉,持續(xù)占用資源础废,如果并發(fā)量高汛骂,服務(wù)器壓力堪憂。
斷開連接需要四次握手评腺,可以形象的比喻為下面的對話:
[Shake 1] 套接字A:“任務(wù)處理完畢帘瞭,我希望斷開連接≥锛ィ”
[Shake 2] 套接字B:“哦蝶念,是嗎?請稍等芋绸,我準備一下媒殉。”
等待片刻后……
[Shake 3] 套接字B:“我準備好了摔敛,可以斷開連接了廷蓉。”
[Shake 4] 套接字A:“好的马昙,謝謝合作苦酱∈勖玻”
下圖演示了客戶端主動斷開連接的場景:
建立連接后,客戶端和服務(wù)器都處于ESTABLISED狀態(tài)疫萤。這時颂跨,客戶端發(fā)起斷開連接的請求:
- 客戶端調(diào)用 close() 函數(shù)后,向服務(wù)器發(fā)送 FIN 數(shù)據(jù)包扯饶,進入FIN_WAIT_1狀態(tài)恒削。FIN 是 Finish 的縮寫,表示完成任務(wù)需要斷開連接尾序。
- 服務(wù)器收到數(shù)據(jù)包后钓丰,檢測到設(shè)置了 FIN 標志位,知道要斷開連接每币,于是向客戶端發(fā)送“確認包”携丁,進入CLOSE_WAIT狀態(tài)。
注意:服務(wù)器收到請求后并不是立即斷開連接兰怠,而是先向客戶端發(fā)送“確認包”梦鉴,告訴它我知道了,我需要準備一下才能斷開連接揭保。 - 客戶端收到“確認包”后進入FIN_WAIT_2狀態(tài)肥橙,等待服務(wù)器準備完畢后再次發(fā)送數(shù)據(jù)包。
- 等待片刻后秸侣,服務(wù)器準備完畢存筏,可以斷開連接,于是再主動向客戶端發(fā)送 FIN 包味榛,告訴它我準備好了椭坚,斷開連接吧。然后進入LAST_ACK狀態(tài)搏色。
- 客戶端收到服務(wù)器的 FIN 包后善茎,再向服務(wù)器發(fā)送 ACK 包,告訴它你斷開連接吧继榆。然后進入TIME_WAIT狀態(tài)巾表。
- 服務(wù)器收到客戶端的 ACK 包后汁掠,就斷開連接略吨,關(guān)閉套接字,進入CLOSED狀態(tài)考阱。
4.7翠忠、關(guān)于 TIME_WAIT 狀態(tài)的說明
客戶端最后一次發(fā)送 ACK包后進入 TIME_WAIT 狀態(tài),而不是直接進入 CLOSED 狀態(tài)關(guān)閉連接乞榨,這是為什么呢秽之?
TCP 是面向連接的傳輸方式当娱,必須保證數(shù)據(jù)能夠正確到達目標機器,不能丟失或出錯考榨,而網(wǎng)絡(luò)是不穩(wěn)定的跨细,隨時可能會毀壞數(shù)據(jù),所以機器A每次向機器B發(fā)送數(shù)據(jù)包后河质,都要求機器B”確認“冀惭,回傳ACK包,告訴機器A我收到了掀鹅,這樣機器A才能知道數(shù)據(jù)傳送成功了散休。如果機器B沒有回傳ACK包,機器A會重新發(fā)送乐尊,直到機器B回傳ACK包戚丸。
客戶端最后一次向服務(wù)器回傳ACK包時,有可能會因為網(wǎng)絡(luò)問題導(dǎo)致服務(wù)器收不到扔嵌,服務(wù)器會再次發(fā)送 FIN 包限府,如果這時客戶端完全關(guān)閉了連接,那么服務(wù)器無論如何也收不到ACK包了对人,所以客戶端需要等待片刻谣殊、確認對方收到ACK包后才能進入CLOSED狀態(tài)。那么牺弄,要等待多久呢姻几?
數(shù)據(jù)包在網(wǎng)絡(luò)中是有生存時間的,超過這個時間還未到達目標主機就會被丟棄势告,并通知源主機蛇捌。這稱為報文最大生存時間(MSL,Maximum Segment Lifetime)咱台。TIME_WAIT 要等待 2MSL 才會進入 CLOSED 狀態(tài)络拌。ACK 包到達服務(wù)器需要 MSL 時間,服務(wù)器重傳 FIN 包也需要 MSL 時間回溺,2MSL 是數(shù)據(jù)包往返的最大時間春贸,如果 2MSL 后還未收到服務(wù)器重傳的 FIN 包,就說明服務(wù)器已經(jīng)收到了 ACK 包
4.8.優(yōu)雅的斷開連接–shutdown()
close()/closesocket()和shutdown()的區(qū)別
確切地說遗遵,close() / closesocket() 用來關(guān)閉套接字萍恕,將套接字描述符(或句柄)從內(nèi)存清除,之后再也不能使用該套接字车要,與C語言中的 fclose() 類似允粤。應(yīng)用程序關(guān)閉套接字后,與該套接字相關(guān)的連接和緩存也失去了意義,TCP協(xié)議會自動觸發(fā)關(guān)閉連接的操作类垫。
shutdown() 用來關(guān)閉連接司光,而不是套接字,不管調(diào)用多少次 shutdown()悉患,套接字依然存在残家,直到調(diào)用 close() / closesocket() 將套接字從內(nèi)存清除。
調(diào)用 close()/closesocket() 關(guān)閉套接字時售躁,或調(diào)用 shutdown() 關(guān)閉輸出流時跪削,都會向?qū)Ψ桨l(fā)送 FIN 包。FIN 包表示數(shù)據(jù)傳輸完畢迂求,計算機收到 FIN 包就知道不會再有數(shù)據(jù)傳送過來了碾盐。
默認情況下,close()/closesocket() 會立即向網(wǎng)絡(luò)中發(fā)送FIN包揩局,不管輸出緩沖區(qū)中是否還有數(shù)據(jù)毫玖,而shutdown() 會等輸出緩沖區(qū)中的數(shù)據(jù)傳輸完畢再發(fā)送FIN包。也就意味著凌盯,調(diào)用 close()/closesocket() 將丟失輸出緩沖區(qū)中的數(shù)據(jù)付枫,而調(diào)用 shutdown() 不會
5、OSI模型
TCP/IP對OSI的網(wǎng)絡(luò)模型層進行了劃分如下:
TCP/IP協(xié)議參考模型把所有的TCP/IP系列協(xié)議歸類到四個抽象層中
應(yīng)用層:TFTP驰怎,HTTP阐滩,SNMP,F(xiàn)TP县忌,SMTP掂榔,DNS,Telnet 等等
傳輸層:TCP症杏,UDP
網(wǎng)絡(luò)層:IP装获,ICMP,OSPF厉颤,EIGRP穴豫,IGMP
數(shù)據(jù)鏈路層:SLIP,CSLIP逼友,PPP精肃,MTU
每一抽象層建立在低一層提供的服務(wù)上,并且為高一層提供服務(wù)帜乞,看起來大概是這樣子的
6司抱、Socket常用函數(shù)接口及其原理
圖解socket函數(shù):
6.1、使用socket()函數(shù)創(chuàng)建套接字
int socket(int af, int type, int protocol);
- af 為地址族(Address Family)挖函,也就是 IP 地址類型状植,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的簡寫怨喘,INET是“Inetnet”的簡寫津畸。AF_INET 表示 IPv4 地址,例如 127.0.0.1必怜;AF_INET6 表示 IPv6 地址肉拓,例如 1030::C9B4:FF12:48AA:1A2B。
大家需要記住127.0.0.1梳庆,它是一個特殊IP地址暖途,表示本機地址,后面的教程會經(jīng)常用到膏执。 - type 為數(shù)據(jù)傳輸方式驻售,常用的有 SOCK_STREAM 和 SOCK_DGRAM
- protocol 表示傳輸協(xié)議,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP更米,分別表示 TCP 傳輸協(xié)議和 UDP 傳輸協(xié)議
6.2欺栗、使用bind()和connect()函數(shù)
socket() 函數(shù)用來創(chuàng)建套接字,確定套接字的各種屬性征峦,然后服務(wù)器端要用 bind() 函數(shù)將套接字與特定的IP地址和端口綁定起來迟几,只有這樣,流經(jīng)該IP地址和端口的數(shù)據(jù)才能交給套接字處理栏笆;而客戶端要用 connect() 函數(shù)建立連接
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
sock 為 socket 文件描述符类腮,addr 為 sockaddr 結(jié)構(gòu)體變量的指針,addrlen 為 addr 變量的大小蛉加,可由 sizeof() 計算得出
下面的代碼蚜枢,將創(chuàng)建的套接字與IP地址 127.0.0.1、端口 1234 綁定:
//創(chuàng)建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//創(chuàng)建sockaddr_in結(jié)構(gòu)體變量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每個字節(jié)都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
serv_addr.sin_port = htons(1234); //端口
//將套接字和IP针饥、端口綁定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
connect() 函數(shù)用來建立連接祟偷,它的原型為:
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
6.3、使用listen()和accept()函數(shù)
于服務(wù)器端程序打厘,使用 bind() 綁定套接字后修肠,還需要使用 listen() 函數(shù)讓套接字進入被動監(jiān)聽狀態(tài),再調(diào)用 accept() 函數(shù)户盯,就可以隨時響應(yīng)客戶端的請求了嵌施。
通過** listen() 函數(shù)**可以讓套接字進入被動監(jiān)聽狀態(tài),它的原型為:
int listen(int sock, int backlog);
sock 為需要進入監(jiān)聽狀態(tài)的套接字莽鸭,backlog 為請求隊列的最大長度吗伤。
所謂被動監(jiān)聽,是指當(dāng)沒有客戶端請求時硫眨,套接字處于“睡眠”狀態(tài)足淆,只有當(dāng)接收到客戶端請求時,套接字才會被“喚醒”來響應(yīng)請求。
請求隊列
當(dāng)套接字正在處理客戶端請求時巧号,如果有新的請求進來族奢,套接字是沒法處理的,只能把它放進緩沖區(qū)丹鸿,待當(dāng)前請求處理完畢后越走,再從緩沖區(qū)中讀取出來處理。如果不斷有新的請求進來靠欢,它們就按照先后順序在緩沖區(qū)中排隊廊敌,直到緩沖區(qū)滿。這個緩沖區(qū)门怪,就稱為請求隊列(Request Queue)骡澈。
緩沖區(qū)的長度(能存放多少個客戶端請求)可以通過 listen() 函數(shù)的 backlog 參數(shù)指定,但究竟為多少并沒有什么標準掷空,可以根據(jù)你的需求來定秧廉,并發(fā)量小的話可以是10或者20。
如果將 backlog 的值設(shè)置為 SOMAXCONN拣帽,就由系統(tǒng)來決定請求隊列長度疼电,這個值一般比較大,可能是幾百减拭,或者更多蔽豺。
當(dāng)請求隊列滿時,就不再接收新的請求,對于 Linux,客戶端會收到 ECONNREFUSED 錯誤
注意:listen() 只是讓套接字處于監(jiān)聽狀態(tài)聚假,并沒有接收請求晚伙。接收請求需要使用 accept() 函數(shù)欧啤。
當(dāng)套接字處于監(jiān)聽狀態(tài)時,可以通過 accept() 函數(shù)來接收客戶端請求。它的原型為:
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
它的參數(shù)與 listen() 和 connect() 是相同的:sock 為服務(wù)器端套接字,addr 為 sockaddr_in 結(jié)構(gòu)體變量拾因,addrlen 為參數(shù) addr 的長度,可由 sizeof() 求得旷余。
accept() 返回一個新的套接字來和客戶端通信绢记,addr 保存了客戶端的IP地址和端口號,而 sock 是服務(wù)器端的套接字正卧,大家注意區(qū)分蠢熄。后面和客戶端通信時,要使用這個新生成的套接字炉旷,而不是原來服務(wù)器端的套接字签孔。
最后需要說明的是:listen() 只是讓套接字進入監(jiān)聽狀態(tài)叉讥,并沒有真正接收客戶端請求,listen() 后面的代碼會繼續(xù)執(zhí)行饥追,直到遇到 accept()图仓。accept() 會阻塞程序執(zhí)行(后面代碼不能被執(zhí)行),直到有新的請求到來判耕。
6.4、socket數(shù)據(jù)的接收和發(fā)送
Linux下數(shù)據(jù)的接收和發(fā)送
Linux 不區(qū)分套接字文件和普通文件翘骂,使用 write() 可以向套接字中寫入數(shù)據(jù)壁熄,使用 read() 可以從套接字中讀取數(shù)據(jù)。
前面我們說過碳竟,兩臺計算機之間的通信相當(dāng)于兩個套接字之間的通信草丧,在服務(wù)器端用 write() 向套接字寫入數(shù)據(jù),客戶端就能收到莹桅,然后再使用 read() 從套接字中讀取出來昌执,就完成了一次通信。
write() 的原型為:
ssize_t write(int fd, const void *buf, size_t nbytes);
fd 為要寫入的文件的描述符诈泼,buf 為要寫入的數(shù)據(jù)的緩沖區(qū)地址懂拾,nbytes 為要寫入的數(shù)據(jù)的字節(jié)數(shù)。
write() 函數(shù)會將緩沖區(qū) buf 中的 nbytes 個字節(jié)寫入文件 fd铐达,成功則返回寫入的字節(jié)數(shù)岖赋,失敗則返回 -1。
read() 的原型為:
ssize_t read(int fd, void *buf, size_t nbytes);
fd 為要讀取的文件的描述符瓮孙,buf 為要接收數(shù)據(jù)的緩沖區(qū)地址唐断,nbytes 為要讀取的數(shù)據(jù)的字節(jié)數(shù)。
read() 函數(shù)會從 fd 文件中讀取 nbytes 個字節(jié)并保存到緩沖區(qū) buf杭抠,成功則返回讀取到的字節(jié)數(shù)(但遇到文件結(jié)尾則返回0)脸甘,失敗則返回 -1。
6.5偏灿、socket緩沖區(qū)以及阻塞模式
socket緩沖區(qū)
每個 socket 被創(chuàng)建后丹诀,都會分配兩個緩沖區(qū),輸入緩沖區(qū)和輸出緩沖區(qū)翁垂。
write()/send() 并不立即向網(wǎng)絡(luò)中傳輸數(shù)據(jù)忿墅,而是先將數(shù)據(jù)寫入緩沖區(qū)中,再由TCP協(xié)議將數(shù)據(jù)從緩沖區(qū)發(fā)送到目標機器沮峡。一旦將數(shù)據(jù)寫入到緩沖區(qū)疚脐,函數(shù)就可以成功返回,不管它們有沒有到達目標機器邢疙,也不管它們何時被發(fā)送到網(wǎng)絡(luò)棍弄,這些都是TCP協(xié)議負責(zé)的事情望薄。
TCP協(xié)議獨立于 write()/send() 函數(shù),數(shù)據(jù)有可能剛被寫入緩沖區(qū)就發(fā)送到網(wǎng)絡(luò)呼畸,也可能在緩沖區(qū)中不斷積壓痕支,多次寫入的數(shù)據(jù)被一次性發(fā)送到網(wǎng)絡(luò),這取決于當(dāng)時的網(wǎng)絡(luò)情況蛮原、當(dāng)前線程是否空閑等諸多因素卧须,不由程序員控制。
read()/recv() 函數(shù)也是如此儒陨,也從輸入緩沖區(qū)中讀取數(shù)據(jù)花嘶,而不是直接從網(wǎng)絡(luò)中讀取
這些I/O緩沖區(qū)特性可整理如下:
(1)I/O緩沖區(qū)在每個TCP套接字中單獨存在;
(2)I/O緩沖區(qū)在創(chuàng)建套接字時自動生成蹦漠;
(3)即使關(guān)閉套接字也會繼續(xù)傳送輸出緩沖區(qū)中遺留的數(shù)據(jù)椭员;
(4)關(guān)閉套接字將丟失輸入緩沖區(qū)中的數(shù)據(jù)。
輸入輸出緩沖區(qū)的默認大小一般都是 8K笛园,可以通過 getsockopt() 函數(shù)獲劝鳌:
unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);
阻塞模式
對于TCP套接字(默認情況下),當(dāng)使用 write()/send() 發(fā)送數(shù)據(jù)時:
1) 首先會檢查緩沖區(qū)研铆,如果緩沖區(qū)的可用空間長度小于要發(fā)送的數(shù)據(jù)埋同,那么 write()/send() 會被阻塞(暫停執(zhí)行),直到緩沖區(qū)中的數(shù)據(jù)被發(fā)送到目標機器棵红,騰出足夠的空間莺禁,才喚醒 write()/send() 函數(shù)繼續(xù)寫入數(shù)據(jù)。
2) 如果TCP協(xié)議正在向網(wǎng)絡(luò)發(fā)送數(shù)據(jù)窄赋,那么輸出緩沖區(qū)會被鎖定哟冬,不允許寫入,write()/send() 也會被阻塞忆绰,直到數(shù)據(jù)發(fā)送完畢緩沖區(qū)解鎖浩峡,write()/send() 才會被喚醒。
3) 如果要寫入的數(shù)據(jù)大于緩沖區(qū)的最大長度错敢,那么將分批寫入翰灾。
4) 直到所有數(shù)據(jù)被寫入緩沖區(qū) write()/send() 才能返回。
當(dāng)使用 read()/recv() 讀取數(shù)據(jù)時:
1) 首先會檢查緩沖區(qū)稚茅,如果緩沖區(qū)中有數(shù)據(jù)纸淮,那么就讀取,否則函數(shù)會被阻塞亚享,直到網(wǎng)絡(luò)上有數(shù)據(jù)到來咽块。
2) 如果要讀取的數(shù)據(jù)長度小于緩沖區(qū)中的數(shù)據(jù)長度,那么就不能一次性將緩沖區(qū)中的所有數(shù)據(jù)讀出欺税,剩余數(shù)據(jù)將不斷積壓侈沪,直到有 read()/recv() 函數(shù)再次讀取揭璃。
3) 直到讀取到數(shù)據(jù)后 read()/recv() 函數(shù)才會返回,否則就一直被阻塞亭罪。
這就是TCP套接字的阻塞模式瘦馍。所謂阻塞,就是上一步動作沒有完成应役,下一步動作將暫停情组,直到上一步動作完成后才能繼續(xù),以保持同步性箩祥。
TCP套接字默認情況下是阻塞模式