我是靠谱客的博主 明理黑夜,最近开发中收集的这篇文章主要介绍AF_NetLink结构体及例程一、AF_NETLINK结构体基础二、 netlink 内核数据结构、常用宏及函数三、例二:读取内核路由信息四、例三:自定义通信协议,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

 

一、AF_NETLINK结构体基础

我们从一个实际的数据包发送的例子入手,来看看其发送的具体流程,以及过程中涉及到的相关数据结构。在我们的虚拟机上发送icmp回显请求包,ping另一台主机172.16.48.1。我们使用系统调用sendto发送这个icmp包。

    ssize_t sendto(int s, const void *buf, size_t len, int flags,

                       const struct sockaddr *to, socklen_t tolen);

系统调用sendto最终调用内核函数 asmlinkage long sys_sendto(int fd, void __user * buff, size_t len, unsigned flags, struct sockaddr __user *addr, int addr_len)

sys_sendto构建一个结构体struct msghdr,用于接收来自应用层的数据包 ,下面是结构体struct msghdr的定义:

        struct msghdr {

           void           *msg_name;// 存数据包的目的地址,网络包指向sockaddr_in

                                                  //向内核发数据时,指向sockaddr_nl

           int            msg_namelen;// 地址长度

           struct iovec    *msg_iov;

           __kernel_size_t msg_iovlen;

           void           *msg_control;

           __kernel_size_t msg_controllen;

           unsigned        msg_flags;

        };

    这个结构体的内容可以分为四组:

    第一组是msg_namemsg_namelen,记录这个消息的名字,其实就是数据包的目的地址 。msg_name是指向一个结构体struct sockaddr的指针。长度为16:

        structsockaddr{

           sa_family_t sa_family;

           char        sa_addr[14];

        }

所以,msg_namelen的长度为16。需要注意的是,结构体struct sockaddr只在进行参数传递时使用,无论是在用户态还是在内核态,我们都把其强制转化为结构体struct sockaddr_in:

        strcutsockaddr_in{

           sa_family_t         sin_family;

           unsigned short int sin_port;

           struct in_addr      sin_addr;

           unsigned char       __pad[__SOCK_SIZE__ -sizeof(short int) -

                               sizeof(unsigned short int) - sizeof(struct in_addr)];

        };

        struct in_addr{

            __u32s_addr;

        }

__SOCK_SIZE__的值为16,所以,struct sockaddr中真正有用的数据只有8bytes。在我们的ping例子中,传入到内核的msghdr结构中:

msg.msg_name = { sa_family_t = MY_AF_INET, sin_port = 0, sin_addr.s_addr= 172.16.48.1 }

    msg_msg_namelen = 16。

请求回显icmp包没有目的端地址的端口号。

第二组是msg_iov和msg_iovlen,记录这个消息的内容。msg_iov是一个指向结构体struct iovec的指针,实际上,确切地说,应该是一个结构体strcut iovec的数组 。下面是该结构体的定义:

    struct iovec{

        void__user     *iov_base;

        __kernel_size_t iov_len;

    };

iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度 (即有多少个buff)。在我们的ping程序的实例中:

    msg.msg_iov = { struct iovec = { iov_base= { icmp头+填充字符'E' }, iov_len = 40 } }

    msg.msg_len = 1

第三组是msg_control和msg_controllen,它们可被用于发送任何的控制信息,在我们的例子中,没有控制信息要发送。暂时略过。

第四组是msg_flags。其值即为传入的参数flags。raw协议不支持MSG_ OOB 向标志,即带外数据。 向内核发送msg 时使用msghdrnetlink socket使用自己的消息头nlmsghdr和自己的消息地址sockaddr_nl

struct sockaddr_nl
{
sa_family_t    nl_family;
unsigned short nl_pad;
__u32          nl_pid;
__u32          nl_groups;
};
struct nlmsghdr
{
__u32 nlmsg_len;   /* Length of message */
__u16 nlmsg_type; /* Message type*/
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq;   /* Sequence number */
__u32 nlmsg_pid;   /* Sending process PID */
};

