TCP/IP 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檔案系統: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的定義如下:
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文件或者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)
---------------------
作者:規速
來源:CSDN
原文:https://blog.csdn.net/hguisu/article/details/7445768
版權宣告:本文為博主原創文章,轉載請附上博文連結!