交叉编译Net-SNMP库到Android

 

不久前,一个朋友找我帮忙做一件小事儿——需要在开源鸿蒙平台上集成一个SNMP协议功能。我手头是没有鸿蒙设备的,他的鸿蒙设备本身也是ARM平台的,所以我想,理论上先在android设备上搞一个动态链接库,应该可以平滑移植到鸿蒙。事实上也确实如此,鸿蒙确实可以直接使用Android平台的so库,用鸿蒙自己的NAPI(类似Android上的JNI,都是上层调用Native方法)封装后就能在鸿蒙应用里使用了。

涉及到鸿蒙的部分本文就不班门弄斧了,这里主要讲一下怎样将开源的net-snmp源码做交叉编译,生成android平台的so,以及对so做基本的验证;Net-SNMP 是SNMP协议的一个纯C实现,也是兼容性最好的一个(毕竟有源码,可以到处编译)。

可以从 这里 下载到Net-SNMP的最新版本代码(这个库比较稳定,更新很慢,最新的 v5.9.4 是2023-08-15发布的),然后我们就开始了。

 

1 Ubuntu上的准备工作

 

1.1 安装NDK

交叉编译需要用到NDK,所以需要提前在执行编译的机器(一般得是Ubuntu 64位,我用的是 amd64平台的Ubuntu 20.04)上安装好;

选择相对比较新的即可,我这里选择的是 NDK v27.2 ;

 

1.2 安装snmp相关的deb包

后面我们验证时需要一个snmpd的服务端,这个过程需要用apt-get来安装一些deb包。

但是,按照我自己尝试的经验,如果先在本地执行net-snmp里面snmplib的configure命令(也可能是configure+make,没有深究),可能会导致用apt-get 命令来安装 snmp 相关的deb包时报错失败(大意是,安装 snmpd 依赖于X版本的 snmplib,但将要安装Y版本的 snmplib );

所以,如果读者想要在自己的Ubuntu机器上走一下整个流程,作为“过来人”,我强烈建议你先运行这个命令将必备的snmp相关的deb包安装上:

1
sudo apt-get install snmp snmpd snmp-mibs-downloader

好了,下面正式开始。

 

2 Net-SNMP 源码初探

 

2.1 代码结构

net-snmp的代码解压后如下所示:

src_net-snmp

这套代码能编译出客户端snmp、服务端snmpd等,我们关心的是其中的snmplib这个库;

src_net-snmp_snmplib

另外,我们发现代码目录没有任何Makefile之类的文件,这是因为,net-snmp这样的项目期望自己能被编译到各个平台上,所以其makefile很难通用,那怎么办呢?答案是通过 ./configure 命令,这个命令其实是一个脚本文件,其会根据当前所在的环境和执行时的参数(如果有的话)来生成目标平台的makefile文件,这样就能实现同一套代码针对不同的编译。

 

2.2 确认 snmplib 的依赖关系

虽然我们并不是要在Ubuntu上编译安装,但是为了了解依赖关系,我们还是需要执行一次 ./configure 命令,看看生成的Makefile里面有啥内容。我们直接在 net-snmp根目录执行不带任何参数的 ./configure 命令后,执行完毕后,可以看到 snmplib 目录新增了一个 Makefile 文件,我们把Makefile打开,可以看到这些片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
CSRCS=	snmp_client.c mib.c parse.c snmp_api.c snmp.c 		\
	snmp_auth.c asn1.c md5.c snmp_parse_args.c		\
	system.c vacm.c int64.c read_config.c pkcs.c		\
	snmp_debug.c tools.c  snmp_logging.c text_utils.c	\
	large_fd_set.c cert_util.c snmp_openssl.c 		\
	snmpv3.c lcd_time.c keytools.c                          \
	scapi.c callback.c default_store.c snmp_alarm.c		\
	data_list.c oid_stash.c fd_event_manager.c 		\
	check_varbind.c 					\
	mt_support.c snmp_enum.c snmp-tc.c snmp_service.c	\
	snprintf.c asprintf.c					\
	snmp_transport.c transports/snmpIPv6BaseDomain.c transports/snmpIPBaseDomain.c \
	transports/snmpUDPBaseDomain.c transports/snmpUDPIPv4BaseDomain.c transports/snmpTCPBaseDomain.c \
	transports/snmpSocketBaseDomain.c transports/snmpIPv4BaseDomain.c transports/snmpUDPIPv6Domain.c \
	transports/snmpTCPIPv6Domain.c transports/snmpUDPDomain.c transports/snmpTCPDomain.c \
	transports/snmpAliasDomain.c transports/snmpUnixDomain.c transports/snmpCallbackDomain.c  \
	snmp_secmod.c  snmpusm.c snmpusm.c snmp_version.c        \
	container_null.c container_list_ssll.c container_iterator.c \
	ucd_compat.c dir_utils.c file_utils.c container.c container_binary_array.c

