我們深諳信息交流的價值,那網絡中進程之間如何通信,如我們每天打開瀏覽器瀏覽網頁時,瀏覽器的進程怎么與web服務器通信的?當你用qq聊天時,qq進程怎么與服務器或你好友所在的qq進程通信?這些都得靠socket?那什么是socket?socket的類型有哪些?還有socket的基本函數,這些都是本文想介紹的。本文的主要內容如下:
1、網絡中進程之間如何通信?
本地的進程間通信(ipc)有很多種方式,但可以總結為下面4類:
- 消息傳遞(管道、fifo、消息隊列)
- 同步(互斥量、條件變量、讀寫鎖、文件和寫記錄鎖、信號量)
- 共享內存(匿名的和具名的)
- 遠程過程調用(solaris門和sun rpc)
但這些都不是本文的主題!我們要討論的是網絡中進程之間如何通信?首要解決的問題是如何唯一標識一個進程,否則通信無從談起!在本地可以通過進程pid來唯一標識一個進程,但是在網絡中這是行不通的。其實tcp/ip協議族已經幫我們解決了這個問題,網絡層的“ip地址”可以唯一標識網絡中的主機,而傳輸層的“協議+端口”可以唯一標識主機中的應用程序(進程)。這樣利用三元組(ip地址,協議,端口)就可以標識網絡的進程了,網絡中的進程通信就可以利用這個標志與其它進程進行交互。
使用tcp/ip協議的應用程序通常采用應用編程接口:unix bsd的套接字(socket)和unix system v的tli(已經被淘汰),來實現網絡進程之間的通信。就目前而言,幾乎所有的應用程序都是采用socket,而現在又是網絡時代,網絡中進程通信是無處不在,這就是我為什么說“一切皆socket”。
2、什么是socket?
上面我們已經知道網絡中的進程是通過socket來通信的,那什么是socket呢?socket起源于unix,而unix/linux基本哲學之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關閉close”模式來操作。我的理解就是socket就是該模式的一個實現,socket即是一種特殊的文件,一些socket函數就是對其進行的操作(讀/寫io、打開、關閉),這些函數我們在后面進行介紹。
socket一詞的起源
在組網領域的首次使用是在1970年2月12日發布的文獻ietf rfc33中發現的,撰寫者為stephen carr、steve crocker和vint cerf。根據美國計算機歷史博物館的記載,croker寫道:“命名空間的元素都可稱為套接字接口。一個套接字接口構成一個連接的一端,而一個連接可完全由一對套接字接口規定。”計算機歷史博物館補充道:“這比bsd的套接字接口定義早了大約12年。”
3、socket的基本操作
既然socket是“open—write/read—close”模式的一種實現,那么socket就提供了這些操作對應的函數接口。下面以tcp為例,介紹幾個基本的socket接口函數。
3.1、socket()函數
1
|
int socket( int domain, int type, int protocol); |
socket函數對應于普通文件的打開操作。普通文件的打開操作返回一個文件描述字,而socket()用于創建一個socket描述符(socket descriptor),它唯一標識一個socket。這個socket描述字跟文件描述字一樣,后續的操作都有用到它,把它作為參數,通過它來進行一些讀寫操作。
正如可以給fopen的傳入不同參數值,以打開不同的文件。創建socket的時候,也可以指定不同的參數創建不同的socket描述符,socket函數的三個參數分別為:
- domain:即協議域,又稱為協議族(family)。常用的協議族有,af_inet、af_inet6、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()時系統會自動隨機分配一個端口。
3.2、bind()函數
正如上面所說bind()函數把一個地址族中的特定地址賦給socket。例如對應af_inet、af_inet6就是把一個ipv4或ipv6地址和端口號組合賦給socket。
1
|
int bind( int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
函數的三個參數分別為:
•sockfd:即socket描述字,它是通過socket()函數創建了,唯一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。
•addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址。這個地址結構根據地址創建socket時的地址協議族的不同而不同,如ipv4對應的是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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域對應的是:
1
2
3
4
5
6
|
#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。
3.3、listen()、connect()函數
如果作為一個服務器,在調用socket()、bind()之后就會調用listen()來監聽這個socket,如果客戶端這時調用connect()發出連接請求,服務器端就會接收到這個請求。
1
2
|
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操作。
1
|
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函數,實際上可以把上面的其它函數都替換成這兩個函數。它們的聲明如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#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。
3.6、close()函數
在服務器與客戶端建立連接之后,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述字,好比操作完打開的文件要調用fclose關閉打開的文件。
1
2
|
#include <unistd.h> 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的那幾個函數中呢?請看下圖:
圖1、socket中發送的tcp三次握手
從圖中可以看出,當客戶端調用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中的四次握手釋放連接的過程,請看下圖:
圖2、socket中發送的tcp四次握手
圖示過程如下:
- 某個應用進程首先調用close主動關閉連接,這時tcp發送一個fin m;
- 另一端接收到fin m之后,執行被動關閉,對這個fin進行確認。它的接收也作為文件結束符傳遞給應用進程,因為fin的接收意味著應用進程在相應的連接上再也接收不到額外數據;
- 一段時間之后,接收到文件結束符的應用進程調用close關閉它的socket。這導致它的tcp也發送一個fin n;
- 接收到這個fin的源發送端tcp對它進行確認。
這樣每個方向上都有一個fin和ack。
6、一個例子(實踐一下)
說了這么多了,動手實踐一下。下面編寫一個簡單的服務器、客戶端(使用tcp)——服務器端一直監聽本機的6666號端口,如果收到連接請求,將接收請求并接收客戶端發來的消息;客戶端與服務器端建立連接并發送一條消息。
服務器端代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
#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 listenfd, connfd; struct sockaddr_in servaddr; char buff[4096]; int n; if ( (listenfd = 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); servaddr.sin_port = htons(6666); if ( bind(listenfd, ( struct sockaddr*)&servaddr, sizeof (servaddr)) == -1){ printf ( "bind socket error: %s(errno: %d)\n" , strerror ( errno ), errno ); exit (0); } if ( listen(listenfd, 10) == -1){ printf ( "listen socket error: %s(errno: %d)\n" , strerror ( errno ), errno ); exit (0); } printf ( "======waiting for client's request======\n" ); while (1){ if ( (connfd = accept(listenfd, ( struct sockaddr*)null, null)) == -1){ printf ( "accept socket error: %s(errno: %d)" , strerror ( errno ), errno ); continue ; } n = recv(connfd, buff, maxline, 0); buff[n] = '\0' ; printf ( "recv msg from client: %s\n" , buff); close(connfd); } close(listenfd); } |
客戶端代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
#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; char recvline[4096], sendline[4096]; 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(6666); 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); } close(sockfd); exit (0); } |
當然上面的代碼很簡單,也有很多缺點,這就只是簡單的演示socket的基本函數使用。其實不管有多復雜的網絡程序,都使用的這些基本函數。上面的服務器使用的是迭代模式的,即只有處理完一個客戶端請求才會去處理下一個客戶端的請求,這樣的服務器處理能力是很弱的,現實中的服務器都需要有并發處理能力!為了需要并發處理,服務器需要fork()一個新的進程或者線程去處理請求等。
7、動動手
留下一個問題,歡迎大家回帖回答!!!是否熟悉linux下網絡編程?如熟悉,編寫如下程序完成如下功能:
服務器端:
接收地址192.168.100.2的客戶端信息,如信息為“client query”,則打印“receive query”
客戶端:
向地址192.168.100.168的服務器端順序發送信息“client query test”,“cleint query”,“client query quit”,然后退出。
題目中出現的ip地址可以根據實際情況定。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html