首頁  >  文章  >  php教程  >  Linux的SOCKET編程詳解

Linux的SOCKET編程詳解

高洛峰
高洛峰原創
2016-12-13 10:30:181524瀏覽

1. 網路中進程之間如何通訊

進 程通訊的概念最初來自單機系統。由於每個進程都在自己的位址範圍內運行,為確保兩個相互通信的進

程之間既互不干擾又協調一致工作,操作系統為進程通信提供了相應設施,如

UNIX BSD有:管道(pipe)、命名管道(named pipe)軟中斷信號(signal)

UNIX system V有:消息(message)、共享存儲區(shared memory)和信號量(semaphore)等.

他們都僅限於用在本機進程之間通訊。網路進程通訊要解決的是不同主機進程間的相互通訊問題(可把同機進程通訊看成是其中的特例)。為此,首先要解決的是網間進程標識問題。同一主機上,不同進程可用進程號(process ID)唯一識別。但在網路環境下,各主機獨立分配的進程號不能唯一標識該進程。例如,主機A賦於某進程號5,在B機中也可以存在5號進程,因此,「5號進程」這句話就沒有意義了。 其次,作業系統支援的網路協定眾多,不同協定的工作方式不同,位址格式也不同。因此,網間進程通訊還要解決多重協定的辨識問題。

其實TCP/IP協定族已經幫我們解決了這個問題,網路層的「ip位址」可以唯一標識網路中的主機,而傳輸層的「協定+連接埠」可以唯一標識主機中的應用程式(進程)。這樣利用三元組(ip位址,協議,連接埠)就可以標識網路的進程了,網路中的進程通訊就可以利用這個標誌與其它進程進行互動。

使用TCP/IP協定的應用程式通常採用應用程式介面:UNIX  BSD的套接字(socket)和UNIX System V的TLI(已經被淘汰),來實現網路進程之間的通訊。就目前而言,幾乎所有的應用程式都是採用socket,而現在又是網路時代,網路中進程通訊是無所不在,這就是我為什麼說「一切皆socket」。