# headers
INSTALLHEADERS=\
	config_api.h  definitions.h mib_api.h net-snmp-includes.h output_api.h  \
	pdu_api.h session_api.h snmpv3_api.h types.h utilities.h varbind_api.h version.h

INCLUDESUBDIR=library
INCLUDESUBDIRHEADERS=README \
	asn1.h callback.h cert_util.h check_varbind.h container.h container_binary_array.h \
	container_iterator.h container_list_ssll.h container_null.h data_list.h \
	default_store.h dir_utils.h fd_event_manager.h file_utils.h getopt.h int64.h \
	keytools.h large_fd_set.h lcd_time.h md5.h mib.h mt_support.h netsnmp-attribute-format.h \
	oid.h oid_stash.h parse.h read_config.h scapi.h snmp-tc.h snmp.h snmp_alarm.h snmp_api.h \
	snmp_assert.h snmp_client.h snmp_debug.h snmp_enum.h snmp_impl.h snmp_logging.h \
	snmp_parse_args.h snmp_secmod.h snmp_service.h snmp_transport.h snmpv3.h \
	system.h text_utils.h tools.h transform_oids.h types.h ucd_compat.h \
	vacm.h winpipe.h winservice.h \
	snmpIPv6BaseDomain.h snmpIPBaseDomain.h snmpUDPBaseDomain.h snmpUDPIPv4BaseDomain.h \
	snmpTCPBaseDomain.h snmpSocketBaseDomain.h snmpIPv4BaseDomain.h snmpUDPIPv6Domain.h \
	snmpTCPIPv6Domain.h snmpUDPDomain.h snmpTCPDomain.h snmpAliasDomain.h snmpUnixDomain.h \
	snmpCallbackDomain.h snmpusm.h

# how to build the libraries.
libnetsnmp.$(LIB_EXTENSION)$(LIB_VERSION):    $(TOBJS)
	$(LIB_LD_CMD) $@ $(TOBJS) $(LDFLAGS) -lm  
	$(RANLIB) $@

OK,根据这些obj的列表,我们就能看出来需要哪些头文件和C代码了;

 

然后,我们可以看到这些:

1
CFLAGS = -g -O2 -DNETSNMP_ENABLE_IPV6 -fno-strict-aliasing -DNETSNMP_REMOVE_U64 -g -O2 -Ulinux -Dlinux=linux 

这个CFLAGS是CC编译时的选项;

 

1
2
3
4
5
6
7
srcdir		= .
top_srcdir	= ..
SRC_TOP_INCLUDES            = -I$(top_srcdir)/include
SRC_SNMPLIB_INCLUDES        = -I$(top_srcdir)/snmplib
TOP_INCLUDES            = $(SRC_TOP_INCLUDES)
SNMPLIB_INCLUDES        = $(SRC_SNMPLIB_INCLUDES)
CPPFLAGS = $(TOP_INCLUDES) -I. 	$(SNMPLIB_INCLUDES) 

这几行颠来倒去,其实就是指定了 CPPFLAGS = -I. -I../include ,也就是告知CC去哪些路径找头文件;

搞明白了这些,我们其实已经基本上可以着手写交叉编译的Makefile了;

 

3 交叉编译到Android

 

3.1 configure

交叉编译时,我们必须得让configure脚本知道,将来的target不是本机,所以,需要添加 --host=arm-linux --target=arm-linux 这两个选项;

其实所谓的“配置”,就是检查一下target的硬件环境信息,以及支持哪些类型定义,支持哪些API啥的,继而生成一系列的宏定义(路径在 ./include/net-snmp/net-snmp-config.h );

所以,为了让configure脚本能正确判断,我们需要给它一套target系统的头文件和库文件目录,通常这是由 --sysroot 选项提供的;

