既然將套接字端點表示為文件描述符,那么只要建立連接,就可以使用read和write來通過套接字通信。通過在connect函數里設置對方地址,數據報套接字也可以“連接”。在套接字描述符上采用read和write是非常有意義的,因為可以傳遞套接字描述符到那些原先設計為處理本地文件的函數。而且可以安排傳遞套接字描述符到執行程序的子進程,該子進程并不了解套接字。
盡管可以通過read和write交換數據,但這就是這兩個函數所能做的一切。如果想指定選項、從多個客戶端接收數據包或者發送帶外數據,需要采用6個傳遞數據的套接字函數中的一個。
三個函數用來發送數據,三個用來接收數據。首先,考察用于發送數據的函數。
最簡單的是send,它和write很像,但是可以指定標志來改變處理傳輸數據的方式。
#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);返回值:若成功則返回發送的字節數,若出錯則返回-1
類似write,使用send時套接字必須已經連接。參數buf和nbytes與write中的含義一致。
然而,與write不同的是,send支持第四個參數flags。其中3個標志是Single UNIX Specification規定的,但是其他標志通常實現也支持。表16-7總結了這些標志。
表16-7 send套接字調用標志
如果send成功返回,并不必然表示連接另一端的進程接收數據。所保證的僅是當send成功返回時,數據已經無錯誤地發送到網絡上。
對于支持為報文設限的協議,如果單個報文超過協議所支持的最大尺寸,send失敗并將errno設置為EMSGSIZE;對于字節流協議,send會阻塞直到整個數據被傳輸。
函數sendto和send很類似。區別在于sendto允許在無連接的套接字上指定一個目標地址。
#include <sys/socket.h>ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen);返回值:若成功則返回發送的字節數,若出錯則返回-1
對于面向連接的套接字,目標地址是忽略的,因為目標地址蘊含在連接中。對于無連接的套接字,不能使用send,除非在調用connect時預先設定了目標地址,或者采用sendto來提供另外一種發送報文方式。
可以使用不止一個的選擇來通過套接字發送數據。可以調用帶有msghdr結構的sendmsg來指定多重緩沖區傳輸數據,這和writev很相像(http://www.CUOXin.com/nufangrensheng/p/3559304.html)。
#include <sys/socket.h>ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);返回值:若成功則返回發送的字節數,出錯則返回-1
POSIX.1定義了msghdr結構,它至少應該有如下成員:
struct msghdr { void *msg_name; /* optional address */ socklen_t msg_namelen; /* address size in bytes */ struct iovec *msg_iov; /* array of I/O buffers */ int msg_iovlen; /* number of elements in array */ void *msg_control; /* ancillary data */ socklen_t msg_controllen; /* number of ancillary bytes */ int msg_flags; /* flags for received message */ ...};
iovec結構可參考http://www.CUOXin.com/nufangrensheng/p/3559304.html。
下面是用于接收數據的函數。
函數recv和read很像,但是允許指定選項來控制如何接收數據。
#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);返回值:以字節計數的消息長度,若無可用消息或對方已經按序結束則返回0,出錯則返回-1
表16-8總結了flags標志。其中只有三個標志是Single UNIX Specification規定的。
表16-8 recv套接字調用標志
當指定MSG_PEEK標志時,可以查看下一個要讀的數據但不會真正取走。當再次調用read或recv函數時會返回剛才查看的數據。
對于SOCK_STREAM套接字,接收的數據可以比請求少。標志MSG_WAITALL阻止這種行為,除非所需數據全部收到,recv才會返回。對于SOCK_DGRAM和SOCK_SEQPACKET套接字,MSG_WAITALL標志沒有改變什么行為,因為這些基于報文的套接字類型一次讀取就返回整個報文。
如果發送者已經調用shutdown(http://www.CUOXin.com/nufangrensheng/p/3564695.html)來結束傳輸,或者網絡協議支持默認的順序關閉并且發送端已經關閉,那么當所有數據接收完畢后,recv返回0。
如果有興趣定位發送者,可以使用recvfrom來得到數據發送者的源地址。
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict addrlen);返回值:以字節計數的消息長度,若無可用消息或對方已經按序結束則返回0,若出錯則返回-1
如果addr非空,它將包含數據發送者的套接字端點地址。當調用recvfrom時,需要設置addrlen參數指向一個包含addr所指的套接字緩沖區字節大小的整數。返回時,該整數設為該地址的實際大小。
因為可以獲得發送者的實際地址,recvfrom通常用于無連接套接字。否則recvfrom等同于recv。
為了將接收到的數據送入多個緩沖區(類似于readv(http://www.CUOXin.com/nufangrensheng/p/3559304.html)),或者想接收輔助數據,可以使用recvmsg。
#include <sys/socket.h>ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);返回值:以字節計數的消息長度,若無可用消息或對方已經按序結束則返回0,若出錯則返回-1
結構msghdr(在sendmsg中見過)被recvmsg用于指定接收數據的輸入緩沖區??梢栽O置參數flags來改變recvmsg的默認行為。返回時,msghdr結構中的msg_flags字段被設為所接收數據的各種特征(進入recvmsg時msg_flags被忽略)。從recvmsg中返回的各種可能值總結在表16-9中。
表16-9 從recvmsg中返回的msg_flags標志
實例:面向連接的客戶端
程序清單16-4顯示了一個客戶端命令,該命令用于與服務器通信以獲得系統命令uptime的輸出。該服務成為“remote uptime”(簡稱為“ruptime”)。
程序清單16-4 用于獲取服務器uptime的客戶端命令
#include "apue.h"#include <netdb.h>#include <errno.h>#include <sys/socket.h>#define MAXADDRLEN 256#define BUFLEN 128 extern int connect_retry(int, const struct sockaddr *, socklen_t);void PRint_uptime(int sockfd){ int n; char buf[BUFLEN]; while(( n = recv(sockfd, buf, BUFLEN, 0)) > 0) write(STDOUT_FILENO, buf, n); if(n < 0) err_sys("recv error");}int main(int argc, char *argv[]){ struct addrinfo *ailist, *aip; struct addrinfo hint; int sockfd, err; if(argc != 2) err_quit("usage: ruptime hostname"); hint.ai_flags = 0; hint.ai_family = 0; hint.ai_socktype = SOCK_STREAM; hint.ai_protocol = 0; hint.ai_addrlen = 0; hint.ai_canonname = NULL; hint.ai_addr = NULL; hint.ai_next = NULL; if((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0) err_quit("getaddrinfo error: %s", gai_strerror(err)); for(aip = ailist; aip != NULL; aip = aip->ai_next) { if((sockfd = socket(aip->ai_family, SOCK_STREAM, 0)) < 0) err = errno; if(connect_retry(sockfd, aip->ai_addr, aip->ai_addrlen) < 0) { err = errno; } else { print_uptime(sockfd); exit(0); } } fprintf(stderr, "can't connect to %s: %s/n", argv[1], strerror(err)); exit(1);}
其中,connect_retry函數見:http://www.CUOXin.com/nufangrensheng/p/3565858.html中的程序清單16-2
這個程序連接服務器,讀取服務器發送過來的字符串并將其打印到標準輸出。既然使用SOCK_STREAM套接字,就不能保證在一次recv調用中會讀取整個字符串,所以需要重復調用直到返回0。
如果服務器支持多重網絡接口或多重網絡協議,函數getaddrinfo會返回不止一個候選地址。輪流嘗試每個地址,當找到一個允許連接到服務的地址時便可停止。
編譯上面的程序成功后,執行時出現錯誤:getaddrinfo error:Servname not supported for ai_socktype,后來經查詢在http://blog.163.com/yjie_life/blog/static/16319833720110311528528/找到了解決辦法。其原因是我們在getaddrinfo第二個參數傳入的服務名“ruptime”還沒有分配端口號,我們可以手動為其添加端口號,只需在/etc/services文件中添加一行:ruptime 8888/tcp 其中8888是分配的端口號,需要大于1024且不與其他服務的端口號重復就行,后面的tcp是協議。
實例:面向連接的服務器
程序清單16-5顯示服務器程序,用來提供uptime命令到程序清單16-4的客戶端程序的輸出
程序清單16-5 提供系統uptime的服務器程序
#include "apue.h"#include <netdb.h>#include <errno.h>#include <syslog.h>#include <sys/socket.h>#define BUFLEN 128#define QLEN 10#ifndef HOST_NAME_MAX#define HOST_NAME_MAX 256#endifextern int initserver(int, struct sockaddr *, socklen_t, int);voidserve(int sockfd){ int clfd; FILE *fp; char buf[BUFLEN]; for(;;) { clfd = accept(sockfd, NULL, NULL); if(clfd < 0) { syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno)); exit(1); } if((fp = popen("/usr/bin/uptime", "r")) == NULL) { sprintf(buf, "error: %s/n", strerror(errno)); send(clfd, buf, strlen(buf), 0); } else { while(fgets(buf, BUFLEN, fp) != NULL) send(clfd, buf, strlen(buf), 0); pclose(fp); } close(clfd); }}intmain(int argc, char *argv[]){ struct addrinfo *ailist, *aip; struct addrinfo hint; int sockfd, err, n; char *host; if(argc != 1) err_quit("usage: ruptimed");#ifdef _SC_HOST_NAME_MAX n = sysconf(_SC_HOST_NAME_MAX); if(n < 0) /* best guess */#endif n = HOST_NAME_MAX; host = malloc(n); if(host == NULL) err_sys("malloc error"); if(gethostname(host, n) < 0) err_sys("gethostname error"); daemonize("ruptimed"); hint.ai_flags = AI_CANONNAME; hint.ai_family = 0; hint.ai_socktype = SOCK_STREAM; hint.ai_protocol = 0; hint.ai_addrlen = 0; hint.ai_canonname = NULL; hint.ai_addr = NULL; hint.ai_next = NULL; if((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) { syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err)); exit(1); } for(aip = ailist; aip != NULL; aip = aip->ai_next) { if((sockfd = initserver(SOCK_STREAM, aip->ai_addr, aip->ai_addrlen, QLEN)) >= 0) { serve(sockfd); exit(0); } } exit(1);}
其中,
initserver函數見:http://www.CUOXin.com/nufangrensheng/p/3565858.html中的程序清單16-3
daemonize函數見:http://www.CUOXin.com/nufangrensheng/p/3544104.html中的程序清單13-1
為了找到地址,服務器程序需要獲得其運行時的主機名字。一些系統不定義_SC_HOST_NAME_MAX常量,因此這種情況下使用HOST_NAME_MAX。如果系統不定義HOST_NAME_MAX就自己定義。POSXI.1規定該值的最小值為255字節,不包括終結符,因此定義HOST_NAME_MAX為256以包括終結符。
通過調用gethostname,服務器程序獲得主機名字。并查看遠程uptime服務(ruptime)地址??赡軙卸鄠€地址返回,但簡單地選擇第一個來建立被動套接字端點,在這個端點等待到來的連接請求。
實例:另一個面向連接的服務器
前面說過采用文件描述符來訪問套接字是非常有意義的,因為允許程序對聯網環境的網絡訪問一無所知。程序清單16-6中顯示的服務器程序版本顯示了這一點。為了代替從uptime命令中讀取輸出并發送到客戶端,服務器安排uptime命令的標準輸出和標準出錯替換為連接到客戶端的套接字端點。
程序清單16-6 用于顯示命令直接寫到套接字的服務器程序
#include "apue.h"#include <netdb.h>#include <errno.h>#include <syslog.h>#include <fcntl.h>#include <sys/socket.h>#include <sys/wait.h>#define QLEN 10#ifndef HOST_NAME_MAX#define HOST_NAME_MAX 256#endifextern int initserver(int, struct sockaddr *, socklen_t, int);voidserve(int sockfd){ int clfd, status; pid_t pid; for(;;) { clfd = accept(sockfd, NULL, NULL); if(clfd < 0) { syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno)); exit(1); } if((pid = fork()) < 0) { syslog(LOG_ERR, "ruptimed: fork error: %s", strerror(errno)); exit(1); } else if(pid == 0) /* child */ { /* * The parent called daemonize, so * STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO * are already open to /dev/null. Thus, the call to * close doesn't need to be protected by checks that * clfd isn't already equal to one of these values. */ if(dup2(clfd, STDOUT_FILENO) != STDOUT_FILENO || dup2(clfd, STDERR_FILENO) != STDERR_FILENO) { syslog(LOG_ERR, "ruptimed: unexpected error"); exit(1); } close(clfd); execl("/usr/bin/uptime", "uptime", (char *)0); syslog(LOG_ERR, "ruptimed: unexpected return from exec: %s", strerror(errno)); } else /* parent */ { close(clfd); waitpid(pid, &status, 0); } }}intmain(int argc, char *argv[]){ struct addrinfo *ailist, *aip; struct addrinfo hint; int sockfd, err, n; char *host; if(argc != 1) err_quit("usage: ruptimed");#ifdef _SC_HOST_NAME_MAX n = sysconf(_SC_HOST_NAME_MAX); if(n < 0) /* best guess */#endif n = HOST_NAME_MAX; host = malloc(n); if(host == NULL) err_sys("malloc error"); if(gethostname(host, n) < 0) err_sys("gethostname error"); daemonize("ruptimed"); hint.ai_flags = AI_CANONNAME; hint.ai_family = 0; hint.ai_socktype = SOCK_STREAM; hint.ai_protocol = 0; hint.ai_addrlen = 0; hint.ai_canonname = NULL; hint.ai_addr = NULL; hint.ai_next = NULL; if((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) { syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err)); exit(1); } for(aip = ailist; aip != NULL; aip = aip->ai_next) { if((sockfd = initserver(SOCK_STREAM, aip->ai_addr, aip->ai_addrlen, QLEN)) >= 0) { serve(sockfd); exit(0); } } exit(1);}
以前的方式是采用popen來運行uptime命令,并從連接到命令標準輸出的管道讀取輸出,現在采用fork來創建一個子進程,并使用dup2使子進程的STDIN_FILENO的副本打開到/dev/null、STDOUT_FILENO和STDERR_FILENO打開到套接字端點。當執行uptime時,命令將結果寫到標準輸出,該標準輸出連到套接字,所以數據被送到ruptime客戶端命令。
父進程可以安全地關閉連接到客戶端的文件描述符,因為子進程仍舊打開著。父進程等待子進程處理完畢,所以子進程不會變成僵死進程。既然運行uptime花費時間不會太長,父進程在接受下一個連接請求之前,可以等待子進程退出。不過,這種策略不適合子進程運行時間比較長的情況。
前面的例子采用面向連接的套接字。但如何選擇合適的套接字類型?何時采用面向連接的套接字,何時采用無連接的套接字呢?答案取決于要做的工作以及對錯誤的容忍程度。
對于無連接的套接字,數據包的到來可能已經沒有次序,因此當所有的數據不能放在一個包里時,在應用程序里必須關心包的次序。包的最大尺寸是通信協議的特性。并且對于無連接套接字,包可能丟失。如果應用程序不能容忍這種丟失,必須使用面向連接的套接字。
容忍包丟失意味著兩個選擇。如果想和對方可靠通信,必須對數據報編號,如果發現包丟失,則要求對方重新傳輸。既然包可能因延遲而疑似丟失,我們要求重傳,但該包卻又出現,與重傳過來的包重復。因此必須識別重復包,如果出現重復包,則將其丟棄。
另外一個選擇是通過讓用戶再次嘗試命令來處理錯誤。對于簡單的應用程序,這就足夠;但對于復雜的應用程序,這種處理方式通常不是可行的選擇,一般在這種情況下使用面向連接的套接字更為可取。
面向連接的套接字的缺陷在于需要更多的時間和工作來建立一個連接,并且每個連接需要從操作系統中消耗更多的資源。
實例:無連接的客戶端
程序清單16-7中的程序是采用數據報套接字接口的uptime客戶端命令版本。
程序清單16-7 采用數據報服務的客戶端命令
#include "apue.h"#include <netdb.h>#include <errno.h>#include <sys/socket.h>#define BUFLEN 128#define TIMEOUT 20void sigalrm(int signo){}void print_uptime(int sockfd, struct addrinfo *aip){ int n; char buf[BUFLEN]; buf[0] = 0; if(sendto(sockfd, buf, 1, 0, aip->ai_addr, aip->ai_addrlen) < 0) err_sys("sendto error"); alarm(TIMEOUT); if((n = recvfrom(sockfd, buf, BUFLEN, 0, NULL, NULL)) < 0) { if(errno != EINTR) alarm(0); err_sys("recv error"); } alarm(0); write(STDOUT_FILENO, buf, 0);}int main(int argc, char *argv[]){ struct addrinfo *ailist, *aip; struct addrinfo hint; int sockfd, err; struct sigaction sa; if(argc != 2) err_quit("usage: ruptime hostname"); sa.sa_handler = sigalrm; sa.sa_flags = 0; sigemptyset(&sa.sa_mask); if(sigaction(SIGALRM, &sa, NULL) < 0) err_sys("sigaction error"); hint.ai_flags = 0; hint.ai_family = 0; hint.ai_socktype = SOCK_DGRAM; hint.ai_protocol = 0; hint.ai_addrlen = 0; hint.ai_canonname = NULL; hint.ai_addr = NULL; hint.ai_next = NULL; if((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0) err_quit("getaddrinfo error: %s", gai_strerror(err)); for(aip = ailist; aip != NULL; aip = aip->ai_next) { if((sockfd = socket(aip->ai_family, SOCK_DGRAM, 0)) < 0) { err = errno; } else { print_uptime(sockfd, aip); exit(0); } } fprintf(stderr, "can't contact %s: %s/n", argv[1], strerror(err)); exit(1);}
除了為SIGALRM增加了一個信號處理程序以外,基于數據報的客戶端main函數和面向連接的客戶端中的類似。使用alarm函數來避免調用recvfrom時無限期阻塞。
對于面向連接的協議,需要在交換數據前連接服務器。對于服務器來說,到來的連接請求已經足夠判斷出所需提供給客戶端的服務。但是對于基于數據報的協議,需要有一種方法來通知服務器需要它提供服務。本例中,只是簡單地給服務器發送1字節的消息。服務器接收后從包中得到地址,并使用這個地址來發送響應消息。如果服務器提供多個服務,可以使用這個請求消息來指示所需要的服務,但既然服務器只做一件事情,1字節消息的內容是無關緊要的。
如果服務器不在運行狀態,客戶端調用recvfrom便會無限期阻塞。對于面向連接的例子,如果服務器不運行,connect調用會失敗。為了避免無限期阻塞,調用recvfrom之前設置警告時鐘。
實例:無連接服務器
程序清單16-8中的程序是數據報版本的uptime服務器程序。
程序清單16-8 基于數據報提供系統uptime的服務器程序
#include "apue.h"#include <netdb.h>#include <errno.h>#include <syslog.h>#include <sys/socket.h>#define BUFLEN 128#define MAXADDRLEN 256#ifndef HOST_NAME_MAX#define HOST_NAME_MAX 256#endifextern int initserver(int, struct sockaddr *, socklen_t, int);voidserve(int sockfd){ int n; socklen_t alen; FILE *fp; char buf[BUFLEN]; char abuf[MAXADDRLEN]; for(;;) { alen = MAXADDRLEN; if((n = recvfrom(sockfd, buf, BUFLEN, 0, (struct sockaddr *)abuf, &alen)) < 0) { syslog(LOG_ERR, "ruptimed: recvfrom error: %s", strerror(errno)); exit(1); } if((fp = popen("/usr/bin/uptime", "r")) == NULL) { sprintf(buf, "error: %s/n", strerror(errno)); sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)abuf, alen); } else { if(fgets(buf, BUFLEN, fp) != NULL) sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)abuf, alen); pclose(fp); } } }intmain(int argc, char *argv[]){ struct addrinfo *ailist, *aip; struct addrinfo hint; int sockfd, err, n; char *host; if(argc != 1) { err_quit("usage: ruptimed"); }#ifdef _SC_HOST_NAME_MAX n = sysconf(_SC_HOST_NAME_MAX); if(n < 0) /* best guess */#endif n = HOST_NAME_MAX; host = malloc(n); if(host == NULL) err_sys("malloc error"); if(gethostname(host, n) < 0) err_sys("gethostname error"); daemonize("ruptimed"); hint.ai_flags = AI_CANONNAME; hint.ai_family = 0; hint.ai_socktype = SOCK_DGRAM; hint.ai_protocol = 0; hint.ai_addrlen = 0; hint.ai_canonname = NULL; hint.ai_addr = NULL; hint.ai_next = NULL; if((err = getaddrinfo(host, "ruptime", &hint, &ailist)) != 0) { syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err)); exit(1); } for(aip = ailist; aip != NULL; aip = aip->ai_next) { if((sockfd = initserver(SOCK_DGRAM, aip->ai_addr, aip->ai_addrlen, 0)) >= 0) { serve(sockfd); exit(0); } } exit(1);}
服務器在recvfrom中阻塞等待服務請求。當一個請求到達時,保存請求者地址并使用popen來運行uptime命令。采用sendto函數將輸出發送到客戶端,其目標地址就設為剛才的請求者地址。
本篇博文內容摘自《UNIX環境高級編程》(第2版),僅作個人學習記錄所用。關于本書可參考:http://www.apuebook.com/。
新聞熱點
疑難解答