認證可以作為通訊中所接收的附屬資料的一部分而接收。附屬資料對於通常資料來說是補充或是從屬。這就引出需要在這裡強調的幾點問題:
認證是作為附屬資料的一部分而接收的。
附屬資料必須是補充通常資料的(他不可以獨立傳送)。
附屬資料也可以包含其他的資訊,例如檔案描述符。
附屬資料可以同時包含多個附屬項目(例如同時包含認證與檔案描述符)。
認證是由Linux核心提供的。他們從來不由客戶程式提供。如果是這樣,用戶端就會被允許 個標識。因為核心是可信任的,認證就可以被對認證感興趣的進程所信任。
現在我們已經瞭解檔案描述符是作為附屬資料來傳送和接收的。然而,在我們開始編寫套介面代碼來使用這些附屬資料元素時,我們需要介紹一些新的編程概念。
簡介I/O向量
在我們瞭解使用附屬資料工作的複雜函數之前,我們應該熟悉被readv(2)與writev(2)系統調用所使用的I/O向量。我們不僅將會發現這些函數是十分有用的,而他們的工作方式也被引入了一些附屬資料函數中。這會使得後面的理解更為容易。
I/O向量(struct iovec)
readv(2)與writev(2)函數都使用一個I/O向量的概念。這是由所包含的檔案定義的:
#include
sys/uio.h標頭檔定義了struct iovc,其定義如下:
struct iovec {
ptr_t iov_base; /* Starting address */
size_t iov_len; /* Length in bytes */
};
struct
iovec定義了一個向量元素。通常,這個結構用作一個多元素的數組。對於每一個傳輸的元素,指標成員iov_base指向一個緩衝區,這個緩衝區是存放
的是readv所接收的資料或是writev將要發送的資料。成員iov_len在各種情況下分別確定了接收的最大長度以及實際寫入的長度。
readv(2)與writev(2)函數
這些函數是作為read與write函數的衍生函數而被人所知的。他們以這樣的方式進行設計是因為他們可以在一個原子操作中讀取或是寫入多個緩衝區。這些函數的原型如下:
#include
int readv(int fd, const struct iovec*vector, int count);
int writev(int fd, const struct iovec*vector, int count);
這些函數需要三個參數:
要在其上進行讀或是寫的檔案描述符fd
讀或寫所用的I/O向量(vector)
要使用的向量元素個數(count)
這些函數的傳回值是readv所讀取的位元組數或是writev所寫入的位元組數。如果有錯誤發生,就會返回-1,而errno存有錯誤碼。注意,也其他I/O函數類似,可以返回錯誤碼EINTR來表明他被一個訊號所中斷。
使用writev的例子
下面的程式碼展示了如何使用writev函數將三個獨立的C字串作為一次寫操作寫入標準輸出。
/*
* writev.c
*
* Short writev(2) demo:
*/
#include
int main(int argc,char **argv)
{
static char part2[] = "THIS IS FROM WRITEV";
static char part3[] ="]\n";
static char part1[] = "[";
struct iovec iov[3];
iov[0].iov_base = part1;
iov[0].iov_len = strlen(part1);
iov[1].iov_base = part2;
iov[1].iov_len = strlen(part2);
iov[2].iov_base = part3;
iov[2].iov_len = strlen(part3);
writev(1,iov,3);
return 0;
}
編譯運行程式:
$ make writev
gcc -g -c -D_GNU_SOURCE -Wall -Wreturn-typewritev.c
gcc writev.o -o writev
$ ./writev
[THIS IS FROM WRITEV]
$
當程式運行時,我們可以看到無論所引用的緩衝區是如何分散,所有的緩衝區都會被輸出形成最終的字串。
也許我們希望多花一些時間來修改這個程式並做各種測試,但是要注意一定要將iov[[]數組分配得足夠大。
sendmsg(2)與recvmsg(2)函數
這些函數為程式提供了一些其他的套介面I/O介面所不具備的進階特性。下面的內容我們將會先來看一下sendmsg來介紹這些主題。然後將會完整的介紹recvmsg函數,因為他們的函數介面是相似的。接下來,將會描述msghdr的完整結構。
sendmsg(2)函數
現在是時候進入這個大同盟了。從概念上說,sendmsg函數是所有寫入函數的基礎,而他是從屬於套介面的。下面的列表以複雜增加的順序列出了所有與入函數。在每一個層次上,同時列出了所增加的特性。
函數 增加的特性
write 最簡單的套介面寫入函數
send 增加了flags標記
sendto 增加了套介面地址與套介面長度參數
writev 沒有標記與套介面地址,但是具有分散寫入的能力
sendmsg 增加標記,套介面地址與長度,分散寫入以及附屬資料的能力
sendmsg(2)函數原型如下:
#include
#include
int sendmsg(int s, const struct msghdr*msg, unsigned int flags);
函數參數描述如下:
要在其上發送訊息的套介面s
資訊頭結構指標msg,這會控制函數調用的功能
可選的標記位參數flags。這與send或是sendto函數調用的標記參數相同。
函數的傳回值為實際發送的位元組數。否則,返回-1表明發生了錯誤,而errno表明錯誤原因。
recvmsg(2)函數
recvmsg是與sendmsg函數相對的函數。這個函數原型如下:
#include
#include
int recvmsg(int s, struct msghdr *msg,unsigned int flags);
函數參數如下:
要在其上接收資訊的套介面s
資訊頭結構指標msg,這會控制函數調用的操作。
可選標記位參數flags。這與recv或是recvfrom函數調用的標記參數相同。
這個函數的傳回值為實際接收的位元組數。否則,返回-1表明發生了錯誤,而errno表明錯誤原因。
理解structmsghdr
當我第一次看到他時,他看上去似乎是一個需要建立的巨大的結構。但是不要怕。其結構定義如下:
struct msghdr {
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
size_t msg_iovlen;
void *msg_control;
size_t msg_controllen;
int msg_flags;
};
結構成員可以分為四組。他們是:
套介面地址成員msg_name與msg_namelen。
I/O向量引用msg_iov與msg_iovlen。
附屬資料緩衝區成員msg_control與msg_controllen。
接收資訊標記位msg_flags。
在我們將這個結構分為上面的幾類以後,結構看起來就不那樣巨大了。
成員msg_name與msg_namelen
這些成員只有當我們的套介面是一個資料報套介面時才需要。msg_name成員指向我們要發送或是接收資訊的套介面地址。成員msg_namelen指明了這個套介面地址的長度。
當調用recvmsg時,msg_name會指向一個將要接收的地址的接收地區。當調用sendmsg時,這會指向一個資料報將要發送到的目的地址。
注意,msg_name定義為一個(void *)資料類型。我們並不需要將我們的套介面地址轉換為(structsockaddr *)。
成員msg_iov與msg_iovlen
這些成員指定了我們的I/O向量數組的位置以及他包含多少項。msg_iov成員指向一個struct iovec數組。我們將會回憶起I/O向量指向我們的緩衝區。成員msg_iov指明了在我們的I/O向量數組中有多少元素。
成員msg_control與msg_controllen
這些成員指向了我們附屬資料緩衝區並且表明了緩衝區大小。msg_control指向附屬資料緩衝區,而msg_controllen指明了緩衝區大小。
成員msg_flags
當使用recvmsg時,這個成員用於接收特定的標記位(他並不用於sendmsg)。在這個位置可以接收的標記位如下表所示:
標記位 描述
MSG_EOR 當接收到記錄結尾時會設定這一位。這通常對於SOCK_SEQPACKET套介面類型十分有用。
MSG_TRUNC 這個標記位表明資料的結尾被截短,因為接收緩衝區太小不足以接收全部的資料。
MSG_CTRUNC 這個標記位表明某些控制資料(附屬資料)被截短,因為緩衝區太小。
MSG_OOB 這個標記位表明接收了帶外資料。
MSG_ERRQUEUE 這個標記位表明沒有接收到資料,但是返回一個擴充錯誤。
我們可以在recvmsg(2)與sendmsg(2)的man手冊頁中查看更多的資訊。
附屬資料結構與宏
recvmsg與sendmsg函數允許程式發送或是接收附屬資料。然而,這些額外的資訊受限於一定的格式規則。這一節將會介紹控制資訊頭與程式將會用來管理這些資訊的宏。
簡介structcmsghdr結構
附
屬資訊可以包括0,1,或是更多的單獨附屬資料對象。在每一個對象之前都有一個struct
cmsghdr結構。頭部之後是填充位元組,然後是對象本身。最後,附屬資料對象之後,下一個cmsghdr之前也許要有更多的填充位元組。在這一章,我們將
要關注的附屬資料對象是檔案描述符與認證結構。
顯示了一個包含附屬資料的緩衝區是如何組織的。
我們需要注意以下幾點:
cmsg_len與CMSG_LEN()宏值所顯示的長度相同。
CMSG_SPACE()宏可以計算一個附屬資料對象的所必需的空白。
msg_controllen是CMSG_SPACE()長度之後,並且為每一個附屬資料對象進行計算。
控制資訊頭部本身由下面的C結構定義:
struct cmsghdr {
socklen_t cmsg_len;
int cmsg_level;
int cmsg_type;
/* u_char cmsg_data[]; */
};
其成員描述如下:
成員 描述
cmsg_len 附屬資料的位元組計數,這包含結構頭的尺寸。這個值是由CMSG_LEN()宏計算的。
cmsg_level 這個值表明了原始的協議層級(例如,SOL_SOCKET)。
cmsg_type 這個值表明了控制資訊類型(例如,SCM_RIGHTS)。
cmsg_data 這個成員並不實際存在。他用來指明實際的額外附屬資料所在的位置。
這一章所用的例子程式只使用SOL_SOCKET的cmsg_level值。這一章我們感興趣的控制資訊類型如下(cmsg_level=SOL_SOCKET):
cmsg_level 描述
SCM_RIGHTS 附屬資料對象是一個檔案描述符
SCM_CREDENTIALS 附屬資料對象是一個包含認證資訊的結構
簡介cmsg(3)宏
由於附屬資料結構的複雜性,Linux系統提供了一系列的C宏來簡化我們的工作。另外,這些宏可以在不同的UNIX平台之間進行移植,並且採取了一些措施來防止將來的改變。這些宏是由cmsg(3)的man手冊頁來進行描述的,其概要如下:
#include
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr*msgh);
struct cmsghdr *CMSG_NXTHDR(struct msghdr*msgh, struct cmsghdr *cmsg);
size_t CMSG_ALIGN(size_t length);
size_t CMSG_SPACE(size_t length);
size_t CMSG_LEN(size_t length);
void *CMSG_DATA(struct cmsghdr *cmsg);
CMSG_LEN()宏
這個宏接受我們希望放置在附屬資料緩衝區中的對象尺寸作為輸入參數。如果我們回顧一個我們前面的介紹,我們就會發現這個宏會計算cmsghdr頭結構加上所需要的填充字元的位元組長度。這個值用來設定cmsghdr對象的cmsg_len成員。
下面的例子示範了如果附屬資料是一個檔案描述符,我們應如何來計算cmsg_len成員的值:
int fd; /* File descriptor */
printf("cmsg_len =%d\n",CMSG_LEN(sizeof fd));
CMSG_SPACE()宏
這個宏用來計算附屬資料以及其頭部所需的總空白。儘管CMSG_LEN()宏計算了一個相似的長度,CMSG_LEN()值並不包括可能的結尾的填充字元。CMSG_SPACE()宏對於確定所需的緩衝區尺寸是十分有用的,如下面的範例程式碼所示:
int fd; /* File Descriptor */
char abuf[CMSG_SPACE(sizeof fd)];
這個例子在abuf[]中聲明了足夠的緩衝區空間來存放頭部,填充位元組以及附屬資料本身,和最後的填充位元組。如果在緩衝區中有多個附屬資料對象,一定要同時添加多個CMSG_SPACE()宏調用來得到所需的總空間。
CMSG_DATA()宏
這個宏接受一個指向cmsghdr結構的指標。返回的指標值指向跟隨在頭部以及填充位元組之後的附屬資料的第一個位元組(如果存在)。如果指標mptr指向一個描述檔案描述符的可用的附屬資料資訊頭部,這個檔案描述符可以用下面的代碼來得到:
struct cmsgptr *mptr;
int fd; /* File Descriptor */
. . .
fd = *(int *)CMSG_DATA(mptr);
CMSG_ALIGN()宏
這是一個Linux擴充宏,而不是Posix.1g標準的一部分。指定一個位元組長度作為輸入,這個宏會計算一個新的長度,這個新長度包括為了維護對齊所需要的額外的填充位元組。
CMSG_FIRSTHDR()宏
這
個宏用於返回一個指向附屬資料緩衝區內的第一個附屬對象的struct cmsghdr指標。輸入值為是指向struct
msghdr結構的指標(不要與struct
cmsghdr相混淆)。這個宏會估計msghdr的成員msg_control與msg_controllen來確定在緩衝區中是否存在附屬對象。然
後,他會計算返回的指標。
如果不存在附屬資料對象則返回的指標值為NULL。否則,這個指標會指向存在的第一個struct cmsghdr。這個宏用在一個for迴圈的開始處,來開始在附屬資料對象中遍曆。
CMSG_NXTHDR()宏
這個用於返回下一個附屬資料對象的struct cmsghdr指標。這個宏會接受兩個輸入參數:
指向structmsghdr結構的指標
指向當前structcmsghdr的指標
如果沒有下一個附屬資料對象,這個宏就會返回NULL。
遍曆附屬資料
當接收到一個附屬資料時,我們可以使用CMSG_FIRSTHDR()與CMSG_NXTHDR()宏來在附屬資料對象中進行遍曆。下面的範例程式碼顯示了for迴圈的通常格式以及宏的相應用法:
struct msghdr msgh; /* Message Hdr */
struct cmsghdr *cmsg;0 /*Ptr to ancillary hdr */
int *fd_ptr; /* Ptr to file descript.*/
int received_fd; /* The file descriptor */
for ( cmsg=CMSG_FIRSTHDR(&msgh);cmsg!=NULL; cmsg=CMSG_NXTHDR(&msgh,cmsg) ) {
if ( cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type ==SCM_RIGHTS ) {
fd_ptr = (int *) CMSG_DATA(cmsg);
received_fd = *fd_ptr;
break;
}
}
if ( cmsg == NULL ) {
/* Error: No file descriptor recv'd */
}
建立附屬資料
要發送一個檔案描述符的進程必須使用正確的格式化資料來建立一個附屬資料緩衝區。下面的代碼展示的通常的建立過程:
struct msghdr msg; /* Message header */
struct cmsghdr *cmsg; /* Ptr to ancillaryhdr */
int fd; /* File descriptor to send */
char buf[CMSG_SPACE(sizeof fd)]; /* Anc.buf */
int *fd_ptr; /* Ptr to file descriptor */
msg.msg_control = buf;
msg.msg_controllen = sizeof buf;
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof fd);
/* Initialize the payload: */
fd_ptr = (int *)CMSG_DATA(cmsg);
*fd_ptr = fd;
/*
* Sum of the length of all control
* messages in the buffer:
*/
msg.msg_controllen = cmsg->cmsg_len;