既然我们需要编译的是Android的so,那相关的toolchain自然也得找NDK要,sysroot其实也一样,也必然是由NDK提供的;

以我的机器为例,NDK toolchain位于:

1
/code/Android/Sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/linux-x86_64

在toolchain目录内,Google提供了各个版本和所支持平台的C和C++编译器,我们这里选择Android 31版本的,ARM 32位和64位的编译器在上述toolchain目录下的这些位置:

1
2
3
4
./bin/armv7a-linux-androideabi31-clang
./bin/armv7a-linux-androideabi31-clang++
./bin/aarch64-linux-android31-clang
./bin/aarch64-linux-android31-clang++

sysroot在toolchain内的这个子目录:

1
./sysroot

好了,现在我们终于弄清楚了configure必须的所有信息,可以开始干活了:

 

Step 1:

在Ubuntu当前终端里,执行这些命令,export一些变量供configure使用:

1
2
3
4
5
6
# 指定toolchain
export NDK_TOOLCHAIN="/code/Android/Sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/linux-x86_64"
# 指定C编译器,注意,必须使用双引号,单引号不能展开变量 $NDK_TOOLCHAIN
export CC="$NDK_TOOLCHAIN/bin/aarch64-linux-android31-clang --sysroot=$NDK_TOOLCHAIN/sysroot"
# 指定C++编译器
export CXX="$NDK_TOOLCHAIN/bin/aarch64-linux-android31-clang++ --sysroot=$NDK_TOOLCHAIN/sysroot"

 

Step 2:

执行configure命令:

1
2
cd ~/net-snmp-5.9.4
./configure --host=arm-linux --target=arm-linux --disable-embedded-perl

上面的–disable-embedded-perl 如果不写,在我的Ubuntu上会出现报错,我查了一下,这个内置perl是 snmpd 使用的,我们编译 snmplib用不上,就先disable了算了,不影响啥;

 

3.2 写交叉编译的Makefile

上面执行完毕 configure后,我们进入 net-snmp-5.9.4/snmplib 目录,能看到生成了一个新的Makefile文件,这个文件并不能帮我们完成Android平台so的编译,所以,我们直接用下面的Makefile替换掉它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# Android NDK 工具链路径
NDK_TOOLCHAIN := /code/Android/Sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/linux-x86_64

# 定义 32 位编译器及选项
CC_32  := $(NDK_TOOLCHAIN)/bin/armv7a-linux-androideabi31-clang
CXX_32 := $(NDK_TOOLCHAIN)/bin/armv7a-linux-androideabi31-clang++
SYSROOT_32 := --sysroot=$(NDK_TOOLCHAIN)/sysroot
HEADERS_32 := -I. -I../include
CFLAGS_32  := -fPIC -g -O2 -DNETSNMP_ENABLE_IPV6 -fno-strict-aliasing -Ulinux -Dlinux=linux $(SYSROOT_32) $(HEADERS_32)
LDFLAGS_32 := -shared

# 定义 64 位编译器及选项
CC_64  := $(NDK_TOOLCHAIN)/bin/aarch64-linux-android31-clang
CXX_64 := $(NDK_TOOLCHAIN)/bin/aarch64-linux-android31-clang++
SYSROOT_64 := --sysroot=$(NDK_TOOLCHAIN)/sysroot
HEADERS_64 := -I. -I../include
CFLAGS_64  := -fPIC -g -O2 -DNETSNMP_ENABLE_IPV6 -fno-strict-aliasing -Ulinux -Dlinux=linux $(SYSROOT_64) $(HEADERS_64)
LDFLAGS_64 := -shared

