一、背景
前阶段复刻了B站up主晨少开源的智能家居项目,基于正点原子imx6ull阿尔法开发板实现。但其QT端的源码会有些冗杂,既有界面设计的源码,又有硬件操作的代码。看了小红书某个哥们的优快云文章,知道了韦东山的嵌入式Linux快速入门实验班提出了一个JSONRPC的思想,实现了前后台程序分离。因此想将其实现到我的项目代码里。
二、JSONRPC简介
JSONRPC分JSON和RPC组成。JSON实际上是指JSON格式,它的英文是JavaScript Object Notation, JavaScript 对象表示法。这是一种很常见的数据格式,比如在使用MQTT向某个云平台传输数据的时候就会用到。
它的关键成员是“name:value”, value 有多种形式。就是类似于一下这种键值对的形式,它的值还可以是数组、另一个JSON对象。(这里只是简述,不作过多介绍,具体语法可以自己去了解。这边引用了韦东山的嵌入式Linux应用开发实验班(快速入门)pdf的内容,韦东山老师的资料总是干练简洁直击重点,感兴趣可以下载来看看)
{ "name": "John", "age": 30, "isStudent": false , "ptr": null }
RPC的英文是Remote Procedure Call,也就是远程调用(通过英文全称,来学习一个专业术语挺好)。远程调用就是字面意思,正常调用是定义一个xxx.h文件,然后我include进行,就可以调用函数。远程调用相反,就是在远边调用,比如在服务器进程中调用。JSONRPC是基于网络进行通信的。相当于我可以创建一个服务器进程,将硬件相关的代码放在这个进程里,将QT程序作为客户端进程,客户端进程只包含界面相关的代码,当我需要控制硬件,就通过RPC向服务器进程发信号。这样子就实现了前后端分离的思想,使得程序拥有更加清晰的架构。
本篇文章的目的,就是编写出来一个led测试程序:创建一个服务器进程,这个进程存放led硬件相关代码,如open、write,监听客户端的请求,再将QT程序改造成一个客户端进程,通过按键向服务器发送请求,实现RPC。
三、实现历程
3.1 在ubuntu虚拟机上编译运行测试JSONRPC库
在这边使用的是正点原子的虚拟机,韦东山的嵌入式Linux应用开发实验班(快速入门)pdf。
韦东山把几个需要的文件放置在以下目录当中,要下载这些文件可以观看韦东山的嵌入式Linux应用开发实验班下载。
F:\01_all_series_quickstart\04a_嵌入式Linux应用开发实验班(快速入门)\05_使用JsonRPC实现前后台\source\5-4_JSON-RPC示例与情景分析\5-4-1_JSON-RPC上机实验