2. 什麼是TCP/IP、UDP

     TCP/IP(Transmission Control

     TCP/IP(Transmission Control Protocol/Internet 它是為廣域網路(WANs)設計的。

     TCP/IP協定存在於OS中,網路服務透過OS提供,在OS中增加支援TCP/IP的系統呼叫-Berkeley套接字,如Socket,Connect,Send,Recv等

    UDP(User Data Protocol,用戶資料報協議)是與TCP相對應的協定。它是屬於TCP/IP協定族中的一種。如圖:

Linux的SOCKET編程詳解

      TCP/IP協定族包含運輸層、網路層、連結層,而socket所在位置如圖,Socket是應用層與TCP/IP協定族通訊的中間軟體抽象層。

Linux的SOCKET編程詳解

3. Socket是什麼

1、 socket套接字:


   open –> 讀寫write/read –> 關閉close”模式來操作。 Socket就是這個模式的一個實現,        socket即是一種特殊的文件,一些socket函數就是對其進行的操作(讀/寫IO、打開、關閉).

     說白了Socket是應用層與TCP/IP協議

     說白了Socket是應用層與TCP/IP協定協議通訊的中間軟體抽象層,它是一組介面。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協定族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織數據,以符合指定的協定.

       注意:其實socket也沒有層的概念,它只是一個facade設計模式的應用,讓程式設計變的更簡單。是一個軟體抽象層。在網路程式設計中,我們大量使用的都是透過socket實現的。

2、套接字描述詞

🎜🎜

          其實就是整數,我們最熟悉的句柄是0、1、2三個,0是標準輸入,1是標準輸出,2是標準誤差輸出。 0、1、2是整數表示的,對應的FILE *結構的表示就是stdin、stdout、stderr


套接字API最初是作為UNIX操作系統的一部分而開發的,所以套接字API與系統的其他I/O設備整合在一起。特別是,當應用程式要為因特網通訊而建立一個套接字(socket)時,作業系統就會傳回一個小整數作為描述符(descriptor)來識別這個套接字。然後,應用程式以該描述符作為傳遞參數,透過呼叫函數來完成某種操作(例如透過網路傳送資料或接收輸入的資料)。

在許多作業系統中,套接字描述符和其他I/O描述符是整合在一起的,所以應用程式可以對檔案進行套接字I/O或I/O讀取/寫入操作。

當應用程式要建立一個套接字時,作業系統就會回傳一個小整數作為描述符,應用程式則使用這個描述符來引用該套接字需要I/O請求的應用程式請求作業系統開啟一個文件。作業系統就建立一個檔案描述符提供給應用程式存取檔案。從應用程式的角度來看,檔案描述符是一個整數,應用程式可以用它來讀寫檔案。下圖顯示,作業系統如何把檔案描述符實作為一個指標數組,這些指標指向內部資料結構。

Linux的SOCKET編程詳解

  對於每個程式系統都有一張單獨的表格。精確地講,系統為每個運行的進程維護一張單獨的檔案描述符表。當進程開啟一個檔案時,系統會將一個指向此檔案內部資料結構的指標寫入檔案描述符表,並將該表的索引值傳回給呼叫者 。應用程式只需記住這個描述符,並在以後操作該檔案時使用它。作業系統把該描述符作為索引存取進程描述符表,透過指標找到保存該檔案所有的資訊的資料結構。

      針對套接字的系統資料結構:

   1)、套接字API裡面有個函數socket,它就是用來創造一個套接字。套接字設計的總體想法是,單一系統呼叫就可以創建任何套接字,因為套接字是相當籠統的。一旦套接字創建後,應用程式還需要呼叫其他函數來指定具體細節。例如呼叫socket將創建一個新的描述符條目:

Linux的SOCKET編程詳解

   2)、雖然套接字的內部資料結構包含很多字段,但是系統創建套接字後,大多數字字段沒有填寫。應用程式建立套接字後在該套接字可以使用之前,必須呼叫其他的過程來填入這些欄位。

3、檔案描述符和檔案指標的區別:

檔案描述符:在linux系統中開啟檔案就會獲得檔案描述符,它是個很小的正整數。每個進程在PCB(Process Control Block)中保存著一份檔案描述符表,而檔案描述符就是這個表的索引,每個表項都有一個指向已開啟檔案的指標。

檔案指標:C語言中使用檔案指標做為I/O的句柄。檔案指標指向進程使用者區中的一個稱為FILE結構的資料結構。 FILE結構包括一個緩衝區和一個檔案描述符。而文件描述符是文件描述符表的索引,因此從某種意義上說文件指標就是句柄的句柄(在Windows系統上,文件描述符被稱作文件句柄)。

4. 基本的SOCKET介面函數

在生活中,A要電話給B,A撥號,B聽到電話鈴聲後提起電話,這時A和B就建立起了連接,A和B就可以講話了。等交流結束,掛斷電話結束這次交談。  打電話很簡單解釋了這工作原理:「open—write/read—close」模式。

Linux的SOCKET編程詳解

    伺服器端先初始化Socket,再與連接埠綁定(bind),對連接埠進行監聽(listen),呼叫accept阻塞,等待客戶端連線。在這時如果有個客戶端初始化一個Socket,然後連接伺服器(connect),如果連線成功,這時客戶端與伺服器端的連線就建立了。客戶端發送資料請求,伺服器端接收請求並處理請求,然後把回應資料傳送給客戶端,客戶端讀取數據,最後關閉連接,一次互動結束。

      這些介面的實作都是核心來完成。具體如何實現,可以看看linux的核心