其中,nlmsg_flags:消息标记,它们用以表示消息的类型;

nlmsg_seq:消息序列号,用以将消息排队,有些类似TCP协议中的序号(不完全一样),但是netlink的这个字段是可选的,不强制使用;

nlmsg_pid:发送端口的ID号,对于内核来说该值就是0对于用户进程来说就是其socket所绑定的ID

 

过程如下:

struct msghdr msg; 
memset(&msg, 0,sizeof(msg));
msg.msg_name =(void *)&(nladdr);  //绑定目的地址
msg.msg_namelen = sizeof(nladdr);
{
/*初始化一个strcut nlmsghdr结构存,nlmsghdr为netlink socket自己的消息头部,并使iov->iov_base指向在这个结构*/
char buffer[] = "An example message";
struct nlmsghdr nlhdr;
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
strcpy(NLMSG_DATA (nlhdr),buffer);//将数据存放在消息头指向的数据地址
nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));
nlhdr->nlmsg_pid = getpid(); /* self pid */
nlhdr->nlmsg_flags = 0;
iov.iov_base = (void *)nlhdr;
iov.iov_len = nlh->nlmsg_len;
}
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
fd=socket(AF_NETLINK, SOCK_RAW, netlink_type);
sendmsg(fd,&msg,0)

二、 netlink 内核数据结构、常用宏及函数

netlink消息类型:

#define NETLINK_ROUTE       0  /* Routing/device hook             */
#define NETLINK_UNUSED      1   /* Unused number                */
#define NETLINK_USERSOCK    2   /* Reserved for user mode socket protocols  */
#define NETLINK_FIREWALL    3   /* Unused number, formerly ip_queue     */
#define NETLINK_SOCK_DIAG   4   /* socket monitoring                */
#define NETLINK_NFLOG       5   /* netfilter/iptables ULOG */
#define NETLINK_XFRM        6   /* ipsec */
#define NETLINK_SELINUX     7   /* SELinux event notifications */
#define NETLINK_ISCSI       8   /* Open-iSCSI */
#define NETLINK_AUDIT       9   /* auditing */
#define NETLINK_FIB_LOOKUP  10  
#define NETLINK_CONNECTOR   11
#define NETLINK_NETFILTER   12  /* netfilter subsystem */
#define NETLINK_IP6_FW      13
#define NETLINK_DNRTMSG     14  /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT  15  /* Kernel messages to userspace */
#define NETLINK_GENERIC     16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT   18  /* SCSI Transports */
#define NETLINK_ECRYPTFS    19
#define NETLINK_RDMA        20
#define NETLINK_CRYPTO      21  /* Crypto layer */
#define NETLINK_INET_DIAG   NETLINK_SOCK_DIAG
#define MAX_LINKS 32 

netlink常用宏

#define NLMSG_ALIGNTO   4U
 /* 宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
 /* Netlink 头部长度 */
#define NLMSG_HDRLEN     ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/* 计算消息数据len的真实消息长度(消息体 + 消息头)*/
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
/* 宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/* 宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏 */
#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/* 宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址, 同时len 变为剩余消息的长度 */
#define NLMSG_NEXT(nlh,len)  ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), 
(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
/* 判断消息是否 >len */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && (nlh)->nlmsg_len <= (len))
/* NLMSG_PAYLOAD(nlh,len) 用于返回payload的长度*/
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
 
Linux下虽然也有AF_ROUTE族套接字,但是这个定义只是个别名,请看
/usr/include/linux/socket.h, line 145:
#define AF_ROUTE AF_NETLINK /* Alias to emulate 4.4BSD */
可见在Linux内核当中真正实现routing socket的是AF_NETLINK族套接字。AF_NETLINK族套接字像一个连接用户空间和内核的双工管道,通过它,用户进程可以修改内核运行参数、读取和设置路由信息、控制特定网卡的up/down状态等等,可以说是一个管理网络资源的绝佳途径。
 

