1. 程式人生 > >C++ Socket 學習筆記

C++ Socket 學習筆記

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連線和斷開

這裡寫圖片描述

三次握手

  1. 第一次握手:建立連線。客戶端傳送連線請求報文段,將SYN位置為1,Sequence Number為x;然後,客戶端進入SYN_SEND狀態,等待伺服器的確認;

  2. 第二次握手:伺服器收到SYN報文段。伺服器收到客戶端的SYN報文段,需要對這個SYN報文段進行確認,設定Acknowledgment Number為x+1(Sequence Number+1);同時,自己自己還要傳送SYN請求資訊,將SYN位置為1,Sequence Number為y;伺服器端將上述所有資訊放到一個報文段(即SYN+ACK報文段)中,一併傳送給客戶端,此時伺服器進入SYN_RECV狀態;

  3. 第三次握手:客戶端收到伺服器的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. 第一次分手:主機1(可以使客戶端,也可以是伺服器端),設定Sequence Number和Acknowledgment Number,向主機2傳送一個FIN報文段;此時,主機1進入FIN_WAIT_1狀態;這表示主機1沒有資料要傳送給主機2了;

  2. 第二次分手:主機2收到了主機1傳送的FIN報文段,向主機1回一個ACK報文段,Acknowledgment Number為Sequence Number加1;主機1進入FIN_WAIT_2狀態;主機2告訴主機1,我“同意”你的關閉請求;

  3. 第三次分手:主機2向主機1傳送FIN報文段,請求關閉連線,同時主機2進入LAST_ACK狀態;

  4. 第四次分手:主機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;
}