詳見:https://github.com/ZhangzheBJUT/linux/blob/master/IPC(%E5%9B%9B).md
七 套接字
7.1. 套接字簡介
之前所討論的IPC機制都是依靠一臺計算機共享系統資源實現的,這些資源可以是文件系統(命名管道)、共享的物理內存(共享內存)和消息隊列,這些只有運行在同一臺機器上的進程可以使用。
伯克利版本的UNIX系統引入了一種新的通信工具-套接字接口(socket interface)。一臺機器上的進程可以通過使用套接字和另外一臺機器上的進程通信,這樣就可以支持分佈在網絡中的客戶/服務器系統。同一臺機器上的進程也可以直接通過套接字進行通信。
套接字是一種通信機制,憑藉這種機制客戶/服務器系統的開發工作既可以在本地單機上進行,也可以跨網絡進行。Linux所提供的功能(如打印服務、連接數據庫和提供Web頁面)和網絡工具(如用於遠程登錄的rlogin和用於文件傳輸的ftp)通常都是通過套接字來進行通信的。
套接字的工作過程如上圖所示:
對於服務端來說
- 服務器進程使用系統調用socket來創建一個套接字,它是系統分配給該服務器進程的類似文件描述符的資源,不能與其它進程共享。
- 服務器進程使用系統調用bind來給套接字起個名字。本地套接字的名字是Linux文件系統中的文件名,一般放在/tmp或/usr/tmp目錄中。對於網絡套接字,它的名字是與客戶端連接的特定網絡有關的服務標識符(端口號或訪問點)。這個標識符允許Linux將進入的針對特定端口號的連接轉到正確的服務器進程。例如,Web服務器一般在80端口上創建一個套接字,它是一個專門用於此目的的標識符。而Web客戶端(比如:瀏覽器)知道對於用戶想要訪問的Web的站點應該使用端口號80來建立http連接。
- 服務器進程使用系統調用listen創建一個隊列並將其用於存放來自客戶的進入連接。此時,服務端開始等待客戶端的連接。
- 服務器進程使用系統調用accept接受客戶端的連接。在調用accept時,它會創建一個與原來的命名套接字不同的新的套接字。這個新套接字只用於與這個特定的客戶端進行通信,而命名套接字則被保留下來繼續處理來自其它客戶的連接。
對於客戶端來說
- 基於套接字系統的客戶端更加簡單,客戶首先調用socket創建一個未命名的套接字。
- 然後將服務器的命名套接字作爲一個地址來調用connect與服務端建立連接。
- 一旦建立連接,就可以像是使用底層的文件描述符那樣用套接字來實現雙向的數據通信。
- 在使用完後雙方調用close來關閉連接
7.2 socket相關函數
函數原型:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
int bind(int socke,const struct sockaddr *address,size_t address_len);
int listen(int socket,int backlog);
int accept(int socket,0 struct sockaddr *address,size_t address_len);
int connetc(int socket,const struct sockaddr *address,size_t address_len);
int close(int socket)
函數描述:
socket函數:創建套接字
創建一個套接字並返回一個描述符,該描述符可以用來訪問該套接字。
套接字由三個屬性確定:域(協議族 protocol family),類型和協議。
domain(域): 指定套接字通信中使用的網絡介質,最常見的套接字是AF_INET和AF_UNIX,前者指的是
Internet網絡。AF_UNIX是利用UNIX或Linux文件系統實現本地套接字,這個域的底層
協議就是文件的輸入\輸出,而它的地址就是文件名。 其它的域還包括:AF_ISO 和 AF_XNS。
type(類型): 分爲流套接字和數據包套接字
流套接字 (SOCK_STREAM):提供一個可靠的、有序的、雙向的字節流的連接。
數據報套接字(SOCK_DGRAM):無序的、不可靠、 但速度快是基於數據報的服務。服務器通常不保
留連接信息,所以它們可以在不打擾其客戶的前提下停止並重啓。
protocol(協議): 底層傳輸機制允許不止一個協議來提供要求的套接字類型,可以爲套接字選擇一個特定的協議。
bind函數:命名套接字
將參數address中的地址分配給文件描述符socket關聯的未命名的套接字,地址結構長度由參數address_len指定。
命名對於AF_UNIX域的套接字來說就是關聯到一個文件系統的路徑名,對於AF_INET域的套接字來說就是關聯到一個IP端口號。
端口號用來標識服務,知名服務所分配的端口號所在Linux和UNIX機器上都有一樣的,它們通常小於1024。
套接字地址都有由一個套接字域的成員開始,每個套接字域都有自己的地址格式,對於AF_UNIX套接字來說,它的地址由結構
sockaddr_un來描述,該結構定義在頭文件sys/un.h中:
struct sockaddr_un{
sa_family_t sum_family;
char sunpath[];
}
AF_INET由sockaddr_in來指定,包括套接字域、IP地址和端口號
struct sockaddr_in{
short int sin_family; //AF_INET
unsigned short int; //Prot Number
struct in_addr; //Internet address
}
struct in_addr {
unsigned long int s_addr;
}
地址的長度和格式取決於地址組,bind需要將一個特定的地址結構指針轉化爲指向通用地址類型(struct sockaddr*)。
函數調用成功時,返回0,失敗時返回-1。
listen函數:創建套接字隊列
創建套接字緩存隊列,用於設置等待處理的進入連接的個數
等待處理的進入連接的個數最多不能超過這個數字,再往後的連接將被拒絕,導致客戶的連接請求失敗。
函數調用成功時返回0,失敗時返回-1。
connetc函數: 請求連接
參數socket指定的套接字將連接到參數address指定的服務器套接字,address指向的結構的長度由參數address_len指定。
注:參數socket指定的套接字必須是通過socket調用獲得的一個有效的文件描述符。
成功時返回0,失敗返回-1,可能的錯誤碼:
EBADF 文件描述符無效
EALREADY 該套接字上已有一個正在進行的中的連接
ETIMEDOUT 連接超時
ECONNREFUSED 連接請求被服務器拒絕
accept函數: 接受連接
將創建一個新的套接字來與客戶端通信,並且返回新套接字的描述符。新套接字的類型和服務器
監聽套接字類型是一樣的。
連接客戶的地址被放入address參數指定的sockaddr結構中,如果不關心客戶的地址,可以將address參數指針設爲空。參數
address_len指定客戶結構的長度,如果客戶地址的長度超過這個值,它將被截斷。在調用accept之前,address_len必須
設置爲預期的地址長度。當這個調用返回時,address_len將被設置爲連接客戶地址結構的實際長度。
如果隊列中沒有未處理的連接請求,accept會阻塞直到有客戶請求建立連接爲止。這一行爲可以被改變,例如用
int flag = fcntl(socket, F_GETFL, O_NONBLOCK);
fcntl函數將套接字描述符的flags設爲O_NONBLOCK,因此隊列中沒有未處理的連接時,accept不會
阻塞,而是返回-1,並設errno爲EWOULDBLOCK,如果accept被信號中斷,errno則設爲EINTR。可以
使用fcntl函數將套接字描述符的標誌設回0。
close函數:關閉套接字
用來終止服務器和客戶上的套接字連接,就如同對底層文件描述符進行關閉一樣。
7.3 使用實例
AF_UNIX類型套接字
client1.c
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
int sockfd;
int len;
struct sockaddr_un address;
int result;
char ch = 'A';
//create socket
sockfd = socket(AF_UNIX,SOCK_STREAM,0);
address.sun_family = AF_UNIX;
strcpy(address.sun_path,"server_socket");
len = sizeof(address);
//連接套接字
result = connect(sockfd,(struct sockaddr*)&address,len);
if (result==-1)
{
perror("oops:client");
exit(1);
}
write(sockfd,&ch,1);
read(sockfd,&ch,1);
printf("char from server = %c\n",ch);
close(sockfd);
exit(0);
}
server1.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int server_sockfd,client_sockfd;
int server_len,client_len;
struct sockaddr_un server_address;
struct sockaddr_un client_address;
unlink("server_socket");
//創建套接字
server_sockfd = socket(AF_UNIX,SOCK_STREAM,0);
server_address.sun_family = AF_UNIX;
strcpy(server_address.sun_path,"server_socket");
server_len = sizeof(server_address);
//命名套接字
bind(server_sockfd,(struct sockaddr*)&server_address,server_len);
//創建套接字隊列
listen(server_sockfd,5);
//接受連接
while(1)
{
char ch;
printf("server waiting\n");
client_len = sizeof(client_address);
client_sockfd=accept(server_sockfd,(struct sockaddr*)&client_address,&client_len);
read(client_sockfd,&ch,1);
printf("get data from server:%c\n",ch);
ch++;
write(client_sockfd,&ch,1);
//關閉套接字
close(client_sockfd);
}
}
AF_NET類型套接字
client3.c
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd;
int len;
struct sockaddr_in address;
int result;
char ch= 'A';
sockfd = socket(AF_INET,SOCK_STREAM,0);
address.sin_family = AF_INET;
address.sin_addr.s_addr = inet_addr("127.0.0.1");
address.sin_port = htons(9734);//9734;
len = sizeof(address);
//連接套接字
result = connect(sockfd,(struct sockaddr*)&address,len);
if (result==-1)
{
perror("oops:client");
exit(1);
}
write(sockfd,&ch,1);
read(sockfd,&ch,1);
printf("char from server = %c\n",ch);
close(sockfd);
exit(0);
}
server3.c
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int server_sockfd,client_sockfd;
int server_len,client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
server_sockfd = socket(AF_INET,SOCK_STREAM,0);
server_address.sin_family= AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);//inet_addr("127.0.0.1");
server_address.sin_port = htons(9734);//9734;
server_len = sizeof(server_address);
bind(server_sockfd,(struct sockaddr*)&server_address,server_len);
//創建套接字隊列
listen(server_sockfd,5);
//接受連接
while(1)
{
char ch;
printf("server waiting\n");
client_len = sizeof(client_address);
client_sockfd=accept(server_sockfd,(struct sockaddr*)&client_address,&client_len);
read(client_sockfd,&ch,1);
printf("get data from server:%c\n",ch);
ch++;
write(client_sockfd,&ch,1);
//關閉套接字
close(client_sockfd);
}
}
使用netstat命令來查看網絡連接狀況。
上面的命令用於顯示客戶/服務器正在等待關閉。
- Proto 是 Protocol 的簡稱,它可以是TCP或UDP。
- Recv-Q和Send-Q指的是接收隊列和發送隊列,這些數字一般都應該是 0,如果不是,則表示軟件包正在隊列中堆積。
- Local Address指本機的IP和端口號。
- Foreign Address指所要連接的主機名稱和服務。
- State指現在連接的狀態。三種常見的 TCP 狀態如下所示:
- LISTEN 等待接收連接;
- ESTABLISHED 一個處於活躍狀態的連接;
- TIME_WAIT 一個剛被終止的連接。它只持續1至2分鐘,然後就會變成LISTEN狀態。由於 UDP 是無狀態的,所以其 State 欄總是空白。
此外:
netstat -a 顯示所有的服務,列出所有端口 (包括監聽與非監聽端口),顯示結果可能會有數百行
netstat --inet -a 顯示結果將只有網絡連接,包括所有正處在"LISTEN"狀態和"ESTABLISHED"狀態的連接
netstat -n 查看端口的網絡連接情況,常用netstat -an
netstat -v 查看正在進行的工作
netstat -p 協議名 例:netstat -p tcq/ip 查看某協議使用情況(查看tcp/ip協議使用情況)
netstat -s 查看正在使用的所有協議使用情況
注:主機字節序和網路字節序(大端模式): 通過套接字傳遞的端口號和地址都是二進制數字,不同的計算機使用不同的字節序來表示整數。如果計算機上的主機字序和網絡字序相同,你將不會看到任何差異。爲了使不同類型的計算機就可以通過網絡傳輸的多字節整數的值達成一致,需要定義網絡字節。客戶端和服務端在必須在傳輸之前,將它們的內部整數表示方式轉換爲網絡字節序。
7.4 小結
以上只是網絡通信的最基本的模型,在實際應用中爲了提高併發效率會使用select、poll、epoll等網絡模型。
八 IPC 小結
- 管道( pipe ):管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關係的進程間使用,進程的親緣關係通常是指父子進程關係。 有名管道 (FIFO or named pipe) : 有名管道也是半雙工的通信方式,但是它允許無親緣關係進程間的通信。
- 信號量( semophore ) : 信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作爲一種鎖機制,防止某進程正在訪問共享資源時,其它進程也訪問該資源。因此,主要作爲進程間以及同一進程內不同線程之間的同步手段。
- 信號 ( sinal ) : 信號是一種比較複雜的通信方式,用於通知接收進程某個事件已經發生。
- 共享內存( shared memory ) :共享內存就是映射一段能被其它進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的IPC方式,它是針對其它進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號量,配合使用,來實現進程間的同步和通信。
- 消息隊列( message queue ) : 消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。但是消息隊列和管道的使用都會受到系統的限制(包括創建個數、傳遞消息的長度等)。
- 套接字( socket ) : 套接字也是一種進程間通信機制,與其它通信機制最大的不同的是,它可用於不同機器間的進程通信。