Socket開發(fā)實戰(zhàn)

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通信

網絡通訊要素

  1. 網絡中設備的表示
  2. 不易記憶誓禁,可以用主機名
  3. 本地會換地址127.0.0.1 主機名:localhost
  4. 端口號—定位程序
  5. 用于標識進程的邏輯地址,不同進程的標識
  6. 有效端口: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ù)報結構

帶陰影的幾個字段需要重點說明一下:

  1. 序號:Seq(Sequence Number)序號占32位纳寂,用來標識從計算機A發(fā)送到計算機B的數(shù)據(jù)包的序號,計算機發(fā)送數(shù)據(jù)時對此進行標記泻拦。

  2. 確認號:Ack(Acknowledge Number)確認號占32位毙芜,客戶端和服務器端都可以發(fā)送,Ack = Seq + 1争拐。

  3. 標志位:每個標志位占用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ā)起請求

  1. 當客戶端調用 connect() 函數(shù)后穷绵,TCP協(xié)議會組建一個數(shù)據(jù)包,并設置 SYN 標志位特愿,表示該數(shù)據(jù)包是用來建立同步連接的仲墨。同時生成一個隨機數(shù)字 1000勾缭,填充“序號(Seq)”字段,表示該數(shù)據(jù)包的序號目养。完成這些工作俩由,開始向服務器端發(fā)送數(shù)據(jù)包,客戶端就進入了SYN-SEND狀態(tài)癌蚁。

  2. 服務器端收到數(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)烛卧。

  1. 客戶端收到數(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)驼侠,表示連接已經成功建立姿鸿。

  1. 服務器端收到數(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ā)起斷開連接的請求:

  1. 客戶端調用 close() 函數(shù)后捎稚,向服務器發(fā)送 FIN 數(shù)據(jù)包乐横,進入FIN_WAIT_1狀態(tài)。FIN 是 Finish 的縮寫今野,表示完成任務需要斷開連接葡公。

  2. 服務器收到數(shù)據(jù)包后,檢測到設置了 FIN 標志位条霜,知道要斷開連接催什,于是向客戶端發(fā)送“確認包”,進入CLOSE_WAIT狀態(tài)宰睡。

注意:服務器收到請求后并不是立即斷開連接蒲凶,而是先向客戶端發(fā)送“確認包”,告訴它我知道了夹厌,我需要準備一下才能斷開連接豹爹。

  1. 客戶端收到“確認包”后進入FIN_WAIT_2狀態(tài),等待服務器準備完畢后再次發(fā)送數(shù)據(jù)包矛纹。

  2. 等待片刻后臂聋,服務器準備完畢,可以斷開連接,于是再主動向客戶端發(fā)送 FIN 包孩等,告訴它我準備好了艾君,斷開連接吧。然后進入LAST_ACK狀態(tài)肄方。

  3. 客戶端收到服務器的 FIN 包后冰垄,再向服務器發(fā)送 ACK 包,告訴它你斷開連接吧权她。然后進入TIME_WAIT狀態(tài)虹茶。

  4. 服務器收到客戶端的 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é)議

  1. 建立連接昼弟,形成數(shù)據(jù)傳輸?shù)耐ǖ?/li>
  2. 在連接中進行大量的數(shù)據(jù)傳輸(數(shù)據(jù)大小不受限制)
  3. 通過三次握手完成連接啤它,是可靠的協(xié)議,安全送達必須建立連接舱痘,效率會稍低

UDP:用戶數(shù)據(jù)報協(xié)議

  1. 將數(shù)據(jù)及源和目的封裝在數(shù)據(jù)包中变骡,不需要建立連接
  2. 因為是無需建立連接所以是不可靠的
  3. 每個數(shù)據(jù)報的大小限制在64k之內
  4. 不需要建立連接因此速度快

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 程序,不能夠一直接受客戶端的請求惹骂。

源碼解析

  1. 先說一下 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() 關閉。

  1. 再說一下 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;

}

代碼說明:

  1. 代碼在創(chuàng)建套接字時壤短,向 socket() 第二個參數(shù)傳遞 SOCK_DGRAM,以指明使用 UDP 協(xié)議慨仿。
  2. 使用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);

}

☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆

文章到此結束圃酵,喜歡的留顆星星柳畔!

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市郭赐,隨后出現(xiàn)的幾起案子薪韩,更是在濱河造成了極大的恐慌,老刑警劉巖捌锭,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俘陷,死亡現(xiàn)場離奇詭異,居然都是意外死亡观谦,警方通過查閱死者的電腦和手機拉盾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來豁状,“玉大人捉偏,你說我怎么就攤上這事⌒汉欤” “怎么了夭禽?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長谊路。 經常有香客問我讹躯,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任潮梯,我火速辦了婚禮骗灶,結果婚禮上,老公的妹妹穿的比我還像新娘秉馏。我一直安慰自己矿卑,他們只是感情好,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布沃饶。 她就那樣靜靜地躺著,像睡著了一般轻黑。 火紅的嫁衣襯著肌膚如雪糊肤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天氓鄙,我揣著相機與錄音馆揉,去河邊找鬼。 笑死抖拦,一個胖子當著我的面吹牛升酣,可吹牛的內容都是我干的。 我是一名探鬼主播态罪,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼噩茄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了复颈?” 一聲冷哼從身側響起绩聘,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎耗啦,沒想到半個月后凿菩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡帜讲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年衅谷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片似将。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡获黔,死狀恐怖,靈堂內的尸體忽然破棺而出玩郊,到底是詐尸還是另有隱情肢执,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布译红,位于F島的核電站预茄,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜耻陕,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一拙徽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诗宣,春花似錦膘怕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至篮灼,卻和暖如春忘古,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背诅诱。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工髓堪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人娘荡。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓干旁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親炮沐。 傳聞我的和親對象是個殘疾皇子争群,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內容