1. 服務端要求
1, 服務器程序運行在Linux服務器或同一個樹莓派上(遠程登錄);
2, 通過命令行指定監聽的端口;
3, 程序放到後臺運行,並通過syslog記錄程序的運行出錯、調試日誌;
4, 程序能夠捕捉kill信號正常退出;
5, 服務器要支持多個客戶端併發訪問,可以選擇多路複用、多進程或多線程任意一種實現;
6, 服務器收到每個客戶端的數據都解析後保存到數據庫中,接收到的數據格式爲: SN/時間/溫度
,例如RPI0001/2019-01-05 11:40:30/30.0C
。
該數據庫要求一個客戶端創建一個表,表名爲客戶端樹莓派的ID,如RPI0001, 表裏面有三個字段,分別爲ID(自動增長的主鍵)、時間、溫度值。
2. 服務端流程圖
3. 功能分析
1、服務器端前幾條功能要求與客戶端基本一致,這裏不再贅述;
2、服務器併發訪問有多進程、多線程、IO多路複用(select、poll、epoll),這裏選擇多進程進行socket編程;
這裏關於多進程,多線程和多路複用的詳細對比分析參考下面鏈接:
Linux下多進程/線程區別
Linux下select、poll和epoll區別
3、服務器將接收到的數據保存至數據庫中,這裏選擇sqlite3這種輕型數據庫(適用於嵌入式領域,佔用資源很少),關於數據庫的具體使用方法可以參考下面鏈接:
sqlite數據庫學習
4. 代碼模塊
這裏只是一個server_mult-process.c
文件實現服務器程序,包括數據的創建與插入。
/*********************************************************************************
* Copyright: (C) 2019 Tang Zhiqiang<[email protected]>
* All rights reserved.
*
* Filename: server_mult-process.c
* Description: This file is socket server RPI temperature
*
* Version: 1.0.0(11/04/2019)
* Author: Tang Zhiqiang <[email protected]>
* ChangeLog: 1, Release initial version on "11/04/2019 08:15:46 PM"
*
********************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <time.h>
#include <pthread.h>
#include <getopt.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/resource.h>
#include <signal.h>
#include <syslog.h>
#include <fcntl.h>
#include <sqlite3.h>
//#include "server_init.c"
#define MAX_EVENTS 512
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) //計算結構體數組大小
static inline void print_usage(char *programe);
//void set_socket_rlimit(void);
int g_sigstop = 0;
void signal_stop(int signum)
{
if(SIGTERM == signum)
{
printf("SIGTERM signal detected\n");
g_sigstop = 1;
}
}
int main(int argc, char *argv[])
{
int listenfd,connfd;
int serv_port = 0;
char *serv_ip=NULL;
int daemon_run = 0;
char *progname = NULL;
int opt;
socklen_t cliaddr_len=20;
int rv=0;
int log_fd;
int i;
char buf[1024];
char sql1[28];
pid_t pid;
struct sockaddr_in serv_addr; //在main()中,用到該局部變量
int on=1;
sqlite3 *db=NULL;
char *zErrMsg = NULL;
int rc;
int len;
int ret;
char *sql=NULL;
char delim[]="/";
char id[20];
char data_time[50];
char temper[10];
char *ptr=NULL;
//char *p=NULL;
struct option long_options[]=
{
{"daemon", no_argument, NULL, 'd'},
{"port", required_argument, NULL, 'p'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
progname = basename(argv[0]);
//Parser the command line parameters
while((opt = getopt_long(argc, argv, "dp:h", long_options, NULL)) != -1)
{
switch (opt)
{
case 'd':
daemon_run = 1;
break;
case 'p':
serv_port = atoi(optarg);
break;
case 'h':
print_usage(progname);
break;
default:
break;
}
}
if(!serv_port)
{
print_usage(progname);
return -1;
}
signal(SIGTERM, signal_stop); //15號信號(SIGTERM)可被捕捉
if(daemon_run)
{
printf("program %s running at the backgroud 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", strerror(errno));
return 0;
}
}
if((listenfd=socket(AF_INET, SOCK_STREAM, 0)) < 0 )
{
printf("create socket failure: %s\n", strerror(errno));
return -1;
}
//Set socket port reuseable, fix 'Address already in use' bug when socket server restart
//setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(serv_port);
if(!serv_ip)
{
/* 監聽所有的客戶端的IP地址 */
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
}
else
{
if(inet_pton(AF_INET, serv_ip, &serv_addr.sin_addr) <= 0)
{
printf("inet_pton() set listen IP failure: %s\n", strerror(errno));
close(listenfd);
//goto Cleanup;
return -1;
}
}
//Set socket port reuseable, fix 'Address already in use' bug when socket server restart
if((setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (void*)&on, sizeof(on))) < 0 )
{
printf("setsockopt failure: %s\n", strerror(errno));
return -2;
}
if(bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("bind socket[%d] failure: %s\n", listenfd, strerror(errno));
close(listenfd);
return -3;
}
if(listen(listenfd, 64) < 0)
{
//printf("listen() socket[%d] failure: %s\n", listenfd, strerror(errno));
printf("ERROR: %s server listen on port %d failure\n", argv[0], serv_port);
close(listenfd);
return -4;
}
printf("server socket[%d] start to listen on port %d\n", listenfd, serv_port);
/* Open database 若沒有則創建*/
len = sqlite3_open("temper.db", &db);
if(len != SQLITE_OK)
{
sqlite3_close(db);
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(1);
}
printf("Opened database successfully\n");
/* Create SQL statement */
//sql ="create table if not exists temperature(ID char(10), datetime char(50), temperature char(10))";
sql ="create table temperature(ID char(10), datetime char(50), temperature char(10))";
/* Execute SQL statement */
rc = sqlite3_exec(db, sql, 0, 0, &zErrMsg);
if( rc != SQLITE_OK )
{
sqlite3_close(db);
fprintf(stderr, "Create table error: %s\n", zErrMsg);
return -1;
}
printf("Table created successfully\n");
//printf("programe will start running...\n");
while(!g_sigstop)
{
//program will blocked here
printf("Start accept new cilent...\n");
connfd=accept(listenfd, (struct sockaddr *)&serv_addr, &cliaddr_len); //accepte() 第三個參數可以是siziof(struct sockaddr)
if(connfd < 0)
{
printf("accept new client failure: %s\n", strerror(errno));
continue;
}
printf("accept new client [%s:%d] sucessful.\n", inet_ntoa(serv_addr.sin_addr), ntohs(serv_port));
pid=fork();
if(pid < 0)
{
printf("fork() create child process failure: %s\n", strerror(errno));
//close(connfd);
continue;
}
else if(pid > 0)
{
/* parent process close client fd and goes to accept new cilent again */
close(connfd);
continue;
}
else if(pid == 0)
{
/* child process close the listen socket fd */
close(listenfd);
printf("Child process start to commuicate with socket client...\n");
while(1)
{
memset(buf, 0, sizeof(buf));
rv=read(connfd, buf, sizeof(buf));
if(rv <= 0)
{
printf("read client [%d] data failure or socket disconnected: %s\n", connfd, strerror(errno));
close(connfd);
exit(0);
}
else if(rv > 0)
{
printf("socket[%d] read data: %s\n", connfd, buf);
//讀到數據之後需要處理並保存到數據庫
ptr = strtok(buf, delim);
while(ptr != NULL)
{
strncpy(id, ptr, sizeof(id));
ptr=strtok(NULL, delim);
strncpy(data_time, ptr, sizeof(data_time));
ptr=strtok(NULL, delim);
strncpy((char *)temper, ptr, sizeof(temper));
ptr=strtok(NULL, delim);
}
memset(sql1, 0, sizeof(sql1));
snprintf(sql1, 128, "insert into temperature values('%s', '%s', '%s');", id, data_time, temper);
//保證了數組sql1的內容爲字符串
sql1[127] = '\0';
printf("%s\n", sql1);
//調用sqlite3_exec();將數據存儲至temperature表中
ret = sqlite3_exec(db, sql1, 0 , 0, &zErrMsg);
if (ret != SQLITE_OK) //判斷返回值,如果不等於SQLITE_OK,即插入記錄失敗
{
sqlite3_close(db);
printf("insert data failure ; %s!\n", zErrMsg);
return 0;
}
printf("insert data successfully!\n");
}//else if(rv > 0)
}//while(1)
}//else if(pid == 0)
}//while(!g_sigstop)
close(listenfd);
sqlite3_close(db); //關閉數據庫
return 0;
}//main()
static inline void print_usage(char *progname)
{
printf("Usage: %s [OPTION]...\n", progname);
printf(" %s is a socket server program, which used to verify client and echo back string from it\n",progname);
printf("\nMandatory arguments to long options are mandatory for short options too:\n");
printf(" -b[daemon ] set program running on background\n");
printf(" -p[port ] Socket server port address\n");
printf(" -h[help ] Display this help information\n");
printf("\nExample: %s -b -p 8900\n", progname);
return ;
}
5. 服務器運行結果
服務器需要一直運行,通過參數解析方式,命令行需要指定服務器監聽的端口,且最好大於1024,否則需要root權限。其運行結果爲:
注意:直接指定服務器監聽的端口就行,這裏不傳指定的IP,在程序中默認監聽所有IP。
6. 爲了方便看,貼出客戶端結果
也可以直接跳轉到客戶端詳解