4.1、socket()函數

       int  socket(int protofamily, int type, int protocol);

  socket函數對應於普通文件的開啟操作。普通檔案的開啟操作傳回一個檔案描述字,而socket()用來建立一個socket描述符(socket descriptor),它唯一標識一個socket。這個socket描述字跟檔案描述字一樣,後續的操作都有用到它,把它當作參數,透過它來進行一些讀寫操作。

      如同可以給fopen的傳入不同參數值,以開啟不同的檔案。創建socket的時候,也可以指定不同的參數來建立不同的socket描述符,socket函數的三個參數分別為:

protofamily:即協定域,又稱為協定族(family)。常用的協定族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或稱為AF_UNIX,Unix域socket)、AF_ROUTE等等。協定族決定了socket的位址類型,在通訊中必須採用對應的位址,如AF_INET決定了要用ipv4位址(32位元的)與連接埠號碼(16位元的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。

type:指定socket類型。常用的socket類型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的類型有哪些?)。

protocol:故名思意,就是指定協議。常用的協定有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分別對應TCP傳輸協定、UDP傳輸協定、STCP傳輸協定、TIPC傳輸協定(這個協定我將會單獨開篇討論!)。

注意:並不是上面的type和protocol可以隨意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol為0時,會自動選擇type類型對應的預設協定。

當我們呼叫socket建立一個socket時,回傳的socket描述字它存在於協定族(address family,AF_XXX)空間中,但沒有一個具體的位址。如果想要給它賦值一個位址,就必須呼叫bind()函數,否則就當呼叫connect()、listen()時系統會自動隨機分配一個連接埠。

4.2、bind()函數

正如上面所說bind()函數把一個位址族中的特定位址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6位址和埠號組合賦給socket。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函數的三個參數分別為:

sockfd:即socket描述字,它是透過描述字了一個socket。 bind()函數就是將給這個描述字綁定一個名字。

addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協定位址。這個位址結構依照位址建立socket時的位址協定族的不同而不同,如ipv4對應的是: 

struct sockaddr_in {
   sa_family_t    sin_family; /* address family ;   /* port in network byte order */

   struct in_addr sin_addr;   /* internet address */

};

/* Internet address. */
struct in_addr {
   byte order */
};


ipv6對應的是: 

struct sockaddr_in6 {

   sa_family_t     sin6_family;   /* AF_INET6 */ cume sin6_family;   /* AF_INET6 */ c /
   uint32_t        sin6_flowinfo; /* IPv6 flow information */

   struct in6_addr sin6_addr;     /* IPv6 address */

   uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */
};

struct in6_addr {

};

struct in6_addr {

};

struct in6_addr {

};

struct in6_addr { 🎜 🎜};🎜🎜struct in6_addr { 🎜 🎜};🎜🎜struct in6_addr { 🎜 🎜}; 錯誤🎜};🎜🎜🎜Unix域對應的是: 🎜🎜

#define UNIX_PATH_MAX    108

struct sockaddr_un {
   sa_family_t sun_family;            path[UNIX_PATH_MAX];  /* pathname */
};

addrlen:對應的是位址的長度。

通常伺服器在啟動的時候都會綁定一個眾所周知的位址(如ip位址+連接埠號碼),用於提供服務,客戶可以透過它來接連伺服器;而客戶端就不用指定,有系統自動分配一個連接埠號碼和自身的ip位址組合。這就是為什麼通常伺服器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機產生一個。

網路位元組序與主機位元組序

主機位元組序就是我們平常說的大端和小端模式:不同的CPU有不同的位元組序類型,這些位元組序是指整數在記憶體中保存的順序,這個叫做主機序。引用標準的Big-Endian和Little-Endian的定義如下:

  a) Little-Endian就是低位元組排放在記憶體的低位址端,高位元組排放在記憶體的高位址端。

  b) Big-Endian就是高位元組排放在記憶體的低位址端,低位元組排放在記憶體的高位址端。

