基于WebServer的工业数据采集项目

目录

涉及内容

Modbus协议

Modbus起源

1. 起源

2. 分类

3. 优势

4. 应用场景

5. Modbus TCP协议特点

Modbus TCP通信协议

报文头 

寄存器 

功能码

Modbus库

三方库的使用

库的使用

函数接口

创建modbus实例

设置从机ID

 和从机(slave)建立连接

释放Modbus实例 

关闭套接字

读取线圈状态

读取离散输入寄存器状态

读取保持寄存器的值 

读取输入寄存器的值 

写线圈寄存器 (单个和多个)

 写保持寄存器(单个和多个)

编程流程 

工具使用

modbusSlave/Poll

网络调试助手

 Wirshark使用

WebServer服务器

Web Server的分类

Lighttpd服务器

服务器安装配置(在虚拟机内)

配置文件修改

修改配置文件

运行测试

Postman使用 

作用

注意事项

测试使用

html

开发环境

 安装库 open in browser

 基于WebServer的工业数据采集项目

项目框架

CGI 

CGGI简介

CGI特点

常见的环境变量 

CGI工作原理

源码分析

源码使用

http&html

http协议

http简介

http特点

http协议格式

html语法 

html简介

html标签

标签格式

标签分类

常用标签

代码如下

ModbusTCP端服务程序

CGI代码

makefile

main.c

req_handle.h

req_handle.c

log_console.h

log_console.c

custom_handle.h

custom_handle.c

html网页部分代码


涉及内容

Modbus协议、http协议、html协议

Webserver:网页服务器(postman)

工具:wireshark,Modbus Slave,Modbus Poll,Postman

Modbus协议

Modbus起源

1. 起源

Modbus由Modicon公司于1979年开发,是一种工业现场总线协议标准。Modbus通信协议具有多个变种,其中有支持串口,以太网多个版本,其中最著名的是Modbus RTU、Modbus ASCII和Modbus TCP三种。其中Modbus TCP是在施耐德收购Modicon后1997年发布的。

2. 分类

  1. Modbus RTU:运行在串口上的协议,采用二进制的存储形式以及紧凑的数据结构,通信效率较高,应用比较广泛。
  2. Modbus ASCII:运行在串口上的协议,采用ASCII进行传输,并且用特殊字符作为字节开始和结束的标志,传输效率远远低于Modbus RTU协议,一般只有在通信数据量较小的情况下才会考虑用Modbus ASCII通信。
  3. Modbus TCP:运行在以太网上的协议。

3. 优势

免费、简单、容易使用

4. 应用场景

Modbus协议是现在国内工业领域应用最多的协议,不只PLC设备,各种终端设备都会用到Modbus协议,比如水控机、水表、电表、工业秤、各种采集数据等。

5. Modbus TCP协议特点

  1. 采用主从问答的方式进行通信(只能主机主动发送消息,从机响应)
  2. Modbus TCP属于应用层协议
  3. Modbus TCP默认端口号是502        

Modbus TCP通信协议

Modbus TCP协议包含三部分:报文头、功能码、数据

基于WebServer的工业数据采集项目_第1张图片

Modbus TCP协议最大数据帧长度为260字节。

报文头 

Modbus TCP协议的报文头共有7字节

基于WebServer的工业数据采集项目_第2张图片

寄存器 

Modbus TCP协议包含四种寄存器,分别是线圈、离散量输入、保持寄存器、输入寄存器。

1. 离散量输入和线圈是位寄存器(每个寄存器占一字节),工业上主要用于控制IO设备。

        线圈寄存器:可以类比为开关量,每一个bit都对应一个信号的开关状态。所以一个byte就可以同时控制8路的信号。比如控制外部8路IO的高低,线圈寄存器既可以支持读操作也可以支持写操作,写操作也分为写的单个线圈寄存器和写多个线圈寄存器。

                线圈寄存器的功能码:0x01(读线圈)        0x05(写单个线圈)        0x0f(写多个线圈)

        离散输入寄存器:相当于线圈寄存器的只读模式,每个bit表示一个开关量,离散输入寄存器的开关量只能读取输入的开关信号,不能进行写操作。比如:读取外部按键是按下还是松开。

                离散寄存器的功能码:0x02(读线圈寄存器)

2. 输入寄存器和保持寄存器是字寄存器(每个寄存器数据占2个字节),工业上主要用于存储工业设备的值。

        保持寄存器:该寄存器的单位是byte,并且每个寄存器都占2字节,能够放具体的数据值,保持寄存器支持可读可写操作,并且写操作分为写单个寄存器和写多个寄存器。例如:设置时间(年月日),既可以写入也可以读出。

                保持寄存器的功能码:0x03(读保持寄存器)        0x06(写单个寄存器)        0x10(写多个寄存器)

        输入寄存器:相当于保持寄存器的只读模式,一个寄存器也是占两个字节。例如:通过读取输入寄存器获取的AD采集值。

                输入寄存器的功能码:0x04(读保持寄存器)

功能码

根据四种不同功能的寄存器设置了8中功能码。

基于WebServer的工业数据采集项目_第3张图片

 练习:

读传感器数据,读一个寄存器数据,写出主从数据收发协议

主机给从机发送:

| 事务处理标识符 | 协议类型 | 字节长度 | 从机ID | 功能码 | 起始地址 | 寄存器个数 |

例:   0x0000            0000         0006          01         03         0000       0000 0001

从寄给主机回复:

| 事务处理标识符 | 协议类型 | 字节长度 | 从机ID | 功能码 | 数据长度 | 数据 |

例:   0x0000            0000         0005          01         03          02       0002 0102

练习:

写出控制IO设备开关的协议数据,操作一个线圈

主机给从机发送:

| --------------MBAP报文头-------------- | 功能码 | 起始地址 | 断通标志 |

例:0x0000 0x0000 0x0006 0x11        0x05     0x000b       0xff00

从机给主机回复:

| --------------MBAP报文头-------------- | 功能码 | 起始地址 | 断通标志 |

