程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 詳解Linux的SOCKET編程

詳解Linux的SOCKET編程

編輯:關於C++

詳解Linux的SOCKET編程。本站提示廣大學習愛好者:(詳解Linux的SOCKET編程)文章只能為提供參考,不一定能成為您想要的結果。以下是詳解Linux的SOCKET編程正文


本篇文章對Linux的SOCKET編程停止了具體說明,文章前面分享了一個編程實例供年夜家進修。

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 Protocol/Internet Protocol)即傳輸掌握協定/網間協定,是一個工業尺度的協定集,它是為廣域網(WANs)設計的。   

   TCP/IP協定存在於OS中,收集辦事經由過程OS供給,在OS中增長支撐TCP/IP的體系挪用——Berkeley套接字,如Socket,Connect,Send,Recv等

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

   TCP/IP協定族包含運輸層、收集層、鏈路層,而socket地點地位如圖,Socket是運用層與TCP/IP協定族通訊的中央軟件籠統層。

3. Socket是甚麼

1、socket套接字:
     socket來源於Unix,而Unix/Linux根本哲學之一就是“一切皆文件”,都可以用“翻開open –> 讀寫write/read –> 封閉close”形式來操作。Socket就是該形式的一個完成,        socket等於一種特別的文件,一些socket函數就是對其停止的操作(讀/寫IO、翻開、封閉).
   說白了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要求的運用法式要求操作體系翻開一個文件。操作體系就創立一個文件描寫符供給給運用法式拜訪文件。從運用法式的角度看,文件描寫符是一個整數,運用法式可以用它來讀寫文件。下圖顯示,操作體系若何把文件描寫符完成為一個指針數組,這些指針指向外部數據構造。

    關於每一個法式體系都有一張零丁的表。准確地講,體系為每一個運轉的過程保護一張零丁的文件描寫符表。當過程翻開一個文件時,體系把一個指向此文件外部數據構造的指針寫入文件描寫符表,並把該表的索引值前往給挪用者 。運用法式只需記住這個描寫符,並在今後操作該文件時應用它。操作體系把該描寫符作為索引拜訪過程描寫符表,經由過程指針找到保留該文件一切的信息的數據構造。

針對套接字的體系數據構造:

   1)、套接字API裡有個函數socket,它就是用來創立一個套接字。套接字設計的整體思緒是,單個體系挪用便可以創立任何套接字,由於套接字是相當籠統的。一旦套接字創立後,運用法式還須要挪用其他函數來指定詳細細節。例如挪用socket將創立一個新的描寫符條目:

   2)、固然套接字的外部數據構造包括許多字段,然則體系創立套接字後,年夜多半字字段沒有填寫。運用法式創立套接字後在該套接字可使用之前,必需挪用其他的進程來填充這些字段。

3、文件描寫符和文件指針的差別:

文件描寫符:在linux體系中翻開文件就會取得文件描寫符,它是個很小的正整數。每一個過程在PCB(Process Control Block)中保留著一份文件描寫符表,文件描寫符就是這個表的索引,每一個表項都有一個指向已翻開文件的指針。

文件指針:C說話中應用文件指針做為I/O的句柄。文件指針指向過程用戶區中的一個被稱為FILE構造的數據構造。FILE構造包含一個緩沖區和一個文件描寫符。而文件描寫符是文件描寫符表的一個索引,是以從某種意義上說文件指針就是句柄的句柄(在Windows體系上,文件描寫符被稱作文件句柄)。

具體內容請看linux文件體系

4. 根本的SOCKET接口函數
   在生涯中,A要德律風給B,A撥號,B聽到德律風鈴聲後提起德律風,這時候A和B就樹立起了銜接,A和B便可以講話了。等交換停止,掛斷德律風停止此次攀談。  打德律風很簡略說明了這任務道理:“open—write/read—close”形式。

   辦事器端先初始化Socket,然後與端口綁定(bind),對端口停止監聽(listen),挪用accept壅塞,期待客戶端銜接。在這時候假如有個客戶端初始化一個Socket,然後銜接辦事器(connect),假如銜接勝利,這時候客戶端與辦事器真個銜接就樹立了。客戶端發送數據要求,辦事器端吸收要求並處置要求,然後把回應數據發送給客戶端,客戶端讀取數據,最初封閉銜接,一次交互停止。

這些接口的完成都是內核來完成。詳細若何完成,可以看看linux的內核

4.1、socket()函數
     
     int  socket(int protofamily, int type, int protocol);//前往sockfd
     sockfd是描寫符。

   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()函數創立了,獨一標識一個socket。bind()函數就是將給這個描寫字綁定一個名字。

  addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協定地址。這個地址構造依據地址創立socket時的地址協定族的分歧而分歧,如ipv4對應的是:
    struct sockaddr_in {
       sa_family_t    sin_family; /* address family: AF_INET */
       in_port_t      sin_port;   /* port in network byte order */
       struct in_addr sin_addr;   /* internet address */
     };

    /* Internet address. */
    struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
    };
    ipv6對應的是:
    struct sockaddr_in6 {
      sa_family_t     sin6_family;   /* AF_INET6 */
      in_port_t       sin6_port;     /* port number */
      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 {
       unsigned char   s6_addr[16];   /* IPv6 address */
   };
  Unix域對應的是:
  #define UNIX_PATH_MAX    108

  struct sockaddr_un {
    sa_family_t sun_family;               /* AF_UNIX */
    char        sun_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);

listen函數的第一個參數即為要監聽的socket描寫字,第二個參數為響應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

參數sockfd
       參數sockfd就是下面說明中的監聽套接字,這個套接字用來監聽一個端口,當有一個客戶與辦事器銜接時,它應用這個一個端標語,而此時這個端標語正與這個套接字聯系關系。固然客戶不曉得套接字這些細節,它只曉得一個地址和一個端標語。
參數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()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto()

我推舉應用recvmsg()/sendmsg()函數,這兩個函數是最通用的I/O函數,現實上可以把下面的其它函數都調換成這兩個函數。它們的聲明以下:

    

 #include <unistd.h>

 ssize_t read(int fd, void *buf, size_t count);
 ssize_t write(int fd, const void *buf, size_t count);

 #include <sys/types.h>
 #include <sys/socket.h>

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

 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
   const struct sockaddr *dest_addr, socklen_t addrlen);
 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
   struct sockaddr *src_addr, socklen_t *addrlen);

 ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
 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文檔或許百度、Google,上面的例子中將應用到send/recv。

4.6、close()函數
在辦事器與客戶端樹立銜接以後,會停止一些讀寫操作,完成了讀寫操作就要封閉響應的socket描寫字,比如操作完翻開的文件要挪用fclose封閉翻開的文件。

#include <unistd.h>
int close(int fd);
close一個TCP socket的缺省行動時把該socket標志為以封閉,然後立刻前往到挪用過程。該描寫字不克不及再由挪用過程應用,也就是說不克不及再作為read或write的第一個參數。

留意:close操作只是使響應socket描寫字的援用計數-1,只要當援用計數為0的時刻,才會觸發TCP客戶端向辦事器發送終止銜接要求。

 

5. Socket中TCP的樹立(三次握手)
TCP協定經由過程三個報文段完成銜接的樹立,這個進程稱為三次握手(three-way 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狀況,完成三次握手。
一個完全的三次握手也就是: 要求---應對---再次確認。

對應的函數接口:


      

從圖中可以看出,當客戶端挪用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.0.1 9502開銜接.:
telnet 127.0.0.1 9502

14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378, win 32792, options [mss 16396,sackOK,TS val 255474104 ecr 0,nop,wscale 3], length 0(1)
14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379, win 32768, options [mss 16396,sackOK,TS val 255474104 ecr 255474104,nop,wscale 3], length 0  (2)
14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val 255474104 ecr 255474104], length 0  (3)

14:13:01.415407 IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7
14:13:01.415432 IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0
14:13:01.415747 IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18
14:13:01.415757 IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0

114:12:45.104687 時光帶有准確到奧妙

  • localhost.39870 > localhost.9502 表現通訊的流向,39870是客戶端,9502是辦事器端
  • [S] 表現這是一個SYN要求
  • [S.] 表現這是一個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)三步是樹立tcp:
第一次握手:
14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378
客戶端IP localhost.39870 (客戶真個端口普通是主動分派的) 向辦事器localhost.9502 發送syn包(syn=j)到辦事器》
syn包(syn=j) : syn的seq= 2927179378  (j=2927179378)

第二次握手:
14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379,
收到要求並確認:辦事器收到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.39870 > localhost.9502: Flags [.], ack 1,
客戶端收到辦事器的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. TCP銜接的終止(四次握手釋放)
樹立一個銜接須要三次握手,而終止一個銜接要經由四次握手,這是由TCP的半封閉(half-close)形成的,如圖:

因為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)。

對應函數接口如圖:

進程以下:

某個運用過程起首挪用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'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] = '\0'; 
 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] = '\0'; 
 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

期待輸出新聞

發送一條新聞,輸出:c++

此時辦事器端看到:

客戶端收到新聞:


其實可以不消client,可使用telnet來測試:

telnet 127.0.0.1 8000

留意:

在ubuntu 編譯源代碼的時刻,頭文件types.h能夠找不到。
應用dpkg -L libc6-dev | grep types.h 檢查。
假如沒有,可使用
apt-get install libc6-dev裝置。
假如有了,但不在/usr/include/sys/目次下,手動把這個文件添加到這個目次下便可以了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved