前言
注意:本项目的所有代码中,仅客户端的代码是开源的,其他功能的实现仅给出大致的解决方法,代码则闭源。客户端的程序,仅可以在Linux系统上运行。
开发器材:
安装有温度传感器的树莓派开发板一块、开通有公网IP服务的路由器一台(最好在阿里云上注册有域名)、电源、网线。
项目目标
- 客户可通过客户端程序,实时获取到树莓派开发板上温度传感器所采样到的温度值;
- 树莓派上的温度服务程序可定时进行温度的采样,并把采集到的温度数据上报到树莓派上安装的sqlite3数据库中;
- 树莓派上运行的温度服务程序可以实时捕捉进程信号,在一个指定的文件里面记录进程结束的原因以及捕捉到的信号的类型。
理论来源
Unix环境高级编程的基本内容,包括文件I/O、socket网络编程初步、多进程编程、多线程编程等;sqlite3 数据库基本命令的使用和对应的C接口函数的应用。
开发前的必要准备
- 将树莓派通过有线连接到路由器上,并保证PC可以通过公网IP或域名在SecureCRT上远程登录;
- 在树莓派上预安装好sqlite3数据库(sudo apt-get install sqlite3),以及对应的C sqlite3接口函数库(sudo apt-get install libsqlite3-dev);
- 选定好温度服务程序所需要的端口号(为防止端口的冲突,最好以自己电话号码后四位为端口号);
- 最好在PC上安装好TCP Test Tool,以便于检查客户端程序或服务程序中的逻辑错误。
客户端代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <getopt.h>
#define BUF_SIZE 256
#define IP_LEN 32
#define LOG_PATH "./send.log"
void dns(char *domain_name, char **ip);
void dns(char *domain_name, char **ip)
{
struct hostent *server_name = NULL;
server_name = gethostbyname(domain_name);
inet_ntop(server_name->h_addrtype, server_name->h_addr, *ip, 32 );
}
int main(int argc, char **argv)
{
int bind_fd = -1;
int read_fd = -1;
int server_fd = -1;
int connect_fd = -1;
int send_id = -1;
int opt = -1;
char buf[BUF_SIZE];
char *server_ip = (char *)malloc(IP_LEN);
struct sockaddr_in server_addr;
opt = getopt(argc, argv, "xy");
if( opt!='x' && opt!= 'y' )
{
printf("temperGet usage:\n-s: read the temperature from XXX pi\n-m: read the temperature from YYY pi\n\n");
return -1;
}
if(opt == 'x')
{
dns("XXX.XXX.com", &server_ip);
}
else
{
dns("YYY.YYY.com", &server_ip);
}
server_fd = socket( AF_INET, SOCK_STREAM, 0);
if(server_fd < 0)
{
printf("Fail to create a client socket [%d]: %s\n", server_fd, strerror(errno) );
return 0;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
if(opt == 'x')
{
server_addr.sin_port = htons(6666);
printf("Get temperature from XXX.XXX.com...\n");
}
else
{
server_addr.sin_port = htons(8888);
printf("Get temperature from YYY.YYY.com...\n");
}
inet_aton(server_ip, &server_addr.sin_addr);
connect_fd = connect(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr) );
if(connect_fd < 0)
{
printf("Fail to connect sever!IP:%s error:%s\n\n", inet_ntoa(server_addr.sin_addr), strerror(errno) );
return 0;
}
memset(buf, 0, sizeof(buf) );
read_fd = read(server_fd, buf, sizeof(buf) );
if(read_fd < 0)
{
printf("Fail to get temperature [%d]: %s\n", read_fd, strerror(errno) );
return 0;
}
else
{
printf("%s\n\n",buf);
}
memset(buf, 0, BUF_SIZE);
send_id = open(LOG_PATH, O_CREAT|O_TRUNC|O_RDWR, 0666);
if(send_id < 0)
{
printf("Fail to create a new file to record the client message :%s\n", strerror(errno));
return -1;
}
dup2(send_id, STDOUT_FILENO);
system("echo `date`; who | sed -n '$p'; uname -a");
lseek(send_id, SEEK_SET, 0);
if( (read(send_id, buf, BUF_SIZE)) < 0)
{
printf("Fali to read the file :%s :%s\n", LOG_PATH, strerror(errno));
return -1;
}
if( (write(server_fd, buf, BUF_SIZE)) < 0)
{
printf("Fail to write the server socket fd :%s\n", strerror(errno));
return -1;
}
unlink(LOG_PATH);
close(send_id);
close(server_fd);
close(read_fd);
close(connect_fd);
return 0;
}
简要说明一下客户端的程序逻辑:
- 本人已经在贤川市远大区安插有具有公网IP服务的路由器两台,两台路由器上都安装有可以测量温度的树莓派开发板,两块开发板的域名分别是 XXX.XXX.com 和 YYY.YYY.com(此两域名均为化名),通过getopt函数,可以进行参数解析,即客户端传递参数‘x’的时候,访问前者,通过‘y’可以访问后者,dns函数就是一个通过域名解析为IP地址的一个函数,当传入某台服务器的域名时,可以获得这台服务器的IP地址;
- socket编程:依次使用socket函数、connect函数,就可以成功获得一个带有服务器端数据包的文件描述符,利用read函数去读取这个文件描述符中的内容之后,再利用printf函数,就可以成功把服务器端的温度信息打印到标准输出,这样就算成功获取到了温度;
- 为了安全起见,客户端必须调用Linux的系统命令(如system函数里面所示),将获取到的客户端主机信息重定向输入到一个文件(“./send.log”)中,然后再通过socket编程,将此文件中读取的内容发送给服务器,这样方便于服务器端对应用程序的维护;
- 所有的涉及文件描述符的函数(write、read、socket、connect等),均可能出现函数调用失败的情况,这个时候,就应该将错误信息打印到标准输出( strerror(errno) ),以便于查找错误原因。
以上是客户端的代码,若是读者有看不懂的函数或不知道相关函数的使用方法,请自行利用搜索引擎或是利用Linux中man手册进行查询。