例:0x0000 0x0000 0x0006 0x11        0x05     0x000b       0xff00

Modbus库

三方库的使用

1. 在linux中解压压缩包

        将库压缩包复制到linux下,进行解压

        执行命令:tar -xvf libmodbus-3.1.7.tar.gz

2. 进入源码目录,创建文件夹(存放头文件、库文件)

先执行命令:cd libmodbus-3.1.7

        再执行命令:mkdir install

3. 执行脚本configure,进行安装配置(指定安装目录)

        执行命令:configure

        再执行命令:./configure --prefix=$PWD/install

4. 执行make和make install,执行完成后在install里面会生成对应的头文件、库文件文件夹

        先执行命令:make//编译

        再执行命令:make install//安装

库的使用

要想编译方便,可以将头文件和库文件放到系统路径下,后期编译时,可以直接gcc xx.c -lmodbus

先执行命令:sudo  cp install/include/modbus/*.h  /usr/include 

再执行命令:sudo  cp install/lib/*  -r /lib -d

头文件默认搜索路径:/usr/include  、/usr/local/include

库文件默认搜索路径:/lib、/usr/lib

函数接口

创建modbus实例
格式:modbus_t*   modbus_new_tcp(const char *ip, int port)
功能:以TCP方式创建Modbus实例,并初始化
参数:
    ip:ip地址
    port:端口号
返回值:
    成功:Modbus实例
    失败:NULL
设置从机ID
格式:int modbus_set_slave(modbus_t *ctx, int slave)
功能:设置从机ID
参数:
    ctx:Modbus实例
    slave:从机ID
返回值:
    成功:0
    失败:-1
 和从机(slave)建立连接
格式:int   modbus_connect(modbus_t *ctx)
功能:和从机(slave)建立连接
参数:
    ctx:Modbus实例
返回值:
    成功:0
    失败:-1
释放Modbus实例 
格式:void   modbus_free(modbus_t *ctx)
功能:释放Modbus实例
参数:ctx:Modbus实例
关闭套接字
格式:void   modbus_close(modbus_t *ctx)
功能:关闭套接字
参数:ctx:Modbus实例
读取线圈状态
格式:int modbus_read_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest)
功能:读取线圈状态,可读取多个连续线圈的状态(对应功能码为0x01)
参数:
    ctx:Modbus实例
    addr:寄存器起始地址
    nb:寄存器个数
    dest:得到的状态值
读取离散输入寄存器状态
格式:int  modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest)
功能:读取离散输入寄存器状态,可读取多个连续输入的状态(对应功能码为0x02)
参数:
    ctx:Modbus实例
    addr:寄存器起始地址
    nb:寄存器个数
    dest:得到的状态值
返回值:
    成功:返回nb的值
读取保持寄存器的值 
格式:int  modbus_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest)
功能:读取保持寄存器的值,可读取多个连续保持寄存器的值(对应功能码为0x03)
参数:
    ctx:Modbus实例
    addr:寄存器起始地址
    nb:寄存器个数
    dest:得到的寄存器的值
返回值:
    成功:读到寄存器的个数
    失败:-1
读取输入寄存器的值 
格式:int   modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest)
功能:读输入寄存器的值,可读取多个连续输入寄存器的值(对应功能码为0x04)
参数:
    ctx:Modbus实例
    addr:寄存器起始地址
    nb:寄存器个数
    dest:得到的寄存器的值
返回值:
    成功:读到寄存器的个数
    失败:-1
写线圈寄存器 (单个和多个)
格式:int  modbus_write_bit(modbus_t *ctx, int addr, int status);
功能:写入单个线圈的状态(对应功能码为0x05)
参数:
    ctx:Modbus实例
    addr:线圈地址
    status:线圈状态
返回值:成功:?
      失败:-1
格式:int  modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *src);
功能:写入多个连续线圈的状态(对应功能码为15)
参数:
    ctx:Modbus实例
    addr:线圈地址
    nb:线圈个数
    src:多个线圈状态
返回值:
    成功:?
    失败:-1
 写保持寄存器(单个和多个)
格式:int  modbus_write_register(modbus_t *ctx, int addr, int value);
功能:写入单个寄存器(对应功能码为0x06)
参数: 
    ctx:Modbus实例
    addr:寄存器地址
    value:寄存器的值 
返回值:
    成功:?
    失败:-1
格式:int  modbus_write_registers(modbus_t *ctx, int addr, int nb, const uint16_t *src);
功能:写入多个连续寄存器(对应功能码为16)
参数:
    ctx:Modbus实例
    addr:寄存器地址
    nb:寄存器的个数
    src:多个寄存器的值 
返回值:
    成功:?
    失败:-1
编程流程 
  1. 创建实例:modbus_new_tcp()
  2. 设置从机ID:modbus_set_slave()
  3. 连接从机:modbus_connect()
  4. 寄存器操作:相应的功能码对应的函数
  5. 关闭套接字:modbus_close()
  6. 释放实例:modbus_free()

注意:

追源码操作:ctrl + 鼠标单击

返回:alt + 键盘向左方向的键

        只有工作区顶层目录下有解压的库文件夹可以追到

编程的时候要加头文件:#include "modbus.h"

编译代码的时候需要链接库:-l modbus

练习:

编程实现采集传感器数据和控制硬件设备(传感器和硬件通过slave模拟)

        传感器两个:光线传感器、加速度传感器(x\y\z)

        硬件设备两个:led灯、蜂鸣器

要求:

  1. 多任务编程:多线程
  2. 循环1s采集一次数据,并将数据打印到终端
  3. 同时从终端输入指令控制硬件设备

        0 1:led灯打开

        0 0:led灯关闭

        1 1:蜂鸣器打开

        1 0:蜂鸣器关闭

/*思路*/
//采集数据
void * handler_data(void * arg)
{
    modbus_t *ctx=(modbus_t *)arg;
    循环采集数据,并打印到终端,睡一秒
}

//控制设备
void* handler_ctrl(void * arg)
{
    循环从终端输入,写线圈,睡一秒
    int dev,op;
    scanf("%d %d",&dev,&op);
    (ctx,dev,op)
}


int main()
{
    //1.创建实例
       
    //2.设置从机ID
    
    //3.链接
    
    //4.创建线程
    
    
    //5.阻塞
    
    //6.关闭套接字
    
    //7.释放实例

}
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "modbus.h"

#define ERR_MSG(msg)                                                           \
    do                                                                         \
    {                                                                          \
        fprintf(stderr, "__%s__ __%s__ __%d__", __FILE__, __func__, __LINE__); \
        perror(msg);                                                           \
    } while (0)

#define IP "192.168.50.28"
#define PORT 502

void *Coil(void *arg)
{
    modbus_t *ctx = modbus_new_tcp(IP, PORT);
    int slave = 1;
    modbus_set_slave(ctx, slave);
    modbus_connect(ctx);
    int addr, status;
    while (1)
    {
        scanf("%d %d", &addr, &status);
        if (modbus_write_bit(ctx, addr, status) < 0)
        {
            ERR_MSG("modbus_write_bit err");
            break;
        }
        if (addr == 0 && status == 0)
            printf("led关闭\n");
        else if (addr == 0 && status == 1)
            printf("led打开\n");
        else if (addr == 1 && status == 0)
            printf("蜂鸣器关闭\n");
        else if (addr == 1 && status == 1)
            printf("蜂鸣器打开\n");
        else
        {
            printf("error\n");
        }
    }
    exit(0);
}
void *ReadRegister(void *arg)
{
    //读寄存器
    modbus_t *ctx = modbus_new_tcp(IP, PORT);
    int slave = 1;
    modbus_set_slave(ctx, slave);
    modbus_connect(ctx);
    uint16_t dest[128];
    while (1)
    {

        if (modbus_read_registers(ctx, 0, 4, dest) < 0)
        {
            ERR_MSG("modbus_read_registers err");
            break;
        }
        for (int i = 0; i < 4; i++)
        {
            printf("%#.2x ", dest[i]);
        }
        printf("\n");
        sleep(3);
    }
    exit(0);
}

int main(int argc, char const *argv[])
{
    //多线程
    pthread_t tid1, tid2;
    if (pthread_create(&tid1, NULL, Coil, NULL))
    {
        ERR_MSG("tid1 err");
        return -1;
    }
    printf("Coil create success\n");
    if (pthread_create(&tid2, NULL, ReadRegister, NULL))
    {
        ERR_MSG("tid2 err");
        return -1;
    }
    printf("ReadRegister create success\n");
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

注意事项:

Modbus TCP是应用层协议,是基于TCP协议进行传输数据的

Modbus TCP协议格式:报文头 + 功能码 + 数据

8种功能码,常用的有:0x01,0x03,0x06,0x15

主机给从机发的消息一般是12个字节

modbus库一般在 /lib 或 /usr/include 路径下

工具使用

modbusSlave/Poll

  1. 软件默认安装
  2. 破解:点击connection --> connect,输入序列号即可
  3. 使用:先设置后连接再查询主机ip

                先设置

基于WebServer的工业数据采集项目_第4张图片

        后连接(连接时注意先开启slave端(slave相当于服务器端),再开启poll端(poll相当于客户端))

基于WebServer的工业数据采集项目_第5张图片

        查询主机ip:win + r 然后输入cmd,调出命令提示符界面,输入命令ipconfig即可查询主机ip

网络调试助手

基于WebServer的工业数据采集项目_第6张图片

 Wirshark使用

安装使用wireshark时注意要把杀毒软件和防火墙关闭。

捕获器的选择:

  1. windows如果连接有线网络,选择本地连接/以太网
  2. windows如果连接无线网络,选择WLAN
  3. 如果只是在本机上进行通信,选择NPCAP Loopback apdater或Adapter for loopback traffic capture

过滤条件:

  1. 过滤端口:tcp.port==502
  2. 过滤IP:ip.addr==Windows的ip地址(本机)

基于WebServer的工业数据采集项目_第7张图片

基于WebServer的工业数据采集项目_第8张图片

 练习:

在虚拟机写程序实现poll端功能,编写客户端实现和slave通信。

要求:

实现对slave单个线圈的控制

实现读保持寄存器(03功能码)

分别对以上两个功能封装函数,其中读保持寄存器函数参数可以传递寄存器起始地址、寄存器个数和从机ID

#include 
#include  // atoi
#include 
#include 
#include 
#include 
#include 
#include 

int sockfd; //定义文件描述符

void set_slave_id(uint8_t *p, int id) //设置从机id
{
    *(p + 6) = id;
}

//读保持寄存器      (发送数据首地址,  功能码,   寄存器地址,寄存器数量,存放接受数据首地址)
void read_registers(uint8_t *p, int function, int addr, int nb, uint8_t *dest)
{
    int i;
    *(p + 5) = 6;              //后面字节数
    *(p + 7) = (char)function; //功能码
    *(p + 8) = addr >> 8;      //寄存器高字节地址
    *(p + 9) = addr & 0xff;    //寄存器低字节地址
    *(p + 10) = nb >> 8;       //寄存器数量高位
    *(p + 11) = nb & 0xff;     //寄存器数量低位
   
    send(sockfd, p,12,0);//注意这里不能sizeof(p),p为指针
   
    recv(sockfd, dest,64,0);//注意这里不能sizeof(dest),dest为指针
}

void write_coil(uint8_t *p, int function, int addr, int nb, uint8_t *dest)
{
    int i = 0;
    *(p + 5) = 6;              //后面字节数
    *(p + 7) = (char)function; //功能码
    *(p + 8) = addr >> 8;      //线圈高位地址
    *(p + 9) = addr & 0xff;    //线圈低位地址
    if (nb == 1)
        *(p + 10) = 0xff;
    else if (nb == 0)
        *(p + 10) = 0x00;
    *(p + 11) = 0x00;

    send(sockfd, p, 12, 0);

    recv(sockfd, dest, 64, 0);
}

int main(int argc, char const *argv[])
{
    struct sockaddr_in s;

    uint8_t data[12] = {0};
    uint8_t dest[64] = {0};
    int i;

    //1.socket创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }

    socklen_t len = sizeof(struct sockaddr_in);

    //2.填充结构体
    s.sin_family = AF_INET;                 //协议族
    s.sin_port = htons(atoi(argv[2]));      // htons:小端转大端 atoi:将数字字符串转换为数值
    s.sin_addr.s_addr = inet_addr(argv[1]); //字符串转点分十进制

    //3.connect请求连接
    if (connect(sockfd, (struct sockaddr *)&s, len) < 0)
    {
        perror("connect err");
        return -1;
    }

    //4.设置从机ID
    set_slave_id(data, 1);
    printf("从机id\n");

    //5.循环发送
    while (1)
    {
        printf("开始读\n");
        read_registers(data, 0x03, 0, 2, dest);

        printf("recv data:");
        for (i = 0; i < dest[8]; i++)
            printf("%#x  ", dest[9 + i]);
        printf("\n");

        sleep(1);

        write_coil(data,0x05,0,1,dest);//线圈置一
        printf("线圈置位后:\n");
        printf("%#x  %#x  \n",dest[10],dest[11]);

        sleep(1);
    }

    //5.关闭套接字
    close(sockfd);
    return 0;
}

WebServer服务器

web Server中午呢名叫网页服务器或Web服务器,Web服务器也称为WWW(World Wide Web)服务器,其主要功能就是提供网上信息浏览服务。

Web Server的分类

web Server通常分为Kangle、Nginx、Apache等。在嵌入式中常见的轻量级服务器有Lighttpd、Shttpd、Thttpd、Boa、Mini_httpd、Appweb、Goahead。

Lighttpd服务器

Lighttpd是一个开源的轻量级嵌入式web Server,是一个提供专门针对高性能网站,安全、快速、兼容性好并且较为灵活的Web Server环境,具有非常低的内存开销,cpu占用率低、效能好以及丰富的模块等特点。

服务器安装配置(在虚拟机内)

1. 解压

        执行命令 tar -xvf lighttpd-1.4.54.tar.gz

2. 进入源代码,创建文件夹web

        先执行命令 cd lighttpd-1.4.54

        再执行命令 mkdir web

3. 执行configure脚本文件

        执行命令 ./configure --prefix=$PWD/web

4. 执行Makefile文件

        先执行命令 make

        再执行命令 make install

配置文件修改

1. 在web文件夹下创建文件夹(config、log、run、www)

        先执行命令 cd web

        再执行命令 mkdir  config  log  run  www

2. 将源目录lighttpd-1.4.54下web文件夹移动到某个路径下

        执行命令 mv lighttpd-1.4.54/web  ~/hq/home/demo

3. 将源码目录lighttpd-1.4.54/doc/config下的conf.d lighttpd.conf modules.conf复制到~/hq/home/demo/web/config中

        执行命令 cp conf.d  lighttpd.conf  modules.conf   ~//hq/home/demo/web/config -r

4. 修改log文件夹权限,并在log目录下创建error.log文件修改权限

        先执行命令 chmod  777  log

        再执行命令 touch  log/error.log

        最后执行命令 chmod  777  log/error.log

5. 在www目录下创建htdocs文件夹存放网页文件

        执行命令 mkdir  www/htdocs

修改配置文件

1. 执行命令 vi  ~/work/web/config/lighttpd.conf

##
var.home_dir    = "/home/hq/work/web"   #lighttpd操作的主目录
var.log_root    = home_dir + "/log"			#日志文件目录(程序执行中出现的错误信息)
var.server_root = home_dir + "/www"			#存放html、cgi代码目录
var.state_dir   = home_dir + "/run"			#存放pid文件服务运行起来后自动创建
var.conf_dir    = home_dir + "/config"  #存放配置文件
##
var.vhosts_dir  = home_dir + "/vhosts"
##
var.cache_dir   = home_dir + "/cache"
##
var.socket_dir  = home_dir + "/sockets"
##
server.port = 80    #端口号为80
##
server.use-ipv6 = "disable"	  #设置为禁用
##
#server.bind = "localhost"		#默认即可
##
server.username  = "hq"		#修改为当前用户,nobody为任何人都可以访问
#server.groupname = "nobody"		#将其注释即可
##
server.document-root = server_root + "/htdocs"		#存放html网页的文件
##
server.pid-file = state_dir + "/lighttpd.pid"
##
server.errorlog             = log_root + "/error.log"		#错误日志文件



2. 执行命令 vi  ~/work/web/config/modules.conf

include "conf.d/cgi.conf"  	将此行注释打开(149)

3. 执行命令 vi  ~/work/web/config/conf.d/cgi.conf

$HTTP["url"] =~ "^/cgi-bin" {
 cgi.assign = ( "" => "" )
}			将这三行注释打开28-30行

运行测试

1. 运行

        先执行命令 cd  ~/work/web

        再执行命令 sudo sbin/lighttpd -f config/lighttpd.conf -m lib/

        注意:结束进程命令是 pkill lighttpd

2. 测试

        将 index.html 文件放到www/htdocs目录下

        打开浏览器,在地址栏输入服务器的IP地址(虚拟机IP地址)即可看到index的主页

基于WebServer的工业数据采集项目_第9张图片

容易出现的问题:

403:找一下index.html有没有出错或index.html位置有没有放错

404:连接问题,防火墙管、家有没有退或虚拟机有没有网,服务器有没有开

Postman使用 

作用

模拟浏览器,实现Modbus Slave端数据采集和硬件设备控制。

注意事项

先确保服务器打开

        先执行结束进程命令:pkill lighttpd

        再执行打开命令:sudo sbin/lighttpd -f config/lighttpd.conf -m lib/

基于WebServer的工业数据采集项目_第10张图片

测试使用

按照 上述设置完成postman后,将lighttpd服务器开启,当点击发送时,会在小终端上显示调试信息,同时在postman中也会看到回复的数据。

html

开发环境

在某路径下先新建文件夹,打开VScode打开文件夹,新建文件,文件命名为index.html

基于WebServer的工业数据采集项目_第11张图片

基于WebServer的工业数据采集项目_第12张图片

 安装库 open in browser

库安装完成后,在编写文本位置右击 -> open in other browser -> 选择合适的浏览器即可在网页显示html标签内容。

基于WebServer的工业数据采集项目_第13张图片

基于WebServer的工业数据采集项目_第14张图片

基于WebServer的工业数据采集项目_第15张图片

 基于WebServer的工业数据采集项目

项目框架

基于WebServer的工业数据采集项目_第16张图片

CGI 

CGGI简介

早期的Web服务器只能响应浏览器发来的http静态资源的请求,并将存储在服务器中的静态资源返回给浏览器。随着Web技术的发展,逐渐出现了动态技术,但是Web服务器并不能够直接运行动态脚本,为了解决Web服务器与外部应用程序之间数据互通,于是出现了CGI通用网关接口。简单理解,可以认为CGI是Web服务器和运行其上的应用程序进行“交流”的一种约定。CGI(Common Gateway Interface)通用网关接口,是外部扩展应用程序与 Web 服务器交互的一个标准接口。

CGI特点

CGI是Web服务器和一个独立的进程之间的协议,通过环境变量以及标准输入、标准输出和服务器进行数据交互。

  1. 通过环境变量可以获取到网页的请求方式、地址等。
  2. 通过标准输入可以获取网页的消息正文。
  3. 通过标准输出可以发送网页请求的数据。

基于WebServer的工业数据采集项目_第17张图片

常见的环境变量 

REQUEST_URI:访问此页面需要的URL,比如:“/index.html”

REQUEST_METHOD:获取客户端请求数据的方式:POST或GET

CONTENT_LENGTH:获取用户数据的长度

CONTENT_TYPE:网页中存在的 Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件

CGI工作原理

基于WebServer的工业数据采集项目_第18张图片

当浏览器向Web服务器发送动态数据请求时,Web服务器主进程就会fork()创建出一个新的进程来启动CGI程序,也就是将动态脚本交给CGI程序来处理。当CGI程序启动后会去解析动态脚本然后将结果返回给Web服务器,最后由Web服务器将结果返回给客户端,之前fork()出来的进程也随之关闭。这样每次用户请求动态脚本时Web服务器都要重新fork()创建一个新进程去启动CGI程序,由CGI程序来处理动态脚本,处理完后进程随之关闭。对于一个CGI程序,主要的工作是从环境变量和标准输入中读取数据,然后处理数据,最后向标准输出中输出数据(在这服务器将标准输入和标准输出做了重定向)。

源码分析

在main.c程序中,主函数内的handle_requst() 函数获取网页发给服务器的数据,请求头(环境变量)和消息正文(标准输入)的信息,再调用parse_and_process() 函数,在该函数中根据正文判断网页需要执行什么操作(读传感器数据或控制硬件设备状态),根据请求完成采集数据或控制硬件设备,最终给网页回复(标准输出)数据(遵循http协议)。

源码使用

1. 首先将cgi_demo复制到虚拟机web目录下

2. 用uxterm命令打开简化版终端(小终端),用who am i命令查看当前小终端的文件,根据自己的小终端文件修改log_consloe.h里面的内容。

基于WebServer的工业数据采集项目_第19张图片

 3. 在www/htdocs下创建cgi-bin文件夹,在cgi源码目录(cgi_demo)执行make,会在cgi-bin路径下生成web.cgi

http&html

http协议

http简介

HTTP协议(Hyper Transfer Protocol)是超文本传输协议,是用于web Browser(浏览器)到web Server(服务器)进行数据交互的传输协议,是基于TCP通信协议传输来传送数据(HTML文件,图片文件,查询结果等)的应用层协议,该协议工作于B/S架构上,浏览器作为HTTP客户端通过URL主动向HTTP服务端也就是Web服务器发送请求,Web服务器接收到请求后,向客户端发送响应信息,其中HTTP协议的默认端口号是80,但是可以手动改端口号。

http特点
  1. http是短连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户请求并收到客户端应答后即断开连接,采用这种方法可以节省传输时间。
  2. http是媒体独立的:即只要客户端和服务器知道如何处理数据内容,任何类型的数据都可以通过http发送,客户端以及服务器指定使用适合的MIME-type内容类型。
  3. http是无状态的:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它应答的较快。
http协议格式

1. 客户端请求消息格式

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行、请求头部、空行和请求数据四个部分。一般格式如下:

基于WebServer的工业数据采集项目_第20张图片

请求行:

        请求行是由请求方法字段、url字段、http协议本字段三个部分组成。请求行定义了本次请求的方式。

        格式如下:GET/example.html HTTP/1.1(CRLF)

请求头:

        也被称作消息报头,请求头是有一些键值对组成,每行一对,关键字和值用英文冒号“ : ”分隔。

Accept:作用:描述客户端希望接收的 响应body 数据类型;示例:Accept:text/html

Accept-Charset:作用:浏览器可以接受的字符编码集;示例:Accept-Charset:utf-8

Accept-Language:作用:浏览器可接受的语言;示例:Accept-Language:en

Connection:作用:表示是否需要持久连接,注意HTTP1.1默认进行持久连接;示例:Connection:close

Content-Length:作用:请求的内容长度:示例:Content-Length:348

Content-Type:作用:描述客户端发送的 body 数据类型

2. 服务器响应消息格式

HTTP响应也由四个部分组成,分别是:状态行、消息报头空行和响应正文。

状态行由HTTP协议版本号、状态码以及对状态码的文本描述。例如:HTTP/1.1 200 OK(CRLF)。(200表示请求已经成功)。

基于WebServer的工业数据采集项目_第21张图片

html语法 

html简介

HTML(Hyper Text Markup Language)是超文本标记语言,用来描述网页的一种语言,所谓超文本就是可以加入图片、动画、声音、多媒体等内容,还可以从一个文件跳转到另一文件,与其他主机的文件连接。HTML不是一种编程语言而是一种标记语言。

Web浏览器的作用是读取HTML文档,并且以网页的形式显示,但是浏览器不会显示HTML标签,而是使用标签来解释页面的内容。

html标签

标签格式
  1. 有尖括号包围的关键字,例如:
  2. 通常成对存在,并且后面的一个在尖括号里的内容第一个是 " / " ,例如:
  3. 一对标签中前面的是开始标签,后面的是结束标签
标签分类
  1. 单标签,也称为空标签,其格式为:<标签名/>        例如:
  2. 双标签,都是成对存在,其格式为:<标签名>内容        例如:请输入...
常用标签

1. h1-h6标题标签

格式为:标题文本

h1是一级标签,依次后推,h6是六级标签

2. p段落标签

一个段落中会根据浏览器窗口的大小自动换行

格式为:

文本内容

3. br换行标签(强制换行)

是一个块级元素,可以把文档分割为独立的、不同的部分,可以在div标签中嵌套标签

格式为:

例如:

News headline 1

some text. some text. some text...

注意:div标签可以设置class或id,通过选择器设置属性,则内部成员具有相同属性。

4. Input表单标签

表示输入的意思,是单标签

格式为:

属性有多种:

基于WebServer的工业数据采集项目_第22张图片

 其中type属性的text和radio较为重要。

当type为text时,表示的是文本输入框。

        用法为:

当type为radio时,表示的是单选框

        用法为:

        其中,name为控件名称,同一组单选框设置相同的名字,一组内只能选一个

                   value值必须要填写,是当点击时会提交的数据

                   onclick:点击时会执行双引号中的处理函数

                   checked:默认选中,同一组中只选中一个即可

5. label标签

label标签为input元素定义标注(标签),其作用是用于绑定一个表单元素,当点击label标签的时候,被绑定的表单元素就会获得输入焦点。

例如:


  
注:这里for要跟input中的id一致

项目要求:

编程实现采集传感器数据和控制硬件设备(传感器和硬件通过slave模拟)

        传感器两个:光线传感器、加速度传感器(x\y\z)

        硬件设备两个:led灯、蜂鸣器

要求:

  1. 多任务编程:多线程
  2. 循环1s采集一次数据,并将数据打印到网页
  3. 同时从网页输入指令控制硬件设备

        0 1:led灯打开

        0 0:led灯关闭

        1 1:蜂鸣器打开

        1 0:蜂鸣器关闭

注意事项:

1. 存在共享内存和消息队列数据接收发送出问题时

        解决方案:

        1. 在代码中打印语句,确保两个进程用的是同一个id

        2. 由于程序是强制结束,下次再运行代码时,将已存在的消息队列删除

        查看和删除共享内存、消息队列

ipcs  -m  :查看共享内存

ipcrm  -m  shmid:删除共享内存

ipcs -q:查看消息队列

ipcrm  -q  semid:删除消息队列

2. key值的创建路径指定或目录下的某个新建文件

3. 多使用打印语句,学会通过uxterm转到小终端查看打印信息,方便定位错误位置

代码如下

ModbusTCP端服务程序

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "modbus.h"

#define ERR_MSG(msg)                                                           \
    do                                                                         \
    {                                                                          \
        fprintf(stderr, "__%s__ __%s__ __%d__", __FILE__, __func__, __LINE__); \
        perror(msg);                                                           \
    } while (0)

#define IP "192.168.50.192"//连接windows端ip地址,ipconfig
#define PORT 502
#define filepath "/home/demo/web/cji_demo/main.c"

//消息队列结构体
struct msgbuf
{
    long mtype;
    char buf[128];
} msg;

int SharedMemory(modbus_t *ctx, int shmid)
{
    uint16_t dest[128];
    char *p = shmat(shmid, NULL, 0);
    if (p == (char *)-1)
    {
        perror("shmat error");
        return -1;
    }
    printf("shmat success\n");
    if (modbus_read_registers(ctx, 0, 4, dest) < 0)
    {
        ERR_MSG("modbus_read_registers err");
        return -1;
    }
    for (int i = 0; i < 4; i++)
    {
        printf("%#.2x ", dest[i]);//在终端打印数据部分
    }
    sprintf(p, "%#.2x %#.2x %#.2x %#.2x", dest[0], dest[1], dest[2], dest[3]); //将数据写入共享内存
    printf("%s\n", p);
    printf("\n");
}

void *handler1(void *arg)
{
    //创建modbus实例
    modbus_t *ctx = modbus_new_tcp(IP, PORT);
    int slave = 1;
    modbus_set_slave(ctx, slave);
    if (modbus_connect(ctx) < 0)
    {
        perror("connect error");
        return -1;
    }
    printf("connect success\n");
    //uint16_t dest[128];
    //创建共享内存
    key_t key;
    key = ftok("/home/hq/app.c", 'a');
    int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0)
    {
        if (17 == errno)
        {
            shmid = shmget(key, 128, 0666);//已经打开
        }
        else
        {
            perror("shmget error");
            return -1;
        }
    }
    printf("key:%d shmid:%d\n", key, shmid);
    printf("shmget success\n");
    while (1)
    {

        // char *p = shmat(shmid, NULL, 0);
        // if (p == (char *)-1)
        // {
        //     perror("shmat error");
        //     return -1;
        // }
        // printf("shmat success\n");
        // if (modbus_read_registers(ctx, 0, 4, dest) < 0)
        // {
        //     ERR_MSG("modbus_read_registers err");
        //     break;
        // }
        // for (int i = 0; i < 4; i++)
        // {
        //     printf("%#.2x ", dest[i]);
        // }
        // sprintf(p, "dest:%#.2x %#.2x %#.2x %#.2x", dest[0], dest[1], dest[2], dest[3]); //写入共享内存
        // printf("%s\n", p);
        // printf("\n");
        // sleep(3);
        //对共享内存进行操作
        SharedMemory(ctx, shmid);
    }
    pthread_exit(0);
    modbus_close(ctx);
    modbus_free(ctx);
}

void *handler2(void *arg)
{
    //消息队列
    //连接modbus
    modbus_t *ctx = modbus_new_tcp(IP, PORT);
    int slave = 1;
    modbus_set_slave(ctx, slave);
    modbus_connect(ctx);
    uint16_t dest[128];
    //创建共享内存
    key_t key;
    key = ftok("/home/hq/app.c", 'b');
    int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    if (msgid < 0)
    {
        if (17 == errno)
        {
            msgid = msgget(key, 0666);
        }
        else
        {
            perror("msgget error");
            return -1;
        }
    }
    // struct msgbuf msg;
    while (1)
    {
        //循环写入控制器的内容
        msgrcv(msgid, &msg, sizeof(msg) - sizeof(long), 1, 0);
        printf("msgbuf:%s\n", msg.buf);
        if (msg.buf[4] == '0' && msg.buf[6] == '0')
        {
            printf("led关闭\n");
            //将控制器状态写入到消息队列
            modbus_write_bit(ctx, 0, 0);
        }
        else if (msg.buf[4] == '0' && msg.buf[6] == '1')
        {
            printf("led打开\n");
            modbus_write_bit(ctx, 0, 1);
        }
        else if (msg.buf[4] == '1' && msg.buf[6] == '0')
        {
            printf("蜂鸣器关闭\n");
            modbus_write_bit(ctx, 1, 0);
        }
        else if (msg.buf[4] == '1' && msg.buf[6] == '1')
        {
            printf("蜂鸣器打开\n");
            modbus_write_bit(ctx, 1, 1);
        }
    }
    msgctl(msgid, IPC_RMID, NULL);
    
    modbus_close(ctx);//关闭ctx
    modbus_free(ctx);//释放ctx
    pthread_exit(0);//退出线程
}

