Socket實際開發(fā)中的應用
Socke的概念:
socket 的原意是“插座”挤土,在計算機通信領域,socket 被翻譯為“套接字”,它是計算機之間進行通信的一種約定或一種方式。通過 socket 這種約定,一臺計算機可以接收其他計算機的數(shù)據(jù)此再,也可以向其他計算機發(fā)送數(shù)據(jù)。
☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
socket 編程是基于 TCP 和 UDP 協(xié)議的玲销,它們的層級關系如下圖所示:
【擴展閱讀】開放式系統(tǒng)(Open System)
把協(xié)議分成多個層次有哪些優(yōu)點输拇?協(xié)議設計更容易?當然這也足以成為優(yōu)點之一贤斜。但是還有更重要的原因策吠,就是為了通過標準化操作設計成開放式系統(tǒng)。
標準本身就是對外公開的瘩绒,會引導更多的人遵守規(guī)范猴抹。以多個標準為依據(jù)設計的系統(tǒng)稱為開放式系統(tǒng)(Open System),我們現(xiàn)在學習的 TCP/IP 協(xié)議族也屬于其中之一锁荔。
開放式系統(tǒng)具有哪些優(yōu)點蟀给。
路由器用來完成 IP 層的交互任務。某個網絡原來使用 A 公司的路由器阳堕,現(xiàn)要將其替換成 B 公司的跋理,是否可行?這并非難事恬总,并不一定要換成同一公司的同一型號路由器前普,因為所有生產商都會按照 IP 層標準制造。
再舉個例子壹堰。大家的計算機是否裝有網絡接口卡拭卿,也就是所謂的網卡骡湖?尚未安裝也無妨,其實很容易買到记劈,因為所有網卡制造商都會遵守鏈路層的協(xié)議標準勺鸦。這就是開放式系統(tǒng)的優(yōu)點并巍。
標準的存在意味著高速的技術發(fā)展目木,這也是開放式系統(tǒng)設計最大的原因所在。實際上懊渡,軟件工程中的“面向對象(Object Oriented)”的誕生背景中也有標準化的影子刽射。也就是說,標準對于技術發(fā)展起著舉足輕重的作用剃执。
Socket:套接字
- Socket就是為網絡服務提供的一種機制
- 通訊的兩端都是Socket
- 網絡通訊其實就是Socket間的通信
- 數(shù)據(jù)在兩端socket間通過IO傳輸
- HTTP協(xié)議的傳輸實質就是Socket通信
網絡通訊要素
- 網絡中設備的表示
- 不易記憶誓禁,可以用主機名
- 本地會換地址127.0.0.1 主機名:localhost
- 端口號—定位程序
- 用于標識進程的邏輯地址,不同進程的標識
- 有效端口:065535肾档,其中01024由系統(tǒng)支配
TCP
TCP:
全稱是網絡控制協(xié)議(它使兩臺主機能夠建立連接并交換數(shù)據(jù)流)TCP能保證數(shù)據(jù)的交付摹恰,維持數(shù)據(jù)包的發(fā)送順序。
TCP(Transmission Control Protocol
: 傳輸控制協(xié)議)是一種面向連接的怒见、可靠的俗慈、基于字節(jié)流的通信協(xié)議,數(shù)據(jù)在傳輸前要建立連接遣耍,傳輸完畢后還要斷開連接闺阱。
客戶端在收發(fā)數(shù)據(jù)前要使用 connect() 函數(shù)和服務器建立連接。建立連接的目的是保證IP地址舵变、端口酣溃、物理鏈路等正確無誤,為數(shù)據(jù)的傳輸開辟通道纪隙。
TCP建立連接時要傳輸三個數(shù)據(jù)包赊豌,俗稱三次握手(Three-way Handshaking)∶嘣郏可以形象的比喻為下面的對話:
- [Shake 1] 套接字A:“你好碘饼,套接字B,我這里有數(shù)據(jù)要傳送給你麸拄,建立連接吧粪薛。”
- [Shake 2] 套接字B:“好的耘柱,我這邊已準備就緒猎物。”
- [Shake 3] 套接字A:“謝謝你受理我的請求淮椰∥宕龋”
TCP數(shù)據(jù)報結構
帶陰影的幾個字段需要重點說明一下:
序號:Seq(Sequence Number)序號占32位纳寂,用來標識從計算機A發(fā)送到計算機B的數(shù)據(jù)包的序號,計算機發(fā)送數(shù)據(jù)時對此進行標記泻拦。
確認號:Ack(Acknowledge Number)確認號占32位毙芜,客戶端和服務器端都可以發(fā)送,Ack = Seq + 1争拐。
標志位:每個標志位占用1Bit腋粥,共有6個,分別為 URG架曹、ACK隘冲、PSH、RST绑雄、SYN展辞、FIN,具體含義如下:
- URG:緊急指針(urgent pointer)有效万牺。
- ACK:確認序號有效罗珍。
- PSH:接收方應該盡快將這個報文交給應用層。
- RST:重置連接脚粟。
- SYN:建立一個新連接覆旱。
- FIN:斷開一個連接。
注解:
對英文字母縮寫的總結:Seq 是 Sequence 的縮寫珊楼,表示序列通殃;Ack(ACK) 是 Acknowledge 的縮寫,表示確認厕宗;SYN 是 Synchronous 的縮寫画舌,愿意是“同步的”,這里表示建立同步連接已慢;FIN 是 Finish 的縮寫曲聂,表示完成。
連接的建立(三次握手)
客戶端調用 socket() 函數(shù)創(chuàng)建套接字后佑惠,因為沒有建立連接朋腋,所以套接字處于CLOSED狀態(tài);服務器端調用 listen() 函數(shù)后膜楷,套接字進入LISTEN狀態(tài)旭咽,開始監(jiān)聽客戶端請求。
這個時候赌厅,客戶端開始發(fā)起請求
當客戶端調用 connect() 函數(shù)后穷绵,TCP協(xié)議會組建一個數(shù)據(jù)包,并設置 SYN 標志位特愿,表示該數(shù)據(jù)包是用來建立同步連接的仲墨。同時生成一個隨機數(shù)字 1000勾缭,填充“序號(Seq)”字段,表示該數(shù)據(jù)包的序號目养。完成這些工作俩由,開始向服務器端發(fā)送數(shù)據(jù)包,客戶端就進入了SYN-SEND狀態(tài)癌蚁。
服務器端收到數(shù)據(jù)包幻梯,檢測到已經設置了 SYN 標志位,就知道這是客戶端發(fā)來的建立連接的“請求包”匈勋。服務器端也會組建一個數(shù)據(jù)包礼旅,并設置 SYN 和 ACK 標志位,SYN 表示該數(shù)據(jù)包用來建立連接洽洁,ACK 用來確認收到了剛才客戶端發(fā)送的數(shù)據(jù)包。
服務器生成一個隨機數(shù) 2000菲嘴,填充“序號(Seq)”字段饿自。2000 和客戶端數(shù)據(jù)包沒有關系。
服務器將客戶端數(shù)據(jù)包序號(1000)加1龄坪,得到1001昭雌,并用這個數(shù)字填充“確認號(Ack)”字段。
服務器將數(shù)據(jù)包發(fā)出健田,進入SYN-RECV狀態(tài)烛卧。
- 客戶端收到數(shù)據(jù)包,檢測到已經設置了 SYN 和 ACK 標志位妓局,就知道這是服務器發(fā)來的“確認包”总放。客戶端會檢測“確認號(Ack)”字段好爬,看它的值是否為 1000+1局雄,如果是就說明連接建立成功。
接下來存炮,客戶端會繼續(xù)組建數(shù)據(jù)包炬搭,并設置 ACK 標志位,表示客戶端正確接收了服務器發(fā)來的“確認包”穆桂。同時宫盔,將剛才服務器發(fā)來的數(shù)據(jù)包序號(2000)加1,得到 2001享完,并用這個數(shù)字來填充“確認號(Ack)”字段灼芭。
客戶端將數(shù)據(jù)包發(fā)出,進入ESTABLISED狀態(tài)驼侠,表示連接已經成功建立姿鸿。
- 服務器端收到數(shù)據(jù)包谆吴,檢測到已經設置了 ACK 標志位,就知道這是客戶端發(fā)來的“確認包”苛预。服務器會檢測“確認號(Ack)”字段句狼,看它的值是否為 2000+1,如果是就說明連接建立成功热某,服務器進入ESTABLISED狀態(tài)腻菇。
至此,客戶端和服務器都進入了ESTABLISED狀態(tài)昔馋,連接建立成功筹吐,接下來就可以收發(fā)數(shù)據(jù)了。
最后的說明
三次握手的關鍵是要確認對方收到了自己的數(shù)據(jù)包秘遏,這個目標就是通過“確認號(Ack)”字段實現(xiàn)的丘薛。計算機會記錄下自己發(fā)送的數(shù)據(jù)包序號 Seq,待收到對方的數(shù)據(jù)包后邦危,檢測“確認號(Ack)”字段洋侨,看Ack = Seq + 1是否成立,如果成立說明對方正確收到了自己的數(shù)據(jù)包倦蚪。
詳細分析TCP數(shù)據(jù)的傳輸過程
建立連接后希坚,兩臺主機就可以相互傳輸數(shù)據(jù)了
舉例
主機A分2次(分2個數(shù)據(jù)包)向主機B傳遞200字節(jié)的過程。首先陵且,主機A通過1個數(shù)據(jù)包發(fā)送100個字節(jié)的數(shù)據(jù)裁僧,數(shù)據(jù)包的 Seq 號設置為 1200。主機B為了確認這一點慕购,向主機A發(fā)送 ACK 包聊疲,并將 Ack 號設置為 1301。
為了保證數(shù)據(jù)準確到達脓钾,目標機器在收到數(shù)據(jù)包(包括SYN包售睹、FIN包、普通數(shù)據(jù)包等)包后必須立即回傳ACK包可训,這樣發(fā)送方才能確認數(shù)據(jù)傳輸成功昌妹。
此時 Ack 號為 1301 而不是 1201,原因在于 Ack 號的增量為傳輸?shù)臄?shù)據(jù)字節(jié)數(shù)握截。假設每次 Ack 號不加傳輸?shù)淖止?jié)數(shù)飞崖,這樣雖然可以確認數(shù)據(jù)包的傳輸,但無法明確100字節(jié)全部正確傳遞還是丟失了一部分谨胞,比如只傳遞了80字節(jié)固歪。因此按如下的公式確認 Ack 號:
Ack號 = Seq號 + 傳遞的字節(jié)數(shù) + 1
與三次握手協(xié)議相同,最后加 1 是為了告訴對方要傳遞的 Seq 號。
下面分析傳輸過程中數(shù)據(jù)包丟失的情況
TCP套接字數(shù)據(jù)傳輸過程中發(fā)生錯誤
通過 Seq 1301 數(shù)據(jù)包向主機B傳遞100字節(jié)的數(shù)據(jù)牢裳,但中間發(fā)生了錯誤逢防,主機B未收到。經過一段時間后蒲讯,主機A仍未收到對于 Seq 1301 的ACK確認忘朝,因此嘗試重傳數(shù)據(jù)。
為了完成數(shù)據(jù)包的重傳判帮,TCP套接字每次發(fā)送數(shù)據(jù)包時都會啟動定時器局嘁,如果在一定時間內沒有收到目標機器傳回的 ACK 包,那么定時器超時晦墙,數(shù)據(jù)包會重傳悦昵。
數(shù)據(jù)包丟失的情況,也會有 ACK 包丟失的情況晌畅,一樣會重傳但指。
重傳超時時間(RTO, Retransmission Time Out)
這個值太大了會導致不必要的等待,太小會導致不必要的重傳踩麦,理論上最好是網絡 RTT 時間枚赡,但又受制于網絡距離與瞬態(tài)時延變化,所以實際上使用自適應的動態(tài)算法(例如 Jacobson 算法和 Karn 算法等)來確定超時時間谓谦。
往返時間(RTT,Round-Trip Time)表示從發(fā)送端發(fā)送數(shù)據(jù)開始贪婉,到發(fā)送端收到來自接收端的 ACK 確認包(接收端收到數(shù)據(jù)后便立即確認)反粥,總共經歷的時延。
重傳次數(shù)
TCP數(shù)據(jù)包重傳次數(shù)根據(jù)系統(tǒng)設置的不同而有所區(qū)別疲迂。有些系統(tǒng)才顿,一個數(shù)據(jù)包只會被重傳3次,如果重傳3次后還未收到該數(shù)據(jù)包的 ACK 確認尤蒿,就不再嘗試重傳郑气。但有些要求很高的業(yè)務系統(tǒng),會不斷地重傳丟失的數(shù)據(jù)包腰池,以盡最大可能保證業(yè)務數(shù)據(jù)的正常交互尾组。
建立連接非常重要,它是數(shù)據(jù)正確傳輸?shù)那疤崾竟粩嚅_連接同樣重要讳侨,它讓計算機釋放不再使用的資源。如果連接不能正常斷開奏属,不僅會造成數(shù)據(jù)傳輸錯誤跨跨,還會導致套接字不能關閉,持續(xù)占用資源囱皿,如果并發(fā)量高勇婴,服務器壓力堪憂忱嘹。
建立連接需要三次握手,斷開連接需要四次握手耕渴,可以形象的比喻為下面的對話:
- [Shake 1] 套接字A:“任務處理完畢拘悦,我希望斷開連接∪荩”
- [Shake 2] 套接字B:“哦窄做,是嗎?請稍等慰技,我準備一下椭盏。”
- 等待片刻后……
- [Shake 3] 套接字B:“我準備好了吻商,可以斷開連接了掏颊。”
- [Shake 4] 套接字A:“好的艾帐,謝謝合作乌叶。”
客戶端主動斷開連接的場景:
建立連接后柒爸,客戶端和服務器都處于ESTABLISED狀態(tài)准浴。這時,客戶端發(fā)起斷開連接的請求:
客戶端調用 close() 函數(shù)后捎稚,向服務器發(fā)送 FIN 數(shù)據(jù)包乐横,進入FIN_WAIT_1狀態(tài)。FIN 是 Finish 的縮寫今野,表示完成任務需要斷開連接葡公。
服務器收到數(shù)據(jù)包后,檢測到設置了 FIN 標志位条霜,知道要斷開連接催什,于是向客戶端發(fā)送“確認包”,進入CLOSE_WAIT狀態(tài)宰睡。
注意:
服務器收到請求后并不是立即斷開連接蒲凶,而是先向客戶端發(fā)送“確認包”,告訴它我知道了夹厌,我需要準備一下才能斷開連接豹爹。
客戶端收到“確認包”后進入FIN_WAIT_2狀態(tài),等待服務器準備完畢后再次發(fā)送數(shù)據(jù)包矛纹。
等待片刻后臂聋,服務器準備完畢,可以斷開連接,于是再主動向客戶端發(fā)送 FIN 包孩等,告訴它我準備好了艾君,斷開連接吧。然后進入LAST_ACK狀態(tài)肄方。
客戶端收到服務器的 FIN 包后冰垄,再向服務器發(fā)送 ACK 包,告訴它你斷開連接吧权她。然后進入TIME_WAIT狀態(tài)虹茶。
服務器收到客戶端的 ACK 包后,就斷開連接隅要,關閉套接字蝴罪,進入CLOSED狀態(tài)。
關于 TIME_WAIT 狀態(tài)的說明
客戶端最后一次發(fā)送 ACK包后進入 TIME_WAIT 狀態(tài)步清,而不是直接進入 CLOSED 狀態(tài)關閉連接要门,這是為什么呢?
TCP
是面向連接的傳輸方式廓啊,必須保證數(shù)據(jù)能夠正確到達目標機器欢搜,不能丟失或出錯,而網絡是不穩(wěn)定的谴轮,隨時可能會毀壞數(shù)據(jù)炒瘟,所以機器A每次向機器B發(fā)送數(shù)據(jù)包后,都要求機器B”確認“第步,回傳ACK包唧领,告訴機器A我收到了,這樣機器A才能知道數(shù)據(jù)傳送成功了雌续。如果機器B沒有回傳ACK包,機器A會重新發(fā)送胯杭,直到機器B回傳ACK包驯杜。
UDP中的服務器端和客戶端沒有連接
UDP
不像 TCP,無需在連接狀態(tài)下交換數(shù)據(jù)做个,因此基于 UDP 的服務器端和客戶端也無需經過連接過程鸽心。也就是說,不必調用 listen() 和 accept() 函數(shù)居暖。UDP 中只有創(chuàng)建套接字的過程和數(shù)據(jù)交換的過程顽频。
UDP服務器端和客戶端均只需1個套接字
TCP
中,套接字是一對一的關系太闺。如要向 10 個客戶端提供服務糯景,那么除了負責監(jiān)聽的套接字外,還需要創(chuàng)建 10 套接字。但在 UDP 中蟀淮,不管是服務器端還是客戶端都只需要 1 個套接字最住。之前解釋 UDP 原理的時候舉了郵寄包裹的例子,負責郵寄包裹的快遞公司可以比喻為 UDP 套接字怠惶,只要有 1 個快遞公司涨缚,就可以通過它向任意地址郵寄包裹。同樣策治,只需 1 個 UDP 套接字就可以向任意主機傳送數(shù)據(jù)脓魏。
基于UDP的接收和發(fā)送函數(shù)
創(chuàng)建好 TCP 套接字后,傳輸數(shù)據(jù)時無需再添加地址信息通惫,因為 TCP 套接字將保持與對方套接字的連接茂翔。換言之,TCP 套接字知道目標地址信息讽膏。但 UDP 套接字不會保持連接狀態(tài)檩电,每次傳輸數(shù)據(jù)都要添加目標地址信息,這相當于在郵寄包裹前填寫收件人地址府树。
發(fā)送數(shù)據(jù)使用 sendto() 函數(shù):
1. ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, **struct** sockaddr *to, socklen_t addrlen); //Linux
2. int sendto(SOCKET sock, **const** char *buf, int nbytes, int flags, **const** **struct** sockadr *to, int addrlen);
Windows
Linux 和 Windows 下的 sendto() 函數(shù)類似俐末,下面是詳細參數(shù)說明:
- sock:用于傳輸 UDP 數(shù)據(jù)的套接字;
- buf:保存待傳輸數(shù)據(jù)的緩沖區(qū)地址奄侠;
- nbytes:帶傳輸數(shù)據(jù)的長度(以字節(jié)計)卓箫;
- flags:可選項參數(shù),若沒有可傳遞 0垄潮;
- to:存有目標地址信息的 sockaddr 結構體變量的地址烹卒;
- addrlen:傳遞給參數(shù) to 的地址值結構體變量的長度。
UDP 發(fā)送函數(shù) sendto() 與TCP發(fā)送函數(shù) write()/send() 的最大區(qū)別在于弯洗,sendto() 函數(shù)需要向他傳遞目標地址信息旅急。
接收數(shù)據(jù)使用 recvfrom() 函數(shù):
1. ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, **struct** sockadr *from, socklen_t *addrlen); //Linux
2. int recvfrom(SOCKET sock, char *buf, int nbytes, int flags, **const** **struct** sockaddr *from, int *addrlen);
由于 UDP 數(shù)據(jù)的發(fā)送端不定,所以 recvfrom() 函數(shù)定義為可接收發(fā)送端信息的形式牡整,具體參數(shù)如下:
- sock:用于接收 UDP 數(shù)據(jù)的套接字藐吮;
- buf:保存接收數(shù)據(jù)的緩沖區(qū)地址;
- nbytes:可接收的最大字節(jié)數(shù)(不能超過 buf 緩沖區(qū)的大刑颖础)谣辞;
- flags:可選項參數(shù),若沒有可傳遞 0沐扳;
- from:存有發(fā)送端地址信息的 sockaddr 結構體變量的地址泥从;
- addrlen:保存參數(shù) from 的結構體變量長度的變量地址值。
基于UDP的回聲服務器端/客戶端
下面結合之前的內容實現(xiàn)回聲客戶端沪摄。需要注意的是躯嫉,UDP 不同于 TCP纱烘,不存在請求連接和受理過程,因此在某種意義上無法明確區(qū)分服務器端和客戶端和敬,只是因為其提供服務而稱為服務器端凹炸,希望各位讀者不要誤解。
☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
TCP和UDP總結
TCP:傳輸控制協(xié)議
- 建立連接昼弟,形成數(shù)據(jù)傳輸?shù)耐ǖ?/li>
- 在連接中進行大量的數(shù)據(jù)傳輸(數(shù)據(jù)大小不受限制)
- 通過三次握手完成連接啤它,是可靠的協(xié)議,安全送達必須建立連接舱痘,效率會稍低
UDP:用戶數(shù)據(jù)報協(xié)議
- 將數(shù)據(jù)及源和目的封裝在數(shù)據(jù)包中变骡,不需要建立連接
- 因為是無需建立連接所以是不可靠的
- 每個數(shù)據(jù)報的大小限制在64k之內
- 不需要建立連接因此速度快
TCP與UDP的優(yōu)劣特點:
TCP
是面向連接的傳輸協(xié)議,建立連接時要經過三次握手芭逝,斷開連接時要經過四次握手塌碌,中間傳輸數(shù)據(jù)時也要回復 ACK 包確認,多種機制保證了數(shù)據(jù)能夠正確到達旬盯,不會丟失或出錯台妆。
UDP
是非連接的傳輸協(xié)議,沒有建立連接和斷開連接的過程胖翰,它只是簡單地把數(shù)據(jù)丟到網絡中接剩,也不需要 ACK 包確認。
UDP
傳輸數(shù)據(jù)就好像我們郵寄包裹萨咳,郵寄前需要填好寄件人和收件人地址懊缺,之后送到快遞公司即可,但包裹是否正確送達培他、是否損壞我們無法得知鹃两,也無法保證。UDP 協(xié)議也是如此舀凛,它只管把數(shù)據(jù)包發(fā)送到網絡俊扳,然后就不管了,如果數(shù)據(jù)丟失或損壞猛遍,發(fā)送端是無法知道的拣度,當然也不會重發(fā)。
既然如此螃壤,TCP 應該是更加優(yōu)質的傳輸協(xié)議吧?
如果只考慮可靠性筋帖,TCP 的確比 UDP 好奸晴。但 UDP 在結構上比 TCP 更加簡潔,不會發(fā)送 ACK 的應答消息日麸,也不會給數(shù)據(jù)包分配 Seq 序號寄啼,所以 UDP 的傳輸效率有時會比 TCP 高出很多逮光,編程中實現(xiàn) UDP 也比 TCP 簡單。
UDP 的可靠性雖然比不上TCP墩划,但也不會像想象中那么頻繁地發(fā)生數(shù)據(jù)損毀涕刚,在更加重視傳輸效率而非可靠性的情況下,UDP 是一種很好的選擇乙帮。比如視頻通信或音頻通信杜漠,就非常適合采用 UDP 協(xié)議;通信時數(shù)據(jù)必須高效傳輸才不會產生“卡頓”現(xiàn)象察净,用戶體驗才更加流暢驾茴,如果丟失幾個數(shù)據(jù)包,視頻畫面可能會出現(xiàn)“雪花”氢卡,音頻可能會夾帶一些雜音锈至,這些都是無妨的。
與 UDP 相比译秦,TCP 的生命在于流控制峡捡,這保證了數(shù)據(jù)傳輸?shù)恼_性。
最后需要說明的是:TCP 的速度無法超越 UDP筑悴,但在收發(fā)某些類型的數(shù)據(jù)時有可能接近 UDP们拙。例如,每次交換的數(shù)據(jù)量越大雷猪,TCP 的傳輸速率就越接近于 UDP睛竣。
☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
日常開發(fā)中Socket的應用
示例代碼 : Linux 下的 socket 程序
☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
socket的基本使用
Linux 下的代碼
server.cpp 是服務器端代碼,client.cpp 是客戶端代碼求摇,要實現(xiàn)的功能是:客戶端從服務器讀取一個字符串并打印出來射沟。
服務器端代碼 server.cpp:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
//創(chuàng)建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//將套接字和IP、端口綁定
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); //端口
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//進入監(jiān)聽狀態(tài)与境,等待用戶發(fā)起請求
listen(serv_sock, 20);
//接收客戶端請求
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
//向客戶端發(fā)送數(shù)據(jù)
char str[] = "http://c.biancheng.net/socket/";
write(clnt_sock, str, sizeof(str));
//關閉套接字
close(clnt_sock);
close(serv_sock);
return 0;
}
客戶端代碼 client.cpp:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(){
//創(chuàng)建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
//向服務器(特定的IP和端口)發(fā)起請求
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); //端口
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//讀取服務器傳回的數(shù)據(jù)
char buffer[40];
read(sock, buffer, sizeof(buffer)-1);
printf("Message form server: %s\n", buffer);
//關閉套接字
close(sock);
return 0;
}
啟動一個終端(Shell)验夯,先編譯 server.cpp 并運行:
[admin@localhost ~]$ g++ server.cpp -o server
[admin@localhost ~]$ ./server
等待請求的到來
正常情況下,程序運行到 accept() 函數(shù)就會被阻塞摔刁,等待客戶端發(fā)起請求挥转。
接下再啟動一個終端,編譯 client.cpp 并運行:
[admin@localhost ~]$ g++ client.cpp -o client
[admin@localhost ~]$ ./client
client 接收到從 server發(fā)送過來的字符串就運行結束了共屈,同時绑谣,server 完成發(fā)送字符串的任務也運行結束了。大家可以通過兩個打開的終端來觀察拗引。
client 運行后借宵,通過 connect() 函數(shù)向 server 發(fā)起請求,處于監(jiān)聽狀態(tài)的 server 被激活矾削,執(zhí)行 accept() 函數(shù)壤玫,接受客戶端的請求豁护,然后執(zhí)行 write() 函數(shù)向 client 傳回數(shù)據(jù)。client 接收到傳回的數(shù)據(jù)后欲间,connect() 就運行結束了楚里,然后使用 read() 將數(shù)據(jù)讀取出來。
server 只接受一次 client 請求猎贴,當 server 向 client 傳回數(shù)據(jù)后班缎,程序就運行結束了。如果想再次接收到服務器的數(shù)據(jù)嘱能,必須再次運行 server吝梅,所以這是一個非常簡陋的 socket 程序,不能夠一直接受客戶端的請求惹骂。
源碼解析
- 先說一下 server.cpp 中的代碼苏携。
通過 socket() 函數(shù)創(chuàng)建了一個套接字,參數(shù) AF_INET 表示使用 IPv4 地址对粪,SOCK_STREAM 表示使用面向連接的套接字右冻,IPPROTO_TCP 表示使用 TCP 協(xié)議。在 Linux 中著拭,socket 也是一種文件纱扭,有文件描述符,可以使用 write() / read() 函數(shù)進行 I/O 操作儡遮,這一點已在《socket是什么》中進行了講解乳蛾。
通過 bind() 函數(shù)將套接字 serv_sock 與特定的 IP 地址和端口綁定,IP 地址和端口都保存在 sockaddr_in 結構體中鄙币。
socket() 函數(shù)確定了套接字的各種屬性肃叶,bind() 函數(shù)讓套接字與特定的IP地址和端口對應起來,這樣客戶端才能連接到該套接字十嘿。
套接字處于被動監(jiān)聽狀態(tài)因惭。所謂被動監(jiān)聽,是指套接字一直處于“睡眠”中绩衷,直到客戶端發(fā)起請求才會被“喚醒”蹦魔。
accept() 函數(shù)用來接收客戶端的請求。程序一旦執(zhí)行到 accept() 就會被阻塞(暫停運行)咳燕,直到客戶端發(fā)起請求勿决。
write() 函數(shù)用來向套接字文件中寫入數(shù)據(jù),也就是向客戶端發(fā)送數(shù)據(jù)招盲。
和普通文件一樣剥险,socket 在使用完畢后也要用 close() 關閉。
- 再說一下 client.cpp 中的代碼宪肖。client.cpp 中的代碼和 server.cpp 中有一些區(qū)別表制。
通過 connect() 向服務器發(fā)起請求,服務器的IP地址和端口號保存在 sockaddr_in 結構體中控乾。直到服務器傳回數(shù)據(jù)后么介,connect() 才運行結束。
通過 read() 從套接字文件中讀取數(shù)據(jù)蜕衡。
Windows 下的代碼
服務器端 server.cpp:
#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib") //加載 ws2_32.dll
#define BUF_SIZE 100
int main(){
WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData);
//創(chuàng)建套接字
SOCKET sock = [socket](http://c.biancheng.net/socket/)(AF_INET, SOCK_DGRAM, 0);
//綁定套接字
**struct** sockaddr_in servAddr;
memset(&servAddr, 0, **sizeof**(servAddr)); //每個字節(jié)都用0填充
servAddr.sin_family = PF_INET; //使用IPv4地址
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //自動獲取IP地址
servAddr.sin_port = htons(1234); //端口
bind(sock, (SOCKADDR*)&servAddr, **sizeof**(SOCKADDR));
//接收客戶端請求
SOCKADDR clntAddr; //客戶端地址信息
int nSize = **sizeof**(SOCKADDR);
char buffer[BUF_SIZE]; //緩沖區(qū)
**while**(1){
int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &clntAddr, &nSize);
sendto(sock, buffer, strLen, 0, &clntAddr, nSize);
}
closesocket(sock);
WSACleanup();
**return** 0;
}
代碼說明:
- 代碼在創(chuàng)建套接字時壤短,向 socket() 第二個參數(shù)傳遞 SOCK_DGRAM,以指明使用 UDP 協(xié)議慨仿。
- 使用htonl(INADDR_ANY)來自動獲取 IP 地址久脯。
利用常數(shù) INADDR_ANY 自動獲取 IP 地址有一個明顯的好處,就是當軟件安裝到其他服務器或者服務器 IP 地址改變時镰吆,不用再更改源碼重新編譯帘撰,也不用在啟動軟件時手動輸入。而且万皿,如果一臺計算機中已分配多個 IP 地址(例如路由器)摧找,那么只要端口號一致,就可以從不同的 IP 地址接收數(shù)據(jù)牢硅。所以蹬耘,服務器中優(yōu)先考慮使用 INADDR_ANY;而客戶端中除非帶有一部分服務器功能减余,否則不會采用综苔。
客戶端 client.cpp:
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib") //加載 ws2_32.dll
#define BUF_SIZE 100
int main(){
//初始化DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//創(chuàng)建套接字
SOCKET sock = socket(PF_INET, SOCK_DGRAM, 0);
//服務器地址信息
**struct** sockaddr_in servAddr;
memset(&servAddr, 0, **sizeof**(servAddr)); //每個字節(jié)都用0填充
servAddr.sin_family = PF_INET;
servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servAddr.sin_port = htons(1234);
//不斷獲取用戶輸入并發(fā)送給服務器估脆,然后接受服務器數(shù)據(jù)
**struct** sockaddr fromAddr;
int addrLen = **sizeof**(fromAddr);
**while**(1){
char buffer[BUF_SIZE] = {0};
printf("Input a string: ");
gets(buffer);
sendto(sock, buffer, strlen(buffer), 0, (**struct** sockaddr*)&servAddr, **sizeof**(servAddr));
int strLen = recvfrom(sock, buffer, BUF_SIZE, 0, &fromAddr, &addrLen);
buffer[strLen] = 0;
printf("Message form server: %s\n", buffer);
}
closesocket(sock);
WSACleanup();
**return** 0;
}
從代碼中可以看出拇舀,server.cpp 中沒有使用 listen() 函數(shù)贸宏,client.cpp 中也沒有使用 connect() 函數(shù)鳖宾,因為 UDP 不需要連接富寿。
iOS下的代碼
常用的Socket類型有兩種:流式Socket(SOCK_STREAM)和數(shù)據(jù)報式Socket(SOCK_DGRAM)摘悴。流式是一種面向連接的Socket稚铣,針對于面向連接的TCP服務應用是整;數(shù)據(jù)報式Socket是一種無連接的Socket瞧剖,對應于無連接的UDP服務應用拭嫁。
socket調用庫函數(shù)主要有:
創(chuàng)建套接字
Socket(af,type,protocol)
建立地址和套接字的聯(lián)系
bind(sockid, local addr, addrlen)
服務器端偵聽客戶端的請求
listen(Sockid ,quenlen)
建立服務器/客戶端的連接 (面向連接TCP)
客戶端請求連接
Connect(sockid, destaddr, addrlen)
服務器端等待從編號為Sockid的Socket上接收客戶連接請求
newsockid = accept(Sockid,Clientaddr, paddrlen)
發(fā)送/接收數(shù)據(jù)
- 面向連接:
send(sockid, buff, bufflen)
recv( )
- 面向無連接:
sendto(sockid,buff,…,addrlen)
recvfrom( )
- 釋放套接字
close(sockid)
tcpsocket的具體實現(xiàn)
服務器的工作流程:首先調用socket函數(shù)創(chuàng)建一個Socket抓于,然后調用bind函數(shù)將其與本機地址以及一個本地端口號綁定做粤,然后調用listen在相應的socket上監(jiān)聽,當accpet接收到一個連接服務請求時捉撮,將生成一個新的socket怕品。服務器顯示該客戶機的IP地址,并通過新的socket向客戶端發(fā)送字符串” hi,I am server!”巾遭。最后關閉該socket肉康。
☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
iOS socket服務端代碼:
UDP/IP應用編程接口(API)
- 服務器的工作流程:首先調用socket函數(shù)創(chuàng)建一個Socket闯估,然后調用bind函數(shù)將其與本機
- 地址以及一個本地端口號綁定,接收到一個客戶端時吼和,服務器顯示該客戶端的IP地址涨薪,并將字串
- 返回給客戶端。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#import <arpa/inet.h>
int main(int argc,char **argv)
{
int ser_sockfd;
int len;
//int addrlen;
socklen_t addrlen;
char seraddr[100];
struct sockaddr_in ser_addr;
/*建立socket*/
ser_sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(ser_sockfd < 0)
{
printf("I cannot socket success\n");
return 1;
}
/*填寫sockaddr_in 結構*/
addrlen = sizeof(struct sockaddr_in);
bzero(&ser_addr, addrlen);
ser_addr.sin_family = AF_INET;
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
ser_addr.sin_port = htons(1024);
/*綁定客戶端*/
if(bind(ser_sockfd,(struct sockaddr *)&ser_addr,addrlen)<0)
{
printf("connect");
return 1;
}
while(1)
{
bzero(seraddr,sizeof(seraddr));
len=recvfrom(ser_sockfd,seraddr,sizeof(seraddr),0,(struct sockaddr*)&ser_addr,&addrlen);
/*顯示client端的網絡地址*/
printf("receive from %s\n",inet_ntoa(ser_addr.sin_addr));
/*顯示客戶端發(fā)來的字串*/
printf("recevce:%s",seraddr);
/*將字串返回給client端*/
sendto(ser_sockfd, seraddr,len,0,(struct sockaddr*)&ser_addr,addrlen);
}
}
客戶端的工作流程:首先調用socket函數(shù)創(chuàng)建一個Socket炫乓,填寫服務器地址及端口號刚夺,從標準輸入設備中取得字符串,將字符串傳送給服務器端末捣,并接收服務器端返回的字符串侠姑。最后關閉該socket。
☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
UDP/IP應用編程接口(API)
- 客戶端的工作流程:首先調用socket函數(shù)創(chuàng)建一個Socket箩做,填寫服務器地址及端口號莽红,
- 從標準輸入設備中取得字符串,將字符串傳送給服務器端卒茬,并接收服務器端返回的字
- 符串船老。最后關閉該socket。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include <netinet/in.h>
#import <arpa/inet.h>
int GetServerAddr(char * addrname)
{
printf("please input server addr:");
scanf("%s",addrname);
return 1;
}
int main(int argc,char **argv)
{
int cli_sockfd;
int len;
socklen_t addrlen;
char seraddr[14];
struct sockaddr_in cli_addr;
char buffer[256];
GetServerAddr(seraddr);
/* 建立socket*/
cli_sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(cli_sockfd < 0)
{
printf("I cannot socket success\n");
return 1;
}
/* 填寫sockaddr_in */
addrlen = sizeof(struct sockaddr_in);
bzero(&cli_addr, addrlen);
cli_addr.sin_family = AF_INET;
cli_addr.sin_addr.s_addr = inet_addr(seraddr);
//cli_addr.sin_addr.s_addr = htonl(INADDR_ANY);
cli_addr.sin_port = htons(1024);
bzero(buffer,sizeof(buffer));
/* 從標準輸入設備取得字符串*/
len=read(STDIN_FILENO,buffer,sizeof(buffer));
/* 將字符串傳送給server端*/
sendto(cli_sockfd,buffer,len,0,(struct sockaddr*)&cli_addr,addrlen);
/* 接收server端返回的字符串*/
len=recvfrom(cli_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&cli_addr,&addrlen);
//printf("receive from %s\n",inet_ntoa(cli_addr.sin_addr));
printf("receive: %s",buffer);
close(cli_sockfd);
}
☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