linux----網絡編程(一)

Linux的SOCKET編程詳解

 

Linux的SOCKET編程詳解:http://blog.csdn.net/hguisu/article/details/7445768

 

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文件系統http://blog.csdn.net/hguisu/article/details/6122513#t7

 

 

 

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的定義如下:

  1. Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。
  2. 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文檔或者baidu、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/目錄下,手動把這個文件添加到這個目錄下就可以了。

(部分內容來自吳秦:http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html)

 

 

Linux環境下網絡編程雜談

 

一些Linux網絡編程常用的知識點吧:

大頭序,小頭序,網絡字節序

32bit的地址和10進制的轉化

IP地址和主機名的轉化

----------------------------------------

今天我們說說“Pre-網絡編程”。內容比較雜,但都是在做網絡應用程序開發過程中經常要遇到的問題。

 

一、大端、小端和網絡字節序

小端字節序:little-endian,將低字節存放在內存的起始地址;

大端字節序:big-endian,將高字節存放在內存的其實地址。

      例如,數字index=0x11223344,在大小端字節序方式下其存儲形式爲:

上圖一目瞭然的可以看出大小端字節序的區別。

還有另外一個概念就是網絡字節序。網絡字節順序是TCP/IP中規定好的一種數據表示格式,它與具體的CPU類型、操作系統等無關,從而可以保證數據在不同主機之間傳輸時能夠被正確解釋。網絡字節順序採用big endian方式。注意:X86系列CPU都是小端little-endian字節序,即低字節存低位,高字節存高位。

爲此,Linux專門提供了字節轉換函數.

unsigned long  int htonl(unsigned long  int hostlong)
unsigned short int htons(unisgned short int hostshort)
unsigned long  int ntohl(unsigned long  int netlong)
unsigned short int ntohs(unsigned short int netshort)

在這四個轉換函數中,h代表host,n代表 network,s代表short,l代表long 。htonl()函數的意義是將本機器上的long數據轉化爲網絡上的long。其他幾個函數的意義也差不多。

       看個例子:

        也就是說對於從網絡上接收到的非單子節的基本數據類型數據,首先需要用ntohl(s)將其轉換成本地字節序;同理,發往網絡的非單子節的基本數據類型數據,首先用htonl(s)將其轉換成網絡字節序。這裏最常見的就是IP地址和端口號。

 

         二、點分十進制格式的IP地址和32bit的IP地址

        IP地址都是以點分十進制格式表示,例如“172.18.1.231”。而在程序中基本是以如下的結構表示一個IP:

struct in_addr {
         __be32     s_addr; //其實就是一個32bit的數字
};

        它和點分十進制格式的IP地址可以通過一組API實現相互轉換:

int inet_aton(const char *cp,struct in_addr *inp) 無效的地址cp則返回0;否則返回非0
char *inet_ntoa(struct in_addr in) 將一個32位的IP地址轉換成點分十進制字符串。

    這兩個函數所要求的struct in_addr{}參數均爲網絡字節序。

       繼續看例子:

        “192.168.11.23”轉換成數字就是0xc0a80b17,是網絡字節序的。如果直接打印,那麼本地按照小端字節序來輸出,結果爲net addr = 170ba8c0,剛好和實際相反。當我們先將其轉換成本地字節序,然後再輸出時結果就OK了,即host addr = c0a80b17。同理,inet_ntoa()也類似。

        

         三、網絡主機名和IP地址的對應關係

         在做網絡編程時經常要碰到的一個問題就是根據對方主機名來獲取其IP地址,或者根據IP地址反過來解析主機名和其他信息。Linux提供了兩個常用的API:

struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, int len, int type);

       這兩個函數失敗時返回NULL且設置h_errno錯誤變量,調用hstrerror(h_errno)或者herror("Error");可以得到詳細的出錯信息。成功時均返回如下結構:  

struct hostent {
     char    *h_name;        /* 主機名*/
     char    **h_aliases;    /* 主機別名的列表*/
     int     h_addrtype;     /* 地址類型,AF_INET或其他*/
     int     h_length;       /* 所佔的字節數,IPv4地址都是4字節 */
     char    **h_addr_list;  /* IP地址列表,網絡字節序*/
}
#define h_addr  h_addr_list[0]  /*後向兼容 */

       gethostbyname可以將機器名(如www.google.com)轉換爲一個結構指針,gethostbyaddr可以將一個32位的IP地址(C0A80001)轉換爲結構指針。對於gethostbyaddr函數來說,輸入參數“addr”的類型根據參數“type”來確定,目前type僅取AF_INET或AF_INET6。例如,type=2(即AF_INET),則addr就必須爲struct in_addr{}類型。

       繼續看例子:

#include <stdio.h>
#include <netdb.h>
#include <error.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int arg,char** argv){

  struct hostent *host,*host2;
  if(NULL == (host = gethostbyname(argv[1]))){
          herror("Error");
          return 1;
  }

  printf("name = %s\n",host->h_name);
  printf("aliases = %s\n",*host->h_aliases);
  printf("add type = %d\n",host->h_addrtype);
  printf("len = %d\n",host->h_length);
  printf("IP=%s\n",inet_ntoa(*(struct in_addr*)host->h_addr));
  printf("=================================\n");
  struct in_addr maddr;
  if(0 == inet_aton(argv[2],&maddr)){
          return 0;
  }
  char* c = (char*)&maddr;
  printf("org = %x.%x.%x.%x\n",*(c)&0xff,*(c+1)&0xff,*(c+2)&0xff,*(c+3)&0xff);

  if(NULL == (host2 = gethostbyaddr(&maddr,4,2))){
          printf("Error:%s\n",hstrerror(h_errno));
          return 1;
  }
  printf("name = %s\n",host2->h_name);
  printf("aliases = %s\n",*host2->h_aliases);
  printf("add type = %d\n",host2->h_addrtype);
  printf("len = %d\n",host2->h_length);
  printf("IP=%s\n",inet_ntoa(*(struct in_addr*)host2->h_addr));
  return 0;
}

    運行結果如下:

       當我們調用gethostbyaddr根據CU主頁的IP地址獲取其站點信息時返回的錯誤是“未知的主機”錯誤,原因留給大家自己思考吧。這充分說明對了於gethostbyname()函數和gethostbyaddr()函數的調用一定要判斷其返回值。

 

 

3.《linux網絡編程》宋斌寫的,這本書不錯。寫的清楚,容易懂。是一個比較好的參考資料。粗看了看,有不少收益。

4.Linux網絡編程的一些面試題

        http://blog.csdn.net/chencheng126/article/details/44407981

5.epoll和select

        對於io複用中epoll和select方式介紹的很清楚:http://blog.csdn.net/chencheng126/article/details/44410203

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章