int main(int argc, char const *argv[])
{
    //创建多线程
    pthread_t pid1, pid2;
    if (pthread_create(&pid1, NULL, handler1, NULL))
    {
        perror("pid1 error");
        return -1;
    }
    printf("pid1 success\n");
    if (pthread_create(&pid2, NULL, handler2, NULL))
    {
        perror("pid2 error");
        return -1;
    }
    printf("pid2 success\n");
    pthread_join(pid1, NULL);//回收线程
    pthread_join(pid2, NULL);
    return 0;
}

CGI代码

makefile

#指定编译器

CC = gcc

#指定最终目标名字

OBJ = web.cgi

#指定所有的中间.o文件

OBJS := main.o log_console.o req_handle.o custom_handle.o

#指定cgi的拷贝路径

CGI_DIR = ../www/htdocs/cgi-bin



all:$(OBJS)

	$(CC) -o $(OBJ) $^

	cp $(OBJ) $(CGI_DIR)



# .a.o.d .b.o.d

dep_files := $(foreach f,$(OBJS),.$(f).d)

dep_files := $(wildcard $(dep_files))



ifneq ($(dep_files),)

  include $(dep_files)

endif



%.o : %.c 

	$(CC) -Wp,-MD,[email protected] -c -o $@ $<



clean:

	rm -rf .*.o.d *.o $(OBJ)



