项目开发一:基于WebServer 的工业数据采集项目
A.项目文档及运行流程解析(用于了解项目流程/答辩)
一.modbus.c(重点部分)
#include #include #include #include #include #include "modbus.h" #include #include #include #include #include #include #include struct msgbuf { long mtype; // 第一个成员必须是long类型变量,表示消息的类型 int num1; int num2; }; // 全局变量,用于在线程之间共享数据 modbus_t *ctx; // 数据采集线程函数 void *read_data(void *arg) { // 创建key值 key_t key; key = ftok("./main.c", 'a'); if (key(1)结构体定义解析
c
struct msgbuf { long mtype; // 消息类型,必须作为第一个成员,用于消息队列的消息筛选 int num1; // 存储命令参数1(如控制类型:开灯/关灯等) int num2; // 存储命令参数2(如开关状态:0关/1开) };
- 用于消息队列通信,定义了消息的格式,mtype 让接收方可以根据类型筛选消息,num1 和 num2 传递具体控制命令。
(2)全局变量解析
modbus_t *ctx; // Modbus通信上下文句柄,贯穿整个程序,用于保持Modbus连接状态
- 作为全局变量,在多个函数(线程)中共享,避免重复创建 Modbus 连接。
(3)read_data 线程函数解析(数据采集)
void *read_data(void *arg) { // 1. 共享内存创建与映射 key_t key = ftok("./main.c", 'a'); // 生成共享内存的key值,用于标识共享内存 int shmid; // 创建共享内存,若已存在则重新获取 shmid = shmget(key, 64, IPC_CREAT | IPC_EXCL | 0777); if (shmid
- 功能:通过 Modbus 读取设备数据,存入共享内存,供其他模块(如 WebServer)读取。
- 关键操作:
- ftok + shmget:创建或获取共享内存,实现跨进程数据共享。
- modbus_read_registers:核心 Modbus 读操作,获取设备传感器数据。
(4)write_command 线程函数解析(命令处理)
void *write_command(void *arg) { // 1. 消息队列创建与获取 key_t key = ftok("./main.c", 'a'); int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666); if (msgid
- 功能:通过消息队列接收命令,解析后控制 Modbus 设备(如 LED、蜂鸣器)。
- 关键操作:
msgget + msgrcv:创建消息队列并接收命令消息。
modbus_write_bit:核心 Modbus 写操作,向设备写入控制指令。
(5)main 函数解析
int main(int argc, char const *argv[]) { // 1. Modbus初始化创建实例( ctx = modbus_new_tcp("192.168.50.82", 502); // 创建Modbus TCP连接 if (ctx == NULL) { perror("modbus_new_tcp err"); return -1; } int slave_id = 1; modbus_set_slave(ctx, slave_id); // 设置Modbus从机ID if (modbus_connect(ctx) == -1) { // 建立连接 perror("connect err"); modbus_free(ctx); return -1; } // 2. 线程创建 pthread_t data_thread, command_thread; if (pthread_create(&data_thread, NULL, read_data, NULL) != 0) { // 创建数据采集线程 perror("create data err"); modbus_close(ctx); modbus_free(ctx); return -1; } if (pthread_create(&command_thread, NULL, write_command, NULL) != 0) { // 创建命令处理线程 perror("create command err"); modbus_close(ctx); modbus_free(ctx); return -1; } // 3. 等待线程结束 pthread_join(data_thread, NULL); pthread_join(command_thread, NULL); // 4. 资源释放 modbus_close(ctx); // 关闭Modbus连接 modbus_free(ctx); // 释放Modbus上下文 return 0; }
- 功能:初始化 Modbus 通信,创建数据采集和命令处理线程,协调程序运行与资源释放。
- 关键流程:
- Modbus Tcp :Modbus 连接初始化:modbus_new_tcp → modbus_set_slave → modbus_connect。
- 线程管理:pthread_create 创建线程,pthread_join 等待线程结束。
- 资源清理:确保 Modbus 连接关闭和上下文释放,避免资源泄漏。
(6)相关扩展
一、代码模块与原理图组件的映射关系
- read_data 线程(数据采集)
代码功能:
通过 modbus_read_registers 读取 Modbus 设备寄存器数据(光线、加速度 XYZ)。
利用共享内存(shmget、shmat)存储采集数据,实现数据共享。
与原理图关联:
对应原理图中 “Modbus 采集控制程序” 与 “Modbus 设备” 的交互。该线程模拟了采集程序主动读取 Modbus 设备数据的过程,采集后的数据通过共享内存存储,为 WebServer 读取(如网页展示数据)提供基础,契合原理图中 “采集控制程序采集 Modbus 设备数据” 的逻辑。
2.write_command 线程(命令处理)
代码功能:
通过消息队列(msgget、msgrcv)接收命令数据。根据命令值(如开灯、关灯等),使用 modbus_write_bit 向 Modbus 设备写入控制指令。
与原理图关联:
对应原理图中 “Modbus 采集控制程序” 处理外部命令的功能。消息队列作为进程间通信工具,可接收来自WebServer 通过进程间通信,最终通过 Modbus/TCP 控制 Modbus 设备,实现 “命令输入 → 采集控制程序处理 → Modbus 设备执行” 的流程。
3.main 函数(整体协调)
代码功能:
ModbusTcp实例化, modbus_new_tcp,modbus_set_slave,modbus_connect,建立与 Modbus 设备的通信链路。
创建数据采集线程和命令处理线程,协调两者运行。
与原理图关联:
作为程序入口,统筹 “Modbus 采集控制程序” 的核心功能,确保数据采集和命令处理两个关键流程并行执行,完整实现原理图中 “Modbus 采集控制程序” 的核心职责。
二、关键技术与原理图的交互逻辑
- 共享内存(数据共享)
- 代码实现:通过 shmget 创建共享内存,shmat 映射内存空间,将采集的传感器数据写入共享内存(sprintf(p, "%d#%d #%d #%d\n", reg[0], reg[1], reg[2], reg[3]))。
- 与原理图关联:若将原理图中 “WebServer” 与 “Modbus 采集控制程序” 结合,共享内存可作为两者的通信桥梁。例如,WebServer 读取共享内存中的数据,转发给网页展示,实现 “Modbus 设备 → 采集程序(共享内存) → WebServer → 网页” 的数据展示链路。
- 消息队列(命令传递)
- 代码实现:通过 msgget 创建消息队列,msgrcv 接收消息类型为 1 的命令数据,解析后控制 Modbus 设备。
- 与原理图关联:对应原理图中 “进程间通信” 机制。假设 WebServer 接收网页的控制命令(如用户点击网页按钮),可通过消息队列将命令传递给 “Modbus 采集控制程序”,最终实现 “网页 → WebServer → 消息队列 → 采集程序 → Modbus 设备” 的命令控制链路。
- Modbus 通信(核心交互)
- 代码实现:使用 Modbus 库函数(modbus_read_registers 读数据、modbus_write_bit 写指令)与 Modbus 设备交互。
- 与原理图关联:直接对应原理图中 “Modbus 采集控制程序” 通过 Modbus/TCP 与 “Modbus 设备” 的通信,是整个系统数据采集和设备控制的核心交互层。
三、整体流程与原理图的匹配
- 数据采集流程:
代码中 read_data 线程循环读取 Modbus 设备数据 → 存入共享内存,对应原理图中 “Modbus 设备 → Modbus 采集控制程序” 的数据采集路径,为上层应用(如 WebServer 驱动的网页)提供数据来源。
- 命令控制流程:
消息队列接收命令 → write_command 线程解析命令 → 通过 Modbus 写入设备,对应原理图中 “外部命令(如网页触发) → 进程间通信 → Modbus 采集控制程序 → Modbus 设备” 的控制路径,实现对 Modbus 设备的远程操作。
二.thttpd.c(最重点)
#include "thttpd.h" #include "custom_handle.h" #include #include #include int init_server(int _port) //创建监听套接字 { int sock=socket(AF_INET,SOCK_STREAM,0); if(sock perror("socket failed"); exit(2); } //设置地址重用 int opt=1; setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(_port); local.sin_addr.s_addr=INADDR_ANY; if(bind(sock,(struct sockaddr*)&local,sizeof(local)) perror("bind failed"); exit(3); } if(listen(sock,5) perror("listen failed"); exit(4); } return sock; } static int get_line(int sock,char* buf) //按行读取请求报头 { char ch='\0'; int i=0; ssize_t ret=0; while(i ret=recv(sock,&ch,1,0); if(ret0&&ch=='\r') { ssize_t s=recv(sock,&ch,1,MSG_PEEK); if(s0&&ch=='\n') { recv(sock,&ch,1,0); } else { ch='\n'; } } buf[i++]=ch; } buf[i]='\0'; return i; } static void clear_header(int sock) //清空消息报头 { char buf[SIZE]; int ret=0; do { ret=get_line(sock,buf); }while(ret!=1&&(strcmp(buf,"\n")!=0)); } static void show_404(int sock) //404错误处理 { clear_header(sock); char* msg="HTTP/1.0 404 Not Found\r\n"; send(sock,msg,strlen(msg),0); //发送状态行 send(sock,"\r\n",strlen("\r\n"),0); //发送空行 struct stat st; stat("wwwroot/404.html",&st);//获取网页属性 int fd=open("wwwroot/404.html",O_RDONLY); sendfile(sock,fd,NULL,st.st_size);//发送文件;st.st_size:文件的大小 close(fd); } void echo_error(int sock,int err_code) //错误处理,只有404能进行错误处理 { switch(err_code) { case 403: break; case 404: show_404(sock); break; case 405: break; case 500: break; defaut: break; } } static int echo_www(int sock,const char * path,size_t s) //处理非CGI的请求 { int fd=open(path,O_RDONLY); if(fd echo_error(sock,403); return 7; } char* msg="HTTP/1.0 200 OK\r\n"; send(sock,msg,strlen(msg),0); //发送状态行 send(sock,"\r\n",strlen("\r\n"),0); //发送空行 //sendfile方法可以直接把文件发送到网络对端 if(sendfile(sock,fd,NULL,s) echo_error(sock,500); return 8; } close(fd); return 0; } static int handle_request(int sock,const char* method, const char* path,const char* query_string) { char line[SIZE]; int ret=0; int content_len=-1; if(strcasecmp(method,"GET")==0) { //清空消息报头 clear_header(sock); } else { //获取post方法的参数大小 do { ret=get_line(sock,line); if(strncasecmp(line,"content-length",14)==0) //post的消息体记录正文长度的字段 { content_len=atoi(line+16); //求出正文的长度 } }while(ret!=1&&(strcmp(line,"\n")!=0)); } printf("method = %s\n", method); printf("query_string = %s\n", query_string); printf("content_len = %d\n", content_len); char req_buf[4096] = {0}; //如果是POST方法,那么肯定携带请求数据,那么需要把数据解析出来 if(strcasecmp(method,"POST")==0) { int len = recv(sock, req_buf, content_len, 0); printf("len = %d\n", len); printf("req_buf = %s\n", req_buf); } //先发送状态码 char* msg="HTTP/1.1 200 OK\r\n\r\n"; send(sock,msg,strlen(msg),0); //请求交给自定义代码来处理,这是业务逻辑 parse_and_process(sock, query_string, req_buf); return 0; } int handler_msg(int sock) //浏览器请求处理函数 { char del_buf[SIZE] = {}; //通常recv()函数的最后一个参数为0,代表从缓冲区取走数据 //而当为MSG_PEEK时代表只是查看数据,而不取走数据。 recv(sock,del_buf,SIZE,MSG_PEEK);//MSG_PEEK表示只是吧客户端发过来的数据看一下,recv接收完以后仍在缓存区 #if 1 //初学者强烈建议打开这个开关,看看tcp实际请求的协议格式 puts("---------------------------------------"); printf("recv:%s\n",del_buf); puts("---------------------------------------"); #endif //接下来method方法判断之前的代码,可以不用重点关注 //知道是处理字符串,把需要的信息过滤出来即可 char buf[SIZE]; int count=get_line(sock,buf); int ret=0; char method[32];//存放请求方法 char url[SIZE];//存放URL char *query_string=NULL; int i=0; int j=0; int need_handle=0;//判断是否需要处理 //获取请求方法和请求路径(liaojie) while(j if(isspace(buf[j])) { break; } method[i]=buf[j]; i++; j++; } method[i]='\0'; while(isspace(buf[j])&&j j++; } //这里开始就开始判断发过来的请求是GET还是POST了 if(strcasecmp(method,"POST")&&strcasecmp(method,"GET"))//strcasecmp可以忽略大小写的区别,其余和strcmp一样 { printf("method failed\n"); //如果都不是,那么提示一下 echo_error(sock,405);//405是状态码,表示方法不对 ret=5; goto end;//掉转到end,运行end下面的函数 } if(strcasecmp(method,"POST")==0) { need_handle=1; } i=0; while(j if(isspace(buf[j])) { break; } if(buf[j]=='?') { //将资源路径(和附带数据,如果有的话)保存在url中,并且query_string指向附带数据 query_string=&url[i]; query_string++; url[i]='\0'; } else{ url[i]=buf[j]; } j++; i++; } url[i]='\0'; printf("query_string = %s\n", query_string); //浏览器通过http://192.168.8.208:8080/?test=1234这种形式请求 //是携带参数的意思,那么就需要额外处理了 if(strcasecmp(method,"GET")==0&&query_string!=NULL)//前面的函数已经将指针query_string定位到?,如果问号不等于空,说明有参数,需要进一步处理 { need_handle=1; } //我们把请求资源的路径固定为wwwroot/下的资源,这个自己可以改 char path[SIZE]; sprintf(path,"wwwroot%s",url); //wwwroot/404.html,寻找wwwroot里的404.html printf("path = %s\n", path); //如果请求地址没有携带任何资源,那么默认返回index.html if(path[strlen(path)-1]=='/') //判断浏览器请求的是不是目录 { strcat(path,"index.html"); //如果请求的是目录,则就把该目录下的首页返回回去,strcat是拼接函数 } //如果请求的资源不存在,就要返回传说中的404页面了 struct stat st; if(stat(path,&st) printf("can't find file\n"); echo_error(sock,404); ret=6; goto end; } //到这里基本就能确定是否需要自己的程序来处理后续请求了 printf("need progress handle:%d\n",need_handle); //如果是POST请求或者带参数的GET请求,就需要我们自己来处理了 //这些是业务逻辑,所以需要我们自己写代码来决定怎么处理 if(need_handle) { ret=handle_request(sock,method,path,query_string); } else { clear_header(sock); //如果是GETt,而且没有参数,则直接返回资源 ret=echo_www(sock,path,st.st_size); } end: close(sock); return ret; } int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock 0)且下一个字符是换行符 '\n',则再次调用 recv 函数(不带 MSG_PEEK 标志)将该字符从缓冲区中读取出来。
- else:如果预读的字符不是 '\n',则将 ch 设为 '\n',以确保后续处理能够正确识别换行。
- 注释:MSG_PEEK 的主要功能是 “预读” 套接字接收缓冲区中的数据。当使用 recv 函数并带上 MSG_PEEK 标志时,它会从接收缓冲区中读取数据,但并不会将这些数据从缓冲区中移除。这意味着后续再次调用 recv 函数(不带 MSG_PEEK 标志 )时,仍然可以读取到这些数据 。
5.存储读取的字符
buf[i++] = ch;
将当前读取的字符 ch 存储到 buf 数组中,并将索引 i 加 1。
6.结束字符串并返回结果
buf[i] = '\0'; return i;
- buf[i] = '\0':在 buf 数组的末尾添加字符串结束符 '\0',将其转换为一个以空字符结尾的 C 字符串。
- return i:返回本次读取的字符数量,不包括字符串结束符。
(3)clear_header 函数:清空消息报头
static void clear_header(int sock) //清空消息报头 { char buf[SIZE]; int ret = 0; do { ret = get_line(sock, buf); } while (ret != 1 && (strcmp(buf, "\n") != 0)); }
- 循环读取报头:使用 get_line() 函数循环读取请求报头,直到读取到空行(即 \n)或者读取的字符数为 1。
(4)show_404 函数:404 错误处理
show_404 函数的主要功能是处理客户端请求资源不存在的情况,向客户端发送一个 HTTP 404 错误响应,并将 wwwroot/404.html 文件的内容作为错误页面返回给客户端。整个过程遵循 HTTP 协议的规范,包括发送状态行、空行和文件内容。
static void show_404(int sock) //404错误处理 { clear_header(sock); char *msg = "HTTP/1.0 404 Not Found\r\n"; send(sock, msg, strlen(msg), 0); //发送状态行 send(sock, "\r\n", strlen("\r\n"), 0); //发送空行 struct stat st; stat("wwwroot/404.html", &st); //获取网页属性 int fd = open("wwwroot/404.html", O_RDONLY); sendfile(sock, fd, NULL, st.st_size); //发送文件;st.st_size:文件的大小 close(fd); }
- 清空报头:调用 clear_header() 函数清空请求报头。
- 发送状态行和空行:使用 send() 函数发送 HTTP 状态行 HTTP/1.0 404 Not Found 和空行。
- 获取文件属性:使用 stat() 函数获取 wwwroot/404.html 文件的属性,包括文件大小。
- 发送文件内容:使用 open() 函数打开文件,使用 sendfile() 函数将文件内容直接发送到套接字,最后关闭文件描述符。
精解析:
(1)函数定义与参数
static void show_404(int sock)
- static:表示该函数是一个静态函数,其作用域仅限于当前源文件,其他文件无法调用此函数。
- void:表明该函数没有返回值。
- int sock:是一个套接字描述符,代表与客户端建立的网络连接,函数将通过这个套接字向客户端发送 404 错误响应。
(2)清除请求头
clear_header(sock);
调用 clear_header 函数作用:清除客户端发送的请求头信息。在发送响应之前,通常需要先处理并清除这些请求头,以确保后续的响应能够正确发送。
(3)发送状态行
char *msg = "HTTP/1.0 404 Not Found\r\n"; send(sock, msg, strlen(msg), 0);
- char *msg = "HTTP/1.0 404 Not Found\r\n";:定义一个指向字符串的指针 msg,该字符串是 HTTP 响应的状态行,表示当前响应遵循 HTTP 1.0 协议(非持续连接),请求的资源未找到(状态码 404)。
- send(sock, msg, strlen(msg), 0);:调用 send 函数将状态行信息通过套接字 sock 发送给客户端。strlen(msg) 用于获取状态行字符串的长度,最后一个参数 0 表示使用默认的发送标志。
(4)发送空行
send(sock, "\r\n", strlen("\r\n"), 0);
在 HTTP 协议中,状态行和响应头之后需要有一个空行来分隔请求头和请求体。这里调用 send 函数发送一个空行(\r\n)给客户端。
(5)获取 404 页面文件属性
struct stat st; stat("wwwroot/404.html", &st);
- struct stat st;:定义一个 stat 结构体变量 st,该结构体用于存储文件的各种属性信息,如文件大小、文件权限等。
- stat("wwwroot/404.html", &st);:调用 stat 函数获取 wwwroot/404.html 文件的属性信息,并将结果存储在 st 结构体中。
(6)打开 404 页面文件
int fd = open("wwwroot/404.html", O_RDONLY);
wwwroot:通常是网站项目中存放网页资源的目录名称
调用 open 函数以只读模式(O_RDONLY)打开 wwwroot/404.html 文件,并返回一个文件描述符 fd,后续将使用该文件描述符读取文件内容。
(7)发送 404 页面文件内容
sendfile(sock, fd, NULL, st.st_size);
调用 sendfile 函数将 wwwroot/404.html 文件的内容直接发送给客户端。sendfile 函数是一种高效的文件传输方式,它可以避免在用户空间和内核空间之间进行数据拷贝,从而提高传输效率。
- sock:目标套接字描述符,即要将文件内容发送到的客户端连接。
- fd:源文件描述符,即要发送的 404 页面文件的描述符。
- NULL:偏移量参数,这里设置为 NULL 表示从文件的开头开始发送。
- st.st_size:要发送的文件大小,通过之前调用 stat 函数获取。
(8)关闭文件描述符
close(fd);
调用 close 函数关闭之前打开的 wwwroot/404.html 文件的文件描述符,释放系统资源。
(5)echo_error 函数:错误处理
void echo_error(int sock, int err_code) //错误处理,只有404能进行错误处理 { switch (err_code) { case 403: break; case 404: show_404(sock); break; case 405: break; case 500: break; default: break; } }
- 根据错误码处理错误:使用 switch 语句根据错误码进行相应的处理,目前仅处理 404 错误,调用 show_404() 函数返回 404 错误页面。
(6)echo_www 函数:处理非 CGI 的请求(直接将客户端请求的文件内容发送给客户端)
echo_www 函数的主要功能是处理非 CGI的请求,即直接将客户端请求的文件内容发送给客户端。
整个过程遵循 HTTP 协议的规范,包括发送状态行、空行和文件内容。如果在打开文件或发送文件内容时出现错误,会向客户端发送相应的错误响应。
static int echo_www(int sock, const char *path, size_t s) //处理非CGI的请求 { int fd = open(path, O_RDONLY); if (fd
- 打开文件:使用 open() 函数以只读模式打开请求的文件。
- 发送状态行和空行:使用 send() 函数发送 HTTP 状态行 HTTP/1.0 200 OK 和空行。
- 发送文件内容:使用 sendfile() 函数将文件内容直接发送到套接字,如果发送失败,调用 echo_error() 函数返回 500 错误。
- 关闭文件描述符:最后关闭文件描述符。
精解析:
(1)打开请求的文件
int fd = open(path, O_RDONLY); if (fd
- open(path, O_RDONLY):调用 open 函数以只读模式(O_RDONLY)打开由 path 指定的文件。如果文件成功打开,open 函数将返回一个非负的文件描述符;如果打开失败,将返回 -1。
- if (fd (2)发送 HTTP 状态行和空行
char *msg = "HTTP/1.0 200 OK\r\n";//状态行 send(sock, msg, strlen(msg), 0); send(sock, "\r\n", strlen("\r\n"), 0);
- char *msg = "HTTP/1.0 200 OK\r\n";:定义一个指向字符串的指针 msg,该字符串是 HTTP 响应的状态行,表示当前响应遵循 HTTP 1.0 协议,请求成功处理(状态码 200)。
- send(sock, msg, strlen(msg), 0):调用 send 函数将状态行信息通过套接字 sock 发送给客户端。strlen(msg) 用于获取状态行字符串的长度,最后一个参数 0 表示使用默认的发送标志。
- send(sock, "\r\n", strlen("\r\n"), 0):在 HTTP 协议中,状态行和响应头之后需要有一个空行来分隔请求头和请求体。这里调用 send 函数发送一个空行(\r\n)给客户端。
(3)发送文件内容
if (sendfile(sock, fd, NULL, s)
- sendfile(sock, fd, NULL, s):调用 sendfile 函数将文件内容直接从文件描述符 fd 发送到套接字 sock。sendfile 函数是一种高效的文件传输方式,它可以避免在用户空间和内核空间之间进行数据拷贝,从而提高传输效率。NULL 表示从文件的开头开始发送,s 表示要发送的文件大小。
- if (sendfile(sock, fd, NULL, s) (4)关闭文件描述符
close(fd); return 0;
- close(fd):调用 close 函数关闭之前打开的文件描述符,释放系统资源。
- return 0:如果函数执行成功,返回 0 表示正常结束。
(6)handle_request 函数:处理请求
static int handle_request(int sock, const char *method, const char *path, const char *query_string) { char line[SIZE]; int ret = 0; int content_len = -1; if (strcasecmp(method, "GET") == 0) { //清空消息报头 clear_header(sock); } else { //获取post方法的参数大小 do { ret = get_line(sock, line); if (strncasecmp(line, "content-length", 14) == 0) //post的消息体记录正文长度的字段 { content_len = atoi(line + 16); //求出正文的长度 } } while (ret != 1 && (strcmp(line, "\n") != 0)); } printf("method = %s\n", method); printf("query_string = %s\n", query_string); printf("content_len = %d\n", content_len); char req_buf[4096] = {0}; //如果是POST方法,那么肯定携带请求数据,那么需要把数据解析出来 if (strcasecmp(method, "POST") == 0) { int len = recv(sock, req_buf, content_len, 0); printf("len = %d\n", len); printf("req_buf = %s\n", req_buf); } //先发送状态码 char *msg = "HTTP/1.1 200 OK\r\n\r\n"; send(sock, msg, strlen(msg), 0); //请求交给自定义代码来处理,这是业务逻辑 parse_and_process(sock, query_string, req_buf); return 0; }
- 处理 GET 请求:如果请求方法是 GET,调用 clear_header() 函数清空请求报头。
- 处理 POST 请求:如果请求方法是 POST,循环读取请求报头,查找 Content-Length 字段,获取请求体的长度。
- 读取请求体:如果是 POST 请求,使用 recv() 函数根据 Content-Length 读取请求体数据。
- 发送状态码:使用 send() 函数发送 HTTP 状态行 HTTP/1.1 200 OK。
- 调用自定义处理函数:调用 parse_and_process() 函数处理请求,该函数可能包含具体的业务逻辑。
(7)handler_msg 函数:浏览器请求处理函数
handler_msg 函数的主要作用是处理来自浏览器的 HTTP 请求。它会解析请求的方法(如 GET 或 POST)、请求的 URL,检查请求的资源是否存在,然后根据请求的类型和资源情况做出相应的处理,比如返回静态文件或者调用自定义的处理函数。
int handler_msg(int sock) //浏览器请求处理函数 { char del_buf[SIZE] = {}; //通常recv()函数的最后一个参数为0,代表从缓冲区取走数据 //而当为MSG_PEEK时代表只是查看数据,而不取走数据。 recv(sock, del_buf, SIZE, MSG_PEEK); //MSG_PEEK表示只是把客户端发过来的数据看一下,recv接收完以后仍在缓存区 #if 1 //初学者强烈建议打开这个开关,看看tcp实际请求的协议格式 puts("---------------------------------------"); printf("recv:%s\n", del_buf); puts("---------------------------------------"); #endif //接下来method方法判断之前的代码,可以不用重点关注 //知道是处理字符串,把需要的信息过滤出来即可 char buf[SIZE]; int count = get_line(sock, buf); int ret = 0; char method[32]; //存放请求方法 char url[SIZE]; //存放URL char *query_string = NULL; int i = 0; int j = 0; int need_handle = 0; //判断是否需要处理 //获取请求方法和请求路径(liaojie) while (j
- 预读请求数据:使用 recv() 函数和 MSG_PEEK 标志预读客户端发送的请求数据,方便调试。
- 解析请求行:使用 get_line() 函数读取请求行,提取请求方法和请求路径。
- 判断请求方法:使用 strcasecmp() 函数判断请求方法是否为 GET 或 POST,如果不是,调用 echo_error() 函数返回 405 错误。
- 解析请求路径:遍历请求路径,查找 ? 字符,如果存在,将 query_string 指向 ? 后面的字符串,并在 ? 处截断 url 字符串。
- 处理带参数的 GET 请求和 POST 请求:如果是 POST 请求或者带参数的 GET 请求,将 need_handle 标志置为 1。
- 构造请求资源路径:将请求路径拼接上 wwwroot/ 前缀,如果请求的是目录,默认返回 index.html。
- 检查资源是否存在:使用 stat() 函数检查请求的资源是否存在,如果不存在,调用 echo_error() 函数返回 404 错误。
- 处理请求:根据 need_handle 标志,调用 handle_request() 函数处理带参数的 GET 请求和 POST 请求,或者调用 echo_www() 函数直接返回资源。
- 关闭套接字:最后关闭套接字。
代码精解析:
(1)查看客户端发送的请求数据
int handler_msg(int sock) //浏览器请求处理函数 { char del_buf[SIZE] = {}; //通常recv()函数的最后一个参数为0,代表从缓冲区取走数据 //而当为MSG_PEEK时代表只是查看数据,而不取走数据。 recv(sock, del_buf, SIZE, MSG_PEEK); //MSG_PEEK表示只是把客户端发过来的数据看一下,recv接收完以后仍在缓存区 #if 1 //初学者强烈建议打开这个开关,看看tcp实际请求的协议格式 puts("---------------------------------------"); printf("recv:%s\n", del_buf); puts("---------------------------------------"); #endif
- del_buf 是一个字符数组,用于临时存储从客户端接收的数据。
- recv 函数使用 MSG_PEEK 标志,这意味着只是查看客户端发送的数据,而不会从接收缓冲区中移除这些数据。
- #if 1 部分用于调试,会打印接收到的请求数据,方便查看 HTTP 请求的格式。
(2)解析请求方法和 URL
//接下来method方法判断之前的代码,可以不用重点关注 //知道是处理字符串,把需要的信息过滤出来即可 char buf[SIZE]; int count = get_line(sock, buf); int ret = 0; char method[32]; //存放请求方法(get post 方法) char url[SIZE]; //存放URL char *query_string = NULL; int i = 0; int j = 0; int need_handle = 0; //判断是否需要处理
- buf 数组用于存储从客户端接收的一行数据。
- get_line 函数从套接字中读取一行数据,并返回读取的字符数。
- ret 用于存储函数的返回值。
- method 数组用于存储请求方法(如 GET 或 POST)。
- url 数组用于存储请求的 URL。
- query_string 指针用于指向 URL 中的查询字符串(如果有的话)。
- need_handle 是一个标志,用于判断是否需要对请求进行额外处理(资源处理)。
(3)检查请求方法是否为 GET 或 POST
//获取请求方法和请求路径(liaojie) while (j
- 第一个 while 循环从 buf 中提取请求方法,直到遇到空格为止。
- method[i] = '\0' 用于在方法字符串末尾添加终止符。
- 第二个 while 循环用于过滤掉请求方法和 URL 之间的空格。
(4)提取 URL 中的查询字符串
//这里开始就开始判断发过来的请求是GET还是POST了 if (strcasecmp(method, "POST") && strcasecmp(method, "GET")) //strcasecmp可以忽略大小写的区别,其余和strcmp一样 { printf("method failed\n"); //如果都不是,那么提示一下 echo_error(sock, 405); //405是状态码,表示方法不对 ret = 5; goto end; //掉转到end,运行end下面的函数 } if (strcasecmp(method, "POST") == 0) { need_handle = 1; }
- strcasecmp 函数用于比较字符串,忽略大小写。
- 如果请求方法既不是 GET 也不是 POST,则调用 echo_error 函数返回 405 状态码,表示不支持该请求方法。
- 如果请求方法是 POST,则将 need_handle 标志设置为 1,表示需要对请求进行额外处理。
(5)构建请求资源的实际路径
i = 0; while (j
- 这个 while 循环从 buf 中提取请求的 URL。
- 如果遇到 ? 字符,则将 query_string 指针指向 ? 后面的查询字符串,并在 url 中添加终止符。
- 最后在 url 末尾添加终止符,并打印查询字符串。
(6)检查资源是否存在
//浏览器通过http://192.168.8.208:8080/?test=1234这种形式请求 //是携带参数的意思,那么就需要额外处理了 if (strcasecmp(method, "GET") == 0 && query_string != NULL) //前面的函数已经将指针query_string定位到?,如果问号不等于空,说明有参数,需要进一步处理 { need_handle = 1; }
- 如果请求方法是 GET 且存在查询字符串,则将 need_handle 标志设置为 1,表示需要对请求进行额外处理。
(7)根据请求类型和资源情况,决定是处理请求还是返回静态文件
//我们把请求资源的路径固定为wwwroot/下的资源,这个自己可以改 char path[SIZE]; sprintf(path, "wwwroot%s", url); //wwwroot/404.html,寻找wwwroot里的404.html printf("path = %s\n", path); //如果请求地址没有携带任何资源,那么默认返回index.html if (path[strlen(path) - 1] == '/') //判断浏览器请求的是不是目录 { strcat(path, "index.html"); //如果请求的是目录,则就把该目录下的首页返回回去,strcat是拼接函数 }
- path 数组用于存储请求资源的实际路径,将 wwwroot 和请求的 URL 拼接起来。
- 如果请求的路径以 / 结尾,则表示请求的是一个目录,将 index.html 拼接到路径后面。
(8)关闭套接字并返回处理结果
//如果请求的资源不存在,就要返回传说中的404页面了 struct stat st; if (stat(path, &st)
- stat 函数用于获取请求资源的相关属性,如文件大小、权限等。
- 如果 stat 函数返回值小于 0,表示文件不存在,调用 echo_error 函数返回 404 状态码。
c
//到这里基本就能确定是否需要自己的程序来处理后续请求了 printf("need progress handle:%d\n", need_handle); //如果是POST请求或者带参数的GET请求,就需要我们自己来处理了 //这些是业务逻辑,所以需要我们自己写代码来决定怎么处理 if (need_handle) { ret = handle_request(sock, method, path, query_string); } else { clear_header(sock); //如果是GET,而且没有参数,则直接返回资源 ret = echo_www(sock, path, st.st_size); } end: close(sock); return ret; }
- 根据 need_handle 标志的值,决定是调用 handle_request 函数处理请求,还是调用 echo_www 函数直接返回静态文件。
- clear_header 函数用于清除请求头。
- 最后关闭套接字并返回处理结果。
(8)get与post请求
代码逻辑图:
8.1GET 请求处理(结合原理图的交互逻辑)
8.1.1无参数 GET 请求
-
- 原理对应:网页通过 HTTP 向 WebServer 发送无参数的资源请求(如直接访问页面)。
- 代码逻辑:WebServer 检查请求资源是否存在(如 index.html 或其他静态文件)。若存在,通过 echo_www 函数直接返回资源内容(对应原理图中 WebServer 直接响应网页请求);若资源不存在,返回 404 页面,模拟原理图中 “请求资源异常” 的处理。
8.1.2有参数 GET 请求
-
- 原理对应:网页通过 HTTP 携带参数请求(如 http://xxx?key=value),希望 WebServer 处理特定业务逻辑。
- 代码逻辑:解析到 URL 有参数后,need_handle = 1,触发 handle_request 函数。
在 handle_request 中,清空报头后,通过 parse_and_process 处理参数(类似原理图中 WebServer 接收请求后,可能通过进程间通信将参数传递给 Modbus 采集控制程序,实现对 Modbus 设备的参数化控制或数据查询)。
8.2POST 请求处理(结合原理图的交互逻辑)
- 原理对应:网页通过 POST 提交数据(如表单提交),数据需经 WebServer 处理后,可能进一步作用于 Modbus 设备(如控制设备参数、上传设备配置等)。
- 代码逻辑:解析请求时,识别为 POST 后,need_handle = 1,进入 handle_request。
- 读取 Content-Length 获取请求体长度,再用 recv 读取 POST 数据。
- 最后通过 parse_and_process 处理数据(对应原理图中 WebServer 接收网页 POST 数据后,通过进程间通信将数据交给 Modbus 采集控制程序,由该程序通过 Modbus/TCP 与 Modbus 设备交互,完成数据采集或控制操作)。
8.3 GET 请求处理(结合代码与原理图)
8.3.1. 无参数 GET 请求
- 代码逻辑:
在 handler_msg 函数中,若解析到 GET 请求且无参数(query_string == NULL),则执行 echo_www 函数。例如:
if (!need_handle) { clear_header(sock); ret = echo_www(sock, path, st.st_size); }
- echo_www 会直接打开请求的文件(如 wwwroot 下的静态资源),通过 sendfile 发送给网页。若资源不存在(如 stat 检测失败),则调用 echo_error 返回 404 页面。
- 与原理图的关联:
- 对应原理图中 “网页 → WebServer” 的基础交互。网页请求静态资源(如 HTML、图片),WebServer 直接响应,无需复杂业务处理,如同原理图中 WebServer 作为资源提供者,直接返回网页所需内容。
8.3.2. 有参数 GET 请求
- 代码逻辑:
当 GET 请求带参数(如 http://xxx?key=value),query_string != NULL,need_handle = 1,触发 handle_request 函数:
if (need_handle) { ret = handle_request(sock, method, path, query_string); }
在 handle_request 中,清空报头后,通过 parse_and_process 处理参数。例如:
parse_and_process(sock, query_string, req_buf);
- 这部分可对接业务逻辑,如根据参数查询 Modbus 设备数据(需结合 Modbus采集控制程序)。
- 与原理图的关联:
- 体现原理图中 WebServer 的 “桥梁” 作用。网页通过带参数的 GET 请求,希望触发特定操作(如查询 Modbus 设备状态)。WebServer 解析参数后,可通过进程间通信,将参数传递给 Modbus采集控制程序,最终与 Modbus 设备交互,再将结果返回网页。
8.4POST 请求处理(结合代码与原理图)
- 代码逻辑:
在 handler_msg 中识别 POST 请求后,need_handle = 1,进入 handle_request。代码先解析 Content-Length 确定请求体长度,再读取数据:
if (strcasecmp(method, "POST") == 0) { int len = recv(sock, req_buf, content_len, 0); // 读取请求体数据 } parse_and_process(sock, query_string, req_buf);
- parse_and_process 负责处理 POST 数据,如解析表单字段、命令等。
- 与原理图的关联:
对应原理图中 “网页 → WebServer → Modbus 采集控制程序” 的复杂交互。网页通过 POST 提交数据(如设备控制指令),WebServer 接收后,通过进程间通信将数据交给 Modbus采集控制程序。该程序再通过 Modbus/TCP 与 Modbus 设备通信,执行控制操作,最终将结果反馈给网页,形成完整的数据处理闭环。
8.5代码与原理图的整体映射
8.5.1WebServer 的核心职能:
代码中的 handler_msg、handle_request 等函数,实现了原理图中 WebServer 的 HTTP 交互功能。无论是 GET 还是 POST,WebServer 都承担请求解析、初步处理的角色。
8.5.2业务延伸(Modbus 交互):
代码中的 parse_and_process 是扩展接口,对应原理图中 WebServer 与 Modbus采集控制程序 的进程间通信。通过这一接口,可将 HTTP 请求转化为对 Modbus 设备的操作(如采集数据、下发指令),最终实现网页对 Modbus 设备的间接控制,完整串联起原理图的交互链路。
三.main.c
#include"thttpd.h" #include #include #include #include #include #include #include static void* msg_request(void *arg) { //这里客户端描述符通过参数传进来了 int sock=(int)arg;//强转成int型 // 一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。 //但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。 pthread_detach(pthread_self()); //handler_msg作为所有的请求处理入口 return (void*)handler_msg(sock); } int main(int argc,char* argv[]) { //如果不传递端口,那么使用默认端口80 int port = 80; if(argc > 1) { port = atoi(argv[1]); } //初始化服务器 int lis_sock=init_server(port); while(1) { struct sockaddr_in peer; socklen_t len=sizeof(peer); int sock=accept(lis_sock,(struct sockaddr*)&peer,&len); if(sock perror("accept failed"); continue; } //每次接收一个链接后,会自动创建一个线程,这实际上就是线程服务器模型的应用 pthread_t tid; if(pthread_create(&tid,NULL,msg_request,(void*)sock)0) { perror("pthread_create failed"); close(sock); } } return 0; }
(1)msg_request 函数
static void* msg_request(void *arg) { //这里客户端描述符通过参数传进来了 int sock=(int)arg;//强转成int型 // 一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。 //但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。 pthread_detach(pthread_self()); //handler_msg作为所有的请求处理入口 return (void*)handler_msg(sock); }
- 功能:该函数是每个线程的入口函数,用于处理客户端的请求。
- 参数:arg 是一个 void* 类型的指针,实际传入的是客户端套接字描述符,通过强制类型转换将其转换为 int 类型。
- pthread_detach 函数:将当前线程设置为分离状态,这样线程结束后会自动释放其占用的资源,无需其他线程调用 pthread_join 来回收资源。
- handler_msg 函数:作为请求处理的入口,负责处理客户端的具体请求。函数返回值被强制转换为 void* 类型并返回。
(2)main 函数
int main(int argc,char* argv[]) { //如果不传递端口,那么使用默认端口80 int port = 80; if(argc > 1) { port = atoi(argv[1]); } //初始化服务器 int lis_sock=init_server(port); while(1) { struct sockaddr_in peer; socklen_t len=sizeof(peer); int sock=accept(lis_sock,(struct sockaddr*)&peer,&len); if(sock perror("accept failed"); continue; } //每次接收一个链接后,会自动创建一个线程,这实际上就是线程服务器模型的应用 pthread_t tid; if(pthread_create(&tid,NULL,msg_request,(void*)sock)0) { perror("pthread_create failed"); close(sock); } } return 0; }
- 端口设置:首先设置默认端口为 80,如果在命令行中传递了参数,则将第一个参数转换为整数作为端口号。
- 服务器初始化:调用 init_server 函数初始化服务器,该函数可能会创建监听套接字、绑定地址和端口,并开始监听客户端的连接请求。返回的 lis_sock 是监听套接字描述符。
- 主循环:
- 使用 while(1) 进入一个无限循环,不断等待客户端的连接请求。accept 函数:用于接受客户端的连接请求,返回一个新的套接字描述符 sock,该描述符用于与客户端进行通信。如果 accept 函数调用失败,会输出错误信息并继续循环。
- pthread_create 函数:每当接收到一个新的客户端连接,就创建一个新的线程来处理该客户端的请求。msg_request 函数作为线程的入口函数,将客户端套接字描述符作为参数传递给它。如果线程创建失败,会输出错误信息并关闭客户端套接字。
(3)功能概述
这段代码实现了一个多线程的 HTTP 服务器,通过创建多个线程来处理客户端的请求,提高了服务器的并发处理能力。每个线程独立处理一个客户端的请求,并且在结束后自动释放资源。服务器会不断监听客户端的连接请求,直到程序被手动终止。
四.Makefile
thttpd.out:main.c thttpd.c custom_handle.c gcc -o $@ $^ -lpthread clean: rm -rf *.out
五. custom_handle.c
/*********************************************************************************** Copy right: hqyj Tech. Author: jiaoyue Date: 2023.07.01 Description: http请求处理 ***********************************************************************************/ #include #include #include "custom_handle.h" #include #include #include #include #include #include #include #include #include #define KB 1024 #define HTML_SIZE (64 * KB) struct msgbuf { long mtype; // 第一个成员必须是long类型变量,表示消息的类型 int num1; int num2; }; // 普通的文本回复需要增加html头部 #define HTML_HEAD "Content-Type: text/html\r\n" \ "Connection: close\r\n" static int handle_login(int sock, const char *input) { char reply_buf[HTML_SIZE] = {0}; char *uname = strstr(input, "username="); uname += strlen("username="); char *p = strstr(input, "password"); *(p - 1) = '\0'; printf("username = %s\n", uname); char *passwd = p + strlen("password="); printf("passwd = %s\n", passwd); if (strcmp(uname, "admin") == 0 && strcmp(passwd, "admin") == 0) { sprintf(reply_buf, "localStorage.setItem('usr_user_name', '%s');", uname); strcat(reply_buf, "window.location.href = '/index.html';"); send(sock, reply_buf, strlen(reply_buf), 0); } else { printf("web login failed\n"); //"用户名或密码错误"提示,chrome浏览器直接输送utf-8字符流乱码,没有找到太好解决方案,先过渡 char out[128] = {0xd3, 0xc3, 0xbb, 0xa7, 0xc3, 0xfb, 0xbb, 0xf2, 0xc3, 0xdc, 0xc2, 0xeb, 0xb4, 0xed, 0xce, 0xf3}; sprintf(reply_buf, "alert('%s');", out); strcat(reply_buf, "window.location.href = '/login.html';"); send(sock, reply_buf, strlen(reply_buf), 0); } return 0; } static int handle_add(int sock, const char *input) { int number1, number2; // input必须是"data1=1data2=6"类似的格式,注意前端过来的字符串会有双引号 sscanf(input, "\"data1=%ddata2=%d\"", &number1, &number2); // 从input里面提取两个数值赋值给number1,number2m, printf("num1 = %d\n", number1); char reply_buf[HTML_SIZE] = {0}; printf("num = %d\n", number1 + number2); sprintf(reply_buf, "%d", number1 + number2); printf("resp = %s\n", reply_buf); send(sock, reply_buf, strlen(reply_buf), 0); return 0; } /** * @brief 处理自定义请求,在这里添加进程通信 * @param input * @return */ static int handle_recv(int sock) { key_t key; key = ftok("./main.c", 'a'); int shmid = shmget(key, 64, 0777); // 调用scanf.c文件中的key和shmid if (shmid
(1)handle_login 函数
- 功能:处理登录请求,验证用户名和密码。
- 步骤:从 input 中提取用户名和密码。
- 验证用户名和密码是否为 admin。
- 如果验证通过,使用 JavaScript 脚本将用户名存储到本地存储,并跳转到 index.html 页面。
- 如果验证失败,使用 JavaScript 脚本弹出提示框,显示 “用户名或密码错误”,并跳转到 login.html 页面。
(2)handle_add 函数
static int handle_add(int sock, const char *input) { int number1, number2; // input必须是"data1=1data2=6"类似的格式,注意前端过来的字符串会有双引号 sscanf(input, "\"data1=%ddata2=%d\"", &number1, &number2); printf("num1 = %d\n", number1); char reply_buf[HTML_SIZE] = {0}; printf("num = %d\n", number1 + number2); sprintf(reply_buf, "%d", number1 + number2); printf("resp = %s\n", reply_buf); send(sock, reply_buf, strlen(reply_buf), 0); return 0; }
- 功能:处理求和请求,从 input 中提取两个整数并求和,将结果发送给客户端。
- 步骤:使用 sscanf 函数从 input 中提取两个整数 number1 和 number2。
- 计算两个整数的和。
- 将结果存储在 reply_buf 中,并发送给客户端。
(3)handle_recv 函数
static int handle_recv(int sock) { key_t key; key = ftok("./main.c", 'a'); int shmid = shmget(key, 64, 0777); if (shmid
- 功能:从共享内存中读取数据,并将其发送给客户端。
- 步骤:
- 使用 ftok 函数生成共享内存的键值。
- 使用 shmget 函数获取共享内存的标识符。
- 使用 shmat 函数将共享内存映射到当前进程的地址空间。
- 如果共享内存中有数据,将其复制到 buf 中,并发送给客户端。
(4)handle_send 函数
static int handle_send(int sock, const char *input) { int number1, number2; // input必须是"data1=1data2=6"类似的格式,注意前端过来的字符串会有双引号 sscanf(input, "mange_set=%d %d", &number1, &number2); // 创建key值 key_t key; key = ftok("./main.c", 'a'); if (key
- 功能:从 input 中提取两个整数,将其封装成消息发送到消息队列中,并向客户端发送确认信息。
- 步骤:使用 sscanf 函数从 input 中提取两个整数 number1 和 number2。
- 使用 ftok 函数生成消息队列的键值。
- 使用 msgget 函数创建或获取消息队列的标识符。
- 将 number1 和 number2 封装成 msgbuf 结构体,并使用 msgsnd 函数将消息发送到消息队列中。
- 向客户端发送确认信息 send ok。
(5)parse_and_process 函数
int parse_and_process(int sock, const char *query_string, const char *input) { // query_string不一定能用的到 // 先处理登录操作 if (strstr(input, "username=") && strstr(input, "password=")) // 检测是否存在username { return handle_login(sock, input); } // 处理求和请求 else if (strstr(input, "data1=") && strstr(input, "data2=")) { return handle_add(sock, input); } else if (strstr(input, "mange_get")) { return handle_recv(sock); } else if (strstr(input, "mange_set=")) { return handle_send(sock, input); } else // 剩下的都是json请求,这个和协议有关了 { // 构建要回复的JSON数据 const char *json_response = "{\"message\": \"Hello, client!\"}"; // 发送HTTP响应给客户端 send(sock, json_response, strlen(json_response), 0); } return 0; }
- 功能:根据 input 的内容,调用不同的处理函数来处理请求。
- 步骤:检查 input 中是否包含 username= 和 password=,如果是,则调用 handle_login 函数处理登录请求。
- 检查 input 中是否包含 data1= 和 data2=,如果是,则调用 handle_add 函数处理求和请求。
- 检查 input 中是否包含 mange_get,如果是,则调用 handle_recv 函数从共享内存中读取数据。
- 检查 input 中是否包含 mange_set=,如果是,则调用 handle_send 函数将数据发送到消息队列中。
- 如果以上条件都不满足,则发送一个 JSON 响应给客户端。
六.example.c
整体功能概述
此代码构建了一个名为 “工业信息采集系统” 的网页,其主要功能为采集工业相关的数据(像光照数据、加速度数据),同时可对蜂鸣器和 LED 灯进行控制。
(1)源码示例
基于WebServer 的工业数据采集项目 /* 全局样式 */ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f4f9; color: #333; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: center; min-height: 100vh; } .container { background-color: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); max-width: 600px; width: 100%; } h1 { color: #007bff; text-align: center; margin-bottom: 20px; font-size: 2rem; } h2 { color: #007bff; margin-top: 20px; font-size: 1.5rem; } input[type="text"] { width: calc(100% - 120px); padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 1rem; margin-right: 10px; } input[type="button"] { padding: 10px 20px; background-color: #007bff; color: #fff; border: none; border-radius: 5px; cursor: pointer; font-size: 1rem; transition: background-color 0.3s ease; } input[type="button"]:hover { background-color: #0056b3; } input[type="radio"] { margin-right: 10px; } label { margin-right: 20px; font-size: 1rem; } .blink { animation: blink 1s infinite; } @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } function getinfof() { // 获取光照数据输入框 var input = document.querySelector('input[name="usrname"][value=""]'); if (input) { var xhr = new XMLHttpRequest(); var url = "your_server_url";//请替换为实际的服务器URL xhr.open("post", url, true); xhr.send("mange_get"); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { var t = xhr.responseText; var data = t.split('#'); input.value = data[0]; } } } } function getinfos() { //获取加速度数据输入框 var inputs = document.querySelectorAll('input[name="usrname"]'); if (inputs.length > 1) { var input = inputs[1]; var xhr = new XMLHttpRequest(); var url = "your_server_url";//请替换为实际的服务器URL xhr.open("post", url, true); xhr.send("mange_get"); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { var t = xhr.responseText; var data = t.split('#'); input.value = data[1] + data[2] + data[3]; } } } } function fun1() { var xhr = new XMLHttpRequest(); var url = "your_server_url";//请替换为实际的服务器URL xhr.open("post", url, true); xhr.send("mange_set=1 1"); // 蜂鸣器开 } function fun2() { var xhr = new XMLHttpRequest(); var url = "your_server_url";//请替换为实际的服务器URL xhr.open("post", url, true); xhr.send("mange_set=1 0"); // 蜂鸣器关 } function fun3() { var xhr = new XMLHttpRequest(); var url = "your_server_url";//请替换为实际的服务器URL xhr.open("post", url, true); xhr.send("mange_set=0 1"); // LED开 } function fun4() { var xhr = new XMLHttpRequest(); var url = "your_server_url";//请替换为实际的服务器URL xhr.open("post", url, true); xhr.send("mange_set=0 0"); // LED关 }
工业信息采集系统
光照数据:
加速度数据:
蜂鸣器:
开 关LED灯:
开 关(2)拆解分析
- HTML 部分
整体概述
这段 HTML 代码构建了一个名为 “工业信息采集系统” 的网页,该网页提供了数据采集(光照数据、加速度数据)以及设备控制(蜂鸣器、LED 灯)的功能界面。
工业信息采集系统
工业信息采集系统
光照数据:
加速度数据:
蜂鸣器:
开 关LED灯:
开 关解析:
- 标题:工业信息采集系统 设置了网页标题。
- 主体内容:
- 借助 对内容进行包裹,起到布局的作用。
- 标题
运用了 blink 类,可实现闪烁效果。
- 包含两个输入框与对应的按钮,用于获取光照数据和加速度数据。
- 设有两组单选框,分别用于控制蜂鸣器和 LED 灯的开关。
- 重难点语句:
(1)整体容器
是一个通用的容器元素(盒子), 表示该元素应用了名为 container 的类样式,通常用于对页面内容进行布局和样式设置。
(2)页面标题
html
工业信息采集系统
是一级标题标签,用于显示页面的主要标题。 表明该标题应用了名为 blink 的类样式,可能会实现闪烁效果。
(3)光照数据部分
html
光照数据:
:二级标题标签,用于显示光照数据的标题。
- :创建一个文本输入框,name="usrname" 为该输入框指定名称,value="" 表示输入框初始为空。
- :创建一个按钮,按钮上显示 “获取数据”。name="flash" 为按钮指定名称,οnclick="getinfof()" 表示当用户点击该按钮时,会调用名为 getinfof() 的 JavaScript 函数。
(4)加速度数据部分
html
加速度数据:
这部分与光照数据部分类似,只是按钮点击时调用的 JavaScript 函数为 getinfos()。
(5)蜂鸣器控制部分
html
蜂鸣器:
开 关:二级标题标签,用于显示蜂鸣器控制的标题。
- :创建一个单选框,name="蜂鸣器" 表示这些单选框属于同一组,用户只能选择其中一个。id="on" 为该单选框指定唯一标识符,οnclick="fun1()" 表示当用户选择该单选框时,会调用名为 fun1() 的 JavaScript 函数。
- 标签用于为表单元素提供文本描述,点击标签内的文本也能选中对应的单选框。
(6)LED 控制部分
LED灯:
开 关这部分与蜂鸣器控制部分类似,用于控制 LED 灯的开关,点击单选框时分别调用 fun3() 和 fun4() 函数。
2. CSS 部分
/* 全局样式 */ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f4f9; color: #333; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: center; min-height: 100vh; } .container { background-color: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); max-width: 600px; width: 100%; } h1 { color: #007bff; text-align: center; margin-bottom: 20px; font-size: 2rem; } h2 { color: #007bff; margin-top: 20px; font-size: 1.5rem; } input[type="text"] { width: calc(100% - 120px); padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 1rem; margin-right: 10px; } input[type="button"] { padding: 10px 20px; background-color: #007bff; color: #fff; border: none; border-radius: 5px; cursor: pointer; font-size: 1rem; transition: background-color 0.3s ease; } input[type="button"]:hover { background-color: #0056b3; } input[type="radio"] { margin-right: 10px; } label { margin-right: 20px; font-size: 1rem; } .blink { animation: blink 1s infinite; } @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
- 全局样式:
- 对 body 标签的字体、背景颜色、内边距、布局等样式进行了设置。
- 利用 display: flex 让内容在页面中垂直和水平居中。
- 容器样式:
- .container 类定义了内容容器的样式,包含背景颜色、内边距、圆角和阴影等。
- 输入框与按钮样式:
- 对输入框和按钮的样式进行了设置,并且为按钮添加了悬停效果。
- 闪烁动画:
- blink 类和 @keyframes 规则实现了标题的闪烁效果。
- 全局样式:对 body 标签的字体、背景颜色、内边距、布局等样式进行了设置。
- 利用 display: flex 让内容在页面中垂直和水平居中。
- 容器样式:.container 类定义了内容容器的样式,包含背景颜色、内边距、圆角和阴影等。
- 输入框与按钮样式:对输入框和按钮的样式进行了设置,并且为按钮添加了悬停效果。
- 闪烁动画:blink 类和 @keyframes 规则实现了标题的闪烁效果。
(1)全局样式(body)
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f4f9; color: #333; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
- 字体与颜色:
- font-family: 设置无衬线字体栈,确保不同设备上的可读性。
- background-color: 背景色为浅灰色(#f4f4f9),与容器的白色形成对比。
- color: 文本颜色为深灰色(#333),提升可读性。
- 布局与间距:
- margin: 0: 移除 body 的默认外边距,避免页面边缘留白。
- padding: 20px: 添加内边距,防止内容紧贴浏览器边缘。
- display: flex: 使用 Flexbox 布局,使子元素在容器内灵活排列。
- justify-content: center 和 align-items: center: 使内容在水平和垂直方向上居中。
- min-height: 100vh: 确保页面内容至少占据视口高度(vh = 视口高度的 1%),即使内容较少也能垂直居中。
(2)容器样式(.container)
.container { background-color: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); max-width: 600px; width: 100%; }
- 外观设计:
- background-color: #fff: 容器背景为白色,与页面背景形成对比。
- padding: 30px: 容器内部内容与边框的间距,增强留白效果。
- border-radius: 10px: 圆角设计,使界面更柔和。
- box-shadow: 添加柔和的阴影(0 4px 10px 模糊半径 10px,透明度 0.1),提升层次感。
- 响应式布局:
- max-width: 600px: 限制容器最大宽度,避免在大屏幕上内容过于分散。
- width: 100%: 确保容器在小屏幕下占满整个宽度,实现响应式效果。
(3)标题样式(h1、h2)
h1 { color: #007bff; text-align: center; margin-bottom: 20px; font-size: 2rem; } h2 { color: #007bff; margin-top: 20px; font-size: 1.5rem; }
- 视觉统一:
- color: #007bff: 标题颜色为蓝色(品牌色或强调色),突出重点。
- text-align: center(仅 h1): 使主标题居中,与容器对齐。
- 层级与间距:
- font-size: h1 为 2rem(32px),h2 为 1.5rem(24px),形成视觉层级。
- margin-bottom: 20px(h1)和 margin-top: 20px(h2): 控制标题与相邻元素的间距。
(4)输入框与按钮样式
4.1文本输入框(input [type="text"])
css
input[type="text"] { width: calc(100% - 120px); padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 1rem; margin-right: 10px; }
- 布局与尺寸:
- width: calc(100% - 120px): 输入框宽度占容器宽度减去 120px(为按钮留出空间)。
- padding: 10px: 输入框内部文本与边框的间距。
- 外观与交互:
- border: 1px solid #ddd: 浅灰色边框,区分输入区域。
- border-radius: 5px: 轻微圆角,保持界面统一。
- margin-right: 10px: 与右侧按钮的间距。
4.2按钮(input [type="button"])
css
input[type="button"] { padding: 10px 20px; background-color: #007bff; color: #fff; border: none; border-radius: 5px; cursor: pointer; font-size: 1rem; transition: background-color 0.3s ease; } input[type="button"]:hover { background-color: #0056b3; }
- 基础样式:
- padding: 10px 20px: 按钮的内边距,控制大小。
- background-color: #007bff: 蓝色背景,与标题颜色呼应。
- color: #fff: 白色文本,高对比度。
- border: none: 移除默认边框,使按钮更简洁。
- cursor: pointer: 鼠标悬停时显示手型,提示可点击。
- 交互效果:
- transition: background-color 0.3s ease: 背景色渐变过渡,增强交互反馈。
- :hover 伪类:悬停时颜色加深(#0056b3),提供视觉反馈。
(5)单选框与标签样式
css
input[type="radio"] { margin-right: 10px; } label { margin-right: 20px; font-size: 1rem; }
- 单选框(input [type="radio"]):margin-right: 10px: 单选框与右侧标签的间距。
- 标签(label):
- margin-right: 20px: 标签之间的间距,区分不同选项。
- font-size: 1rem: 标签文本大小,与其他元素保持一致。
(6)闪烁动画(blink 类)
css
.blink { animation: blink 1s infinite; } @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
- 动画定义:
- @keyframes blink: 定义名为 blink 的动画。
- 0% 和 100% 关键帧:透明度为 1(完全不透明)。
- 50% 关键帧:透明度为 0.5(半透明)。
- 应用动画:
- .blink 类:通过 animation 属性应用动画,1s 为周期,infinite 表示无限循环。
- 效果:使标题(如
)每隔 1 秒闪烁一次,突出显示。
整体概述
这段 CSS 通过合理的布局、颜色搭配和交互细节,实现了以下目标:
- 视觉统一:使用蓝色作为主色调,搭配白色背景和浅灰色边框,简洁专业。
- 响应式设计:容器最大宽度限制和 Flexbox 布局确保页面适配不同屏幕尺寸。
- 用户体验:按钮悬停效果、闪烁动画和适当的间距增强了交互反馈。
- 结构清晰:通过类名和选择器组织代码,便于维护和扩展。
3. JavaScript 部分
javascript
function getinfof() { var v = document.getElementsByName("usrname"); console.log(v[0].value); var xhr = new XMLHttpRequest(); var url = ""; xhr.open("post", url, true); xhr.send("mange_get"); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { var t = xhr.responseText; var data = t.split('#'); v[0].value = data[0]; } } } function getinfos() { var v = document.getElementsByName("usrname"); console.log(v[1].value); var xhr = new XMLHttpRequest(); var url = ""; xhr.open("post", url, true); xhr.send("mange_get"); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { var t = xhr.responseText; var data = t.split('#'); v[1].value = data[1] + data[2] + data[3]; } } } function fun1() { var xhr = new XMLHttpRequest(); var url = ""; xhr.open("post", url, true); xhr.send("mange_set=1 1"); // 蜂鸣器开 } function fun2() { var xhr = new XMLHttpRequest(); var url = ""; xhr.open("post", url, true); xhr.send("mange_set=1 0"); // 蜂鸣器关 } function fun3() { var xhr = new XMLHttpRequest(); var url = ""; xhr.open("post", url, true); xhr.send("mange_set=0 1"); // LED开 } function fun4() { var xhr = new XMLHttpRequest(); var url = ""; xhr.open("post", url, true); xhr.send("mange_set=0 0"); // LED关 }
- 数据获取函数:getinfof() 和 getinfos() 函数借助 XMLHttpRequest 对象向服务器发送 POST 请求,以获取光照数据和加速度数据。当请求成功时,将服务器返回的数据解析并显示在输入框中。
- 设备控制函数:fun1()、fun2()、fun3() 和 fun4() 函数同样使用 XMLHttpRequest 对象向服务器发送 POST 请求,用于控制蜂鸣器和 LED 灯的开关。
整体功能概述
这段 JavaScript 代码通过 XMLHttpRequest(XHR)实现了以下功能:
- 数据获取:从服务器获取光照数据和加速度数据,并显示在页面上。
- 设备控制:通过发送指令控制蜂鸣器和 LED 灯的开关。
(1)数据获取函数(getinfof() 和 getinfos())
javascript
function getinfof() { // 获取名为 "usrname" 的第一个输入框 var v = document.getElementsByName("usrname"); console.log(v[0].value); // 打印当前值(调试用) // 创建 XHR 对象 var xhr = new XMLHttpRequest(); var url = ""; // 需替换为实际 API 地址 xhr.open("post", url, true); // POST 请求,异步模式 // 发送请求参数 xhr.send("mange_get"); // 监听请求状态变化 xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { // 解析响应数据 var t = xhr.responseText; var data = t.split('#'); // 假设数据以 # 分隔 v[0].value = data[0]; // 更新第一个输入框的值 } }; } // getinfos() 逻辑类似,但操作第二个输入框(v[1])
- DOM 操作:
- getElementsByName("usrname"):获取页面中所有 name="usrname" 的元素(两个输入框)。
- 通过索引 v[0] 和 v[1] 分别操作光照和加速度数据的输入框。
- XHR 请求:
- 请求方法:POST(用于发送数据到服务器)。
- 请求参数:mange_get(服务器根据此参数识别为 “获取数据” 请求)。
- 响应处理:
当 readyState === 4(请求完成)且 status === 200(成功)时,解析响应数据。
假设响应数据格式为 data0#data1#data2#data3,用 split('#') 分割后更新输入框。
(2)设备控制函数(fun1() 到 fun4())
javascript
function fun1() { var xhr = new XMLHttpRequest(); var url = ""; xhr.open("post", url, true); xhr.send("mange_set=1 1"); // 蜂鸣器开 } // fun2() 到 fun4() 类似,仅参数不同: // fun2(): "mange_set=1 0"(蜂鸣器关) // fun3(): "mange_set=0 1"(LED 开) // fun4(): "mange_set=0 0"(LED 关)
- 指令格式:
- mange_set=设备类型 状态:设备类型:1 代表蜂鸣器,0 代表 LED。
- 状态:1 代表开,0 代表关。
- mange_set=设备类型 状态:设备类型:1 代表蜂鸣器,0 代表 LED。
- XHR 请求:
- 通过 POST 发送指令到服务器,服务器根据 mange_set 参数执行相应操作。
七.代码原理图与项目模块联系(录屏重点讲解部分)
一、WebServer(thttpd 代码核心)与网页的 HTTP 交互实现
- 代码功能:在 parse_and_process 函数中,通过检测请求内容(如 strstr(input, "username=") 识别登录请求、strstr(input, "data1=") 识别数据操作请求),调用 handle_login(处理登录逻辑,验证用户名密码并返回跳转脚本)、handle_add(解析数据参数并执行求和计算)等函数,最终通过 send 向网页返回响应(包括登录结果、数据处理结果或默认 JSON 数据)。
- 与原理图关联:
完全对应原理图中 “网页 → WebServer” 的 HTTP 通信链路。WebServer 作为 HTTP 服务核心,接收网页的登录、数据操作等请求,执行解析与业务处理后,向网页反馈结果,实现网页与服务器的双向交互,体现 WebServer 在原理图中 “HTTP 通信枢纽” 的定位。
二、WebServer 与 Modbus 采集控制程序的进程间通信实现
- 代码功能:
- 共享内存:handle_recv 函数通过 shmget、shmat 操作共享内存,读取 Modbus 采集控制程序存入的设备数据(如传感器数值),并通过 WebServer 返回给网页。
- 消息队列:handle_send 函数利用 msgget、msgsnd 创建消息队列,将网页的控制命令(如 mange_set 携带的参数)封装成消息发送给 Modbus 采集控制程序。
- 与原理图关联:
- 精准对应原理图中 “WebServer ↔ Modbus 采集控制程序” 的进程间通信。WebServer 通过共享内存获取 Modbus 设备采集的数据(如 read_data 线程采集的光线、加速度数据),供网页展示;通过消息队列接收网页命令(如控制 LED、蜂鸣器的指令),传递给 Modbus 采集控制程序,驱动 write_command 线程执行 Modbus 设备控制操作,实现原理图中 “通过进程间通信协调两者功能” 的设计。
三、Modbus 采集控制程序与 Modbus 设备的交互实现
- 代码功能:read_data 线程通过 modbus_read_registers 读取 Modbus 设备寄存器数据(光线、加速度 XYZ),并存入共享内存。
- write_command 线程通过消息队列接收命令,利用 modbus_write_bit 向 Modbus 设备写入控制指令(如开灯、关蜂鸣器)。
- 与原理图关联:
- 直接对应原理图中 “Modbus 采集控制程序 → Modbus 设备” 的 Modbus/TCP 通信链路。采集程序作为中间执行者,一方面采集 Modbus 设备数据,通过共享内存供 WebServer 转发给网页;另一方面接收 WebServer 通过消息队列传递的命令,控制 Modbus 设备,完成原理图中 “设备数据采集” 和 “设备控制” 的核心功能。
四、整体代码对原理图的完整复现
- 交互链路复现:
- 代码通过 “WebServer 处理 HTTP 请求 → 进程间通信(共享内存 / 消息队列)→ Modbus 采集控制程序操作 Modbus 设备” 的流程,完整复现原理图中 “网页 —WebServer—Modbus 采集控制程序 —Modbus 设备” 的交互链路。
- 模块功能映射:
WebServer 代码模块实现原理图中 WebServer 的 HTTP 通信与请求处理功能。
-
- 共享内存 / 消息队列代码实现原理图中进程间通信功能。
- Modbus 采集控制程序代码(read_data、write_command 线程)实现原理图中 Modbus 设备的数据采集与控制功能。
三者协同,从代码层面完整落地原理图的系统设计,实现网页对 Modbus 设备的间接控制与数据获取。
- 项目测试流程解析 (主要是应对录屏/答辩)
(1)项目文档层次
(2)进入modbus thttpd 目录
(3)make
(4)windows IP
(5)编译 modbus 线程
(6)编译 thttpd
(7)登录 项目界面(虚拟网址)
(8)打开slave 进行编辑
(9)传递信息成功
(10)接着设置slave
(11)传递灯光信号成功
(12)项目最终成功获取灯罩数据与加速度数据
(13)项目最终成功结果
- 项目扩展方向思路
(1)数据库模块扩展(SQLite3 实现)
1. 数据库初始化代码(新增 db_util.c)
#include #include #include // 定义数据库文件路径 #define DB_PATH "sensor_data.db" // 创建传感器数据表 int init_sensor_table() { sqlite3 *db; char *err_msg = 0; int rc = sqlite3_open(DB_PATH, &db); if (rc != SQLITE_OK) { fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return -1; } const char *sql = "CREATE TABLE IF NOT EXISTS sensor_data (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "timestamp DATETIME DEFAULT CURRENT_TIMESTAMP," "light INT NOT NULL," "accel_x INT NOT NULL," "accel_y INT NOT NULL," "accel_z INT NOT NULL);"; rc = sqlite3_exec(db, sql, 0, 0, &err_msg); if (rc != SQLITE_OK) { fprintf(stderr, "SQL error: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } sqlite3_close(db); return 0; } // 插入传感器数据 int insert_sensor_data(int light, int x, int y, int z) { sqlite3 *db; sqlite3_stmt *stmt; const char *sql = "INSERT INTO sensor_data (light, accel_x, accel_y, accel_z) " "VALUES (?, ?, ?, ?);"; if (sqlite3_open(DB_PATH, &db) != SQLITE_OK) { fprintf(stderr, "Database open error\n"); return -1; } if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { fprintf(stderr, "Prepare statement failed\n"); sqlite3_close(db); return -1; } sqlite3_bind_int(stmt, 1, light); sqlite3_bind_int(stmt, 2, x); sqlite3_bind_int(stmt, 3, y); sqlite3_bind_int(stmt, 4, z); if (sqlite3_step(stmt) != SQLITE_DONE) { fprintf(stderr, "Insert failed\n"); sqlite3_finalize(stmt); sqlite3_close(db); return -1; } sqlite3_finalize(stmt); sqlite3_close(db); return 0; }
2. 修改 read_data 线程(modbus.c)
void *read_data(void *arg) { // ...原有共享内存代码... while (1) { // 原有数据读取逻辑 int rc = modbus_read_registers(ctx, 0, 4, reg); if (rc != 4) { /* 错误处理 */ } // 新增数据库插入 if (insert_sensor_data(reg[0], reg[1], reg[2], reg[3]) != 0) { fprintf(stderr, "Failed to save sensor data to DB\n"); } // 原有共享内存写入逻辑 sprintf(p, "%d#%d#%d#%d\n", reg[0], reg[1], reg[2], reg[3]); } return NULL; }
(2)WebServer 新增历史查询接口
1. 新增 handle_history 函数(custom_handle.c)
// 查询历史数据 static int handle_history(int sock, const char *start, const char *end) { sqlite3 *db; sqlite3_stmt *stmt; char sql[512]; char json[4096] = "{ \"data\": ["; snprintf(sql, sizeof(sql), "SELECT * FROM sensor_data WHERE timestamp BETWEEN '%s' AND '%s'", start, end); if (sqlite3_open(DB_PATH, &db) != SQLITE_OK) { send_error_response(sock, 500, "Database error"); return -1; } if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { send_error_response(sock, 500, "Query failed"); sqlite3_close(db); return -1; } int first = 1; while (sqlite3_step(stmt) == SQLITE_ROW) { if (!first) strcat(json, ","); first = 0; snprintf(json + strlen(json), sizeof(json) - strlen(json), "{\"time\":\"%s\",\"light\":%d,\"x\":%d,\"y\":%d,\"z\":%d}", sqlite3_column_text(stmt, 1), sqlite3_column_int(stmt, 2), sqlite3_column_int(stmt, 3), sqlite3_column_int(stmt, 4), sqlite3_column_int(stmt, 5)); } strcat(json, "]}"); // 发送 JSON 响应 send_json_response(sock, json); sqlite3_finalize(stmt); sqlite3_close(db); return 0; }
2. 修改 parse_and_process 函数
int parse_and_process(int sock, const char *query, const char *input) { // 原有逻辑... // 新增历史查询处理 else if (strstr(input, "action=history")) { char start[64], end[64]; sscanf(input, "start=%[^&]&end=%s", start, end); return handle_history(sock, start, end); } // 其他处理... }
(3)前端页面扩展(example.html)
1. 新增历史查询界面
历史数据查询
查询- 新增 JavaScript 查询逻辑
function queryHistory() { const start = document.getElementById('startTime').value; const end = document.getElementById('endTime').value; const xhr = new XMLHttpRequest(); xhr.open('POST', '/api', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(`action=history&start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { const data = JSON.parse(xhr.responseText).data; let html = '
'; document.getElementById('historyResult').innerHTML = html; } }; } '; data.forEach(item => { html += `时间 光照 X Y Z `; }); html += '${item.time} ${item.light} ${item.x} ${item.y} ${item.z} 3. 新增 CSS 样式
.history-section { margin-top: 30px; padding: 20px; border-top: 1px solid #ddd; } .history-result { margin-top: 15px; max-height: 400px; overflow-y: auto; } table { width: 100%; border-collapse: collapse; } th, td { padding: 8px; border: 1px solid #ddd; text-align: center; } th { background-color: #f8f9fa; }
(4)编译与部署调整
1.修改 Makefile
添加 SQLite3 依赖:
httpd.out: main.c thttpd.c custom_handle.c db_util.c gcc -o $@ $^ -lpthread -lsqlite3
2.初始化数据库
在 main.c 的初始化阶段调用:
int main() { init_sensor_table(); // 新增 // 原有代码... }
(5)扩展方向优化建议
- 数据分页查询
在 handle_history 中增加 LIMIT 和 OFFSET 参数,支持大数据量分页。
- 数据可视化
- 用户权限在前端使用 ECharts 绘制光照和加速度的历史趋势图。
- 异常处理 在数据库中增加用户表,修改登录逻辑实现多用户权限控制。
在数据库操作中添加事务机制,确保数据一致性。
-
- 代码功能:read_data 线程通过 modbus_read_registers 读取 Modbus 设备寄存器数据(光线、加速度 XYZ),并存入共享内存。
- 精准对应原理图中 “WebServer ↔ Modbus 采集控制程序” 的进程间通信。WebServer 通过共享内存获取 Modbus 设备采集的数据(如 read_data 线程采集的光线、加速度数据),供网页展示;通过消息队列接收网页命令(如控制 LED、蜂鸣器的指令),传递给 Modbus 采集控制程序,驱动 write_command 线程执行 Modbus 设备控制操作,实现原理图中 “通过进程间通信协调两者功能” 的设计。
- 代码功能:
- 动画定义:
- 基础样式:
- 布局与尺寸:
- 视觉统一:
- 外观设计:
- 字体与颜色:
- 全局样式:对 body 标签的字体、背景颜色、内边距、布局等样式进行了设置。
- blink 类和 @keyframes 规则实现了标题的闪烁效果。
- 全局样式:
- 代码逻辑:
- 代码逻辑:
- 代码逻辑:
-
- 如果请求方法是 GET 且存在查询字符串,则将 need_handle 标志设置为 1,表示需要对请求进行额外处理。
- 根据错误码处理错误:使用 switch 语句根据错误码进行相应的处理,目前仅处理 404 错误,调用 show_404() 函数返回 404 错误页面。
- 循环读取报头:使用 get_line() 函数循环读取请求报头,直到读取到空行(即 \n)或者读取的字符数为 1。
- 作为全局变量,在多个函数(线程)中共享,避免重复创建 Modbus 连接。