# 源文件及头文件
HEADERS = $(wildcard ./*.h)                      \
    $(wildcard ./transports/*.h)                 \
    $(wildcard ./openssl/*.h)                    \
    $(wildcard ../include/net-snmp/*.h)          \
    $(wildcard ../include/net-snmp/library/*.h)

SRCS := snmp_client.c mib.c parse.c snmp_api.c snmp.c                  \
    snmp_auth.c asn1.c md5.c snmp_parse_args.c                         \
    system.c vacm.c int64.c read_config.c pkcs.c                       \
    snmp_debug.c tools.c  snmp_logging.c text_utils.c                  \
    large_fd_set.c cert_util.c snmp_openssl.c                          \
    snmpv3.c lcd_time.c keytools.c                                     \
    scapi.c callback.c default_store.c snmp_alarm.c                    \
    data_list.c oid_stash.c fd_event_manager.c                         \
    check_varbind.c mt_support.c snmp_enum.c snmp-tc.c                 \
    snmp_service.c snprintf.c asprintf.c snmp_transport.c              \
    transports/snmpIPv6BaseDomain.c transports/snmpIPBaseDomain.c      \
    transports/snmpUDPBaseDomain.c transports/snmpUDPIPv4BaseDomain.c  \
    transports/snmpTCPBaseDomain.c transports/snmpSocketBaseDomain.c   \
    transports/snmpIPv4BaseDomain.c transports/snmpUDPIPv6Domain.c     \
    transports/snmpTCPIPv6Domain.c transports/snmpUDPDomain.c          \
    transports/snmpTCPDomain.c transports/snmpAliasDomain.c            \
    transports/snmpUnixDomain.c transports/snmpCallbackDomain.c        \
    snmp_secmod.c  snmpusm.c snmp_version.c                            \
    container_null.c container_list_ssll.c container_iterator.c        \
    ucd_compat.c    strlcat.c strlcpy.c                                \
    dir_utils.c file_utils.c container.c container_binary_array.c


# 中间目标目录
BUILD_DIR_32 := obj32
BUILD_DIR_64 := obj64

# 生成的中间目标文件 (.o)
OBJS_32 := $(patsubst %.c,$(BUILD_DIR_32)/%.o,$(SRCS))
OBJS_64 := $(patsubst %.c,$(BUILD_DIR_64)/%.o,$(SRCS))

# 最终生成的目标文件
TARGET_32 := libnetsnmp_32.so
TARGET_64 := libnetsnmp_64.so

# 默认目标
all: $(TARGET_32) $(TARGET_64)

# 32位so的规则
$(TARGET_32): $(OBJS_32)
	$(CC_32) $(CFLAGS_32) $(LDFLAGS_32) -o $@ $^

# 64位so的规则
$(TARGET_64): $(OBJS_64)
	$(CC_64) $(CFLAGS_64) $(LDFLAGS_64) -o $@ $^

# 生成32位obj文件
$(BUILD_DIR_32)/%.o: %.c $(HEADERS)
	@mkdir -p $(dir $@)
	$(CC_32) $(CFLAGS_32) -c $< -o $@

# 生成64位obj文件
$(BUILD_DIR_64)/%.o: %.c $(HEADERS)
	@mkdir -p $(dir $@)
	$(CC_64) $(CFLAGS_64) -c $< -o $@

# 清理规则
clean:
	rm -rf $(BUILD_DIR_32) $(BUILD_DIR_64) $(TARGET_32) $(TARGET_64)

# 声明伪目标
.PHONY: all clean

关于这个Makefile,有几点需要说明的:

  1. 所有头文件列表 HEADERS,我们没有用configure自动生成的Makefile里那么长一个列表,而是偷了个懒,反正snmplib相关的所有头文件都在上面第21行提到的几个目录;

  2. 编译选项 CFLAGS_32 和 CFLAGS_64,相比configure自动生成的Makefile,我们添加了一个 -fPIC 选项,这个选项很重要,编译android平台的so时是必须的。

 

关于 -fPIC 选项的一些知识:【来自ChatGPT】

  1. 什么是 -fPIC

-fPIC 是 GCC 和 Clang 编译器的一个选项,全称是 “Position-Independent Code”(位置无关代码)

当使用 -fPIC 编译代码时,生成的二进制代码可以在内存中的任何位置加载运行,而无需在运行时对地址进行修正(重定位)。这使得代码更灵活,并且适合于动态库(Shared Libraries)。

  1. 为何动态库需要 -fPIC

动态库通常会被多个进程加载到内存中。为了减少内存使用和加快加载速度,操作系统通常会:

  1. 共享代码段:动态库的代码段可以在多个进程之间共享,而无需为每个进程单独加载。
  2. 随机加载地址:现代操作系统使用 地址空间随机化(ASLR) 技术,在每次加载动态库时将其放置在内存中的不同位置。

为了实现上述目标,动态库的代码必须是位置无关的(Position-Independent)。否则:

  • 如果代码中包含硬编码的内存地址,加载到不同位置时需要对这些地址进行修正(重定位),这会增加加载时间。
  • 无法在多个进程之间共享代码段,因为每个进程的加载地址不同。

-fPIC 选项会生成位置无关代码,确保动态库可以在任何内存地址加载且无需修改。

  1. 不使用 -fPIC 会怎样?

如果在编译动态库时未使用 -fPIC

  1. **运行时可能失败**:操作系统可能无法正确加载和运行动态库。
  2. **性能影响**:即使能运行,加载器需要对二进制代码中的绝对地址进行修正,增加了启动时间。
  3. **内存浪费**:修正后的动态库代码无法共享,导致每个进程都需要单独存储一份代码段。
  1. -fPIC 的作用

启用 -fPIC 后,编译器会:

  • 使用相对地址访问变量和函数,而不是硬编码的绝对地址。

  • 生成可以在任意位置加载的代码,避免运行时重定位。

    例如:

  • 普通代码可能直接使用绝对地址(如变量在内存中的地址)。

  • -fPIC 生成的代码会使用当前程序计数器(Program Counter,PC)的偏移量来访问变量。

  1. 为什么在 Android 中是必须的?

Android 的动态库(SO 文件)必须支持 ASLR共享内存机制,这是现代操作系统的一个安全和性能要求:

  • ASLR:动态库的加载地址是随机的,要求代码是位置无关的。
  • 共享内存:Android 系统中的动态库通常会被多个应用或进程共享,位置无关代码可以被直接共享,减少内存占用。

因此,在编译 Android 动态库时,-fPIC 是一个必不可少的选项。

 

3.3 错误排除

 

使用上述Makefile后,可能遇到两类错误:

第一类,跟 fd_set 有关的:

compile-error-fd_set

这个错误是由于不识别fd_set 这个类型导致的,可能有20个左右的C代码都会报同样的错误,逐个修改太麻烦了。其实分析上述图片的引用关系可知,其实就是types.h 这个文件不认识 fd_set 这个类型,而在NDK的sysroot里面的文件 sys/select.h 里是有 fd_set 这个类型定义的。

所以,我们的修改方法就是,在 include/net-snmp/library/types.h 里面#include <net-snmp/types.h> 这一行之后,添加对这个 fd_set 类型的引用(见下面第3~7行):

1
2
3
4
5
6
7
8
9
#include <net-snmp/types.h>

// zjwang: fix compiling error, BEGIN
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif
// zjwang: fix compiling error, END

typedef struct netsnmp_index_s {

 

第二类,跟 NETSNMP_FD_MASK_TYPE 有关的:

compile-error-fd_mask

这个错误是由于config后生成的 include/net-snmp/net-snmp-config.h 内,默认生成的是

1
#define NETSNMP_FD_MASK_TYPE unknown

修改成 fd_mask 即可:

1
#define NETSNMP_FD_MASK_TYPE fd_mask

一般只会有这两个错误,修改完毕后,就能看到编译好的两个so文件了;

 

4 验证

 

4.1 在Ubuntu上开启snmpd服务

Step 1:

首先确保上面第1.2节里面的命令能执行成功;

Step 2:

修改snmpd的配置文件 /etc/snmp/snmpd.conf,主要改动有两处:

第一处,是将

1
agentaddress 127.0.0.1,[::1]

修改成

1
agentAddress udp:161,udp6:[::1]:161

这个改动意思是允许来自局域网的访问,不修改的话,默认只能从本机访问;

第二处,是将下面两行注释掉:

1
2
view systemonly included .1.3.6.1.2.1.1
view systemonly included .1.3.6.1.2.1.25.1

再添加一行:

1
view systemonly included .1.3.6.1

这样改可以让客户端访问到更多节点的信息;

Step 3:

重启snmpd服务,使刚才的配置生效;

1
sudo service snmpd restart

并检查其运行状态,显示中包含 Active: active (running) 即可;

1
systemctl status snmpd

Step 4:

在Ubuntu上用命令行检查snmpd 的工作状态,即自己查自己的节点信息:

ubuntu-snmpwalk

上面的命令是查询设备描述信息;【具体细节参考SNMP的OID结点语法,这里不讨论了】

能看到类似上述的信息,说明本机查本机工作正常;

 

4.2 Windows上SnmpUtil测试工具

Windows上有一个名为SnmpUtil的测试工具,提供了cmd和GUI两种界面,我们使用cmd工具查询如下(Ubuntu的IP是192.168.0.128):

windows-snmputil-get

能在Windows上查询Ubuntu设备的描述信息,说明局域网查询OK;

 

4.3 一个简单的测试APK

接下来,我们写一个测试apk,验证一下刚才编译的so能否工作。

 

通过Android Studio 创建一个C/C++工程;

Android视图以及 MainActivity 如下图所示:

android-studio–Activity

Project视图以及Snmp类的定义如下图:

android-studio–Snmp

 

需要说明一点的是,app/src/main/cpp 下有一个 include目录,这个其实就是 ~/net-snmp-5.9.4/include 目录的所有文件内容,snmp的应用(也就是我们要做的JNI代码)里面需要引用部分include里面的类型;

另外,因为我们需要通过局域网访问Ubuntu机器,所以AndroidManifest.xml 里面需要添加一下对网络的访问;

1
2
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

 

编写JNI和C代码;

调用刚刚编译的so里面的功能,其实就是参考 Net-SNMP 给出的样例 ,做了少量改动;

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/*
 * Class:     com_zjwang_snmp_Snmp
 * Method:    get
 * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL snmp_get(JNIEnv *env, jclass obj,
                                   jstring jPeer, jstring jCommunity, jstring jOid) {

    char * peer = (char *) (*env)->GetStringUTFChars(env, jPeer, 0);
    char * community = (char *) (*env)->GetStringUTFChars(env, jCommunity, 0);
    char * oid = (char *) (*env)->GetStringUTFChars(env, jOid, 0);
    LOGD("called snmp_get %s %s %s", peer, community, oid);

    char* read_buffer = malloc(256);
    if(read_buffer == NULL) {
        printf("error, no memory\n");
        exit(1);
    }
    memset(read_buffer, 0, 256);

    get(peer, community, oid, read_buffer); // 这是实际干活的函数

    (*env)->ReleaseStringUTFChars(env, jPeer, peer);
    (*env)->ReleaseStringUTFChars(env, jCommunity, community);
    (*env)->ReleaseStringUTFChars(env, jOid, oid);

    return (*env)->NewStringUTF(env, read_buffer);
}

// 这个函数是参考了Net-SNMP的例子
// See: http://www.net-snmp.org/wiki/index.php/TUT:Simple_Application
int get(const char* peer, const char* community, const char* id, char* result) {
    netsnmp_session session, *ss;
    netsnmp_pdu *pdu;
    netsnmp_pdu *response;

    oid anOID[MAX_OID_LEN];
    size_t anOID_len;

    netsnmp_variable_list *vars;
    int status;
    int count = 1;

    u_char         *buf = NULL;
    size_t          buf_len = 256, out_len = 0;

    if ((buf = (u_char *) calloc(buf_len, 1)) == NULL) {
        sprintf(result, "[ERROR][no memory]\n");
        return -1;
    }

    // Initialize the SNMP library
    init_snmp("snmpdemoapp");

    // Initialize a "session" that defines who we're going to talk to
    snmp_sess_init(&session);    // set up defaults
    session.peername = strdup(peer);

    // set up the authentication parameters for talking to the server
#ifdef DEMO_USE_SNMP_VERSION_3

    /* Use SNMPv3 to talk to the experimental server */

    /* set the SNMP version number */
    session.version = SNMP_VERSION_3;

    /* set the SNMPv3 user name */
    session.securityName = strdup("MD5User");
    session.securityNameLen = strlen(session.securityName);

    /* set the security level to authenticated, but not encrypted */
    session.securityLevel = SNMP_SEC_LEVEL_AUTHNOPRIV;

    /* set the authentication method to MD5 */
    session.securityAuthProto = usmHMACMD5AuthProtocol;
    session.securityAuthProtoLen = sizeof(usmHMACMD5AuthProtocol) / sizeof(oid);
    session.securityAuthKeyLen = USM_AUTH_KU_LEN;

    /* set the authentication key to a MD5 hashed version of our
       passphrase "The Net-SNMP Demo Password" (which must be at least 8
       characters long) */
    if (generate_Ku(session.securityAuthProto,
                    session.securityAuthProtoLen,
                    (u_char *) our_v3_passphrase, strlen(our_v3_passphrase),
                    session.securityAuthKey,
                    &session.securityAuthKeyLen) != SNMPERR_SUCCESS) {
        snmp_perror(argv[0]);
        snmp_log(LOG_ERR,
                 "Error generating Ku from authentication pass phrase. \n");
        exit(1);
    }

#else /* we'll use the insecure (but simplier) SNMPv1 */

    /* set the SNMP version number */
    session.version = SNMP_VERSION_2c;

    /* set the SNMPv1 community name used for authentication */
    session.community = community;
    session.community_len = strlen(session.community);

#endif /* SNMPv1 */

    ss = snmp_open(&session);  // establish the session
    if (!ss) {
        snmp_sess_perror("ack", &session);
        memcpy(result, "[ERROR] snmp session open error", strlen("[ERROR] snmp session open error"));
        LOGE("[ERROR] snmp session open error");
        return -1;
    }

    /*
     * Create the PDU for the data for our request.
     *   1) We're going to GET the system.sysDescr.0 node.
     */
    pdu = snmp_pdu_create(SNMP_MSG_GET);
    anOID_len = MAX_OID_LEN;
    if (!snmp_parse_oid(id, anOID, &anOID_len)) {
        snmp_perror(id);
        memcpy(result, "[ERROR] oid parse error", strlen("[ERROR] oid parse error"));
        DEBUG_WHERE
        clean_up(ss, response);
        return -1;
    }

    snmp_add_null_var(pdu, anOID, anOID_len);

    //Send the Request out.
    status = snmp_synch_response(ss, pdu, &response);

    // SUCCESS: Print the result variables
    if (status == STAT_SUCCESS && response->errstat == SNMP_ERR_NOERROR) {
        // TODO: 如果入参oid是一个值,则结果也只会有一个值,暂时保留for循环,以便后续查询多个值
        for (vars = response->variables; vars; vars = vars->next_variable) {
            print_variable(vars->name, vars->name_length, vars);
            if (sprint_realloc_variable(&buf, &buf_len, &out_len, 1,
                                        anOID, anOID_len, response->variables)) {
                LOGD("sprint_realloc_variable: got response->variables = %s", buf);
                memcpy(result, buf, out_len);
            }
            else {
                LOGD("[ERROR]sprint_realloc_variable failed");
                memcpy(result, "[ERROR] parse response failed...", 30);
                DEBUG_WHERE
                clean_up(ss, response);
                return  -1;
            }
        }
    }
    // FAILURE: print what went wrong!
    else {
        if (status == STAT_SUCCESS)
            fprintf(stderr, "Error in packet\nReason: %s\n",
                    snmp_errstring(response->errstat));
        else if (status == STAT_TIMEOUT)
            fprintf(stderr, "Timeout: No response from %s.\n",
                    session.peername);
        else {
            snmp_sess_perror("snmpdemoapp", ss);
        }
        LOGD("[ERROR]snmp_synch_response failed");
        memcpy(result, "[ERROR]snmp_synch_response failed", 
               strlen("[ERROR]snmp_synch_response failed"));
        DEBUG_WHERE
        clean_up(ss, response);
        return  -1;
    }

    DEBUG_WHERE
    clean_up(ss, response);
    return (0);
}

void clean_up(netsnmp_session *ss, netsnmp_pdu *response) {
    if (response) snmp_free_pdu(response);
    if (ss) snmp_close(ss);
}

 

CMakeLists.txt 的核心内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
cmake_minimum_required(VERSION 3.22.1)

# Declares the project name
project("snmp")

add_library(${CMAKE_PROJECT_NAME}
        SHARED
        # List C/C++ source files with relative paths to this CMakeLists.txt.
        snmp.c)

add_library(netsnmp SHARED IMPORTED)
set_target_properties(netsnmp PROPERTIES 
        IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/net-snmp-libs/${ANDROID_ABI}/libnetsnmp.so)

target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ${CMAKE_SOURCE_DIR}/include)

set_target_properties(netsnmp PROPERTIES IMPORTED_NO_SONAME 1)

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log )

# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(
        ${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        ${log-lib}

        # 第三方so库
        netsnmp
        )

 

编译完毕app后,直接到Pixel 6设备上运行(请先让手机和Ubuntu设备连接同一个Wi-Fi,确保在手机的adb shell里可以ping通Ubuntu),效果如下图。

android-test-result

能看到和Windows上的SnmpUtil一样的描述信息,说明整个调用栈都跑通了。

测试成功!

0%