三、例二:读取内核路由信息

上面提到的nlmsghdr,它只是一个信息头,后面可以接任意长的数据,上面的例子我们只是填充了一个字符串,实际上这里是针对某一需求所采用的特定数据结构。先来看nlmsghdr:
struct nlmsghdr {
_u32 nlmsg_len; /* Length of msg including header */
_u32 nlmsg_type; /* 操作命令 */
_u16 nlmsg_flags; /* various flags */
_u32 nlmsg_seq; /* Sequence number */
_u32 nlmsg_pid; /* 进程PID */
};
/* 紧跟着是实际要发送的数据,长度可以任意 */
其中nlmsg_type决定这次要执行的操作,如查询当前路由表信息,所使用的就是RTM_GETROUTE。标准nlmsg_type包括:NLMSG_NOOP, NLMSG_DONE, NLMSG_ERROR等。根据采用的nlmsg_type不同,还要选取不同的数据结构来填充到nlmsghdr后面:
操作                数据结构
RTM_NEWLINK ifinfomsg
RTM_DELLINK
RTM_GETLINK
RTM_NEWADDR ifaddrmsg
RTM_DELADDR
RTM_GETADDR
RTM_NEWROUTE rtmsg
RTM_DELROUTE
RTM_GETROUTE
RTM_NEWNEIGH ndmsg/nda_chcheinfo
RTM_DELNEIGH
RTM_GETNEIGH
RTM_NEWRULE rtmsg
RTM_DELRULE
RTM_GETRULE
RTM_NEWQDISC tcmsg
RTM_DELQDISC
RTM_GETQDISC
RTM_NEWTCLASS tcmsg
RTM_DELTCLASS
RTM_GETTCLASS
RTM_NEWTFILTER tcmsg
RTM_DELTFILTER

由于情形众多,这里以从内核读取IPV4路由表信息为例。从上面表看,nlmsg_type一定使用RTM_xxxROUTE操作,对应的数据结构是rtmsg。既然是读取,那么应该是RTM_GETROUTE了。

structrtmsg {
unsigned char rtm_family; /* 路由表地址族 */
unsigned char rtm_dst_len; /* 目的长度 */
unsigned char rtm_src_len; /* 源长度 */ (2.4.10头文件的注释标反了?)
unsigned char rtm_tos; /* TOS */

unsigned char rtm_table; /* 路由表选取 */
unsigned char rtm_protocol; /* 路由协议 */
unsigned char rtm_scope;
unsigned char rtm_type;

unsigned int rtm_flags;
};

对于RTM_GETROUTE操作来说,我们只需指定两个成员:rtm_family:AF_INET, rtm_table: RT_TABLE_MAIN。其他成员都初始化为0即可。将这个结构体跟nlmsghdr结合起来,得到我们自己的新结构体:
struct {
struct nlmsghdr nl;
struct rtmsg rt;
}req;

填充好rt结构之后,还要调整nl结构相应成员的值。Linux定义了多个宏来处理nlmsghdr成员的值,我们这里用到的是NLMSG_LENGTH(size_tlen);
req.nl.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
这将计算nlmsghdr长度与rtmsg长度的和(其中包括了将rtmsg进行4字节边界对齐的调整),并存储到nlmsghdr的nlmsg_len成员中。

接下来要做的就是将这个新结构体req放到sendmsg()函数的msghdr.iov处,并调用函数:sendmsg(sockfd, &msg, 0);