網路位元組序:4個位元組的32 bit值以下面的次序傳送:首先是0~7bit,其次8~15bit,然後16~23bit,最後是24~31bit。這種傳輸次序稱為大端位元組序。由於TCP/IP首部中所有的二進位整數在網路中傳輸時都要求以這種次序,因此它又稱作網路位元組序。位元組序,顧名思義位元的順序,就是大於一個位元組類型的資料在記憶體中的存放順序,一個位元組的資料沒有順序的問題了。

所以:在將一個位址綁定到socket的時候,請先將主機字節序轉換成為網路字節序,而不要假定主機字節序跟網路字節序一樣使用的是Big-Endian。由於這個問題曾引發血案!公司專案代碼中由於存在這個問題,導致了很多莫名其妙的問題,所以請謹記對主機字節序不要做任何假定,務必將其轉化為網絡字節序再賦給socket。

4.3、listen()、connect()函數

如果作為一個伺服器,在呼叫socket()、bind()之後就會呼叫listen()來監聽這個socket,如果客戶端這時呼叫connect()發出連線請求,伺服器端就會接收到這個請求。

int listen(int sockfd, int backlog);

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listenen函數的第一個參數描述為要監聽的參數是要監聽的第二個字。個參數為對應socket可以排隊的最大連接個數。 socket()函數所建立的socket預設是一個主動類型的,listen函數將socket變成被動型的,等待客戶的連線請求。

connect函數的第一個參數即為客戶端的socket描述字,第二參數為伺服器的socket位址,第三個參數為socket位址的長度。客戶端透過呼叫connect函數來建立與TCP伺服器的連線。

4.4、accept()函數

TCP伺服器端依序呼叫socket()、bind()、listen()之後,就會監聽指定的socket位址了。 TCP客戶端依序呼叫socket()、connect()之後就向TCP伺服器發送了一個連線請求。 TCP伺服器監聽到這個請求之後,就會呼叫accept()函數取接收請求,這樣連線就建立好了。之後就可以開始網路I/O操作了,也就是類別同於一般檔案的讀寫I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //傳回連接connect_fd


參數sockfdd監聽一個端口,當有一個客戶與伺服器連接時,它使用這個一個端口號,而此時這個端口號正與這個套接字關聯。當然客戶不知道套接字這些細節,它只知道一個位址和一個連接埠號碼。

參數addr

這是一個結果參數,它用來接受一個回傳值,這回傳值指定客戶端的位址,當然這個位址是透過某個位址結構來描述的,使用者應該知道這一個什麼樣的位址結構。如果對客戶的地址不感興趣,那麼可以把這個值設為NULL。

參數len

如同大家所認為的,它也是結果的參數,用來接受上述addr的結構的大小的,它指明addr結構所佔有的位元組個數。同樣的,它也可以被設定為NULL。

如果accept成功返回,則伺服器與客戶已經正確建立連接了,此時伺服器透過accept傳回的套接字來完成與客戶的通訊。

注意:

      accept預設會阻塞進程,直到有一個客戶連接建立後返回,它返回的是一個新可用的套接字,這個套接字是連接套接字。

此時我們需要區分兩種套接字,

       監聽套接字: 監聽套接字正如accept的參數sockfd,它是監聽套接字,在呼叫listen函數之後,是伺服器開始呼叫socket()函數產生的,稱為監聽socket描述字(監聽套接字)

       連接套接字:一個套接字會從主動連接的套接字變身為一個監聽套接字;而accept函數返回的是已連接socket描述字(一個連接套接字),它代表著一個網路已經存在的點點連線。

        一個伺服器通常通常只建立一個監聽socket描述字,它在該伺服器的生命週期內一直存在。核心為每個由伺服器程序接受的客戶連接創建了一個已連接socket描述字,當伺服器完成了對某個客戶的服務,相應的已連接socket描述字就被關閉。

        自然要問的是:為什麼要有兩種套接字?原因很簡單,如果使用一個描述字的話,那麼它的功能太多,使得使用很不直觀,同時在內核確實產生了一個這樣的新的描述字。