main.c

#include "req_handle.h"

#include "log_console.h"



int main(int argc, char *argv[])

{

    //先初始化log,标准输出已被重定向到网络

    int ret = log_console_init();

    if(ret < 0)

    {

        perror("open console err");

        system("echo open log err > err.log");

        exit(-1);

    }



    //argc和argv web server会自动传给cgi程序

    handle_request(argc, argv);



	return 0;

}

req_handle.h



#ifndef REQ_HANDLE_H

#define REQ_HANDLE_H



int handle_request(int argc, char *argv[]);



#endif  // REQ_HANDLE_H

req_handle.c

#include "req_handle.h"

#include "log_console.h"

#include "custom_handle.h"



/**

 * @brief 处理请求

 * @param argc

 * @param argv

 * @return

 */

int handle_request(int argc, char *argv[])

{

    int ret;



	if (argc <= 0)

	{

        log_console("argc error\n");

		return -1;

	}



	//获取访问此页面所需的URI。例如,“/index.html”。

    char *uri = getenv("REQUEST_URI");

    //获取前端请求方式

    char *request_method = getenv("REQUEST_METHOD");



    log_console("uri = %s\n", uri);

    log_console("req = %s\n", request_method);



    if(NULL == request_method)

    {

        log_console("error to get request_method\n");

        exit(-1);

    }



    //get方法我们不处理,交由服务器自己处理

    if (strcasecmp(request_method, "POST") != 0)//相当于strcmp,比较两个字符串的值

    {

        log_console("only handle post\n");

        return 0;

    }



    //获取用户数据长度

    char *len_tmp = getenv("CONTENT_LENGTH");  //get CONTENT_LENGTH from env



    int content_length;

    if (len_tmp != NULL)

    {

        content_length = atoi(len_tmp);



        if (content_length <= 0)

        {

            return -1;

        }

    }

    else

    {

        log_console("request_content length error");

        return -1;

    }



    char *content_type = getenv("CONTENT_TYPE");

    //打印下CONTENT_TYPE看看,我们只处理普通请求

    log_console("content_type=%s\n", content_type);



    //普通请求处理

    char *request_content = malloc(content_length + 1);

    if (!request_content)

    {

        return -1;

    }



    int len = 0;

    //从标准输入中读取数据到content中

	//使用循环的目的防止数据一次没有读完

    while (len < content_length)

    {

        ret = fread(request_content + len, 1, content_length - len, stdin);



        if (ret < 0)

        {

            free(request_content);

            return -1;

        }

        else if (ret > 0)

        {

            len += ret;

        }

        else

        {

            break;

        }

    }



    if (len != content_length)

    {

        log_console("fread len != content_length");

        free(request_content);

        return -1;

    }



    //此时所有的请求内容都存到request_content中了

    request_content[len] = '\0';

    log_console("notice request_content = %s\n", request_content);

    ret = parse_and_process(request_content);

    if(ret < 0)

    {

        log_console("error to parse\n");

    }

    free(request_content);



    return ret;

}