接下来的操作是recv()操作,从该套接字读取内核返回的数据,并进行分析处理。
recv(sockfd, p, sizeof(buf) - nll, 0);
其中p是指向一个缓冲区buf的指针,nll是已接收到的nlmsghdr数据的长度。
由于内核返回信息是一个字节流,需要调用者检查消息结尾。这是通过检查返回的nlmsghdr的nlmsg_type是否等于NLMSG_DONE来完成的。返回的数据格式如下:
-----------------------------------------------------------
| nlmsghdr+route entry | nlmsghdr+route entry | ......... 
-----------------------------------------------------------
| 解出routeentry
V
-----------------------------------------------------------
| dst_addr | gateway | Output interface| ...............
-----------------------------------------------------------
可以看出,返回消息由多个(nlmsghdr+ route entry)组成,当某个nlmsghdr的nlmsg_type == NLMSG_DONE时就表示信息输出已经完毕。而每一个routeentry由多个rtattr结构体组成,每个结构体表示该路由项的某个属性,如目的地址,网关等等。根据这个示意图我们就能够轻松解析需要的数据了。

 

四、例三:自定义通信协议

本节通过详解一个简单的实例程序来说明用户进程通过netlink机制如何主动向内核发起会话。在该程序中,用户进程向内核发送一段字符串,内核接收到后再将该字符串后再重新发给用户进程。用户态程序netlink是一种特殊的套接字,在用户态除了一些参数的传递对其使用的方法与一般套接字无较大差异。

1.宏与数据结构的定义

在使用netlink进行用户进程和内核的数据交互时,最重要的是定义好通信协议。协议一词直白的说就是用户进程和内核应该以什么样的形式发送数据,以什么样的形式接收数据。而这个“形式”通常对应程序中的一个特定数据结构。

本文所演示的程序并没有使用netlink已有的通信协议,因此我们自定义一种协议类型NETLINK_TEST

1 #defineNETLINK_TEST 18
2 #define MAX_PAYLOAD 1024
3 
4 struct req {
5     struct nlmsghdr nlh;
6     char buf[MAX_PAYLOAD];
7 };
除此之外,我们应该再自定义一个数据报类型 req ,该结构包含了 netlink 数据包头结构的变量 nlh 和一个 MAX_PAYLOAD 大小的缓冲区。这里我们为了演示简单,并没有像上文中描述的那样将一个特定数据结构与 nlmsghdr 封装起来。

2.创建netlink套接字

要使用netlink,必须先创建一个netlink套接字。创建方法同样采用socket(),只是这里需要注意传递的参数:
1 int sock_fd;
2 sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
3 if (sock_fd < 0) {
4     eprint(errno, "socket", __LINE__);
5     return errno;
6 }

第一个参数必须指定为PF_NETLINKAF_NETLINK。第二个参数必须指定为SOCK_RAWSOCK_DGRAM,因为netlink提供的是一种无连接的数据报服务。第三个参数则指定具体的协议类型,我们这里使用自定义的协议类型NETLINK_TEST。另外,eprint()是一个自定义的出错处理函数,实现如下:
1 void eprint(int err_no, char *str, int line)
2 {
3     printf("Error %d in line %d:%s() with %sn",err_no, line, str, strerror(errno));
4 }

3.将本地套接字与源地址绑定

将本地的套接字与源地址进行绑定通过bind()完成。在绑定之前,需要将源地址进行初始化,nl_pid字段指明发送消息一方的pidnl_groups表示多播组的掩码,这里我们并没有涉及多播,因此默认为0

1 struct sockaddr_nl src_addr;
2 memset(&src_addr, 0, sizeof(src_addr));
3 src_addr.nl_family = AF_NETLINK;
4 src_addr.nl_pid = getpid();
5 src_addr.nl_groups = 0;
6 
7 if (bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr)) < 0){
8     eprint(errno, "bind", __LINE__);
9     return errno;
10 }

4.初始化msghdr结构

用户进程最终发送的是msghdr结构的消息,因此必须对这个结构进行初始化。而此结构又与sockaddr_nliovecnlmsghdr三个结构相关,因此必须依次对这些数据结构进行初始化。首先初始化目的套接字的地址结构,该结构与源套接字地址结构初始化的方法稍有不同,即nl_pid必须为0,表示接收方为内核。

