https://man.cx/ip(7)/zh_CN

LinuxC下获取UDP包中的路由目的IP地址和头标识目的地址

在接收到UDP包后,有时候我们需要根据所接收到得UDP包,获取它的路由目的IP地址和头标识目的地址。

(一)主要的步骤:

setsockopt(sock, IPPROTO_IP, IP_PKTINFO, &opt, sizeof(opt));

int setsockopt(int sockfd,
int level,
int optname,
const void *optval,
int optlen);sockfd(套接字): 指向一个打开的套接口描述字
level:(级别): 指定选项代码的类型。
SOL_SOCKET: 基本套接口
IPPROTO_IP: IPv4套接口
IPPROTO_IPV6: IPv6套接口
IPPROTO_TCP: TCP套接口
optname(选项名): 选项名称
optval(选项值): 是一个指向变量的指针
optlen(选项长度) :optval 的大小返回值:标志打开或关闭某个特征的二进制选项

在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 255int 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

结果截图

image

因为通过虚拟网卡的,所以我们看到目标IP地址并不是网络调试助手中设置的IP,而是虚拟网卡的地址,通过Linux下的tcpdump我们可以看到其中网卡转发的过程。

image

下面我将本篇涉及到的和结构体函数原型都附在下方

(一)涉及到的结构体

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

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显示了一个包含附属数据的缓冲区是如何组织的。

图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学习

作者:kissazi2
出处:http://www.cnblogs.com/kissazi2/p/3158603.html
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

Socket API: setsockopt(), recvmsg(), sendmsg()函数相关推荐

  1. socket编程基础2(socket API函数介绍)

    "一切皆Socket!" 话虽些许夸张,但是事实也是,现在的网络编程几乎都是用的socket. --有感于实际编程和开源项目研究. 我们深谙信息交流的价值,那网络中进程之间如何通信 ...

  2. Socket API: I/O函数recvmsg()与sendmsg()

    转自: https://memorymyann.iteye.com/blog/648513 作者: memorymyann 以下为正文 17.I/O函数recvmsg与sendmsg 博客分类: 网络 ...

  3. 网络编程学习笔记(recvmsg和sendmsg函数)

    这两个函数是最通用的I/O函数.实际上,可以用recvmsg代替read.readv.recv和redvfrom.同样,各种输出函数都可以用sendmsg取代 #include<sys/sock ...

  4. C语言socket getsockopt() setsockopt()函数(获取和设置套接口的选项?)

    文章目录 man 文档(越看越懵逼啊!) 解释 getsockopt()函数用于获取任意类型.任意状态套接口的选项当前值,并把结果存入optval. setsockopt()函数用于任意类型.任意状态 ...

  5. C语言socket getsockopt() setsockopt()函数(获取和设置套接口的选项?)(套接字级别SOL_SOCKET)

    文章目录 man 文档(越看越懵逼啊!) 解释 getsockopt()函数用于获取任意类型.任意状态套接口的选项当前值,并把结果存入optval. setsockopt()函数用于任意类型.任意状态 ...

  6. 在C++ Builder中用socket api来写网络通讯程序(同时支持TCP和UDP协议)

    原标题:在C++ Builder中用socket api来写网络通讯程序(同时支持TCP和UDP协议) 原文:  http://www.csdn.net/develop/read_article.as ...

  7. Socket编程 涵盖代码和函数参数介绍

    Socket是针对端系统,也就是用户主机上开发程序,不涉及网络设备(交换机.路由器) 独立于网卡驱动层之上,不涉及硬件,即基于Packet Driver编程 端:是指通信双方两台电脑 应用编程接口AP ...

  8. lwip网络通信socket_lwIP在Socket模式下接口:BSD Socket API

    http://home.eeworld.com.cn/my/space-uid-162102-blogid-52270.html 2011 原文: 关于BSD Socket API 在网上找到的两个网 ...

  9. api有哪些 javasocket_Java Socket编程以及与Linux Socket API关系

    Socket 编程(基于Linux) Socket独立于具体协议的网络编程接口,在ISO模型中,主要位于会话层和传输层之间:在通用的计算机网络五层模型中,主要位于应用层和传输层之间. Linux So ...

最新文章

  1. 一名算法工程师,对流量和时代红利的思考
  2. 深入浅出WPF——附加事件(Attached Event)
  3. 统计学习方法笔记(五)-线性可分支持向量机原理及python实现
  4. python操作csv文件
  5. 复习1 - String,StringBuilder,StringBuffer的执行效率区别
  6. 请谈一下@Autowired 和@Resource区别是什么?
  7. linux supervisor
  8. 34 CO配置-控制-产品成本控制-成本对象控制-期末结算-检查差异变式
  9. nginx下部署vue项目概览 - (资源篇)
  10. php declaration of,PHP 7.2中的新功能(参数类型声明)
  11. .Net 开源项目资源大全
  12. 滴滴辞退2000人启示:牛逼的人,都有自己的铁饭碗
  13. SQL读书笔记SQL操作语句
  14. python 学习之路1-如何入门
  15. 在线教育平台源码组成概述
  16. c语言结构体定义常量,C语言结构体
  17. centos7安装wget
  18. eliminate什么意思_eliminate是什么意思_eliminate在线翻译_英语_读音_用法_例句_海词词典...
  19. lvgl v8之Styling the scrollbars
  20. 基于SSM框架CRM客户管理系统

热门文章

  1. Python基础-循环
  2. window.open打开页面并传值,window. location.search遍历获取到的请求链接中的所有参数
  3. html带取消的谈窗框,HTML参考
  4. cf修改游戏客户端是什么意思_cf游戏客户端是什么
  5. 实验3-4 统计字符 (15 分)
  6. 天梯—打印沙漏以及剩余个数(C语言)
  7. python字符串isalnum()函数
  8. 云存储是否能拯救数据泛滥
  9. android 图片任意拖拉,android 拖拉图片 Gallery 用SimpleAdater实现
  10. 详解linux运维工程师入门级必备技能