1、如何唯一標識一個進程:
在本地我們通過進程PID來唯一標識一個進程,但這在網絡中是行不通的。TCP/IP協議族幫我們解決了這個問題,網絡層ip地址可以唯一標識網絡中的主機,而傳輸層的協議+端口可以唯一標識主機中的進程。這樣利用三元組(ip地址,協議,端口)就可以標識網絡的進程了,網絡中的進程通信就可以利用這個標志與其他進程進行交互。
2、socket
使用TCP/IP協議的應用程序通常采用應用編程接口:socket來實現網絡進程之間的通信。socket起源于Unix,而Unix基本哲學之一就是“一切皆文件”,都可以用“打開open”->”讀寫write/read”->“關閉close”模式來操作。socket就是該模式的一個實現,一些socket函數就是對其進行的操作(I/O、打開、關閉)。
3、socket基本操作
3.1socket函數
int socket(int domain,int type,int protocol);
socket函數 對應于普通文件的打開操作。普通文件的打開操作返回一個文件描述字,而socket()用于創建一個socket描述符,它唯一標識一個socket。這個socket描述字跟文件描述字一樣,后續的操作都有用到它,把它作為參數,通過它來進行一些讀寫操作。正如可以給fopen傳入不同參數值以打開不同的文件。創建socket的時候,也可以指定不同的參數創建不同的socket描述符,socket函數的三個參數分別為:
domain:即協議域,又稱為協議族(family)。常用的協議族有,AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE等等。協議族決定了socket的地址類型,在通信中必須采用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合。
type:指定socket類型。常用的socket類型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等
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描述字存在于協議族中,但沒有一個具體的地址。如果想要給它賦值一個地址,就必須調用bind()函數,否則就當調用connect()、listen()時系統會自動隨機分配一個端口。
3.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 */
};
addrlen:對應的是地址的長度。
通常服務器在啟動的時候都會綁定一個眾所周知的地址(如ip地址+端口號),用于提供服務,客戶就可以通過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是為什么通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。
3.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服務器的連接。
3.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);
accept函數的第一個參數為服務器的socket描述字,第二個參數為指向struct sockaddr *的指針,用于返回客戶端的協議地址,第三個參數為協議地址的長度。如果accpet成功,那么其返回值是由內核自動生成的一個全新的描述字,代表與返回客戶的TCP連接。
注意:accept的第一個參數為服務器的socket描述字,是服務器開始調用socket()函數生成的,稱為監聽socket描述字;而accept函數返回的是已連接的socket描述字。一個服務器通常通常僅僅只創建一個監聽socket描述字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建了一個已連接socket描述字,當服務器完成了對某個客戶的服務,相應的已連接socket描述字就被關閉。
3.5、read()、write()等函數
萬事具備只欠東風,至此服務器與客戶已經建立好連接了。可以調用網絡I/O進行讀寫操作了,即實現了網咯中不同進程之間的通信!網絡I/O操作有下面幾組:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
我推薦使用recvmsg()/sendmsg()函數,這兩個函數是最通用的I/O函數,實際上可以把上面的其它函數都替換成這兩個函數。
3.6、close()函數
在服務器與客戶端建立連接之后,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述字,好比操作完打開的文件要調用fclose關閉打開的文件。
int close(int fd);
close一個TCP socket的缺省行為時把該socket標記為以關閉,然后立即返回到調用進程。該描述字不能再由調用進程使用,也就是說不能再作為read或write的第一個參數。
注意:close操作只是使相應socket描述字的引用計數-1,只有當引用計數為0的時候,才會觸發TCP客戶端向服務器發送終止連接請求。
4、socket中TCP的三次握手建立連接詳解
我們知道tcp建立連接要進行“三次握手”,即交換三個分組。大致流程如下:
客戶端向服務器發送一個SYN J
服務器向客戶端響應一個SYN K,并對SYN J進行確認ACK J+1
客戶端再想服務器發一個確認ACK K+1
只有就完了三次握手,但是這個三次握手發生在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返
回,至此三次握手完畢,連接建立。
總結:客戶端的connect在三次握手的第二次返回,而服務器端的accept在三次握手的第三次返回。
5、socket中TCP的四次握手釋放連接詳解
上面介紹了socket中TCP的三次握手建立過程,及其涉及的socket函數。現在我們介紹socket中的四次握手釋放連接的過程,請看下圖:
圖示過程如下:
某個應用進程首先調用close主動關閉連接,這時TCP發送一個FIN M;
另一端接收到FIN M之后,執行被動關閉,對這個FIN進行確認。它的接收也作為文件結束符傳遞給應用進程,因為FIN的接收意味著應用進程在相應的連接上再也接收不到額外數據;
一段時間之后,接收到文件結束符的應用進程調用close關閉它的socket。這導致它的TCP也發送一個FIN N;
接收到這個FIN的源發送端TCP對它進行確認。
這樣每個方向上都有一個FIN和ACK。