1, 服务器程序运行在PC端服务器(需要开端口)或同一个树莓派上(不需要开端口);
2, 通过命令行指定监听的端口;
3, 程序放到后台运行,并通过syslog记录程序的运行出错、调试日志;
4, 程序能够捕捉kill信号正常退出;
5, 服务器要支持多个客户端并发访问;
6, 服务器收到每个客户端的数据都解析后保存到数据库中,接收到的数据格式为: “ID/时间/温度”,如RPI0001/2019-01-
05 11:40:30/30.0C”。
1、服务器端前几条功能要求与客户端基本一致,这里不再赘述;
2、服务器并发访问即需要用到多进程、多线程、IO多路复用(select、poll、epoll),这里,我选择多进程进行socket编程;
3、服务器将接收到的数据保存至数据库中,这里选择sqlite3这种轻型数据库(适用于嵌入式领域,占用资源很少),关于数据库的具体使用方法将在下一篇博客中详细介绍;
代码(receive_temper.c)如下:
(服务器端代码未拆分成多个.c文件,所以看起来十分冗杂)
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "sqlite3.h"
#define BACKLOG 13
//函数声明,打印帮助信息;捕捉信号并结束程序
void print_usage(char *progname);
void sig_stop(int signum);
//定义一个全局变量g_stop ,与sig_stop()函数配合,达到结束程序的目的
int g_stop = 0;
int main(int argc, char *argv[])
{
int rv;
int ret;
int opt;
int idx;
int port;
int log_fd;
int ch = 1;
int daemon_run = 0;
int ser_fd = -1;
int cli_fd = -1;
pid_t pid = -1;
struct sockaddr_in ser_addr;
struct sockaddr_in cli_addr;
socklen_t cliaddr_len = 20;
/*命令行参数解析
deamon:程序后台运行
port:指定服务器开设端口
help:打印帮助信息
*/
struct option opts[] = {
{"daemon", no_argument, NULL, 'd'},
{"port", required_argument, NULL, 'p'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
while ((opt = getopt_long(argc, argv, "dp:h", opts, &idx)) != -1)
{
switch(opt)
{
case 'd':
daemon_run = 1;
break;
case 'p':
port = atoi(optarg); //将字符串形式的端口号转换成整型
break;
case 'h':
print_usage(argv[0]); //打印帮助信息
return 0;
}
}
//判断是否指定端口或正确端口
if (!port)
{
print_usage(argv[0]);
return 0;
}
if (daemon_run)
{
printf("Program %s is running at the background now\n", argv[0]);
//建立日志系统
log_fd = open("receive_temper.log", O_CREAT|O_RDWR, 0666);
if (log_fd < 0)
{
printf("Open the logfile failure : %s\n", strerror(errno));
return 0;
}
//标准输出、标准出错重定向
dup2(log_fd, STDOUT_FILENO);
dup2(log_fd, STDERR_FILENO);
//程序后台运行
if ((daemon(1, 1)) < 0)
{
printf("Deamon failure : %s\n", strerror(errno));
return 0;
}
}
//安装SIGUSR1信号
signal(SIGUSR1, sig_stop);
//创建socket_fd,以供后续使用
//第一个参数为协议域,这里使用AF_INET(32位ipv4地址、16位端口)
//第二个参数为type,指定socket类型,这里使用SOCK_STREAM
//第三个参数为指定协议,当其为0时,自动选择与type类型相对应的默认协议
ser_fd = socket(AF_INET, SOCK_STREAM, 0);
if (ser_fd < 0)
{
printf("Creat socket failure:%s\n", strerror(errno));
return 0;
}
printf("Creat the ser_fd[%d]!\n",ser_fd);
//设置服务器地址信息
memset(&ser_addr, 0, sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(port); //调用htons(),将端口号由主机字节序转变成网络字节序
//设置服务器IP地址(INADDR_ANY,代表监听所有IP),并调用htonl(0,将其由主机字节序转变成网络字节序
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//解决服务器程序停止后立刻运行,出现(端口)被占用的问题
setsockopt(ser_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&ch, sizeof(ch));
//绑定服务器地址(IP+端口),用于提供服务
rv = bind(ser_fd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
if (rv < 0)
{
printf("Bind server's port failure:%s\n", strerror(errno));
return 0;
}
printf("The port[%d] has been bind by this server!\n",port);
//调用listen(),将主动类型的socket套接字变为被动类型(等待客户端连接)
//第一个参数为sockfd,指定所要监听的socket套接字,用于接收外部请求
//第二个参数为backlog, TCP连接是一个过程,内核会在进程空间中维护一个连接队列以跟踪与服务器建立连接但还未着手处理的连接
listen(ser_fd, BACKLOG);
while (!g_stop)
{
//接受来自客户端的请求,并创建一个cli_fd与客户端进行连接
cli_fd = accept(ser_fd, (struct sockaddr *)&cli_addr, &cliaddr_len);
if (cli_fd < 0)
{
printf("Accept the request from client failure:%s\n", strerror(errno));
continue;
}
pid = fork(); //调用fork(),实现多进程
if (pid < 0)
{
printf("Creat child process failure:%s.\n", strerror(errno));
continue;
}
else if (pid > 0)
{
close(cli_fd); //父进程关闭cli_fd,保留ser_fd,等待与其他客户端连接
continue;
}
else if (pid == 0)
{
close(ser_fd); //子进程关闭ser_fd,保留cli_fd,保持与客户端连接
int a;
int len;
char *sn;
char *cut;
char *temp;
char buf[512];
char *datetime;
char *zErrMsg = NULL;
char sql1[128];
//描述创建数据库中表的信息
char *sql = "create table if not exists temperature(sn char(10), datetime char(50), temperature char(10))";
sqlite3 *db = NULL;
//调用sqlite3_open(),打开.db数据库文件。若没有,则创建
len = sqlite3_open("temper.db",&db);
if (len)
{
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
exit(1);
}
else
{
printf("You have opened a sqlite3 database named temper.db successfully!\n");
}
//创建temperature表,用来存放来自客户端的数据
ret = sqlite3_exec(db, sql, 0, 0, &zErrMsg);
if (ret != SQLITE_OK)
{
sqlite3_close(db);
printf("Creat table failure \n");
return 0;
}
printf("Create table successfully!\n");
printf("Child process start to communicate with client by cli_fd[%d]...\n", cli_fd);
printf("Now ready to connect the client and recive message from client...\n");
printf("\n");
while (1) //设置while循环,持续接收来自客户端的信息
{
memset(buf, 0, sizeof(buf));
a = read(cli_fd, buf, sizeof(buf)); //从buf中读数据(客户端写入)
if (a < 0) //返回值小于零,则表示接收数据出错
{
printf("Read information from client failure:%s\n", strerror(errno));
close(cli_fd);
exit(0);
}
else if (a == 0) //返回值等于0,则表示客户端主动断开连接
{
printf("The connection with client has broken!\n");
close(cli_fd);
exit(0);
}
else //返回值大于零,则表示接收数据成功
{
printf("The message recived from client[%s,%d] is \"%s\"\n",
inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);
//对接收到的数据进行分割操作,便于存储至数据库
cut = strtok(buf, "/");
if (cut != NULL)
{
sn = cut;
//printf("%s\n", sn);
if ((cut = strtok(NULL, "/")) != NULL)
{
datetime = cut;
//printf("%s\n", datetime);
if ((cut = strtok(NULL, "/")) != NULL)
{
temp = cut;
//printf("%s\n", temp);
}
}
}
snprintf(sql1, 128, "insert into temperature values('%s', '%s', '%s');",
sn, datetime, temp);
sql1[127] = '\0';
printf("%s\n", sql1);
//将数据存储至temperature表中
ret = sqlite3_exec(db, sql1, 0 , 0, &zErrMsg);
if (ret != SQLITE_OK)
{
sqlite3_close(db);
printf("insert data failure ; %s!\n", zErrMsg);
return 0;
}
printf("insert data successfully!\n");
printf("\n");
}
}
}
}
close(ser_fd);
return 0;
}
//打印帮助信息函数
void print_usage(char *progname)
{
printf("-d(--daemon):let program run in the background.\n");
printf("-p(--port):enter server port.\n");
printf("-h(--help):print this help information.\n");
return ;
}
//回调函数sig_stop()
void sig_stop(int signum)
{
if (SIGUSR1 == signum) /判断捕捉到信号是否为SIGUSR1
{
g_stop = 1; //g_stop为1,while(!g_stop)循环结束,程序关闭
}
return ;
}
另注:
1、服务器端代码是在PC端linux服务器下编写,进行域名解析时得到的时服务器的公网IP,故须在服务器连接的路由器下开设相应的端口,以供客户端正常连接!
2、程序执行结果会在下篇博客中与sqlite3数据库的基本使用一起介绍!
3、本程序是存在一定的bug,倘若多个客户端同时连接服务器,即会同时操作同一个数据库文件,而多个进程间互不干扰,则会出现相关问题,我会在接下来的博客中,介绍多进程、多线程、IO多路复用的相关知识,其中会提到锁的概念!