連接套接字socketfd_new 並沒有佔用新的端口與客戶端通信,依然使用的是與監聽套接字socketfd一樣的端口號

4.5、read()、write()等函數

萬事具備只欠東風,至此伺服器與客戶已經建立好連線了。可以呼叫網路I/O進行讀寫操作了,也就是實現了網咯中不同進程之間的通訊!網路I/O操作有以下幾組:

read()/write()

recv()/send()

readv()/writev()

recvm()/senrecvm()

vsg()

v )/sendto()

我推薦使用recvmsg()/sendmsg()函數,這兩個函數是最通用的I/O函數,實際上可以把上面的其它函數都替換成這兩個函數。它們的陳述如下:

      #include

      ssize_t read(int fd, void *buf, size_t count);
,
      #include
      #include

      ssize_t send(int sockfd, const void *buf, size_t len, int flags); f, size_t len , int flags);

      ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
    5     addr. t addrlen);
      ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                       struct sockaddr *src_addr, socklen_t *addrlen);
      ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);


read函數是負責從fd中讀取內容.當讀成功時,read返回實際所讀的位元組數,如果返回的值是0表示已經讀到文件的結束了,小於0表示出現了錯誤。如果錯誤為EINTR說明讀取是由中斷引起的,如果是ECONNREST表示網路連線出了問題。

write函數將buf中的nbytes位元組內容寫入檔案描述子fd.成功時傳回寫的位元組數。失敗時返回-1,並設定errno變數。 在網路程式中,當我們向套接字檔案描述符寫時有兩個可能。 1)write的回傳值大於0,表示寫了部分或全部的資料。 2)傳回的值小於0,此時出現了錯誤。我們要根據錯誤類型來處理。如果錯誤為EINTR表示寫的時候出現了中斷錯誤。如果為EPIPE表示網路連線出現了問題(對方已經關閉了連線)。

其它的我就不一一介紹這幾對I/O函數了,具體參見man文檔或者baidu、Google,下面的例子中將使用到send/recv。

4.6、close()函數

在伺服器與客戶端建立連線之後,會進行一些讀取與寫入操作,完成了讀取作業就要關閉對應的socket描述字,好比操作完開啟的檔案要呼叫fclose關閉打開的文件。

#include

int close(int fd);

close一個TCP socket的缺省行為時把該socket標記為以關閉,然後立即返回到調用進程。這個描述字不能再由呼叫程序使用,也就是說不能再當作read或write的第一個參數。

注意:close操作只是使對應socket描述字的參考計數-1,只有當引用計數為0的時候,才會觸發TCP客戶端向伺服器發送終止連線請求。



5. Socket中TCP的建立(三次握手)

TCPreeP的建立(三次握手)

TCPreeP. handshake),過程如下圖所示。


第一次握手:建立連線時,客戶端發送syn包(syn=j)到伺服器,並進入SYN_SEND狀態,等待伺服器確認;SYN:同步序號(Synchronize Sequence Numbers)。

第二次握手:伺服器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態;
第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和伺服器進入ESTABLISHED狀態,完成三次握手。
一個完整的三次握手也就是: 請求---應答---再次確認。

對應的函數介面:

Linux的SOCKET編程詳解

從圖中可以看出,當客戶端呼叫connect時,觸發了連接請求,向伺服器發送了SYN J包,這時connect進入阻塞狀態;伺服器監聽到連接請求,即收到SYN J包,呼叫accept函數接收請求向客戶端發送SYN K ,ACK J+1,這時accept進入阻塞狀態;客戶端收到伺服器的SYN K ,ACK J+1之後,這時connect返回,並對SYN K進行確認;伺服器收到ACK K+1時,accept返回,至此三次握手完畢,連線建立。


我們可以透過網路抓包的查看具體的流程:

例如我們伺服器開啟9502的連接埠。使用tcpdump來抓包:


 tcpdump -iany tcp port 9502


然後我們使用telnet 127.0.


然後我們使用telnet 127.0.0.1 95020.7502050.050509120.0.0.


14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378, win 32792, options [mss 16396,sackOK,TS val 255474104 ecr0,104 ecr. 701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379, win 32768, options [mss 16396,sackOK,TS 40255, 405, ength 0  (2)
14 :12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val 255474104 ecr 255474104] 01.415407 IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], 18453:41474104], 4741041514741021147413213333330000142299:4100142141251342201321333300002133:41002142593:401 9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0

14:13:001475:00189 host hostahost gs [P .], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18

14:13:01.415757 10785878:000 4189 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0




9870 > localhost.9502 表示通信的流向,39870是客戶端,9502是伺服器端

[S] 表示這是一個SYN請求

[S.] 表示這是一個SYN+ACK確認包: 

[.] 表示這是一個SYN+ACK確認包: 

[.] 表示這是一個ACT確認包, (client)SYN->(server)SYN->(client)ACT 就是3次握手過程


[P] 表示這個是一個資料推送,可以是從伺服器端向客戶端推送,也可以從客戶端向伺服器端推

[F] 表示這是一個FIN包,是關閉連線操作,client/server都有可能發起

[R] 表示這是RST包,與F包作用相同,但RST表示連線關閉時,仍有資料未被處理。可以理解為是強制切割連接

win 4099 是指滑動視窗大小

length 18指封包的大小

我們看到 (1)(2)(3)三步是建立第一次握手:


14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378

客戶端IP localhost.39870 9502 發送syn包(syn=j)到伺服器》

syn包(syn=j) : syn的seq= 2927179378  (j=2927179378)

151501250151501515157500315150003151525303030338533833833個host .9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379,

並確認:伺服器收到syn包,並必須確認客戶的SYN(ack=j+1),同時收到請求並確認:伺服器收到syn包,並必須確認客戶的SYN(ack=j+1),同時收到請求並確認發送一個SYN套件(syn=k),即SYN+ACK套件:

此時伺服器主機自己的SYN:seq:y= syn seq 1721825043。
ACK為j+1 =(ack=j+1)=ack 2927179379 


第三次握手:

14:12:45.104711 IP localhost.398709711 IP localhost. ,


客戶端收到伺服器的SYN+ACK包,向伺服器發送確認包ACK(ack=k+1)

客戶端和伺服器進入ESTABLISHED狀態後,可以進行通訊資料互動。此時和accept介面沒有關係,即使沒有accepte,也進行3次握手完成。

连接出现连接不上的问题,一般是网路出现问题或者网卡超负荷或者是连接数已经满啦。

紫色背景的部分:

IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7

客户端向服务器发送长度为7个字节的数据,


IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0

服务器向客户确认已经收到数据


 IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18

然后服务器同时向客户端写入数据。


 IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0

客户端向服务器确认已经收到数据

这个就是tcp可靠的连接,每次通信都需要对方来确认。


6. Linux的SOCKET編程詳解

建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的,如图:

Linux的SOCKET編程詳解

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。

