前一篇文章,Linux進程間通信——使用流套接字介紹了一些有關socket(套接字)的一些基本內容,並講解了流套接字的使用,這篇文章將會給大家講講,數據報套接字的使用。
一、簡單回顧——什么是數據報套接字
socket,即套接字是一種通信機制,憑借這種機制,客戶/服務器(即要進行通信的進程)系統的開發工作既可以在本地單機上進行,也可以跨網絡進行。也就是說它可以讓不在同一台計算機但通過網絡連接計算機上的進程進行通信。也因為這樣,套接字明確地將客戶端和服務器區分開來。
相對於流套接字,數據報套接字的使用更為簡單,它是由類型SOCK_DGRAM指定的,它不需要建立連接和維持一個連接,它們在AF_INET中通常是通過UDP/IP協議實現的。它對可以發送的數據的長度有限制,數據報作為一個單獨的網絡消息被傳輸,它可能會丟失、復制或錯亂到達,UDP不是一個可靠的協議,但是它的速度比較高,因為它並一需要總是要建立和維持一個連接。
二、基於流套接字的客戶/服務器的工作流程
使用數據報socket進行進程通信的進程采用的客戶/服務器系統是如何工作的呢?
1、服務器端
與使用流套接字一樣,首先服務器應用程序用系統調用socket()來創建一個套接安,它是系統分配給該服務器進程的類似文件描述符的資源,它不能與其他的進程共享。
接下來,服務器進程會給套接字起個名字(監聽),我們使用系統調用bind()來給套接字命名。然后服務器進程就開始等待客戶連接到這個套接字。
不同的是,然后系統調用recvfrom()來接收來自客戶程序發送過來的數據。服務器程序對數據進行相應的處理,再通過系統調用sendto()把處理后的數據發送回客戶程序。
與流套接字程序相比:
- 在流套接字中的程序中,接收數據是通過系統調用read,而發送數據是通過系統調用write()來實現,而在數據報套接字程序中,這是通過recvfrom()和sendto()調用來實現的。
- 使用數據報套接字的服務器程序並不需要listen()調用來創建一個隊列來存儲連接,也不需要accept()調用來接收連接並創建一個新的socket描述符
2、客戶端
基於數據報socket的客戶端比服務器端簡單,同樣,客戶應用程序首先調用socket來創建一個未命名的套接字,與服務器一樣,客戶也是通過sendto()和recvfrom()來向服務器發送數據和從服務器程序接收數據。
與流套接字程序相比:
使用數據報套接字的客戶程序並不需要使用connect()系統調用來連接服務器程序,它只要在需要時向服務器所監聽的IP端口發送信息和接收從服務器發送回來的數據即可。
三、數據報socket的接口及作用
socket的接口函數聲明在頭文件 sys/types.h 和 sys/socket.h 中。
1、創建套接字——socket()系統調用
該函數用來創建一個套接字,並返回一個描述符,該描述符可以用來訪問該套接字,它的原型如下:
int socket(int domain, int type, int protocol);
函數中的三個參數分別對應前面所說的三個套接字屬性。protocol參數設置為0表示使用默認協議。
2、命名(綁定)套接字——bind()系統調用
該函數把通過socket()調用創建的套接字命名,從而讓它可以被其他進程使用。對於AF_UNIX,調用該函數后套接字就會關聯到一個文件系統路徑名,對於AF_INET,則會關聯到一個IP端口號。函數原型如下:
int bind( int socket, const struct sockaddr *address, size_t address_len);
成功時返回0,失敗時返回-1;
3、發送數據——sendto()系統調用
該函數把緩沖區buffer中的信息給送給指定的IP端口的程序,原型如下:
int sendto(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *to, socklen_t tolen);
buffer中儲存着將要發送的數據,len是buffer的長度,而flags在應用中通常被設置為0,to是要發送數據到的程序的IP端口,tolen是to參數的長度。
成功時返回發送的數據的字節數,失敗時返回-1。
4、接收數據——recvfrom()系統調用
該函數把發送給程序的信息儲存在緩沖區buffer中,並記錄數據來源的程序IP端口,原型如下:
int recvfrom(int sockfd, void *buffer, size_t len,int flags, struct sockaddr *src_from, socklen_t *src_len);
buffer用於儲存接收到的數據,len指定buffer的長度,而flags在應用中通常被設置0,src_from若不為空,則記錄數據來源程序的IP端口,若src_len不為空,則其長度信息記錄在src_len所指向的變量中。
注意:默認情況下,recvfrom()是一個阻塞的調用,即直到它接收到數據才會返回。
5、關閉socket——close()系統調用
該系統調用用來終止服務器和客戶上的套接字連接,我們應該總是在連接的兩端(服務器和客戶)關閉套接字。
四、進程使用數據報socket進行通信
下面用多個客戶程序實例和一個服務器程序來演示多個進程如何通過使用數據報socket來進行通信。
sockserver2.c是一個服務器程序,它接收客戶程序發來的數據,並創建一個子進程來處理客戶發送過來的數據,處理過程非常簡單,就是把大寫字母改為小寫。然后把處理后的數據(大寫字母對應的小寫字母)發送回給客戶端。
sockclient2.c是一個客戶程序,它向服務器程序發送數據,並接收服務器發送過來的處理后的數據(即小寫字母),然后把接收到的數據輸出到屏幕上。在運行客戶程序時,你可以為它提供一個字符作為參數,此時客戶程序將把些字符作為要處理的數據發送給服務器,如果不提供一個參數,則默認發送字符A。
sockserver2.c的源代碼如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <signal.h> int main(int arc, char **argv) { int server_sockfd = -1; socklen_t server_len = 0; socklen_t client_len = 0; char buffer[512]; ssize_t result = 0; struct sockaddr_in server_addr; struct sockaddr_in client_addr; // 創建數據報套接字 server_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 設置監聽的端口、IP server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(9739); server_len = sizeof(server_addr); // 綁定(命名)套接字 bind(server_sockfd, (struct sockaddr *)&server_addr, server_len); // 忽略子進程停止 或 退出 的信息 signal(SIGCHLD, SIG_IGN); while (1) { // 接收數據,用 client_addr 來儲存數據來源程序的IP端口 result = recvfrom(server_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_len); if (fork() == 0) { // 利用子進程來處理數據 buffer[0] += 'a' - 'A'; sleep(1); // 發送處理后的數據 sendto(server_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, client_len); printf("%c\n", buffer[0]); // 注意,一定要關閉子進程,否則程序運行會不正常 exit(0); } } // 關閉套接字 close(server_sockfd); }
sockclient2.c的源代碼如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> int main(int argc, char **argv) { struct sockaddr_in server_addr; socklen_t server_len = 0; int sockfd = -1; char c = 'A'; // 取第一個參數的第一個字符 if (argc > 1) { c = argv[1][0]; } // 創建數據報套接字 sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 設置服務器IP、端口 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); server_addr.sin_port = htons(9739); server_len = sizeof(server_addr); // 向服務器發送數據 sendto(sockfd, &c, sizeof(char), 0, (struct sockaddr *)&server_addr, server_len); // 接收服務器處理后發送過來的數據,由於不關心數據來源,所以把后兩個參數設為0 recvfrom(sockfd, &c, sizeof(char), 0, 0, 0); printf("char from server = %c\n", c); // 關閉套接字 close(sockfd); exit(0); }
運行結果如下:
先運行服務器程序,如下:
再運行三個客戶端:如下:
在本例子中,我們啟動了一個服務器程序和三個客戶程序,從運行的結果來看,客戶端發送給服務器程序的所有請求都得到了處理,即把大寫字母變成了小寫。recvfrom()調用是阻塞的調用,即只有當接收到數據才會返回。
五、數據報套接字與流套接字的比較
1、從使用的便利和效率來講
我們可以看到使用數據報套接字的確是比使用流套接字簡單,而且快速。
因為使用流套接字的程序,客戶程序需要調用connect()來創建一個到服務器程序的連接,並需要維持這個連接,服務器程序也需要調用listen()來創建一個隊列來保存未處理的請求,當有數據到達時,服務器也不需要調用accept()來接受連接並創建一個新socket描述符來處理請求。
再來看看使用數據報套接字的程序,服務器程序與客戶程序所使用的系統調用大致相同,服務器程序只比客戶程序多使用了一個bind()調用。基於數據報套接字的程序,只需要使用sendto()調用來向指定IP端口的程序發送信息,使用recvfrom()調用從指向的IP端口接收信息即可。因為它並不需要建立一個連接,接受連接等,所以省去了很多的功夫。
2、從使用場合來講
我們知道流套接字是基於TCP/IP協議的,它是一種安全的協議,提供的是一個有序、可靠、雙向字節流的連接,發送的數據可以確保不會丟失、重復或亂序到達,而且它還有一定的出錯后重新發送的機制。所以它比較適合用來發送信息量大的數據文件,或對數據完整性要求較高的文件,如壓縮文件、視頻文件等
而數據報套接字是基於UDP/IP協議實現的。它對可以發送的數據的長度有限制,數據報作為一個單獨的網絡消息被傳輸,它可能會丟失、復制或錯亂到達,UDP不是一個可靠的協議,但是它的速度比較高。所以它比較適合發送一些對實時性要求較高,但是對安全性和完整性要求不太高的數據。如我們熟悉的聊天信息,即使有一點的丟失也不會造成理解上的大的問題。
參考:
http://blog.csdn.net/ljianhui/article/details/10697935