log_console.h



#ifndef LOG_CONSOLE_H

#define LOG_CONSOLE_H



#include 

#include 

#include 

#include 

#include 

#include 

#include 

#include 

#include 

#include 



#define CONFIG_ARCH_X86



#ifdef CONFIG_ARCH_X86

#define LOG_CONSOLE         "/dev/pts/20"//改成对应的小终端的文件名

#else

#define LOG_CONSOLE         "/dev/tty1"

#endif



int log_console_init();

int log_console(const char *format, ...);



#endif  // LOG_CONSOLE_H

log_console.c

/***********************************************************************************

Copy right:	    Coffee Tech.

Author:         jiaoyue

Date:           2022-03-23

Description:    console模块

***********************************************************************************/



#include 

#include 

#include 

#include 

#include 



#include "log_console.h"



static int console_fd = -1;



/**

 * @brief 初始化log_console

 * @return 0 -1

 */

int log_console_init()

{

    console_fd = open(LOG_CONSOLE, O_WRONLY);

    if (console_fd < 0)

    {

        printf("open %s error\n", LOG_CONSOLE);

        return -1;

    }



	return 0;

}



static ssize_t log_console_write(const void *buf, size_t count)

{

    int ret = write(console_fd, buf, count);

    if(ret < 0)

    {

        perror("write err");

    }

}



#define MAX_LOG_BUF_LEN (20*1024)  //打印内容不能超过这个



int log_console(const char *format, ...)//相当于重写printf,写到小终端而不是网页

{

    char str_tmp[MAX_LOG_BUF_LEN];

    va_list arg;

    int len;



    va_start(arg, format);

    len = vsnprintf(str_tmp, MAX_LOG_BUF_LEN, format, arg);

    va_end(arg);

    str_tmp[len] = '\0';



    return log_console_write(str_tmp, strlen(str_tmp));

}

custom_handle.h



#ifndef CUSTOM_HANDLE_H

#define CUSTOM_HANDLE_H



int parse_and_process(char *input);



#endif  // REQ_HANDLE_H

custom_handle.c



#include "req_handle.h"

#include "log_console.h"



#define KB 1024

#define HTML_SIZE (64 * KB)

#define filepath "/home/demo/web/cji_demo/main.c"



//普通的文本回复需要增加html头部

#define HTML_HEAD "Content-Type: text/html\r\n" \

                  "Connection: close\r\n"



/**

 * @brief 处理自定义请求,在这里添加进程通信

 * @param input

 * @return

 */

//消息队列结构体

struct msgbuf

{

    long mtype;//消息类型

    char buf[128];//接收到的数据

};

int parse_and_process(char *input)

{

    char val_buf[2048] = {0}; //获取的回应数据部分

    //strcpy(val_buf, input);



    //这里可以根据接收的数据请求进行处理

    //共享内存,判断是set还是get,然后做出相应的回应



    //读:共享内存读内容,然后原文返回

    //创建key值

    key_t key;

    //从共享内存读数据

    if (strcmp(input, "get") == 0)

    {

        key = ftok("/home/hq/app.c", 'a');

        if (key < 0)

        {

            log_console("1111");

            perror("ftok error");

            return -1;

        }

        log_console("ftok success\n");

        int shmid;

        shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);

        if (shmid < 0)

        {

            if (17 == errno)

            {

                shmid = shmget(key, 128, 0666);

            }

            else

            {

                perror("shmget error");

                return -1;

            }

        }

        log_console("key:%d shmid:%d",key,shmid);

        log_console("shmget success\n");

        char *p = shmat(shmid, NULL, 0);

        if (p == (char *)-1)

        {

            perror("shmat error");

            return -1;

        }

        log_console("shmat success\n");

        //将数据放到val_buf里面以便后面组合成响应数据

        strcpy(val_buf, p);

        log_console("p:%s\n", p);

        log_console("val_buf:%s\n", val_buf);

    }



    //写:用消息队列写

    else

    {

        log_console("write\n");

        key = ftok("/home/hq/app.c", 'b');

        if(key<0)

        {

            log_console("key err\n");

            perror("ftok err");

            return -1;

        }



        int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);

        if (msgid <= 0)

        {

            if (17 == errno)

            {

                msgid=msgget(key, 0666);

            }

            else

            {

                log_console("msgget\n");

                perror("msgget error");

                return -1;

            }

        }

        log_console("msgget success\n");

        log_console("key:%d msgid:%d\n",key,msgid);

        struct msgbuf msg;

        msg.mtype = 1; //消息类型全部设为1

        strcpy(msg.buf, input);

        msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0);

        strcpy(val_buf, msg.buf);

        log_console("val_buf:%s\n", val_buf);

    }



    //数据处理完成后,需要给服务器回复,回复内容按照http协议格式

    char reply_buf[HTML_SIZE] = {0};

    //reply_buf先获取请求头

    sprintf(reply_buf, "%sContent-Length: %ld\r\n\r\n", HTML_HEAD, strlen(val_buf)); //\r\n组合起来是回车换行

    //将正文部分接到请求头后面

    strcat(reply_buf, val_buf);

    log_console("post json_str = %s", reply_buf);



    //向标准输出写内容(标准输出服务器已做重定向)

    fputs(reply_buf, stdout);



    return 0;

}

html网页部分代码





    
    
    
    Document
    
    



    

基于WebServer的工业数据采集项目

------信息采集------



光线传感器
光线传感器:  


加速度传感器
加速度传感器x:
加速度传感器y:
加速度传感器z:


Modbus设备

led灯状态: 开
蜂鸣器状态: 开

你可能感兴趣的:(网络高级,linux,vscode,c语言,服务器,tcp)