(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。

(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。

(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。

Linux的SOCKET編程詳解如图:

Linux的SOCKET編程詳解

过程如下:

某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;

另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;

一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;

接收到这个FIN的源发送端TCP对它进行确认。

这样每个方向上都有一个FIN和ACK。

1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。


2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。


7. Socket编程实例

服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息。

/* File Name: server.c */  
#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
#include<errno.h>  
#include<sys/types.h>  
#include<sys/socket.h>  
#include<netinet/in.h>  
#define DEFAULT_PORT 8000  
#define MAXLINE 4096  
int main(int argc, char** argv)  
{  
    int    socket_fd, connect_fd;  
    struct sockaddr_in     servaddr;  
    char    buff[4096];  
    int     n;  
    //初始化Socket  
    if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){  
    printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
    //初始化  
    memset(&servaddr, 0, sizeof(servaddr));  
    servaddr.sin_family = AF_INET;  
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。  
    servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT  
  
    //将本地地址绑定到所创建的套接字上  
    if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){  
    printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
    //开始监听是否有客户端连接  
    if( listen(socket_fd, 10) == -1){  
    printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
    printf("======waiting for client&#39;s request======\n");  
    while(1){  
//阻塞直到有客户端连接,不然多浪费CPU资源。  
        if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){  
        printf("accept socket error: %s(errno: %d)",strerror(errno),errno);  
        continue;  
    }  
//接受客户端传过来的数据  
    n = recv(connect_fd, buff, MAXLINE, 0);  
//向客户端发送回应数据  
    if(!fork()){ /*紫禁城*/  
        if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1)  
        perror("send error");  
        close(connect_fd);  
        exit(0);  
    }  
    buff[n] = &#39;\0&#39;;  
    printf("recv msg from client: %s\n", buff);  
    close(connect_fd);  
    }  
    close(socket_fd);  
}

客户端:

/* File Name: client.c */  
  
#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
#include<errno.h>  
#include<sys/types.h>  
#include<sys/socket.h>  
#include<netinet/in.h>  
  
#define MAXLINE 4096  
  
  
int main(int argc, char** argv)  
{  
    int    sockfd, n,rec_len;  
    char    recvline[4096], sendline[4096];  
    char    buf[MAXLINE];  
    struct sockaddr_in    servaddr;  
  
  
    if( argc != 2){  
    printf("usage: ./client <ipaddress>\n");  
    exit(0);  
    }  
  
  
    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){  
    printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);  
    exit(0);  
    }  
  
  
    memset(&servaddr, 0, sizeof(servaddr));  
    servaddr.sin_family = AF_INET;  
    servaddr.sin_port = htons(8000);  
    if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){  
    printf("inet_pton error for %s\n",argv[1]);  
    exit(0);  
    }  
  
  
    if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){  
    printf("connect error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
  
  
    printf("send msg to server: \n");  
    fgets(sendline, 4096, stdin);  
    if( send(sockfd, sendline, strlen(sendline), 0) < 0)  
    {  
    printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);  
    exit(0);  
    }  
    if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {  
       perror("recv error");  
       exit(1);  
    }  
    buf[rec_len]  = &#39;\0&#39;;  
    printf("Received : %s ",buf);  
    close(sockfd);  
    exit(0);  
}

inet_pton 是Linux下IP地址转换函数,可以在将IP地址在“点分十进制”和“整数”之间转换 ,是inet_addr的扩展。

int inet_pton(int af, const char *src, void *dst);//转换字符串到网络地址:

第一个参数af是地址族,转换后存在dst中
    af = AF_INET:src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中
  af =AF_INET6:src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中
如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。


测试:

编译server.c

gcc -o server server.c

启动进程:

./server

显示结果:

======waiting for client's request======

并等待客户端连接。

编译 client.c

gcc -o client server.c

客户端去连接server:

./client 127.0.0.1 

等待输入消息

Linux的SOCKET編程詳解

发送一条消息,输入:c++

Linux的SOCKET編程詳解

此时服务器端看到:

Linux的SOCKET編程詳解

客户端收到消息:

Linux的SOCKET編程詳解

其实可以不用client,可以使用telnet来测试:

telnet 127.0.0.1 8000

Linux的SOCKET編程詳解

注意:

在ubuntu 编译源代码的时候,头文件types.h可能找不到。
使用dpkg -L libc6-dev | grep types.h 查看。
如果没有,可以使用
apt-get install libc6-dev安装。
如果有了,但不在/usr/include/sys/目录下,手动把这个文件添加到这个目录下就可以了。


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
上一篇:Socket詳解下一篇:Socket詳解