1 struct sockaddr_nl dest_addr;
2 memset(&dest_addr, 0, sizeof(dest_addr));
3 dest_addr.nl_family = AF_NETLINK;
4 dest_addr.nl_pid = 0;
5 dest_addr.nl_groups = 0;
接下来对 req 类型的数据报进行初始化,即依次对其封装的两个数据结构初始化:
1 struct req r;
2 r.nlh.nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
3 r.nlh.nlmsg_pid = getpid();
4 r.nlh.nlmsg_flags = 0;
5 memset(r.buf, 0, MAX_PAYLOAD);
6 strcpy(NLMSG_DATA(&(r.nlh)), "hello, I am edsionte!");

这里的nlmsg_len为为sizeof(struct nlmsghdr)+MAX_PAYLOAD的总和。宏NLMSG_SPACE会自动将两者的长度相加。接下来对缓冲区向量iov进行初始化,让iov_base字段指向数据报结构,而iov_len为数据报长度。
1 struct iovec iov;
2 iov.iov_base = (void *)&r;
3 iov.iov_len = sizeof(r);

一切就绪后,将目的套接字地址与当前要发送的消息msg绑定,即将目的套接字地址复制给msg_name。再将要发送的数据iovmsg_iov绑定,如果一次性要发送多个数据包,则创建一个iovec类型的数组。

1 struct msghdr msg;
2 msg.msg_name = (void *)&dest_addr;
3 msg.msg_namelen = sizeof(dest_addr);
4 msg.msg_iov = &iov;5msg.msg_iovlen = 1;
5.
向内核发送消息发送消息则很简单,通过 sendmsg 函数即可完成,前提是正确的创建 netlink 套接字和要发送的消息。
1 if (sendmsg(sock_fd, &msg, 0) < 0) {
2     eprint(errno, "sendmsg", __LINE__);
3     return errno;
4 }
6.

