前言
之前的示例中,基于Linux的使用read&write函數完成數據I/O,基于Windows的則使用send&recv函數。這次的Linux示例也將使用send& recv函數,并講解其與read&write函數相比的優點。還將介紹幾種其他的I/O函數。
一、send & recv 函數
1.Linux 中的 send & recv
#include <sys/socket.h>
ssize_t send(int sockfd, const void * buf, size_t nbytes, int flags);
// 成功時返回發送的字節數,失敗時返回-1。
// sockfd:表示與數據傳輸對象的連接的套接字文件描述符。
// buf:保存待傳輸數據的緩沖地址值。
// nbytes:待傳輸的字節數。
// flags:傳輸數據時指定的可選項信息。
#include <sys/socket.h>
ssize_t recv(int sockfd, void * buf, size_t nbytes, int flags);
// 成功時返回接收的字節數(收到EOF時返回0),失敗時返回-1。
// sockfd:表示數據接收對象的連接的套接字文件描述符。
// buf:保存接收數據的緩沖地址值。
// nbytes:可接收的最大字節數。
// flags:接收數據時指定的可選項信息。
send函數和recv函數的最后一個參數是收發數據時的可選項。該可選項可利用位或(bitOR)
運算(運算符)同時傳遞多個信息。通過表查看選項的種類及含義。
可選項(Option) 含 義 send recv
MSG_OOB 用于傳輸帶外數據(Out-of-band data) * *
MSG_PEEK 驗證輸人緩沖中是否存在接收的數據 *
MSG_DONTROUTE 數據傳輸過程中不參照路由(Routing)表,在本地(Local)網絡中尋找目的地 *
MSG_DONTWAIT 調用I/O函數時不阻塞,用于使用非阻塞(Non-blocking)I/O * *
MSG_WAITALL 防止函數返回,直到接收全部請求的字節數 *
另外,不同操作系統對上述可選項的支持也不同。因此,為了使用不同可選項,各位需要對實際開發中采用的操作系統有一定了解。下面選取表中的一部分(主要是不受操作系統差異影響的)進行詳細講解。
2.MSG_OOB:發送緊急消息
MSG_OOB可選項用于發送“帶外數據”緊急消息。假設醫院里有很多病人在等待看病,此時若有急診患者該怎么辦?
“當然應該優先處理。”
如果急診患者較多,需要得到等待看病的普通病人的諒解。正因如此,醫院一般會設立單獨的急診室。需緊急處理時,應采用不同的處理方法和通道。MSG_OOB可選項就用于創建特殊發送方法和通道以發送緊急消息。下列示例將通過MSG_OOB可選項收發數據。使用MSG_OOB時
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>#define BUF_SIZE 30
void error_handling(char *message);int main(int argc, char *argv[])
{int sock;struct sockaddr_in recv_adr;if(argc!=3) {printf("Usage : %s <IP> <port>\n", argv[0]);exit(1);}sock=socket(PF_INET, SOCK_STREAM, 0);memset(&recv_adr, 0, sizeof(recv_adr));recv_adr.sin_family=AF_INET;recv_adr.sin_addr.s_addr=inet_addr(argv[1]);recv_adr.sin_port=htons(atoi(argv[2]));if(connect(sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr))==-1)error_handling("connect() error!");write(sock, "123", strlen("123"));send(sock, "4", strlen("4"), MSG_OOB);write(sock,"567",strlen("567"));send(sock, "890", strlen("890"), MSG_OOB);close(sock);return 0;
}void error_handling(char *message)
{fputs(message,stderr);fputc('\n', stderr);exit(1);
}
第29~32行:傳輸數據。第30和第32行緊急傳輸數據。正常順序應該是123、4、567、890,但緊急傳輸了4和890,由此可知接收順序也將改變。
從上述示例可以看出,緊急消息的傳輸比即將介紹的接收過程要簡單,只需在調用send函數時指定MSG_OOB可選項。接收緊急消息的過程要相對復雜一些。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>#define BUF_SIZE 30
void error_handling(char*message);
void urg_handler(int signo);int acpt_sock;
int recv_sock;int main(int argc, char *argv[])
{struct sockaddr_in recv_adr, serv_adr;int str_len, state;socklen_t serv_adr_sz;struct sigaction act;char buf[BUF_SIZE];if(argc!=2){printf("Usage: %s <port>\n", argv[0]);exit(1);}act.sa_handler=urg_handler;sigemptyset(&act.sa_mask);act.sa_flags=0;acpt_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&recv_adr, 0, sizeof(recv_adr));recv_adr.sin_family=AF_INET;recv_adr.sin_addr.s_addr=htonl(INADDR_ANY);recv_adr.sin_port=htons(atoi(argv[1]));if(bind(acpt_sock,(struct sockaddr*)&recv_adr, sizeof(recv_adr))==-1)error_handling("bind() error");listen(acpt_sock, 5);serv_adr_sz=sizeof(serv_adr);recv_sock=accept(acpt_sock, (struct sockaddr*)&serv_adr, &serv_adr_sz);fcntl(recv_sock, F_SETOWN, getpid());state=sigaction(SIGURG, &act, 0);while((str_len=recv(recv_sock, buf, sizeof(buf), 0))!= 0){if(str_len==-1)continue;buf[str_len]=0;puts(buf);}close(recv_sock);close(acpt_sock);return 0;
}void urg_handler(int signo)
{int str_len;char buf[BUF_SIZE];str_len=recv(recv_sock, buf, sizeof(buf)-1, MSG_OOB);buf[str_len]=0;printf("Urgent message: %s \n", buf);
}void error_handling(char *message){fputs(message, stderr);fputc('\n', stderr);exit(1);
}
第29、47行:該示例中需要重點觀察SIGURG信號相關部分。收到MSG_OOB緊急消息時,操作系統將產生SIGURG信號,并調用注冊的信號處理函數。另外需要注意的是,第61行的信號處理函數內部調用了接收緊急消息的recv函數。第46行:調用fcntl函數,關于此函數將單獨說明。
上面的v示例中插人了未曾講解的函數調用語句,關于此函數只講解必要部分,過多的解釋將脫離本章主題(之后將再次說明)。
fcntl(recv_sock, F_SETOwN, getpid(O));
fcntl函數用于控制文件描述符,上述調用語句的含義:
“將文件描述符recv_sock指向的套接字擁有者(F_SETOWN)改為把getpid函數返回值用作ID的進程。”
大家或許感覺“套接字擁有者”的概念有些生疏。操作系統實際創建并管理套接字,所以從嚴格意義上說,“套接字擁有者”是操作系統。只是此處所謂的“擁有者”是指負責套接字所有事務的主體。上面的描述可簡要概括成這樣:
“文件描述符recv_sock指向的套接字引發的SIGURG信號處理進程變為將getpid函數返回值用作ID的進程。”
當然,上述描述中的“處理SIGURG信號”指的是“調用SIGURG信號處理函數”。但之前講過,多個進程可以共同擁有1個套接字的文件描述符。例如,通過調用fork函數創建子進程并同時復制文件描述符。此時如果發生SIGURG信號,應該調用哪個進程的信號處理函數呢?可以肯定的是,不會調用所有進程的信號處理函數(想想就知道這會引發更多問題)。因此,處理SIGURG
信號時必須指定處理信號的進程,而getpid函數返回調用此函數的進程ID。上述調用語句指定當前進程為處理SIGURG信號的主體。該程序中只創建了1個進程,因此,理應由該進程處理SIGURG信號。接下來先給出運行結果,再討論剩下的問題。
輸出結果可能出乎大家預料,尤其是如下事實令人極為失望:
“通過MSG_OOB可選項傳遞數據時只返回1個字節?而且也不是很快啊!”
的確!令人遺憾的是,通過MSG_OOB可選項傳遞數據時不會加快數據傳輸速度,而且通過信號處理函數urg_handler讀取數據時也只能讀1個字節。剩余數據只能通過未設置MSG_OOB可選項的普通輸人函數讀取。這是因為TCP不存在真正意義上的“帶外數據”。實際上,MSG_OOB中的OOB是指Out-of-band,而“帶外數據”的含義是:
“通過完全不同的通信路徑傳輸的數據。”
即真正意義上的Out-of-band需要通過單獨的通信路徑高速傳輸數據,但TCP不另外提供,只利用TCP的緊急模式(Urgent mode)進行傳輸。
3.緊急模式工作原理
先給出結論,再補充說明緊急模式。MSG_OOB可選項可以帶來如下效果:
– “嗨!這里有數據需要緊急處理,別磨蹭啦!” –
MSG_OOB的真正的意義在于督促數據接收對象盡快處理數據。這是緊急模式的全部內容,而且TCP“保持傳輸順序”的傳輸特性依然成立。
“那怎能稱為緊急消息呢!”
這確實是緊急消息!因為發送消息者是在催促數據處理的情況下傳輸數據的。急診患者的及時救治需要如下兩個條件:
■ 迅速入院
■ 醫院急救
無法快速把病人送到醫院,并不意味著不需要醫院進行急救。TCP的緊急消息無法保證及時人院,但可以要求急救。當然,急救措施應由程序員完成。之前的示例oob_recv.c的運行過程中也傳遞了緊急消息,這可以通過事件處理函數確認。這就是MSG_OOB模式數據傳輸的實際意義。
send(sock, “890”, strlen(“890”), MSG_OOB);
如果將緩沖最左端的位置視作偏移量為0,字符0保存于偏移量為2的位置。另外,字符0右側偏移量為3的位置存有緊急指針(UrgentPointer)。緊急指針指向緊急消息的下一個位置(偏移量加1),同時向對方主機傳遞如下信息:
– “緊急指針指向的偏移量為3之前的部分就是緊急消息!” –
也就是說,實際只用1個字節表示緊急消息信息。
TCP數據包實際包含更多信息,但圖中只標注了與我們的主題相關的內容。TCP頭中含有如下兩種信息:
■ URG=1:載有緊急消息的數據包
■ URG指針:緊急指針位于偏移量為3的位置
指定MSG_OOB選項的數據包本身就是緊急數據包,并通過緊急指針表示緊急消息所在位置。但通過圖無法得知以下事實:
“緊急消息是字符串890,還是90?如若不是,是否為單個字符0?”
但這并不重要。如前所述,除緊急指針的前面1個字節外,數據接收方將通過調用常用輸人函數讀取剩余部分。換言之,緊急消息的意義在于督促消息處理,而非緊急傳輸形式受限的消息。
4.檢查輸入緩沖
同時設置MSG_PEEK選項和MSG_DONTWAIT選項,以驗證輸入緩沖中是否存在接收的數據。設置MSG_PEEK選項并調用recv函數時,即使讀取了輸人緩沖的數據也不會刪除。因此,該選項通常與MSG_DONTWAIT合作,用于調用以非阻塞方式驗證待讀數據存在與否的函數。下面我們通過示例了解二者含義。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void error_handling(char *message);int main(int argc, char *argv[])
{int sock;struct sockaddr_in send_adr;if(argc!=3) {printf("Usage : %s <IP> <port>\n", argv[0]);exit(1);}sock=socket(PF_INET,SOCK_STREAM,0);memset(&send_adr,0, sizeof(send_adr));send_adr.sin_family=AF_INET;send_adr.sin_addr.s_addr=inet_addr(argv[1]);send_adr.sin_port=htons(atoi(argv[2]));if(connect(sock, (struct sockaddr*)&send_adr, sizeof(send_adr))==-1)error_handling("connect() error!");write(sock,"123456789", strlen("123456789"));close(sock);return 0;
}void error_handling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}
上述示例在第24行發起連接請求,第27行發送字符串123。下個例子給出了使用MSG_PEEK和MSG_DONTWAIT選項的結果。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>#define BUF_SIZE 30
void error_handling(char *message);int main(int argc, char *argv[])
{int acpt_sock, recv_sock;struct sockaddr_in acpt_adr, recv_adr;int str_len, state;socklen_t recv_adr_sz;char buf[BUF_SIZE];if(argc!=2) {printf("Usage : %s <port>\n", argv[0]);exit(1);}acpt_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&acpt_adr, 0, sizeof(acpt_adr));acpt_adr.sin_family=AF_INET;acpt_adr.sin_addr.s_addr=htonl(INADDR_ANY);acpt_adr.sin_port=htons(atoi(argv[1]));if(bind(acpt_sock, (struct sockaddr*)&acpt_adr, sizeof(acpt_adr))==-1)error_handling("bind() error");listen(acpt_sock, 5);recv_adr_sz=sizeof(recv_adr);recv_sock=accept(acpt_sock, (struct sockaddr*)&recv_adr, &recv_adr_sz);while(1){str_len=recv(recv_sock, buf,sizeof(buf)-1, MSG_PEEK|MSG_DONTWAIT);if(str_len>0)break;}buf[str_len]=0;printf("Buffering %d bytes: %s \n", str_len, buf);str_len=recv(recv_sock, buf, sizeof(buf)-1, 0);buf[str_len]=0;printf("Read again: %s \n",buf);close(acpt_sock);close(recv_sock);return 0;
}void error_handling(char *message)
{fputs(message, stderr);fputc('\n',stderr);exit(1);
}
第38行:調用recv函數的同時傳遞MSG_PEEK可選項,這是為了保證即使不存在待讀取數據也不會進入阻塞狀態。
第46行:再次調用recv函數。這次并未設置任何可選項,因此,本次讀取的數據將從輸入緩沖中刪除。
通過運行結果可以驗證,僅發送1次的數據被讀取了2次,因為第一次調用recv函數時設置了MSG_PEEK可選項。以上就是MSG_PEEK可選項的功能啦!
二、readv & writev 函數
這次介紹的readv&writev函數有助于提高數據通信效率。先介紹這些函數的使用方法,再討論其合理的應用場景哦。
1.使用readv & writev 函數
readv&writev函數的功能可概括如下:
“對數據進行整合傳輸及發送的函數。”
也就是說,通過writev函數可以將分散保存在多個緩沖中的數據一并發送,通過readv函數可以由多個緩沖分別接收。因此,適當使用這2個函數可以減少I/O函數的調用次數。下面先介紹writev函數。
#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec * iov, int iovcnt);
// 成功時返回發送的字節數,失敗時返回-1。
// filedes:表示數據傳輸對象的套接字文件描述符。但該函數并不只限于套接字,因此,可以像read函數一樣向其傳遞文件或標準輸出描述符。
// iov:iovec結構體數組的地址值,結構體iovec中包含待發送數據的位置和大小信息。
// iovcnt:向第二個參數傳遞的數組長度。
可以看到,結構體iovec由保存待發送數據的緩沖(char型數組)地址值和實際發送的數據長度信息構成。
圖13-4中writev的第一個參數1是文件描述符,因此向控制臺輸出數據,ptr是存有待發送數據信息的iovec數組指針。第三個參數為2,因此,從ptr指向的地址開始,共瀏覽2個iovec結構體變量,發送這些指針指向的緩沖數據。接下來仔細觀察圖中的iovec結構體數組。ptr[0](數組第一個元素)的iov_base指向以A開頭的字符串,同時iov_len為3,故發送ABC。而ptr[1](數組的第二個元素)的iov_base指向數字1,同時iov_len為4,故發送1234。
相信大家已掌握writev函數的使用方法和特性,接下來栗子來了:
#include <stdio.h>
#include <sys/uio.h>int main(int argc, char *argv[])
{struct iovec vec[2];char buf1[]="ABCDEFG";char buf2[]="1234567";int str_len;vec[0].iov_base=buf1;vec[0].iov_len=3;vec[1].iov_base=buf2;vec[1].iov_len=4;str_len=writev(1, vec, 2);puts("");printf("Write bytes: %d \n", str_len);return 0;
}
第11、12行:寫入第一個傳輸數據的保存位置和大小。
第13、14行:寫入第二個傳輸數據的保存位置和大小。
第16行:writev函數的第一個參數為1,故向控制臺輸出數據。
下面介紹readv函數,它與writev函數正好相反:
#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec * iov, int iovcnt);
// 成功時返回接收的字節數,失敗時返回-1。
// filedes:傳遞接收數據的文件(或套接字)描述符。
// iov:包含數據保存位置和大小信息的iovec結構體數組的地址值。
// iovcnt:第二個參數中數組的長度。
我們已經學習了writev函數,因此直接通過例子給出readv函數的使用方法。
#include <stdio.h>
#include <sys/uio.h>
#define BUF_SIZE 100int main(int argc, char *argv[])
{struct iovec vec[2];char buf1[BUF_SIZE]={0, };char buf2[BUF_SIZE]={0, };int str_len;vec[0].iov_base=buf1;vec[0].iov_len=5;vec[1].iov_base=buf2;vec[1].iov_len=BUF_SIZE;str_len=readv(0, vec, 2);printf("Read bytes: %d \n",str_len);printf("First message: %s \n",buf1);printf("Second message: %s \n",buf2);return 0;
}
第12、13行:設置第一個數據的保存位置和大小。接收數據的大小已指定為5,因此,無論buf1的大小是多少,最多僅能保存5個字節。
第14、15行:vec[0]中注冊的緩沖中保存5個字節,剩余數據將保存到vec[1]中注冊的緩沖。結構體iovec的成員iov_len中應寫入接收的最大字節數。
第17行:「eadv函數的第一個參數為0,因此從標準輸入接收數據。
2.合理使用 readv&writev函數
哪種情況適合使用readv和writev函數?實際上,能使用該函數的所有情況都適用。例如,需要傳輸的數據分別位于不同緩沖(數組)時,需要多次調用write函數。此時可以通過1次writev函數調用替代操作,當然會提高效率。同樣,需要將輸人緩沖中的數據讀人不同位置時,可以不必多次調用read函數,而是利用1次readv函數就能大大提高效率。
即使僅從C語言角度看,減少函數調用次數也能相應提高性能。但其更大的意義在于減少數據包個數。假設為了提高效率而在服務器端明確阻止了Nagle算法。其實writev函數在不采用Nagle算法時更有價值。
這個示例中待發送的數據分別存在3個不同的地方,此時如果使用write函數則需要3次函數調用。但若為提高速度而關閉了Nagle算法,則極有可能通過3個數據包傳遞數據。反之,若使用writev函數將所有數據一次性寫人輸出緩沖,則很有可能僅通過1個數據包傳輸數據。所以writev函數和readv函數非常有用。
再考慮一種情況:將不同位置的數據按照發送順序移動(復制)到1個大數組,并通過1次write函數調用進行傳輸。這種方式是否與調用writev函數的效果相同?當然!但使用writev函數更為便利。因此,如果遇到writev函數和readv函數的適用情況,希望大家不要錯過機會。
總結
`這些函數大家理解一下,應該沒有那么難哦!