文章出處

在接受到UDP包后,有時候我們需要根據所接收到得UDP包,獲取它的路由目的IP地址和頭標識目的地址。

(一)主要的步驟:

setsockopt中設置IP_PKTINFO,然后通過recvmsg來獲取struct in_pktinfo(struct in_pktinfo是struct msghdr中msg_control的成員).in_pktinfo 結構體(如下所示),我們可以從in_pktinfo中獲取路由目的地址(destination address of the packet)、頭標識目的地址(source address of the packet)。這種方法只能用于UDP(數據報)傳輸中。

struct in_pktinfo
    {
        unsigned int ipi_ifindex;    /* 接口索引 */
        struct in_addr ipi_spec_dst; /* 路由目的地址 */
        struct in_addr ipi_addr;     /* 頭標識目的地址 */
    };

ipi_ifindex指的是接收包的接口的唯一索引,ipi_spec_dst指的是路由表記錄中的目的地址,而ipi_addr 指的是包頭中的目的地址。如果給 setsockopt傳遞了IP_PKTINFO,那么外發的包會通過在ipi_ifindex中指定的接口發送出去,同時把ipi_spec_dst設置為目的地址。

(二)下面的例子簡單地說明如何獲取UDP包中的源地址(interface addresses)、目標地址(destination addresses)。為了代碼的簡單,下面代碼段省去了錯誤檢查。

// sock 使用AF_INET協議族,  socket類型SOCK_DGRAM
setsockopt(sock, IPPROTO_IP, IP_PKTINFO, &opt, sizeof(opt));
// 這里,控制數據是臟數據。
char cmbuf[0x100];
// 目標IP地址
struct sockaddr_in peeraddr;

//如果你想要獲取UDP包中的數據,那么還需要為msg_iovec字段初始化
struct msghdr mh = {
    .msg_name = &peeraddr,
    .msg_namelen = sizeof(peeraddr),
    .msg_control = cmbuf,
    .msg_controllen = sizeof(cmbuf),
};
recvmsg(sock, &mh, 0);
 struct cmsghdr *cmsg ;
for ( // 遍歷所有的控制頭(the control headers)
    cmsg = CMSG_FIRSTHDR(&mh);
    cmsg != NULL;
    cmsg = CMSG_NXTHDR(&mh, cmsg))
{
    // 忽略我們不需要的控制頭(the control headers)
    if (cmsg->cmsg_level != IPPROTO_IP ||
        cmsg->cmsg_type != IP_PKTINFO)
    {
        continue;
    }
    struct in_pktinfo *pi = CMSG_DATA(cmsg);
    // 在這里, peeraddr是本機的地址(the source sockaddr)
    // pi->ipi_spec_dst 是UDP包中路由目的地址(the destination in_addr)
    // pi->ipi_addr 是UDP包中的頭標識目的地址(the receiving interface in_addr)
}

(三)下面我將給出一個完整可運行的例子,這個例子實現了接收UDP廣播包,發送UDP廣播包,并在接收的時候,打印出UDP包的路由目的IP地址和頭標識目的地址。

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFLEN 255

int main ( int argc, char **argv )
{
    struct sockaddr_in peeraddr, localaddr;
    int sockfd;
    int socklen, n;
//(1)創建UDP數據報socket描述符
    sockfd = socket ( AF_INET, SOCK_DGRAM, 0 );
    if ( sockfd<0 )
    {
        printf ( "socket creating err in udptalk\n" );
        exit ( EXIT_FAILURE );
    }
    printf ( "IP address Checking!\n" );
    socklen = sizeof ( struct sockaddr_in );
    memset ( &peeraddr, 0, socklen );
//(2)設置目標主機IP和端口,這里我們使用廣播方式
    peeraddr.sin_family=AF_INET;
    peeraddr.sin_port=htons ( atoi ( "8903" ) );

    peeraddr.sin_addr.s_addr = htonl ( INADDR_BROADCAST );
//(3設置本機IP和端口,這里我們設置可以接收符合端口的所有的包
    memset ( &localaddr, 0, socklen );
    localaddr.sin_family=AF_INET;
    localaddr.sin_addr.s_addr = htonl ( INADDR_ANY );  //設置接收任何主機

    printf ( "try to bind local address \n" );
    localaddr.sin_port=htons ( atoi ( "8904" ) );

//(4)設置IPPROTO_IP標志,以便獲取UDP包中的信息
    int opt = 1;
    setsockopt ( sockfd, IPPROTO_IP, IP_PKTINFO, &opt, sizeof ( opt ) );
    int nb = 0;
//(5)設置為廣播方式
    nb = setsockopt ( sockfd, SOL_SOCKET, SO_BROADCAST, ( char * ) &opt,
                      sizeof ( opt ) );
    if ( nb == -1 )
    {
        printf ( "set socket error..." );
        exit ( EXIT_FAILURE );
    }
    printf ( "IP address Checking!\n" );

    char cmbuf[100];// 這里只是為控制數據申請一個空間
//(6)初始化msg_iovec字段,以便獲取UDP包數據域
    char buffer[BUFLEN+1];
    struct iovec iov[1];
    iov[0].iov_base=buffer;
    iov[0].iov_len=sizeof ( buffer );
//(7)初始化struct msghdr,以便獲取UDP包中目標IP地址和源地址
    struct msghdr mh =
    {
        .msg_name = &localaddr,
        .msg_namelen = sizeof ( localaddr ),
        .msg_control = cmbuf,
        .msg_controllen = sizeof (cmbuf ),
        .msg_iov=iov,                                                           
        .msg_iovlen=1
    };
//(8)將本機的地址信息與sockfd綁定起來
    if ( bind ( sockfd, &localaddr, socklen ) <0 )
    {
        printf ( "bind local address err in udptalk!\n" );
        exit ( 2 );
    }
    //發一個消息給目標主機
    if ( sendto ( sockfd, "HELLO", strlen ( "HELLO" ), 0, &peeraddr, socklen )
<0 )
    {
        printf ( "sendto err in udptalk!\n" );
        exit ( 3 );
    }
    printf ( "end of sendto \n" );
    printf ( "start of recv&send message loop!\n" );
    for ( ;; )//接收消息循環
    {
        
        printf ( "Waiting For Message...!\n" );
        n=recvmsg ( sockfd, &mh, 0 );
        //判斷socket是否有錯誤發生
        if ( n<0 )
        {
            printf ( "recvfrom err in udptalk!\n" );
            exit ( 4 );
        }
        else
        {
            cmbuf[n]=0;
            printf ( "Receive:%dByte。\tThe Message Is:%s\n", n,buffer );
        }
//(9)初始化cmsghdr以便處理mh中的附屬數據,通過遍歷附屬數據對象,找出我們感興趣的信息
        struct cmsghdr *cmsg ;
        for ( cmsg = CMSG_FIRSTHDR ( &mh );
                cmsg != NULL;
                cmsg = CMSG_NXTHDR ( &mh, cmsg ) )
        {
         // 忽略我們不需要的控制頭(the control headers)
            if ( cmsg->cmsg_level != IPPROTO_IP ||
                    cmsg->cmsg_type != IP_PKTINFO )
            {
                continue;
            }
            struct in_pktinfo *pi = CMSG_DATA ( cmsg );    
           
//(10)將地址信息轉換后輸出
            char dst[100],ipi[100];//用來保存轉化后的源IP地址,目標主機地址   
// pi->ipi_spec_dst 是UDP包中的路由目的IP地址(the destination in_addr) // pi->ipi_addr 是UDP包中的頭標識目的地址(the receiving interface in_addr) if ( ( inet_ntop ( AF_INET,& ( pi->ipi_spec_dst ),dst,sizeof ( dst ) ) ) !=NULL ) { printf ( "路由目的IP地址IPdst=%s\n",dst); } if ( ( inet_ntop ( AF_INET,& ( pi->ipi_addr ),ipi,sizeof ( ipi ) ) ) !=NULL ) { printf ("頭標識目的地址ipi_addr=%s\n",ipi); } } printf ( "Send Some Message To Server\n" ); if ( sendto ( sockfd, "Hello", strlen ( buffer ), 0, &peeraddr, socklen) <0 ) { printf ( "sendto err in udptalk!\n" ); exit ( 3 ); } } }

例子的使用說明

1、開啟虛擬機下面的例子程序

2、通過windows下面的網絡調試助手向虛擬機發送數據

 image

結果截圖

因為通過虛擬網卡的,所以我們看到目標IP地址并不是網絡調試助手中設置的IP,而是虛擬網卡的地址,通過Linux下的tcpdump我們可以看到其中網卡轉發的過程。

下面我將本篇涉及到的結構體函數原型都附在下方

(一)涉及到的結構體

1、struct in_addr

 
struct in_addr {
    in_addr_t s_addr;
};

結構體in_addr 用來表示一個32位的IPv4地址.

in_addr_t 一般為 32位的unsigned long,其字節順序為網絡順序(network byte ordered),即該無符號整數采用大端字節序

2、struct msghdr

recvmsg()使用 msghdr 結構體(structure )減少參數傳遞的數目。這個結構體定義在 <sys/socket.h>中,如下所示
struct iovec {                   /* Scatter/gather array items */
   void  *iov_base;              /* Starting address */
   size_t iov_len;               /* Number of bytes to transfer */
};

struct msghdr {
   void         *msg_name;       /* optional address */
   socklen_t     msg_namelen;    /* size of address */
   struct iovec *msg_iov;        /* scatter/gather array */
   size_t        msg_iovlen;     /* # elements in msg_iov */
   void         *msg_control;    /* ancillary data, see below */
   size_t        msg_controllen; /* ancillary data buffer len */
   int           msg_flags;      /* flags on received message */
};

struct msghdr看上去似乎是一個需要創建的巨大的結構。但是不要怕。其結構成員可分為四組:

套接口地址成員: 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 *)數據類型。我們并不需要將我們的套接口地址轉換為(struct sockaddr *)。

成員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 個標記位表明沒有接收到數據,但是返回一個擴展錯誤。

 

3、struct cmsghdr結構

recvmsg與sendmsg函數允許程序發送或是接收附屬數據。然而,這些額外的信息受限于一定的格式規則。下面將會介紹控制信息頭與程序將會用來管理這些信息的宏。

屬信息可以包括0,1,或是更多的單獨附屬數據對象。在每一個對象之前都有一個struct cmsghdr結構。頭部之后是填充字節,然后是對象本身。最后,附屬數據對象之后,下一個cmsghdr之前也許要有更多的填充字節。在這里,我們將要關注的附屬數據對象是文件描述符與證書結構。
圖1顯示了一個包含附屬數據的緩沖區是如何組織的。

 

                    graphics/17fig01.gif

                                                  圖1  輔助數據結構是由各種子結構、數據區, 填充字節構成
我們需要注意以下幾點:
cmsg_len與CMSG_LEN()宏值所顯示的長度相同。
CMSG_SPACE()宏可以計算一個附屬數據對象的所必需的空白。
msg_controllen是CMSG_SPACE()長度之后,并且為每一個附屬數據對象進行計算。

 

struct cmsghdr {
   socklen_t cmsg_len;    /* data byte count, including header */
   int       cmsg_level;  /* originating protocol */
   int       cmsg_type;   /* protocol-specific type */
   /* followed by unsigned 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 附屬數據對象是一個包含證書信息的結構

     

(二)涉及的函數

1、setsockopt函數原型

//setsockopt函數原型
#include <sys/types.h>       
#include <sys/socket.h>
int setsockopt(int s, int level, int optname,
                      const void *optval, socklen_t optlen);

 

2、cmsg 宏
由于附屬數據結構的復雜性,Linux系統提供了一系列的C宏來簡化我們的工作。另外,這些宏可以在不同的UNIX平臺之間進行移植,并且采取了一些措施來防止將來的改變。這些宏是由cmsg(3)的man手冊頁來進行描述的,其概要如下:

#include <sys/socket.h>
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_DATA()宏
這個宏接受一個指向cmsghdr結構的指針。返回的指針值指向跟隨在頭部以及填充字節之后的附屬數據的第一個字節(如果存在)。如果指針mptr指向一個描述文件描述符的可用的附屬數據信息頭部,這個文件描述符可以用下面的代碼來得到:
struct cmsgptr *mptr;
int fd; /* File Descriptor */
. . .
fd = *(int *)CMSG_DATA(mptr);
CMSG_FIRSTHDR()宏
這個宏用于返回一個指向附屬數據緩沖區內的第一個附屬對象的struct cmsghdr指針。輸入值為是指向struct msghdr結構的指針(不要與struct cmsghdr相混淆)。這個宏會估計msghdr的成員msg_control與msg_controllen來確定在緩沖區中是否存在附屬對象。然后,他會計算返回的指針。
如果不存在附屬數據對象則返回的指針值為NULL。否則,這個指針會指向存在的第一個struct cmsghdr。這個宏用在一個for循環的開始處,來開始在附屬數據對象中遍歷。
CMSG_NXTHDR()宏
這個用于返回下一個附屬數據對象的struct cmsghdr指針。這個宏會接受兩個輸入參數:
指向struct msghdr結構的指針
指向當前struct cmsghdr的指針
如果沒有下一個附屬數據對象,這個宏就會返回NULL。

 

參考鏈接

Get destination address of a received UDP packet

套接字選項(四)

inet_pton & inet_ntop函數

in_addr

Listen for and receive UDP datagrams in C

關于struct msghdr和struct cmsghdr

Linux Socket Programming by Example - Warren Gay

Linux Socket學習


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜

    大師兄 發表在 痞客邦 留言(0) 人氣()