接受内核发来的消息如果用户进程需要接收内核发送的消息,则需要通过 recvmsg 完成,只不过在接收之前需要将数据报 r 重新初始化,因为发送和接收时传递的数据结构可能是不同的。为了简单演示 netlink 的用法,本文所述的用户进程发送的是一段字符串,这一点从数据报结构 req 的定义可以看出。而内核向用户进程发送的也是一段字符串,具体情况下面将会具体说明。
1 memset(&r.nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
2 if (recvmsg(sock_fd, &msg, 0) < 0) {
3         eprint(errno,"recvmsg", __LINE__);
4         return errno;
5 }
6
7 printf("Received message payload:%sn", (char*)NLMSG_DATA(&r.nlh));
8 close(sock_fd);
接收完毕后,通过专门的宏 NLMSG_DATA 对数据报进行操作。 netlink 对数据报的的访问和操作都是通过一系列标准的宏 NLMSG_XXX 来完成的,具体的说明可以通过 man netlink 查看。这里的 NLMSG_DATA 传递进去的是 nlh ,但它获取的是紧邻 nlh 的真正数据。本程序中传递的是字符串,所以取数据时候用 char * 强制类型转换,如果传递的是其他数据结构,则相应转换数据类型即可。

内核模块netlink既然是一种用户态和内核态之间的双向通信机制,那么除了编写用户程序还要编写内核模块,也就是说用户进程和内核模块之间对数据的处理要彼此对应起来。

1.内核模块加载和卸载函数内核模块加载函数主要通过netlink_kernel_create函数申请服务器端的套接字nl_sk,内核中对套接字表示为sock结构。另外,在创建套接字时还需要传递和用户进程相同的netlink协议类型NETLINK_TEST。创建套接字函数的第一个参数默认为init_net,第三个参数为多播时使用,我们这里不使用多播因此默认值为0nl_data_handler是一个钩子函数,每当内核接收到一个消息时,这个钩子函数就被回调对用户数据进行处理。

1 #define NETLINK_TEST 17
2 struct sock *nl_sk = NULL;
3 static int __init hello_init(void)
4 {
5     printk("hello_init is starting..n");
6     nl_sk = netlink_kernel_create(&init_net,NETLINK_TEST, 0, nl_data_ready, NULL, THIS_MODULE);
7     if (nl_sk == 0)
8     {
9         printk("can not createnetlink socket.n");
10         return -1;
11     }
12     return 0;
13 }
内核模块卸载函数所做的工作与加载函数相反,通过 sock_release 函数释放一开始申请的套接字。
1 static void __exit hello_exit(void)
2 {
3     sock_release(nl_sk->sk_socket);
4     printk("hello_exit is leaving..n");
5 }

2.钩子函数的实现
在内核创建netlink套接字时,必须绑定一个钩子函数,该钩子函数原型为:
1 void (*input)(struct sk_buff *skb);
钩子函数的实现主要是先接收用户进程发送的消息,接收以后内核再发送一条消息到用户进程。在钩子函数中,先通过skb_get函数对套接字缓冲区增加一次引用值,再通过nlmsg_hdr函数获取netlink消息头指针nlh。接着使用NLMSG_DATA宏获取用户进程发送过来的数据str。除此之外,再打印发送者的pid

1 void nl_data_handler(struct sk_buff *__skb)
2 {
3     struct sk_buff *skb;
4     struct nlmsghdr *nlh;
5     u32 pid;
6     int rc;
7     char str[100];
8     int len = NLMSG_SPACE(MAX_PAYLOAD);
9
10     printk("read data..n");
11     skb = skb_get(__skb);
12
13     if (skb->len >= NLMSG_SPACE(0)) {
14         nlh = nlmsg_hdr(skb);
15         printk("Recv:%sn", (char *)NLMSG_DATA(nlh));
16         memcpy(str, NLMSG_DATA(nlh),sizeof(str));
17         pid = nlh->nlmsg_pid;
18         printk("pid is%dn", pid);
19         kfree_skb(skb);

接下来重新申请一个套接字缓冲区,为内核发送消息到用户进程做准备, nlmsg_put 函数将填充 netlink 数据报头。接下来将用户进程发送的字符串复制到 nlh 紧邻的数据缓冲区中,等待内核发送。 netlink_unicast 函数将以非阻塞的方式发送数据包到用户进程, pid 具体指明了接收消息的进程。
1         skb = alloc_skb(len,GFP_ATOMIC);
2         if (!skb){
3             printk(KERN_ERR"net_link: allocate failed.n");
4             return;
5         }
6         nlh = nlmsg_put(skb, 0, 0, 0,MAX_PAYLOAD, 0);
7         NETLINK_CB(skb).pid = 0;
8
9         memcpy(NLMSG_DATA(nlh), str,sizeof(str));
10         printk("net_link: goingto send.n");
11         rc = netlink_unicast(nl_sk,skb, pid, MSG_DONTWAIT);
12         if (rc < 0) {
13             printk(KERN_ERR"net_link: can not unicast skb (%d)n", rc);
14         }
15         printk("net_link: sendis ok.n");
16     }
17 }

这样就完成了内核模块的编写,它与用户进程通信共同完成数据交互。

最后

以上就是明理黑夜为你收集整理的AF_NetLink结构体及例程一、AF_NETLINK结构体基础二、 netlink 内核数据结构、常用宏及函数三、例二:读取内核路由信息四、例三:自定义通信协议的全部内容,希望文章能够帮你解决AF_NetLink结构体及例程一、AF_NETLINK结构体基础二、 netlink 内核数据结构、常用宏及函数三、例二:读取内核路由信息四、例三:自定义通信协议所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(31)

评论列表共有 0 条评论

立即
投稿
返回
顶部