Socket學習筆記
以下均為整理,做參考之用。
IP Address
IP地址是指互聯網協議地址(英語:Internet Protocol Address,又譯為網際協議地址),是IP Address的縮寫。IP地址是IP協議提供的一種統一的地址格式,它為互聯網上的每一個網絡和每一臺主機分配一個邏輯地址,以此來屏蔽物理地址的差異。
IP地址被用來給Internet上的電腦一個編號。大家日常見到的情況是每臺聯網的PC上都需要有IP地址,才能正常通信。我們可以把“個人電腦”比作“一臺電話”,那麽“IP地址”就相當於“電話號碼”,而Internet中的路由器,就相當於電信局的“程控式交換機”。
IP地址是一個32位的二進制數,通常被分割為4個“8位二進制數”(也就是4個字節)。IP地址通常用“點分十進制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之間的十進制整數。例:點分十進IP地址(100.4.5.6),實際上是32位二進制數(01100100.00000100.00000101.00000110)。
每臺計算機都有自己獨一無二的IP地址,根據IP地址判斷與哪臺計算機進行通信。
Port
一臺計算機可以同時提供多種網絡服務,例如Web服務、FTP服務(文件傳輸服務)、SMTP服務(郵箱服務)等,僅有 IP 地址,計算機雖然可以正確接收到數據包,但是卻不知道要將數據包交給哪個網絡程序來處理,所以通信失敗。
為了區分不同的網絡程序,計算機會為每個網絡程序分配一個獨一無二的端口號(Port Number),例如,Web服務的端口號是 80,FTP 服務的端口號是 21,SMTP 服務的端口號是 25。
端口(Port)是一個虛擬的、邏輯上的概念。可以將端口理解為一道門,數據通過這道門流入流出,每道門有不同的編號,就是端口號。
Protocol
Protocol為進行網絡中的數據交換而建立的規則、標準或約定。用於不同系統中實體間的通信。兩個實體要想通信,必須有“同一種語言”,而且,對於通信內容,怎樣通信和何時通信,都必須遵守一定的規定,這些規定就是協議。亦可簡單地定義為:控制兩實體間數據交換的一套規則。在電子通訊連接中,各個不同的層次都有自己的協議。
協議僅僅是一種規範,實現由計算機軟件來完成,常見的協議TCP/IP協議:是目前世界上應用最為廣泛的協議,TCP/IP協議是傳輸層的協議,HTTP超文本傳輸協議,FTP文件傳輸協議,SMTP簡單郵件傳送協議,Telnet遠程登錄服務是應用層協議。
TCP/IP、Http、Socket的區別
網絡由下往上分為:物理層、數據鏈路層、網絡層、傳輸層、會話層、表示層和應用層。
IP協議對應於網絡層,TCP協議對應於傳輸層,而HTTP協議對應於應用層,三者從本質上來說沒有可比性,socket則是對TCP/IP協議的封裝和應用(程序員層面上)。也可以說,TPC/IP協議是傳輸層協議,主要解決數據如何在網絡中傳輸,而HTTP是應用層協議,主要解決如何包裝數據。
簡單的來說,當應用程序產生數據需要傳輸時,首先使用HTTP協議封裝成文本信息,然後使用TCP/IP協議進行網絡上傳輸,而Socket則是對TCP/IP協議的封裝,Socket本身並不是協議,而是一個調用接口(API)。通過Socket,我們才能使用TCP/IP協議。從而形成了一些最基本的函數接口,比如create、listen、connect、accept、send、read和write等等。
TCP是什麽?
具體的關於TCP是什麽,我不打算詳細的說了;當你看到這篇文章時,我想你也知道TCP的概念了,想要更深入的了解TCP的工作,我們就繼續。它只是一個超級麻煩的協議,而它又是互聯網的基礎,也是每個程序員必備的基本功。首先來看看OSI的七層模型:
我們需要知道TCP工作在網絡OSI的七層模型中的第四層——Transport層,IP在第三層——Network層,ARP在第二層——Data Link層;在第二層上的數據,我們把它叫Frame,在第三層上的數據叫Packet,第四層的數據叫Segment。 同時,我們需要簡單的知道,數據從應用層發下來,會在每一層都會加上頭部信息,進行封裝,然後再發送到數據接收端。這個基本的流程你需要知道,就是每個數據都會經過數據的封裝和解封裝的過程。 在OSI七層模型中,每一層的作用和對應的協議如下:
TCP是一個協議,那這個協議是如何定義的,它的數據格式是什麽樣子的呢?要進行更深層次的剖析,就需要了解,甚至是熟記TCP協議中每個字段的含義。
上面就是TCP協議頭部的格式,由於它太重要了,是理解其它內容的基礎,下面就將每個字段的信息都詳細的說明一下。
- Source Port和Destination Port:分別占用16位,表示源端口號和目的端口號;用於區別主機中的不同進程,而IP地址是用來區分不同的主機的,源端口號和目的端口號配合上IP首部中的源IP地址和目的IP地址就能唯一的確定一個TCP連接;
- Sequence Number:用來標識從TCP發端向TCP收端發送的數據字節流,它表示在這個報文段中的的第一個數據字節在數據流中的序號;主要用來解決網絡報亂序的問題;
- Acknowledgment Number:32位確認序列號包含發送確認的一端所期望收到的下一個序號,因此,確認序號應當是上次已成功收到數據字節序號加1。不過,只有當標誌位中的ACK標誌(下面介紹)為1時該確認序列號的字段才有效。主要用來解決不丟包的問題;
- Offset:給出首部中32 bit字的數目,需要這個值是因為任選字段的長度是可變的。這個字段占4bit(最多能表示15個32bit的的字,即4*15=60個字節的首部長度),因此TCP最多有60字節的首部。然而,沒有任選字段,正常的長度是20字節;
TCP Flags:TCP首部中有6個標誌比特,它們中的多個可同時被設置為1,主要是用於操控TCP的狀態機的,依次為URG,ACK,PSH,RST,SYN,FIN。每個標誌位的意思如下:
- URG:此標誌表示TCP包的緊急指針域(後面馬上就要說到)有效,用來保證TCP連接不被中斷,並且督促中間層設備要盡快處理這些數據;
- ACK:此標誌表示應答域有效,就是說前面所說的TCP應答號將會包含在TCP數據包中;有兩個取值:0和1,為1的時候表示應答域有效,反之為0;
- PSH:這個標誌位表示Push操作。所謂Push操作就是指在數據包到達接收端以後,立即傳送給應用程序,而不是在緩沖區中排隊;
- RST:這個標誌表示連接復位請求。用來復位那些產生錯誤的連接,也被用來拒絕錯誤和非法的數據包;
- SYN:表示同步序號,用來建立連接。SYN標誌位和ACK標誌位搭配使用,當連接請求的時候,SYN=1,ACK=0;連接被響應的時候,SYN=1,ACK=1;這個標誌的數據包經常被用來進行端口掃描。掃描者發送一個只有SYN的數據包,如果對方主機響應了一個數據包回來 ,就表明這臺主機存在這個端口;但是由於這種掃描方式只是進行TCP三次握手的第一次握手,因此這種掃描的成功表示被掃描的機器不很安全,一臺安全的主機將會強制要求一個連接嚴格的進行TCP的三次握手;
- FIN: 表示發送端已經達到數據末尾,也就是說雙方的數據傳送完成,沒有數據可以傳送了,發送FIN標誌位的TCP數據包後,連接將被斷開。這個標誌的數據包也經常被用於進行端口掃描。
Window:窗口大小,也就是有名的滑動窗口,用來進行流量控制;這是一個復雜的問題,這篇博文中並不會進行總結的;
TCP連接和斷開
三次握手
第一次握手:建立連接。客戶端發送連接請求報文段,將SYN位置為1,Sequence Number為x;然後,客戶端進入SYN_SEND狀態,等待服務器的確認;
第二次握手:服務器收到SYN報文段。服務器收到客戶端的SYN報文段,需要對這個SYN報文段進行確認,設置Acknowledgment Number為x+1(Sequence Number+1);同時,自己自己還要發送SYN請求信息,將SYN位置為1,Sequence Number為y;服務器端將上述所有信息放到一個報文段(即SYN+ACK報文段)中,一並發送給客戶端,此時服務器進入SYN_RECV狀態;
第三次握手:客戶端收到服務器的SYN+ACK報文段。然後將Acknowledgment Number設置為y+1,向服務器發送ACK報文段,這個報文段發送完畢以後,客戶端和服務器端都進入ESTABLISHED狀態,完成TCP三次握手。
既然總結了TCP的三次握手,那為什麽非要三次呢?怎麽覺得兩次就可以完成了。那TCP為什麽非要進行三次連接呢?在謝希仁的《計算機網絡》中是這樣說的:
為了防止已失效的連接請求報文段突然又傳送到了服務端,因而產生錯誤。
在書中同時舉了一個例子,如下:
“已失效的連接請求報文段”的產生在這樣一種情況下:client發出的第一個連接請求報文段並沒有丟失,而是在某個網絡結點長時間的滯留了,以致延誤到連接釋放以後的某個時間才到達server。本來這是一個早已失效的報文段。但server收到此失效的連接請求報文段後,就誤認為是client再次發出的一個新的連接請求。於是就向client發出確認報文段,同意建立連接。假設不采用“三次握手”,那麽只要server發出確認,新的連接就建立了。由於現在client並沒有發出建立連接的請求,因此不會理睬server的確認,也不會向server發送數據。但server卻以為新的運輸連接已經建立,並一直等待client發來數據。這樣,server的很多資源就白白浪費掉了。采用“三次握手”的辦法可以防止上述現象發生。例如剛才那種情況,client不會向server的確認發出確認。server由於收不到確認,就知道client並沒有要求建立連接。”
四次分手
第一次分手:主機1(可以使客戶端,也可以是服務器端),設置Sequence Number和Acknowledgment Number,向主機2發送一個FIN報文段;此時,主機1進入FIN_WAIT_1狀態;這表示主機1沒有數據要發送給主機2了;
第二次分手:主機2收到了主機1發送的FIN報文段,向主機1回一個ACK報文段,Acknowledgment Number為Sequence Number加1;主機1進入FIN_WAIT_2狀態;主機2告訴主機1,我“同意”你的關閉請求;
第三次分手:主機2向主機1發送FIN報文段,請求關閉連接,同時主機2進入LAST_ACK狀態;
第四次分手:主機1收到主機2發送的FIN報文段,向主機2發送ACK報文段,然後主機1進入TIME_WAIT狀態;主機2收到主機1的ACK報文段以後,就關閉連接;此時,主機1等待2MSL後依然沒有收到回復,則證明Server端已正常關閉,那好,主機1也可以關閉連接了。
那四次分手又是為何呢?TCP協議是一種面向連接的、可靠的、基於字節流的運輸層通信協議。TCP是全雙工模式,這就意味著,當主機1發出FIN報文段時,只是表示主機1已經沒有數據要發送了,主機1告訴主機2,它的數據已經全部發送完畢了;但是,這個時候主機1還是可以接受來自主機2的數據;當主機2返回ACK報文段時,表示它已經知道主機1沒有數據發送了,但是主機2還是可以發送數據到主機1的;當主機2也發送了FIN報文段時,這個時候就表示主機2也沒有數據要發送了,就會告訴主機1,我也沒有數據要發送了,之後彼此就會愉快的中斷這次TCP連接。
出處
socket 函數
套接字是通信端點的抽象,實現端對端之間的通信。與應用程序要使用文件描述符訪問文件一樣,訪問套接字需要套接字描述符。任何套接字編程都必須調用socket 函數獲得套接字描述符,這樣才能對套接字進行操作。以下是該函數的描述:
/* 套接字 */ /* * 函數功能:創建套接字描述符; * 返回值:若成功則返回套接字非負描述符,若出錯返回-1; * 函數原型: */ #include <sys/socket.h> int socket(int family, int type, int protocol); /* * 說明: * socket類似與open對普通文件操作一樣,都是返回描述符,後續的操作都是基於該描述符; * family 表示套接字的通信域,不同的取值決定了socket的地址類型,其一般取值如下: * (1)AF_INET IPv4因特網域 * (2)AF_INET6 IPv6因特網域 * (3)AF_UNIX Unix域 * (4)AF_ROUTE 路由套接字 * (5)AF_KEY 密鑰套接字 * (6)AF_UNSPEC 未指定 * * type確定socket的類型,常用類型如下: * (1)SOCK_STREAM 有序、可靠、雙向的面向連接字節流套接字 * (2)SOCK_DGRAM 長度固定的、無連接的不可靠數據報套接字 * (3)SOCK_RAW 原始套接字 * (4)SOCK_SEQPACKET 長度固定、有序、可靠的面向連接的有序分組套接字 * * protocol指定協議,常用取值如下: * (1)0 選擇type類型對應的默認協議 * (2)IPPROTO_TCP TCP傳輸協議 * (3)IPPROTO_UDP UDP傳輸協議 * (4)IPPROTO_SCTP SCTP傳輸協議 * (5)IPPROTO_TIPC TIPC傳輸協議 * */
connect 函數
在處理面向連接的網絡服務時,例如 TCP ,交換數據之前必須在請求的進程套接字和提供服務的進程套接字之間建立連接。TCP 客戶端可以調用函數connect 來建立與 TCP 服務器端的一個連接。該函數的描述如下:
/* * 函數功能:建立連接,即客戶端使用該函數來建立與服務器的連接; * 返回值:若成功則返回0,出錯則返回-1; * 函數原型: */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); /* * 說明: * sockfd是系統調用的套接字描述符,即由socket函數返回的套接字描述符; * servaddr是目的套接字的地址,該套接字地址結構必須包含目的IP地址和目的端口號,即想與之通信的服務器地址; * addrlen是目的套接字地址的大小; * * 如果sockfd沒有綁定到一個地址,connect會給調用者綁定一個默認地址,即內核會確定源IP地址,並選擇一個臨時端口號作為源端口號; */
TCP 客戶端在調用函數 connect 前不必非得調用 bind 函數,因為內核會確定源 IP 地址,並選擇一個臨時端口作為源端口號。若 TCP 套接字調用connect 函數將建立 TCP 連接(執行三次握手),而且僅在連接建立成功或出錯時才返回,其中出錯返回可能有以下幾種情況:
若 TCP 客戶端沒有收到 SYN 報文段的響應,則返回 ETIMEOUT 錯誤;
若客戶端的 SYN 報文段的響應是 RST (表示復位),則表明該服務器主機在我們指定的端口上沒有進程在等待與之連接。只是一種硬錯誤,客戶端一接收到 RST 就立即返回ECONNERFUSED 錯誤; RST 是 TCP 在發生錯誤時發送的一種 TCP 報文段。產生 RST 的三個條件時:
- 目的地為某端口的 SYN 到達,然而該端口上沒有正在監聽的服務器;
- TCP 想取消一個已有連接;
- TCP 接收到一個不存在的連接上的報文段;
- 若客戶端發出的 SYN 在中某個路由器上引發一個目的地不可達的 ICMP 錯誤,這是一個軟錯誤。客戶端主機內核保存該消息,並在一定的時間間隔繼續發送 SYN (即重發)。在某規定的時間後仍未收到響應,則把保存的消息(即 ICMP 錯誤)作為EHOSTUNREACH 或ENETUNREACH 錯誤返回給進行。
bind 函數
調用函數 socket 創建套接字描述符時,該套接字描述符是存儲在它的協議族空間中,沒有具體的地址,要使它與一個地址相關聯,可以調用函數 bind 使其與地址綁定。客戶端的套接字關聯的地址一般可由系統默認分配,因此不需要指定具體的地址。若要為服務器端套接字綁定地址,可以通過調用函數 bind 將套接字綁定到一個地址。下面是該函數的描述:
/* 套接字的基本操作 */ /* * 函數功能:將協議地址綁定到一個套接字;其中協議地址包含IP地址和端口號; * 返回值:若成功則返回0,若出錯則返回-1; * 函數原型: */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); /* * 說明: * sockfd 為套接字描述符; * addr是一個指向特定協議地址結構的指針; * addrlen是地址結構的長度; */
對於 TCP 協議,調用 bind 函數可以指定一個端口號,或指定一個 IP 地址,也可以兩者都指定,還可以都不指定。若 TCP 客戶端或服務器端不調用bind 函數綁定一個端口號,當調用connect 或 listen 函數時,內核會為相應的套接字選擇一個臨時端口號。一般 TCP 客戶端使用內核為其選擇一個臨時的端口號,而服務器端通過調用bind 函數將端口號與相應的套接字綁定。進程可以把一個特定的 IP 地址捆綁到它的套接字上,但是這個 IP 地址必須屬於其所在主機的網絡接口之一。對於 TCP 客戶端,這就為在套接字上發送的 IP 數據報指派了源 IP 地址。對於 TCP 服務器端,這就限定該套接字只接收那些目的地為這個 IP 地址的客戶端連接。TCP 客戶端一般不把 IP 地址捆綁到它的套接字上。當連接套接字時,內核將根據所用外出網絡接口來選擇源 IP 地址,而所用外出接口則取決於到達服務器端所需的路徑。若 TCP 服務器端沒有把 IP 地址捆綁到它的套接字上,內核就把客戶端發送的 SYN 的目的 IP 地址作為服務器端的源 IP 地址。
在地址使用方面有下面一些限制:
- 在進程所運行的機器上,指定的地址必須有效,不能指定其他機器的地址;
- 地址必須和創建套接字時的地址族所支持的格式相匹配;
- 端口號必須不小於1024,除非該進程具有相應的特權(超級用戶);
- 一般只有套接字端點能夠與地址綁定,盡管有些協議允許多重綁定;
listen 函數
在編寫服務器程序時需要使用監聽函數 listen 。服務器進程不知道要與誰連接,因此,它不會主動地要求與某個進程連接,只是一直監聽是否有其他客戶進程與之連接,然後響應該連接請求,並對它做出處理,一個服務進程可以同時處理多個客戶進程的連接。listen 函數描述如下:
/* * 函數功能:接收連接請求; * 函數原型: */ #include <sys/socket.h> int listen(int sockfd, int backlog);//若成功則返回0,若出錯則返回-1; /* * sockfd是套接字描述符; * backlog是該進程所要入隊請求的最大請求數量; */
listen 函數僅由 TCP 服務器調用,它有以下兩種作用:
- 當 socket 函數創建一個套接字時,若它被假設為一個主動套接字,即它是一個將調用connect 發起連接的客戶端套接字。listen 函數把一個未連接的套接字轉換成一個被動套接字,指示內核應該接受指向該套接字的連接請求;
- listen 函數的第二個參數規定內核應該為相應套接字排隊的最大連接個數;
listen 函數一般應該在調用socket 和bind 這兩個函數之後,並在調用accept 函數之前調用。 內核為任何一個給定監聽套接字維護兩個隊列:
- 未完成連接隊列,每個這樣的 SYN 報文段對應其中一項:已由某個客戶端發出並到達服務器,而服務器正在等待完成相應的 TCP 三次握手過程。這些套接字處於 SYN_REVD 狀態;
- 已完成連接隊列,每個已完成 TCP 三次握手過程的客戶端對應其中一項。這些套接字處於 ESTABLISHED 狀態;
accept 函數
accept 函數由 TCP 服務器調用,用於從已完成連接隊列隊頭返回下一個已完成連接。如果已完成連接隊列為空,那麽進程被投入睡眠。該函數的返回值是一個新的套接字描述符,返回值是表示已連接的套接字描述符,而第一個參數是服務器監聽套接字描述符。一個服務器通常僅僅創建一個監聽套接字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建一個已連接套接字(表示 TCP 三次握手已完成),當服務器完成對某個給定客戶的服務時,相應的已連接套接字就會被關閉。該函數描述如下:
/* 函數功能:從已完成連接隊列隊頭返回下一個已完成連接;若已完成連接隊列為空,則進程進入睡眠; * 函數原型: */ int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);//返回值:若成功返回套接字描述符,出錯返回-1; /* * 說明: * 參數 cliaddr 和 addrlen 用來返回已連接的對端(客戶端)的協議地址; * * 該函數返回套接字描述符,該描述符連接到調用connect函數的客戶端; * 這個新的套接字描述符和原始的套接字描述符sockfd具有相同的套接字類型和地址族,而傳給accept函數的套接字描述符sockfd沒有關聯到這個鏈接, * 而是繼續保持可用狀態並接受其他連接請求; * 若不關心客戶端協議地址,可將cliaddr和addrlen參數設置為NULL,否則,在調用accept之前,應將參數cliaddr設為足夠大的緩沖區來存放地址, * 並且將addrlen設為指向代表這個緩沖區大小的整數指針; * accept函數返回時,會在緩沖區填充客戶端的地址並更新addrlen所指向的整數為該地址的實際大小; * * 若沒有連接請求等待處理,accept會阻塞直到一個請求到來; */
fork 和 exec 函數
/* 函數功能:創建子進程; * 返回值: * (1)在子進程中,返回0; * (2)在父進程中,返回新創建子進程的進程ID; * (3)若出錯,則範回-1; * 函數原型: */ #include <unistd.h> pid_t fork(void); /* 說明: * 該函數調用一次若成功則返回兩個值: * 在調用進程(即父進程)中,返回新創建進程(即子進程)的進程ID; * 在子進程返回值是0; * 因此,可以根據返回值判斷進程是子進程還是父進程; */ /* exec 序列函數 */ /* * 函數功能:把當前進程替換為一個新的進程,新進程與原進程ID相同; * 返回值:若出錯則返回-1,若成功則不返回; * 函數原型: */ #include <unistd.h> int execl(const char *pathname, const char *arg, ...); int execv(const char *pathnam, char *const argv[]); int execle(const char *pathname, const char *arg, ... , char *const envp[]); int execve(const char *pathnam, char *const argv[], char *const envp[]); int execlp(const char *filename, const char *arg, ...); int execvp(const char *filename, char *const argv[]); /* 6 個函數的區別如下: * (1)待執行的程序文件是 文件名 還是由 路徑名 指定; * (2)新程序的參數是 一一列出 還是由一個 指針數組 來引用; * (3)把調用進程的環境傳遞給新程序 還是 給新程序指定新的環境; */
exec 6個函數在函數名和使用語法的規則上都有細微的區別,下面就從可執行文件查找方式、參數傳遞方式及環境變量這幾個方面進行比較。
- 查找方式:前4個函數的查找方式都是完整的文件目錄路徑 pathname ,而最後兩個函數(也就是以p結尾的兩個函數)可以只給出文件名 filename,系統就會自動按照環境變量 “$PATH” 所指定的路徑進行查找。
- 參數傳遞方式:exec 序列函數的參數傳遞有兩種方式:一種是逐個列舉的方式,而另一種則是將所有參數整體構造指針數組傳遞。在這裏是以函數名的第5位字母來區分的,字母為 “l”(list)的表示逐個列舉參數的方式,其語法為 const char *arg;字母為 “v”(vertor)的表示將所有參數整體構造指針數組傳遞,其語法為 char *const argv[]。讀者可以觀察 execl()、execle()、execlp() 的語法與 execv()、execve()、execvp() 的區別。這裏的參數實際上就是用戶在使用這個可執行文件時所需的全部命令選項字符串(包括該可執行程序命令本身)。要註意的是,這些參數必須以NULL結束。
- 環境變量:exec 序列函數可以默認系統的環境變量,也可以傳入指定的環境變量。這裏以 “e”(environment)結尾的兩個函數 execle() 和 execve() 就可以在 envp[] 中指定當前進程所使用的環境變量。
並發服務器
當要求一個服務器同時為多個客戶服務時,需要並發服務器。TCP 並發服務器,它們為每個待處理的客戶端連接調用 fork 函數派生一個子進程。當一個連接建立時,accept 返回,服務器接著調用 fork 函數,然後由子進程服務客戶端,父進程則等待另一個連接,此時,父進程必須關閉已連接套接字。
close 和 shutdown 函數
當要關閉套接字時,可使用 close 和 shutdown 函數,其描述如下:
/* 函數功能:關閉套接字,若是在 TCP 協議中,並終止 TCP 連接; * 返回值:若成功則返回0,若出錯則返回-1; * 函數原型: */ #include <unistd.h> int close(int sockfd); /* * 函數功能:關閉套接字上的輸入或輸出; * 返回值:若成功則返回0,若出錯返回-1; * 函數原型: */ #include <sys/socket.h> int shutdown(int sockfd, int how); /* * 說明: * sockfd表示待操作的套接字描述符; * how表示具體操作,取值如下: * (1)SHUT_RD 關閉讀端,即不能接收數據 * (2)SHUT_WR 關閉寫端,即不能發送數據 * (3)SHUT_RDWR 關閉讀、寫端,即不能發送和接收數據 * */
getsockname 和 getpeername 函數
為了獲取已綁定到套接字的地址,我們可以調用函數 getsockname 來實現:
/* * 函數功能:獲取已綁定到一個套接字的地址; * 返回值:若成功則返回0,若出錯則返回-1; * 函數原型: */ #include <sys/socket.h> int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp); /* * 說明: * 調用該函數之前,設置alenp為一個指向整數的指針,該整數指定緩沖區sockaddr的大小; * 返回時,該整數會被設置成返回地址的大小,如果該地址和提供的緩沖區長度不匹配,則將其截斷而不報錯; */ /* * 函數功能:獲取套接字對方連接的地址; * 返回值:若成功則返回0,若出錯則返回-1; * 函數原型: */ #include <sys/socket.h> int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp); /* * 說明: * 該函數除了返回對方的地址之外,其他功能和getsockname一樣; */
出處
測試實例
客戶端
#include <iostream> #include <cstdio> #include <Winsock2.h> using namespace std; #pragma comment(lib, "ws2_32.lib") #define xPort 8000 #define xIP "127.0.0.1" int main() { const int xBUF_SIZE = 64; WSADATA wsd; //WSADATA變量 SOCKET xServer; //服務器套接字 SOCKET xClient; //客戶端套接字 SOCKADDR_IN xaddrServ; //服務器地址 char recBuf[xBUF_SIZE]; //接受數據緩沖區 char sendBuf[xBUF_SIZE]; //返回數據緩沖區 int retVal; //返回值 //初始化套結字動態庫,請求2.2版本winsock if (WSAStartup(MAKEWORD(2,2), &wsd) != 0) { cout << "WSAStartup failed!" << endl; return 1; } /* //創建socket操作 // SOCKET socket(int af, int type, int protocol); // 第一個參數,指定地址簇(TCP/IP只能是AF_INET,也可寫成PF_INET) // 第二個,選擇套接字的類型(流式套接字),第三個,特定地址家族相關協議(0為自動) */ //創建套接字 xClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(INVALID_SOCKET == xClient) { cout << "socket failed!" << endl; WSACleanup();//釋放套接字資源; return -1; } //服務器套接字地址 xaddrServ.sin_family = AF_INET; xaddrServ.sin_port = htons(xPort); xaddrServ.sin_addr.s_addr = inet_addr(xIP); /* // 將套接字xClient與遠程主機相連 // int connect( SOCKET s, const struct sockaddr* name, int namelen); // 第一個參數:需要進行連接操作的套接字 // 第二個參數:設定所需要連接的地址信息 // 第三個參數:地址的長度 */ //連接服務器 retVal = connect(xClient, (LPSOCKADDR)&xaddrServ, sizeof(xaddrServ)); if(SOCKET_ERROR == retVal) { cout << "connect failed!" << endl; closesocket(xClient); //關閉套接字 WSACleanup(); //釋放套接字資源 return -1; } ///三次握手完成 // 客戶端與用戶端進行通信 /* // send(), 在套接字上發送數據 // int send( SOCKET s, const char* buf, int len, int flags); // 第一個參數,需要發送信息的套接字, // 第二個參數,包含了需要被傳送的數據, // 第三個參數是buffer的數據長度, // 第四個參數,一些傳送參數的設置 // recv(), 在套接字上接收數據 // int recv( SOCKET s, char* buf, int len, int flags); // 第一個參數,建立連接後的套接字, // 第二個參數,接收數據 // 第三個參數,接收數據的長度, // 第四個參數,一些傳送參數的設置 */ cout << "*************************客戶端**********************"<< endl; while(true) { memset(sendBuf, '/0', xBUF_SIZE); cout << "向服務器發送數據:"; cin >> sendBuf; retVal = send(xClient, sendBuf, xBUF_SIZE, 0); if (SOCKET_ERROR == retVal) { cout << "send failed!" << endl; closesocket(xClient); //關閉套接字 WSACleanup(); //釋放套接字資源; return -1; } if(sendBuf[0] == '0') break; memset(recBuf, '/0', sizeof(recBuf)); recv(xClient, recBuf, xBUF_SIZE , 0); // 接收服務器端的數據, 只接收5個字符 cout <<"從服務器接收數據:"<< recBuf << endl; cout<<endl; } //退出 closesocket(xClient); //關閉套接字 WSACleanup(); //釋放套接字資源; return 0; }
服務器
#include <iostream> #include <cstdio> #include <Winsock2.h> #include <cstring> using namespace std; #pragma comment(lib, "ws2_32.lib") #define xPort 8000 #define xIP "127.0.0.1" int main() { const int xBUF_SIZE = 64; WSADATA wsd; //WSADATA變量 SOCKET xServer; //服務器套接字 SOCKET xClient; //客戶端套接字 SOCKADDR_IN xaddrServ; //服務器地址 char recBuf[xBUF_SIZE]; //接受數據緩沖區 char sendBuf[xBUF_SIZE]; //返回數據緩沖區 int retVal; //返回值 //初始化套結字動態庫,請求2.2版本winsock if (WSAStartup(MAKEWORD(2,2), &wsd) != 0) { cout << "WSAStartup failed!" << endl; return 1; } /* //創建socket操作 // SOCKET socket(int af, int type, int protocol); // 第一個參數,指定地址簇(TCP/IP只能是AF_INET,也可寫成PF_INET) // 第二個,選擇套接字的類型(流式套接字),第三個,特定地址家族相關協議(0為自動) */ //創建套接字 xServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(INVALID_SOCKET == xServer) { cout << "socket failed!" << endl; WSACleanup();//釋放套接字資源; return -1; } //服務器套接字地址 xaddrServ.sin_family = AF_INET; xaddrServ.sin_port = htons(xPort); xaddrServ.sin_addr.s_addr = INADDR_ANY; /* //套接字xServer與本地地址相連 // int bind(SOCKET s, const struct sockaddr* name, int namelen); // 第一個參數,指定需要綁定的套接字; // 第二個參數,指定該套接字的本地地址信息,該地址結構會隨所用的網絡協議的不同而不同 // 第三個參數,指定該網絡協議地址的長度 */ //綁定套接字 retVal = bind(xServer, (SOCKADDR *)&xaddrServ, sizeof(SOCKADDR_IN)); if(SOCKET_ERROR == retVal) { cout << "bind failed!" << endl; closesocket(xServer); //關閉套接字 WSACleanup(); //釋放套接字資源; return -1; } /* // 將套接字設置為監聽模式(連接請求), listen()通知TCP服務器準備好接收連接 // int listen(SOCKET s, int backlog); // 第一個參數指定需要設置的套接字,第二個參數為(等待連接隊列的最大長度) */ //開始監聽 retVal = listen(xServer, 1); if(SOCKET_ERROR == retVal) { cout << "listen failed!" << endl; closesocket(xServer); //關閉套接字 WSACleanup(); //釋放套接字資源; return -1; } /* // accept(),接收連接,等待客戶端連接 // SOCKET accept( SOCKET s, struct sockaddr* addr, int* addrlen); // 第一個參數,接收一個處於監聽狀態下的套接字 // 第二個參數,sockaddr用於保存客戶端地址的信息 // 第三個參數,用於指定這個地址的長度 // 返回的是向與這個監聽狀態下的套接字通信的套接字 */ //接受客戶端請求 sockaddr_in addrClient; int addrClientLen = sizeof(addrClient); xClient = accept(xServer, (sockaddr FAR*)&addrClient, &addrClientLen); if(INVALID_SOCKET == xClient) { cout << "accept failed!" << endl; closesocket(xServer); //關閉套接字 WSACleanup(); //釋放套接字資源; return -1; } ///三次握手完成 // 客戶端與用戶端進行通信 /* // send(), 在套接字上發送數據 // int send( SOCKET s, const char* buf, int len, int flags); // 第一個參數,需要發送信息的套接字, // 第二個參數,包含了需要被傳送的數據, // 第三個參數是buffer的數據長度, // 第四個參數,一些傳送參數的設置 // recv(), 在套接字上接收數據 // int recv( SOCKET s, char* buf, int len, int flags); // 第一個參數,建立連接後的套接字, // 第二個參數,接收數據 // 第三個參數,接收數據的長度, // 第四個參數,一些傳送參數的設置 */ cout << "*************************服務器**********************"<< endl; while(true) { memset(recBuf, '/0', xBUF_SIZE); retVal = recv(xClient, recBuf, xBUF_SIZE, 0); if (SOCKET_ERROR == retVal) { cout << "recv failed!" << endl; closesocket(xServer); //關閉套接字 closesocket(xClient); //關閉套接字 WSACleanup(); //釋放套接字資源; return -1; } if(recBuf[0] == '0') break; cout << "客戶端發送的數據: " << recBuf <<endl; memset(sendBuf, '/0', xBUF_SIZE); cout << "向客戶端發送數據: " ; cin >> sendBuf; cout << endl; send(xClient, sendBuf, xBUF_SIZE, 0); } //退出 closesocket(xServer); //關閉套接字 closesocket(xClient); //關閉套接字 WSACleanup(); //釋放套接字資源; return 0; }
Tags: Internet 個人電腦 網絡服務 Socket IP協議
文章來源: