0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Linux的SOCKET編程介紹

科技綠洲 ? 來源:Linux開發(fā)架構(gòu)之路 ? 作者:Linux開發(fā)架構(gòu)之路 ? 2023-11-13 15:25 ? 次閱讀

1. 網(wǎng)絡(luò)中進程之間如何通信

進程通信的概念最初來源于單機系統(tǒng)。由于每個進程都在自己的地址范圍內(nèi)運行,為保證兩個相互通信的進程之間既互不干擾又協(xié)調(diào)一致工作,操作系統(tǒng)為進程通信提供了相應(yīng)設(shè)施,如

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

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

他們都僅限于用在本機進程之間通信。網(wǎng)間進程通信要解決的是不同主機進程間的相互通信問題(可把同機進程通信看作是其中的特例)。為此,首先要解決的是網(wǎng)間進程標識問題。同一主機上,不同進程可用進程號(process ID)唯一標識。但在網(wǎng)絡(luò)環(huán)境下,各主機獨立分配的進程號不能唯一標識該進程。例如,主機A賦于某進程號5,在B機中也可以存在5號進程,因此,“5號進程”這句話就沒有意義了。 其次,操作系統(tǒng)支持的網(wǎng)絡(luò)協(xié)議眾多,不同協(xié)議的工作方式不同,地址格式也不同。因此,網(wǎng)間進程通信還要解決多重協(xié)議的識別問題。

其實TCP/IP協(xié)議族已經(jīng)幫我們解決了這個問題,網(wǎng)絡(luò)層的“ip地址”可以唯一標識網(wǎng)絡(luò)中的主機,而傳輸層的“協(xié)議+端口”可以唯一標識主機中的應(yīng)用程序(進程)。這樣利用三元組(ip地址,協(xié)議,端口)就可以標識網(wǎng)絡(luò)的進程了,網(wǎng)絡(luò)中的進程通信就可以利用這個標志與其它進程進行交互。

使用TCP/IP協(xié)議的應(yīng)用程序通常采用應(yīng)用編程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已經(jīng)被淘汰),來實現(xiàn)網(wǎng)絡(luò)進程之間的通信。就目前而言,幾乎所有的應(yīng)用程序都是采用socket,而現(xiàn)在又是網(wǎng)絡(luò)時代,網(wǎng)絡(luò)中進程通信是無處不在,這就是我為什么說“一切皆socket”。

2. 什么是TCP/IP、UDP

TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協(xié)議/網(wǎng)間協(xié)議,是一個工業(yè)標準的協(xié)議集,它是為廣域網(wǎng)(WANs)設(shè)計的。

TCP/IP協(xié)議存在于OS中,網(wǎng)絡(luò)服務(wù)通過OS提供,在OS中增加支持TCP/IP的系統(tǒng)調(diào)用——Berkeley套接字,如Socket,Connect,Send,Recv等

UDP(User Data Protocol,用戶數(shù)據(jù)報協(xié)議)是與TCP相對應(yīng)的協(xié)議。它是屬于TCP/IP協(xié)議族中的一種。如圖:

圖片

TCP/IP協(xié)議族包括運輸層、網(wǎng)絡(luò)層、鏈路層,而socket所在位置如圖,Socket是應(yīng)用層與TCP/IP協(xié)議族通信的中間軟件抽象層。

圖片

  1. Socket是什么

1、 socket套接字:

socket起源于Unix,而Unix/Linux基本哲學(xué)之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關(guān)閉close”模式來操作。Socket就是該模式的一個實現(xiàn), socket即是一種特殊的文件,一些socket函數(shù)就是對其進行的操作(讀/寫IO、打開、關(guān)閉).
說白了Socket是應(yīng)用層與TCP/IP協(xié)議族通信的中間軟件抽象層,它是一組接口。在設(shè)計模式中,Socket其實就是一個門面模式,它把復(fù)雜的TCP/IP協(xié)議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數(shù)據(jù),以符合指定的協(xié)議。

注意:其實socket也沒有層的概念,它只是一個facade設(shè)計模式的應(yīng)用,讓編程變的更簡單。是一個軟件抽象層。在網(wǎng)絡(luò)編程中,我們大量用的都是通過socket實現(xiàn)的。

2、套接字描述符

其實就是一個整數(shù),我們最熟悉的句柄是0、1、2三個,0是標準輸入,1是標準輸出,2是標準錯誤輸出。0、1、2是整數(shù)表示的,對應(yīng)的FILE *結(jié)構(gòu)的表示就是stdin、stdout、stderr

套接字API最初是作為UNIX操作系統(tǒng)的一部分而開發(fā)的,所以套接字API與系統(tǒng)的其他I/O設(shè)備集成在一起。特別是,當應(yīng)用程序要為因特網(wǎng)通信而創(chuàng)建一個套接字(socket)時,操作系統(tǒng)就返回一個小整數(shù)作為描述符(descriptor)來標識這個套接字。然后,應(yīng)用程序以該描述符作為傳遞參數(shù),通過調(diào)用函數(shù)來完成某種操作(例如通過網(wǎng)絡(luò)傳送數(shù)據(jù)或接收輸入的數(shù)據(jù))。

在許多操作系統(tǒng)中,套接字描述符和其他I/O描述符是集成在一起的,所以應(yīng)用程序可以對文件進行套接字I/O或I/O讀/寫操作。

當應(yīng)用程序要創(chuàng)建一個套接字時,操作系統(tǒng)就返回一個小整數(shù)作為描述符,應(yīng)用程序則使用這個描述符來引用該套接字需要I/O請求的應(yīng)用程序請求操作系統(tǒng)打開一個文件。操作系統(tǒng)就創(chuàng)建一個文件描述符提供給應(yīng)用程序訪問文件。從應(yīng)用程序的角度看,文件描述符是一個整數(shù),應(yīng)用程序可以用它來讀寫文件。下圖顯示,操作系統(tǒng)如何把文件描述符實現(xiàn)為一個指針數(shù)組,這些指針指向內(nèi)部數(shù)據(jù)結(jié)構(gòu)。

圖片

對于每個程序系統(tǒng)都有一張單獨的表。精確地講,系統(tǒng)為每個運行的進程維護一張單獨的文件描述符表。當進程打開一個文件時,系統(tǒng)把一個指向此文件內(nèi)部數(shù)據(jù)結(jié)構(gòu)的指針寫入文件描述符表,并把該表的索引值返回給調(diào)用者 。應(yīng)用程序只需記住這個描述符,并在以后操作該文件時使用它。操作系統(tǒng)把該描述符作為索引訪問進程描述符表,通過指針找到保存該文件所有的信息的數(shù)據(jù)結(jié)構(gòu)。

針對套接字的系統(tǒng)數(shù)據(jù)結(jié)構(gòu):

1)、套接字API里有個函數(shù)socket,它就是用來創(chuàng)建一個套接字。套接字設(shè)計的總體思路是,單個系統(tǒng)調(diào)用就可以創(chuàng)建任何套接字,因為套接字是相當籠統(tǒng)的。一旦套接字創(chuàng)建后,應(yīng)用程序還需要調(diào)用其他函數(shù)來指定具體細節(jié)。例如調(diào)用socket將創(chuàng)建一個新的描述符條目:

圖片

2)、雖然套接字的內(nèi)部數(shù)據(jù)結(jié)構(gòu)包含很多字段,但是系統(tǒng)創(chuàng)建套接字后,大多數(shù)字字段沒有填寫。應(yīng)用程序創(chuàng)建套接字后在該套接字可以使用之前,必須調(diào)用其他的過程來填充這些字段。

3、文件描述符和文件指針的區(qū)別:

文件描述符:在linux系統(tǒng)中打開文件就會獲得文件描述符,它是個很小的正整數(shù)。每個進程在PCB(Process Control Block)中保存著一份文件描述符表,文件描述符就是這個表的索引,每個表項都有一個指向已打開文件的指針。

文件指針:C語言中使用文件指針做為I/O的句柄。文件指針指向進程用戶區(qū)中的一個被稱為FILE結(jié)構(gòu)的數(shù)據(jù)結(jié)構(gòu)。FILE結(jié)構(gòu)包括一個緩沖區(qū)和一個文件描述符。而文件描述符是文件描述符表的一個索引,因此從某種意義上說文件指針就是句柄的句柄(在Windows系統(tǒng)上,文件描述符被稱作文件句柄)。

4. 基本的SOCKET接口函數(shù)

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

圖片

服務(wù)器端先初始化Socket,然后與端口綁定(bind),對端口進行監(jiān)聽(listen),調(diào)用accept阻塞,等待客戶端連接。在這時如果有個客戶端初始化一個Socket,然后連接服務(wù)器(connect),如果連接成功,這時客戶端與服務(wù)器端的連接就建立了。客戶端發(fā)送數(shù)據(jù)請求,服務(wù)器端接收請求并處理請求,然后把回應(yīng)數(shù)據(jù)發(fā)送給客戶端,客戶端讀取數(shù)據(jù),最后關(guān)閉連接,一次交互結(jié)束。

這些接口的實現(xiàn)都是內(nèi)核來完成。具體如何實現(xiàn),可以看看linux的內(nèi)核

4.1、socket()函數(shù)

int  socket(int protofamily, int type, int protocol);//返回sockfd

sockfd是描述符。

socket函數(shù)對應(yīng)于普通文件的打開操作。普通文件的打開操作返回一個文件描述字,而socket()用于創(chuàng)建一個socket描述符(socket descriptor),它唯一標識一個socket。這個socket描述字跟文件描述字一樣,后續(xù)的操作都有用到它,把它作為參數(shù),通過它來進行一些讀寫操作。

正如可以給fopen的傳入不同參數(shù)值,以打開不同的文件。創(chuàng)建socket的時候,也可以指定不同的參數(shù)創(chuàng)建不同的socket描述符,socket函數(shù)的三個參數(shù)分別為:

  • protofamily:即協(xié)議域,又稱為協(xié)議族(family)。常用的協(xié)議族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。協(xié)議族決定了socket的地址類型,在通信中必須采用對應(yīng)的地址,如AF_INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。
  • type:指定socket類型。常用的socket類型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的類型有哪些?)。
  • protocol:故名思意,就是指定協(xié)議。常用的協(xié)議有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分別對應(yīng)TCP傳輸協(xié)議、UDP傳輸協(xié)議、STCP傳輸協(xié)議、TIPC傳輸協(xié)議(這個協(xié)議我將會單獨開篇討論?。?/li>

注意:并不是上面的type和protocol可以隨意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol為0時,會自動選擇type類型對應(yīng)的默認協(xié)議。

當我們調(diào)用socket創(chuàng)建一個socket時,返回的socket描述字它存在于協(xié)議族(address family,AF_XXX)空間中,但沒有一個具體的地址。如果想要給它賦值一個地址,就必須調(diào)用bind()函數(shù),否則就當調(diào)用connect()、listen()時系統(tǒng)會自動隨機分配一個端口。

4.2、bind()函數(shù)

正如上面所說bind()函數(shù)把一個地址族中的特定地址賦給socket。例如對應(yīng)AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。

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

函數(shù)的三個參數(shù)分別為:

  • sockfd:即socket描述字,它是通過socket()函數(shù)創(chuàng)建了,唯一標識一個socket。bind()函數(shù)就是將給這個描述字綁定一個名字。
  • addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協(xié)議地址。這個地址結(jié)構(gòu)根據(jù)地址創(chuàng)建socket時的地址協(xié)議族的不同而不同,如ipv4對應(yīng)的是:
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對應(yīng)的是:

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域?qū)?yīng)的是:

#define UNIX_PATH_MAX    108
struct sockaddr_un { 
    sa_family_t sun_family;               /* AF_UNIX */ 
    char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
};
  • addrlen:對應(yīng)的是地址的長度。

通常服務(wù)器在啟動的時候都會綁定一個眾所周知的地址(如ip地址+端口號),用于提供服務(wù),客戶就可以通過它來接連服務(wù)器;而客戶端就不用指定,有系統(tǒng)自動分配一個端口號和自身的ip地址組合。這就是為什么通常服務(wù)器端在listen之前會調(diào)用bind(),而客戶端就不會調(diào)用,而是在connect()時由系統(tǒng)隨機生成一個。

網(wǎng)絡(luò)字節(jié)序與主機字節(jié)序
主機字節(jié)序就是我們平常說的大端和小端模式:不同的CPU有不同的字節(jié)序類型,這些字節(jié)序是指整數(shù)在內(nèi)存中保存的順序,這個叫做主機序。引用標準的Big-Endian和Little-Endian的定義如下:
a) Little-Endian就是低位字節(jié)排放在內(nèi)存的低地址端,高位字節(jié)排放在內(nèi)存的高地址端。
b) Big-Endian就是高位字節(jié)排放在內(nèi)存的低地址端,低位字節(jié)排放在內(nèi)存的高地址端。
網(wǎng)絡(luò)字節(jié)序:4個字節(jié)的32 bit值以下面的次序傳輸:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。這種傳輸次序稱作大端字節(jié)序。由于TCP/IP首部中所有的二進制整數(shù)在網(wǎng)絡(luò)中傳輸時都要求以這種次序,因此它又稱作網(wǎng)絡(luò)字節(jié)序。字節(jié)序,顧名思義字節(jié)的順序,就是大于一個字節(jié)類型的數(shù)據(jù)在內(nèi)存中的存放順序,一個字節(jié)的數(shù)據(jù)沒有順序的問題了。
所以:在將一個地址綁定到socket的時候,請先將主機字節(jié)序轉(zhuǎn)換成為網(wǎng)絡(luò)字節(jié)序,而不要假定主機字節(jié)序跟網(wǎng)絡(luò)字節(jié)序一樣使用的是Big-Endian。由于這個問題曾引發(fā)過血案!公司項目代碼中由于存在這個問題,導(dǎo)致了很多莫名其妙的問題,所以請謹記對主機字節(jié)序不要做任何假定,務(wù)必將其轉(zhuǎn)化為網(wǎng)絡(luò)字節(jié)序再賦給socket。

4.3、listen()、connect()函數(shù)

如果作為一個服務(wù)器,在調(diào)用socket()、bind()之后就會調(diào)用listen()來監(jiān)聽這個socket,如果客戶端這時調(diào)用connect()發(fā)出連接請求,服務(wù)器端就會接收到這個請求。

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

listen函數(shù)的第一個參數(shù)即為要監(jiān)聽的socket描述字,第二個參數(shù)為相應(yīng)socket可以排隊的最大連接個數(shù)。socket()函數(shù)創(chuàng)建的socket默認是一個主動類型的,listen函數(shù)將socket變?yōu)楸粍宇愋偷?,等待客戶的連接請求。

connect函數(shù)的第一個參數(shù)即為客戶端的socket描述字,第二參數(shù)為服務(wù)器的socket地址,第三個參數(shù)為socket地址的長度??蛻舳送ㄟ^調(diào)用connect函數(shù)來建立與TCP服務(wù)器的連接。

4.4、accept()函數(shù)

TCP服務(wù)器端依次調(diào)用socket()、bind()、listen()之后,就會監(jiān)聽指定的socket地址了。TCP客戶端依次調(diào)用socket()、connect()之后就向TCP服務(wù)器發(fā)送了一個連接請求。TCP服務(wù)器監(jiān)聽到這個請求之后,就會調(diào)用accept()函數(shù)取接收請求,這樣連接就建立好了。之后就可以開始網(wǎng)絡(luò)I/O操作了,即類同于普通文件的讀寫I/O操作。

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

參數(shù)sockfd

參數(shù)sockfd就是上面解釋中的監(jiān)聽套接字,這個套接字用來監(jiān)聽一個端口,當有一個客戶與服務(wù)器連接時,它使用這個一個端口號,而此時這個端口號正與這個套接字關(guān)聯(lián)。當然客戶不知道套接字這些細節(jié),它只知道一個地址和一個端口號。

參數(shù)addr

這是一個結(jié)果參數(shù),它用來接受一個返回值,這返回值指定客戶端的地址,當然這個地址是通過某個地址結(jié)構(gòu)來描述的,用戶應(yīng)該知道這一個什么樣的地址結(jié)構(gòu)。如果對客戶的地址不感興趣,那么可以把這個值設(shè)置為NULL。

參數(shù)len

如同大家所認為的,它也是結(jié)果的參數(shù),用來接受上述addr的結(jié)構(gòu)的大小的,它指明addr結(jié)構(gòu)所占有的字節(jié)個數(shù)。同樣的,它也可以被設(shè)置為NULL。

如果accept成功返回,則服務(wù)器與客戶已經(jīng)正確建立連接了,此時服務(wù)器通過accept返回的套接字來完成與客戶的通信。

注意:

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

此時我們需要區(qū)分兩種套接字,

監(jiān)聽套接字: 監(jiān)聽套接字正如accept的參數(shù)sockfd,它是監(jiān)聽套接字,在調(diào)用listen函數(shù)之后,是服務(wù)器開始調(diào)用socket()函數(shù)生成的,稱為監(jiān)聽socket描述字(監(jiān)聽套接字)

連接套接字:一個套接字會從主動連接的套接字變身為一個監(jiān)聽套接字;而accept函數(shù)返回的是已連接socket描述字(一個連接套接字),它代表著一個網(wǎng)絡(luò)已經(jīng)存在的點點連接。

一個服務(wù)器通常通常僅僅只創(chuàng)建一個監(jiān)聽socket描述字,它在該服務(wù)器的生命周期內(nèi)一直存在。內(nèi)核為每個由服務(wù)器進程接受的客戶連接創(chuàng)建了一個已連接socket描述字,當服務(wù)器完成了對某個客戶的服務(wù),相應(yīng)的已連接socket描述字就被關(guān)閉。

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

連接套接字socketfd_new 并沒有占用新的端口與客戶端通信,依然使用的是與監(jiān)聽套接字socketfd一樣的端口號

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

萬事具備只欠東風,至此服務(wù)器與客戶已經(jīng)建立好連接了??梢哉{(diào)用網(wǎng)絡(luò)I/O進行讀寫操作了,即實現(xiàn)了網(wǎng)咯中不同進程之間的通信!網(wǎng)絡(luò)I/O操作有下面幾組:

  • read()/write()
  • recv()/send()
  • readv()/writev()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto()

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

#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函數(shù)是負責從fd中讀取內(nèi)容.當讀成功時,read返回實際所讀的字節(jié)數(shù),如果返回的值是0表示已經(jīng)讀到文件的結(jié)束了,小于0表示出現(xiàn)了錯誤。如果錯誤為EINTR說明讀是由中斷引起的,如果是ECONNREST表示網(wǎng)絡(luò)連接出了問題。

write函數(shù)將buf中的nbytes字節(jié)內(nèi)容寫入文件描述符fd.成功時返回寫的字節(jié)數(shù)。失敗時返回-1,并設(shè)置errno變量。 在網(wǎng)絡(luò)程序中,當我們向套接字文件描述符寫時有倆種可能。1)write的返回值大于0,表示寫了部分或者是全部的數(shù)據(jù)。2)返回的值小于0,此時出現(xiàn)了錯誤。我們要根據(jù)錯誤類型來處理。如果錯誤為EINTR表示在寫的時候出現(xiàn)了中斷錯誤。如果為EPIPE表示網(wǎng)絡(luò)連接出現(xiàn)了問題(對方已經(jīng)關(guān)閉了連接)。

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

4.6、close()函數(shù)

在服務(wù)器與客戶端建立連接之后,會進行一些讀寫操作,完成了讀寫操作就要關(guān)閉相應(yīng)的socket描述字,好比操作完打開的文件要調(diào)用fclose關(guān)閉打開的文件。

#include < unistd.h >
int close(int fd);

close一個TCP socket的缺省行為時把該socket標記為以關(guān)閉,然后立即返回到調(diào)用進程。該描述字不能再由調(diào)用進程使用,也就是說不能再作為read或write的第一個參數(shù)。

注意:close操作只是使相應(yīng)socket描述字的引用計數(shù)-1,只有當引用計數(shù)為0的時候,才會觸發(fā)TCP客戶端向服務(wù)器發(fā)送終止連接請求。

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

TCP協(xié)議通過三個報文段完成連接的建立,這個過程稱為三次握手(three-way handshake),過程如下圖所示。

第一次握手:建立連接時,客戶端發(fā)送syn包(syn=j)到服務(wù)器,并進入SYN_SEND狀態(tài),等待服務(wù)器確認;SYN:同步序列編號(Synchronize Sequence Numbers)。

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

對應(yīng)的函數(shù)接口:

圖片

從圖中可以看出,當客戶端調(diào)用connect時,觸發(fā)了連接請求,向服務(wù)器發(fā)送了SYN J包,這時connect進入阻塞狀態(tài);服務(wù)器監(jiān)聽到連接請求,即收到SYN J包,調(diào)用accept函數(shù)接收請求向客戶端發(fā)送SYN K ,ACK J+1,這時accept進入阻塞狀態(tài);客戶端收到服務(wù)器的SYN K ,ACK J+1之后,這時connect返回,并對SYN K進行確認;服務(wù)器收到ACK K+1時,accept返回,至此三次握手完畢,連接建立。

我們可以通過網(wǎng)絡(luò)抓包的查看具體的流程:

比如我們服務(wù)器開啟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是服務(wù)器端
  • [S] 表示這是一個SYN請求
  • [S.] 表示這是一個SYN+ACK確認包:
  • [.] 表示這是一個ACT確認包, (client)SYN->(server)SYN->(client)ACT 就是3次握手過程
  • [P] 表示這個是一個數(shù)據(jù)推送,可以是從服務(wù)器端向客戶端推送,也可以從客戶端向服務(wù)器端推
  • [F] 表示這是一個FIN包,是關(guān)閉連接操作,client/server都有可能發(fā)起
  • [R] 表示這是一個RST包,與F包作用相同,但RST表示連接關(guān)閉時,仍然有數(shù)據(jù)未被處理??梢岳斫鉃槭菑娭魄袛噙B接
  • win 4099 是指滑動窗口大小
  • length 18指數(shù)據(jù)包的大小

我們看到 (1)(2)(3)三步是建立tcp:

第一次握手:

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

客戶端IP localhost.39870 (客戶端的端口一般是自動分配的) 向服務(wù)器localhost.9502 發(fā)送syn包(syn=j)到服務(wù)器》

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

第二次握手:

14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379,

收到請求并確認:服務(wù)器收到syn包,并必須確認客戶的SYN(ack=j+1),同時自己也發(fā)送一個SYN包(syn=k),即SYN+ACK包:
此時服務(wù)器主機自己的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,

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

客戶端和服務(wù)器進入ESTABLISHED狀態(tài)后,可以進行通信數(shù)據(jù)交互。此時和accept接口沒有關(guān)系,即使沒有accepte,也進行3次握手完成。

連接出現(xiàn)連接不上的問題,一般是網(wǎng)路出現(xiàn)問題或者網(wǎng)卡超負荷或者是連接數(shù)已經(jīng)滿啦。

紫色背景的部分:

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

客戶端向服務(wù)器發(fā)送長度為7個字節(jié)的數(shù)據(jù),

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

服務(wù)器向客戶確認已經(jīng)收到數(shù)據(jù)

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

然后服務(wù)器同時向客戶端寫入數(shù)據(jù)。

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

客戶端向服務(wù)器確認已經(jīng)收到數(shù)據(jù)

這個就是tcp可靠的連接,每次通信都需要對方來確認。

6. TCP連接的終止(四次握手釋放)

建立一個連接需要三次握手,而終止一個連接要經(jīng)過四次握手,這是由TCP的半關(guān)閉(half-close)造成的,如圖:

圖片

由于TCP連接是全雙工的,因此每個方向都必須單獨進行關(guān)閉。這個原則是當一方完成它的數(shù)據(jù)發(fā)送任務(wù)后就能發(fā)送一個FIN來終止這個方向的連接。收到一個 FIN只意味著這一方向上沒有數(shù)據(jù)流動,一個TCP連接在收到一個FIN后仍能發(fā)送數(shù)據(jù)。首先進行關(guān)閉的一方將執(zhí)行主動關(guān)閉,而另一方執(zhí)行被動關(guān)閉。

(1)客戶端A發(fā)送一個FIN,用來關(guān)閉客戶A到服務(wù)器B的數(shù)據(jù)傳送(報文段4)。

(2)服務(wù)器B收到這個FIN,它發(fā)回一個ACK,確認序號為收到的序號加1(報文段5)。和SYN一樣,一個FIN將占用一個序號。

(3)服務(wù)器B關(guān)閉與客戶端A的連接,發(fā)送一個FIN給客戶端A(報文段6)。

(4)客戶端A發(fā)回ACK報文確認,并將確認序號設(shè)置為收到序號加1(報文段7)。

對應(yīng)函數(shù)接口如圖:

圖片

過程如下:

  • 某個應(yīng)用進程首先調(diào)用close主動關(guān)閉連接,這時TCP發(fā)送一個FIN M;
  • 另一端接收到FIN M之后,執(zhí)行被動關(guān)閉,對這個FIN進行確認。它的接收也作為文件結(jié)束符傳遞給應(yīng)用進程,因為FIN的接收意味著應(yīng)用進程在相應(yīng)的連接上再也接收不到額外數(shù)據(jù);
  • 一段時間之后,接收到文件結(jié)束符的應(yīng)用進程調(diào)用close關(guān)閉它的socket。這導(dǎo)致它的TCP也發(fā)送一個FIN N;
  • 接收到這個FIN的源發(fā)送端TCP對它進行確認。

這樣每個方向上都有一個FIN和ACK。

1.為什么建立連接協(xié)議是三次握手,而關(guān)閉連接卻是四次握手呢?

這是因為服務(wù)端的LISTEN狀態(tài)下的SOCKET當收到SYN報文的建連請求后,它可以把ACK和SYN(ACK起應(yīng)答作用,而SYN起同步作用)放在一個報文里來發(fā)送。但關(guān)閉連接時,當收到對方的FIN報文通知時,它僅僅表示對方?jīng)]有數(shù)據(jù)發(fā)送給你了;但未必你所有的數(shù)據(jù)都全部發(fā)送給對方了,所以你可以未必會馬上會關(guān)閉SOCKET,也即你可能還需要發(fā)送一些數(shù)據(jù)給對方之后,再發(fā)送FIN報文給對方來表示你同意現(xiàn)在可以關(guān)閉連接了,所以它這里的ACK報文和FIN報文多數(shù)情況下都是分開發(fā)送的。

2.為什么TIME_WAIT狀態(tài)還需要等2MSL后才能返回到CLOSED狀態(tài)?

這是因為雖然雙方都同意關(guān)閉連接了,而且握手的4個報文也都協(xié)調(diào)和發(fā)送完畢,按理可以直接回到CLOSED狀態(tài)(就好比從SYN_SEND狀態(tài)到ESTABLISH狀態(tài)那樣);但是因為我們必須要假想網(wǎng)絡(luò)是不可靠的,你無法保證你最后發(fā)送的ACK報文會一定被對方收到,因此對方處于LAST_ACK狀態(tài)下的SOCKET可能會因為超時未收到ACK報文,而重發(fā)FIN報文,所以這個TIME_WAIT狀態(tài)的作用就是用來重發(fā)可能丟失的ACK報文。

7. Socket編程實例

服務(wù)器端:一直監(jiān)聽本機的8000號端口,如果收到連接請求,將接收請求并接收客戶端發(fā)來的消息,并向客戶端返回消息。

/* 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地址設(shè)置成INADDR_ANY,讓系統(tǒng)自動獲取本機的IP地址。
    servaddr.sin_port = htons(DEFAULT_PORT);//設(shè)置的端口為DEFAULT_PORT

 //將本地地址綁定到所創(chuàng)建的套接字上
 if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
 printf("bind socket error: %s(errno: %d)n",strerror(errno),errno);
 exit(0);
    }
 //開始監(jiān)聽是否有客戶端連接
 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;
    }
//接受客戶端傳過來的數(shù)據(jù)
    n = recv(connect_fd, buff, MAXLINE, 0);
//向客戶端發(fā)送回應(yīng)數(shù)據(jù)
 if(!fork()){ /*紫禁城*/
 if(send(connect_fd, "Hello,you are connected!n", 26,0) == -1)
        perror("send error");
        close(connect_fd);
 exit(0);
    }
    buff[n] = '?';
 printf("recv msg from client: %sn", 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 %sn",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]  = '?';
 printf("Received : %s ",buf);
    close(sockfd);
 exit(0);
}

inet_pton 是Linux下IP地址轉(zhuǎn)換函數(shù),可以在將IP地址在“點分十進制”和“整數(shù)”之間轉(zhuǎn)換 ,是inet_addr的擴展。

int inet_pton(int af, const char *src, void *dst);//轉(zhuǎn)換字符串到網(wǎng)絡(luò)地址:

第一個參數(shù)af是地址族,轉(zhuǎn)換后存在dst中
af = AF_INET:src為指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函數(shù)將該地址轉(zhuǎn)換為in_addr的結(jié)構(gòu)體,并復(fù)制在dst中
af =AF_INET6:src為指向IPV6的地址,函數(shù)將該地址轉(zhuǎn)換為in6_addr的結(jié)構(gòu)體,并復(fù)制在
dst中
如果函數(shù)出錯將返回一個負值,并將errno設(shè)置為EAFNOSUPPORT,如果參數(shù)af指定的地址族和src格式不對,函數(shù)將返回0。

測試:

編譯server.c

gcc -o server server.c

啟動進程:

./server

顯示結(jié)果:

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

并等待客戶端連接。

編譯 client.c

gcc -o client client.c

客戶端去連接server:

./client 127.0.0.1

等待輸入消息

圖片

發(fā)送一條消息,輸入:c++

圖片

此時服務(wù)器端看到:

圖片

客戶端收到消息:

圖片

其實可以不用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/目錄下,手動把這個文件添加到這個目錄下就可以了。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 通信
    +關(guān)注

    關(guān)注

    18

    文章

    5931

    瀏覽量

    135722
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11182

    瀏覽量

    208540
  • 操作系統(tǒng)
    +關(guān)注

    關(guān)注

    37

    文章

    6625

    瀏覽量

    123052
  • 編程
    +關(guān)注

    關(guān)注

    88

    文章

    3545

    瀏覽量

    93500
收藏 人收藏

    評論

    相關(guān)推薦

    基于LinuxSocket網(wǎng)絡(luò)編程如何實現(xiàn)?

    隨著Intenet的日益發(fā)展和普及,網(wǎng)絡(luò)在嵌入式系統(tǒng)中應(yīng)用非常廣泛,越來越多的嵌入式設(shè)備采用Linux操作系統(tǒng)。Linux是一個源代碼公開的免費操作系統(tǒng),具有強移植性,所以對基于LinuxS
    發(fā)表于 09-03 08:03

    實戰(zhàn)Linux Socket編程

    實戰(zhàn)Linux Socket編程
    發(fā)表于 03-03 10:17

    基于LinuxSocket網(wǎng)絡(luò)編程的性能優(yōu)化

    介紹Linux操作系統(tǒng)環(huán)境下Socket網(wǎng)絡(luò)編程的原理、流程和最終實現(xiàn)。編程采用客戶端/服務(wù)器模式。提出解決多個客戶端連接服務(wù)器時無法處理
    發(fā)表于 12-28 16:34 ?31次下載

    基于LinuxSocket網(wǎng)絡(luò)編程的性能優(yōu)化

    基于LinuxSocket網(wǎng)絡(luò)編程的性能優(yōu)化 隨著Intenet的日益發(fā)展和普及,網(wǎng)絡(luò)在嵌入式系統(tǒng)中應(yīng)用非常廣泛,越來越多的嵌入式設(shè)備采用Linux操作系統(tǒng)。
    發(fā)表于 10-22 20:48 ?1067次閱讀
    基于<b class='flag-5'>Linux</b>的<b class='flag-5'>Socket</b>網(wǎng)絡(luò)<b class='flag-5'>編程</b>的性能優(yōu)化

    Linux+Socket編程

    本內(nèi)容詳細講述了Linux+Socket編程技巧指南,適合所有學(xué)習(xí)編程的廣大用戶使用
    發(fā)表于 06-10 11:19 ?0次下載
    <b class='flag-5'>Linux+Socket</b><b class='flag-5'>編程</b>

    SOCKEt編程介紹

    這個文檔是一個指南,而不是參考書。如果你剛開始 socket 編程并想找一本入門書,那么你是我的讀者。但這不是一本完全的 socket 編程書。
    發(fā)表于 03-02 15:36 ?4次下載

    Linux下基于Socket的網(wǎng)絡(luò)編程

    linux開發(fā)編程教程資料——Linux下基于Socket的網(wǎng)絡(luò)編程,感興趣的小伙伴們可以看一看。
    發(fā)表于 08-23 16:23 ?0次下載

    Linux-socket網(wǎng)絡(luò)編程

    linux開發(fā)編程教程資料——Linux-socket網(wǎng)絡(luò)編程,感興趣的小伙伴們可以看一看。
    發(fā)表于 08-23 16:23 ?0次下載

    LinuxSocket網(wǎng)絡(luò)編程

    linux開發(fā)編程教程資料——LinuxSocket網(wǎng)絡(luò)編程,感興趣的小伙伴們可以看一看。
    發(fā)表于 08-23 16:23 ?0次下載

    linux-socket網(wǎng)絡(luò)編程詳解

    linux開發(fā)編程教程資料——linux-socket網(wǎng)絡(luò)編程詳解,感興趣的小伙伴們可以看一看。
    發(fā)表于 08-23 16:23 ?0次下載

    linux-網(wǎng)絡(luò)編程-socket編程

    linux開發(fā)編程教程資料——linux-網(wǎng)絡(luò)編程-socket編程,感興趣的小伙伴們可以看一看
    發(fā)表于 08-23 16:23 ?0次下載

    Socket網(wǎng)絡(luò)基礎(chǔ)編程

    10.2.1 socket概述 1.socket定義 在Linux中的網(wǎng)絡(luò)編程是通過socket接口來進行的。人們常說的
    發(fā)表于 10-18 17:13 ?3次下載

    C++中網(wǎng)絡(luò)編程Socket介紹

    Socket Linux Socket 編程(不限 LinuxSocket 中的 read(
    的頭像 發(fā)表于 10-26 10:10 ?2054次閱讀
    C++中網(wǎng)絡(luò)<b class='flag-5'>編程</b><b class='flag-5'>Socket</b><b class='flag-5'>介紹</b>

    Socket 網(wǎng)絡(luò)編程框架介紹

    Socket 網(wǎng)絡(luò)編程框架 Socket(套接字)是一個網(wǎng)絡(luò)編程概念,描述了一個通信端點(Endpoint),用于建立網(wǎng)絡(luò)連接(Connection)并傳輸數(shù)據(jù)。
    的頭像 發(fā)表于 11-09 14:19 ?958次閱讀
    <b class='flag-5'>Socket</b> 網(wǎng)絡(luò)<b class='flag-5'>編程</b>框架<b class='flag-5'>介紹</b>

    如何理解socket編程接口

    Socket編程接口是一種網(wǎng)絡(luò)編程的基本概念,它提供了一種在不同計算機之間進行通信的方法。 Socket編程接口的基本概念 1.1
    的頭像 發(fā)表于 08-16 10:48 ?289次閱讀