7. 回调配置
为了控制一些函数的行为, libnl
库在许多地方都设置了回调函数挂钩点并允许某些函数
的覆写。所有的回调和覆写函数都是封装在 struct nl_cb
结构体中,这个结构存在于
netlink
套接字中或者作为函数的参数直接传递。
学而时习之,不亦说乎
为了控制一些函数的行为, libnl
库在许多地方都设置了回调函数挂钩点并允许某些函数
的覆写。所有的回调和覆写函数都是封装在 struct nl_cb
结构体中,这个结构存在于
netlink
套接字中或者作为函数的参数直接传递。
如果可能的话,无论什么时候你都应该把 netlink
消息的有效载荷编码成 netlink
属
性。使用属性可以使得日后对任何 netlink
协议进行扩展的时候都不会破坏它的二进制
兼容性。比如:假设你的设备现在可能使用一个 32 位的计数器来统计数据,但是几年后设
备改成维护一个 64 位的计数器来记录更快的网络硬件。如果你的协议使用了属性,那么过
度到 64 位计数器是一个非常简单的事情,你只是需要发送一个新的属性来包含这个 64 位
的变量,与此同时仍然提供旧的 32 位的计数器。如果你的协议没有使用属性的话,你将无
法在不祸及已有的协议使用者的前提下转化这个数据类型。
属性嵌套的概念也允许你的协议的子系统实现和维护自己的属性模式。假如现在引入了新一 代的网络设备,而它需要设置一系列的协议设计之初没有考虑到的新的配置选项。通过使用 属性,新一代的设备可以定义一个新的属性然后用自己的子属性结构来填充这个属性,这些 子属性可以扩展甚至是完全废弃原来的属性。
所以,请始终使用属性,即便是在你几乎可以肯定你的消息格式永远都不会改变的情况下 也是如此。
关于 netlink
协议以及它的消息格式请查看 netlink 协议基础 一
节。
大部分的 netlink
协议对所有的边界都有严格的对齐方案。对齐值是由
NLMSG_ALIGNTO
定义的,这个值固定为 4 个字节。所以所有的 netlink
消息头部、有
效载荷段的开头、协议相关头部和属性段都必须从一个是 NLMSG_ALIGNTO
倍数的偏移量
开始。
使用 netlink 套接字发送一条 netlink 消息的标准方式是调用 nl_send_auto()
函数。
这个函数会通过填充缺失的字段以及填充 netlink 消息头部自动完成这条 netlink 消息的
构建。此外这个函数还会根据 netlink 套接字设置的选项和地址来处理寻址问题。之后这
个消息会被传递给 nl_send() 函数。
为了使用 netlink 协议,我们首先需要一个 netlink 套接字[^1]。每个套接字都定义了一 个独立的消息发送和接收的上下文环境。一个应用程序可以使用多个套接字,比如一个套接 字用于发送请求消息和接受应答消息另一个套接字用来订阅某个多播组以便接收通知消息。
Netlink 协议是基于套接字的进程间通信(IPC)机制,它可用于用户空间进程和内核之间 或者用户空间进程之间的通信。Netlink 协议基于 BSD 套接字并使用 AF_NETLINK 地址簇。 每一个 Netlink 协议都有自己的协议号(比如:NETLINK_ROUTE,NETLINK_NETFILTER, 等等)。它的寻址方案是基于 32 位的端口号(之前被称为 PID),这个端口号用来唯一的 标识每一个对等通信节点。
核心库(core library)提供了使用 netlink 套接字进行通信的基础功能。它处理套接 字的连接建立和断开、发送和接收数据、构造和解析消息、提供可配置的接收状态机。除此 之外它还提供了一套抽象数据类型的框架,这套框架使得基于对象的 netlink 协议实现 起来更加的简单,在这种协议中,对象可以通过基于 netlink 的协议来添加、删除、 或者修改。
通常在 CS 通信模式中,服务端通过 socket->bind->listen->accept
流程开始监听客户
端的连接。而在客户端则是通过 socket->connect
流程来建立和服务端的连接。我们在
前面的文章中已经分析过服务器端使用到的 4 个 API,这篇文章开始我们将会分析客户端
用到的 API。
首先要分析的第一个 API 是 connect
,因为客户端创建 socket 的函数和服务端是一个
样的,都是使用 socket 函数。connect
函数的原型如下:
1 | int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
其中 sockfd
是客户端建立的 socket,addr
参数是服务端的地址,addrlen
则是服
务端地址的长度。下面分析 connect
API 的具体实现。
注:本文基于 2.6.32 版本的内核
正常的 socket 编程函数调用顺序一般是 socket
-> bind
-> listen
->
accept
。我们在前面的文章中分析了前面三个函数,现在我们分析第四个函数
accept
。
1 | accept(server_fd, (struct sockaddr *))&client_addr, client_len); |
这个函数在 socket 开始监听之后调用,如果成功接受链接请求,则返回新的客户端
socket 文件描述符,并把客户端的地址放入到 client_addr
表示的地址中去。
我们将在这篇博文中详细的讲解 accept
函数在内核内部的处理流程。