简述一下这几个文件,第一个是测试程序,第二个就是JSONRPC的库,第三个是libev,JSONRPC依赖于这个库。接下来就是韦东山的PDF里面的内容(在pdf的5.5.7 上机实验里,pdf写的很详细),先把这三个文件放在同个文件夹。
1、编译libev库
$ tar xjf libev_pc.tar.bz2
$ cd libev_pc/
$ CC=gcc ./configure --prefix=$PWD/tmp
$ make -j 16
$ make install
$ ls tmp/
include lib share
$ sudo cp -rf tmp/include/* /usr/include/
$ sudo cp -rfd tmp/lib/* /usr/lib
2、编译JSONRPC库
$ sudo apt install libtool
$ tar xjf jsonrpc-c_pc.tar.bz2
$ cd jsonrpc-c_pc/
$ autoreconf -i
$ CC=gcc ./configure --prefix=$PWD/tmp
$ make -j 16
$ make install
$ ls tmp/
include lib
$ sudo cp -rf tmp/include/* /usr/include/
$ sudo cp -rfd tmp/lib/* /usr/lib
3、编译测试程序
$ tar xjf json-rpc_test.tar.bz2
$ cd json-rpc_test/
$ make
4、执行程序
$ ./rpc server & #开启rpc服务器
[1] 15028
$ ./rpc add 2 6 #远程调用服务器里的add函数
sum = 8
碰到的问题
在运行测试程序时,碰到了问题
/home/alientek/Desktop/json-rpc/libev_pc/ev.c:3363: undefined reference to `floor’
floor函数是数学库math.h中的函数,在LInux系统中,使用GCC编译时,需要显式链接数学库libm,否则会出现undefined reference to floor的错误。在Makefile中添加-lm指令即可

测试成功

3.2 在Arm板上运行JSONRPC
交叉编译rpc库
韦东山的教程如下
1、 编译 libev 库
$ tar xjf libev.tar.bz2
$ cd libev/
$ ./configure --host=arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp
$ make -j 16
$ make install
$ ls tmp/
include lib share
2、编译jsonrpc库
$ sudo apt install libtool
$ tar xjf jsonrpc-c.tar.bz2
$ cd jsonrpc-c/
$ autoreconf -i
$ ./configure --host=arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp
CFLAGS="-I$PWD/../libev/tmp/include" LDFLAGS="-L$P
WD/../libev/tmp/lib"
$ make -j 16
$ make install
$ ls tmp/
include lib
3、编译测试程序
$ cd rpc_server
$ make
$ adb push rpc_server /root
$ cd ../rpc_client
$ make
$ adb push rpc_client /root
4、执行程序
$ ./rpc_server &
$ ./rpc_client led 0
$ ./rpc_client led 1
$ ./rpc_client dht11
基本跟在ubuntu上编译没啥区别,就是把编译器换成交叉编译工具链。但我在这边碰到的问题层出不穷(因为太疏忽了)
碰到的问题
问题1 make: arm-buildroot-linux-gnueabihf-gcc: Command not found
我首先是查看了Makefile,发现他是这样定义的。

我查看了一下之前编译驱动的Makefile,如以下所示,所以我就把上面那个改成CROSS_COMPILE的形式(此时我并没有注意到问题的关键是那个buildroot,我自己的交叉编译工具链是没有buildroot的,韦东山的是有这个)。

后面确实没有再报错,我就以为我解决了,接下来就出现了问题。
问题2 运行程序,一堆莫名其妙报错,错误如下
# ./rpc_server
./rpc_server: line 2: : not found
./rpc_server: line 3: : not found
./rpc_server: line 4: syntax error: unexpected "("
很奇怪,我运行的应用程序,怎么会有语法错误。问ai,ai的答复是
错误信息 ./rpc_server: line 2: : not found 和 syntax error: unexpected "(" 等,通常表明系统试图将文件当作脚本执行,而不是可执行文件。这可能是因为文件的格式或内容不符合当前系统的预期。这种情况下,文件可能是一个为其他架构编译的可执行文件,或者文件的格式被错误地处理了。学到了,还得是AI。我用file命令检查了rpc_server。发现这个文件是x86-64架构的
alientek@ubuntu16:~/Desktop/json-rpc_control_hardware/rpc_server$ file rpc_server
rpc_server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=455967b3c8e920170fd396b11fbcce2fac0bb090, not stripped
同时我也疑惑,我用的明明是交叉编译工具链,怎么会整出来个x86呢?于是echo $(CROSS_COMPILE),发现为空, 我很疑惑,那我以前是如何成功编译驱动的?ai说可能是我之前编译驱动时,内核模块的构建使用了内核的构建系统。
make -C $(KERN_DIR) M=$(PWD) modules
这部分构建可能会设置或覆盖一些环境变量,影响后续用户空间程序的构建(存疑)。
后续我重新编译了一下,详细看了输出信息,发现了问题,输出信息指示没有找到arm-buildroot-linux-gnueabihf-gcc,这边有个buildroot,正点原子的是没有的。因为没有找到交叉编译工具链,所以才退化成gcc,才编译出来的x86架构。
alientek@ubuntu16:~/Desktop/json-rpc_control_hardware/jsonrpc-c$ ./configure --host=arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp CFLAGS="-I$PWD/../libev/tmp/include" LDFLAGS="-L$PWD/../libev/tmp/lib"
checking for arm-buildroot-linux-gnueabihf-gcc... no
checking for gcc... gcc
...........................................
make[2]: Entering directory '/home/alientek/Desktop/json-rpc_control_hardware/jsonrpc-c/example'
gcc -DHAVE_CONFIG_H -I. -I.. -lm -I../include -I/home/alientek/Desktop/json-rpc_control_hardware/jsonrpc-c/../libev/tmp/include -MT server-server.o -MD -MP -MF .deps/server-server.Tpo -c -o server-server.o `test -f 'server.c' || echo './'`server.c
mv -f .deps/server-server.Tpo .deps/server-server.Po
/bin/bash ../libtool --tag=CC --mode=link gcc -I/home/alientek/Desktop/json-rpc_control_hardware/jsonrpc-c/../libev/tmp/include -lev ../src/libjsonrpcc.la -L/home/alientek/Desktop/json-rpc_control_hardware/jsonrpc-c/../libev/tmp/lib -o server server-server.o
libtool: link: gcc -I/home/alientek/Desktop/json-rpc_control_hardware/jsonrpc-c/../libev/tmp/include -o .libs/server server-server.o /home/alientek/Desktop/json-rpc_control_hardware/libev/tmp/lib/libev.so ../src/.libs/libjsonrpcc.so -L/home/alientek/Desktop/json-rpc_control_hardware/jsonrpc-c/../libev/tmp/lib -Wl,-rpath -Wl,/home/alientek/Desktop/json-rpc_control_hardware/libev/tmp/lib -Wl,-rpath -Wl,/home/alientek/Desktop/json-rpc_control_hardware/jsonrpc-c/tmp/lib
make[2]: Leaving directory '/home/alientek/Desktop/json-rpc_control_hardware/jsonrpc-c/example'
make[2]: Entering directory '/home/alientek/Desktop/json-rpc_control_hardware/jsonrpc-c'
make[2]: Leaving directory '/home/alientek/Desktop/json-rpc_control_hardware/jsonrpc-c'
make[1]: Leaving directory '/home/alientek/Desktop/json-rpc_control_hardware/jsonrpc-c'
所以,折腾这么久,发现只是我疏忽了一个buildroot,血亏,浪费了很多时间,下次还是要仔细一点。解决了这个问题,就没啥大问题了,程序也能在板子运行。从中我也感受到了,其实我并不需要在板子上下载这些库,只要在虚拟机上搞好,链接成应用程序就可以了,之前我还以为也需要在板子上下载jsonrpc的库。
3.3 改造QT程序
浅析一下改造好的QT程序。分两个进程,一个是rpcserver进程,一个是QT进程。
rpcserver进程
在该进程的主函数里,运行两个初始化函数,一个是led的初始化,其实只是打开节点,还有一个服务器的初始化函数。
int main(int argc, char **argv)
{
led_init();
//dht11_init();
RPC_Server_Init();
return 0;
}
服务器的初始化代码如下所示,创建jrpc_server结构体,然后初始化这个服务器,然后往里注册一个函数,server_led_control。
static struct jrpc_server my_server;
int RPC_Server_Init(void)
{
int err;
err = jrpc_server_init(&my_server, PORT);
if (err)
{
printf("jrpc_server_init err : %d\n", err);
}
printf("jrpc_server_init success \r\n");
jrpc_register_procedure(&my_server, server_led_control, "led_control", NULL );
//jrpc_register_procedure(&my_server, server_dht11_read, "dht11_read", NULL );
printf("jrpc_register_procedure run \r\n");
jrpc_server_run(&my_server);
jrpc_server_destroy(&my_server);
return 0;
}
server_led_control函数如下,可以看到,里面的核心是获取rpc客户端传送过来的参数,调用led_control控制灯亮灯灭。
cJSON * server_led_control(jrpc_context * ctx, cJSON * params, cJSON *id) {
cJSON * status = cJSON_GetArrayItem(params,0);
led_control(status->valueint);
return cJSON_CreateNumber(0);
}
rpcclient进程(QT端代码)
在客户端,同样,首先会初始化客户端的代码,
int main(int argc, char *argv[])
{
//RPC_Client_Init();
QApplication a(argc, argv);
RPC_Client_Init();
MainWindow w;
w.show();
return a.exec();
}
客户端初始化代码如下,创建一个客户端的socket对象,最后再去connect服务器。可以看出,connect的第一个参数是客户端的文件描述符数值,第二个参数则是服务器的地址。这样客户端和服务器就联系起来了。
int RPC_Client_Init(void)
{
int iSocketClient;
struct sockaddr_in tSocketServerAddr;
int iRet;
iSocketClient = socket(AF_INET, SOCK_STREAM, 0);
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(PORT); /* host to net, short */
//tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
inet_aton("127.0.0.1", &tSocketServerAddr.sin_addr);
memset(tSocketServerAddr.sin_zero, 0, 8);
iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("connect error!\n");
return -1;
}
g_iSocketClient = iSocketClient;
printf("g_iSocketClient = %d \n", g_iSocketClient);
return iSocketClient;
}
QT端通过按钮向服务器发送rpc请求。
/* control led */
void MainWindow::on_pushButton_clicked()
{
static int status = 1;
/* 2. control LED */
rpc_led_control(status);
status = !status;
}
rpc_led_control代码如下,sprintf那里构造了一个json格式的buf,然后发送给服务器,从中可以看到客户端构造的json格式里,method也为led_control(同服务器那里注册的参数一致),服务器会解析这个json格式数据,如果发现method的值和服务器那边注册的参数一致,就会调用led_control,并一段json格式的数据。
在这里,如果成功发送,send函数会返回一个和buf一样长的数据,在while循环里,会反复读看看服务器有没有发来数据,当数据为1,并且为回车换行符时,数据无效,继续读。当读到一个有效数据,cJSON_Parse是创建一个JSON数据格式,cJSON_GetObjectItem是从中获取result的值。
int rpc_led_control(int on)
{
char buf[100];
int iLen;
int ret = -1;
int iSocketClient = g_iSocketClient;
sprintf(buf, "{\"method\": \"led_control\", \"params\": [%d], \"id\": \"2\" }", on);
iLen = send(iSocketClient, buf, strlen(buf), 0);
if (iLen == strlen(buf))
{
while (1)
{
iLen = read(iSocketClient, buf, sizeof(buf));
buf[iLen] = 0;
if (iLen == 1 && (buf[0] == '\r' || buf[0] == '\n'))
continue;
else
break;
}
if (iLen > 0)
{
cJSON *root = cJSON_Parse(buf);
cJSON *result = cJSON_GetObjectItem(root, "result");
ret = result->valueint;
cJSON_Delete(root);
return ret;
}
else
{
printf("read rpc reply err : %d\n", iLen);
return -1;
}
}
else
{
printf("send rpc request err : %d, %s\n", iLen, strerror(errno));
return -1;
}
}
7210

被折叠的 条评论
为什么被折叠?



