目录
涉及内容
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. 分类
- Modbus RTU:运行在串口上的协议,采用二进制的存储形式以及紧凑的数据结构,通信效率较高,应用比较广泛。
- Modbus ASCII:运行在串口上的协议,采用ASCII进行传输,并且用特殊字符作为字节开始和结束的标志,传输效率远远低于Modbus RTU协议,一般只有在通信数据量较小的情况下才会考虑用Modbus ASCII通信。
- Modbus TCP:运行在以太网上的协议。
3. 优势
免费、简单、容易使用
4. 应用场景
Modbus协议是现在国内工业领域应用最多的协议,不只PLC设备,各种终端设备都会用到Modbus协议,比如水控机、水表、电表、工业秤、各种采集数据等。
5. Modbus TCP协议特点
- 采用主从问答的方式进行通信(只能主机主动发送消息,从机响应)
- Modbus TCP属于应用层协议
- Modbus TCP默认端口号是502
Modbus TCP通信协议
Modbus TCP协议包含三部分:报文头、功能码、数据
Modbus TCP协议最大数据帧长度为260字节。
报文头
Modbus TCP协议的报文头共有7字节
寄存器
Modbus TCP协议包含四种寄存器,分别是线圈、离散量输入、保持寄存器、输入寄存器。
1. 离散量输入和线圈是位寄存器(每个寄存器占一字节),工业上主要用于控制IO设备。
线圈寄存器:可以类比为开关量,每一个bit都对应一个信号的开关状态。所以一个byte就可以同时控制8路的信号。比如控制外部8路IO的高低,线圈寄存器既可以支持读操作也可以支持写操作,写操作也分为写的单个线圈寄存器和写多个线圈寄存器。
线圈寄存器的功能码:0x01(读线圈) 0x05(写单个线圈) 0x0f(写多个线圈)
离散输入寄存器:相当于线圈寄存器的只读模式,每个bit表示一个开关量,离散输入寄存器的开关量只能读取输入的开关信号,不能进行写操作。比如:读取外部按键是按下还是松开。
离散寄存器的功能码:0x02(读线圈寄存器)
2. 输入寄存器和保持寄存器是字寄存器(每个寄存器数据占2个字节),工业上主要用于存储工业设备的值。
保持寄存器:该寄存器的单位是byte,并且每个寄存器都占2字节,能够放具体的数据值,保持寄存器支持可读可写操作,并且写操作分为写单个寄存器和写多个寄存器。例如:设置时间(年月日),既可以写入也可以读出。
保持寄存器的功能码:0x03(读保持寄存器) 0x06(写单个寄存器) 0x10(写多个寄存器)
输入寄存器:相当于保持寄存器的只读模式,一个寄存器也是占两个字节。例如:通过读取输入寄存器获取的AD采集值。
输入寄存器的功能码:0x04(读保持寄存器)
功能码
根据四种不同功能的寄存器设置了8中功能码。
练习:
读传感器数据,读一个寄存器数据,写出主从数据收发协议
主机给从机发送:
| 事务处理标识符 | 协议类型 | 字节长度 | 从机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
编程流程
- 创建实例:modbus_new_tcp()
- 设置从机ID:modbus_set_slave()
- 连接从机:modbus_connect()
- 寄存器操作:相应的功能码对应的函数
- 关闭套接字:modbus_close()
- 释放实例:modbus_free()
注意:
追源码操作:ctrl + 鼠标单击
返回:alt + 键盘向左方向的键
只有工作区顶层目录下有解压的库文件夹可以追到
编程的时候要加头文件:#include "modbus.h"
编译代码的时候需要链接库:-l modbus
练习:
编程实现采集传感器数据和控制硬件设备(传感器和硬件通过slave模拟)
传感器两个:光线传感器、加速度传感器(x\y\z)
硬件设备两个:led灯、蜂鸣器
要求:
- 多任务编程:多线程
- 循环1s采集一次数据,并将数据打印到终端
- 同时从终端输入指令控制硬件设备
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 <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>
#include <semaphore.h>
#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
- 软件默认安装
- 破解:点击connection --> connect,输入序列号即可
- 使用:先设置后连接再查询主机ip
先设置
后连接(连接时注意先开启slave端(slave相当于服务器端),再开启poll端(poll相当于客户端))
查询主机ip:win + r 然后输入cmd,调出命令提示符界面,输入命令ipconfig即可查询主机ip
网络调试助手
Wirshark使用
安装使用wireshark时注意要把杀毒软件和防火墙关闭。
捕获器的选择:
- windows如果连接有线网络,选择本地连接/以太网
- windows如果连接无线网络,选择WLAN
- 如果只是在本机上进行通信,选择NPCAP Loopback apdater或Adapter for loopback traffic capture
过滤条件:
- 过滤端口:tcp.port==502
- 过滤IP:ip.addr==Windows的ip地址(本机)
练习:
在虚拟机写程序实现poll端功能,编写客户端实现和slave通信。
要求:
实现对slave单个线圈的控制
实现读保持寄存器(03功能码)
分别对以上两个功能封装函数,其中读保持寄存器函数参数可以传递寄存器起始地址、寄存器个数和从机ID
#include <stdio.h>
#include <stdlib.h> // atoi
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
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的主页
容易出现的问题:
403:找一下index.html有没有出错或index.html位置有没有放错
404:连接问题,防火墙管、家有没有退或虚拟机有没有网,服务器有没有开
Postman使用
作用
模拟浏览器,实现Modbus Slave端数据采集和硬件设备控制。
注意事项
先确保服务器打开
先执行结束进程命令:pkill lighttpd
再执行打开命令:sudo sbin/lighttpd -f config/lighttpd.conf -m lib/
测试使用
按照 上述设置完成postman后,将lighttpd服务器开启,当点击发送时,会在小终端上显示调试信息,同时在postman中也会看到回复的数据。
html
开发环境
在某路径下先新建文件夹,打开VScode打开文件夹,新建文件,文件命名为index.html
安装库 open in browser
库安装完成后,在编写文本位置右击 -> open in other browser -> 选择合适的浏览器即可在网页显示html标签内容。
基于WebServer的工业数据采集项目
项目框架
CGI
CGGI简介
早期的Web服务器只能响应浏览器发来的http静态资源的请求,并将存储在服务器中的静态资源返回给浏览器。随着Web技术的发展,逐渐出现了动态技术,但是Web服务器并不能够直接运行动态脚本,为了解决Web服务器与外部应用程序之间数据互通,于是出现了CGI通用网关接口。简单理解,可以认为CGI是Web服务器和运行其上的应用程序进行“交流”的一种约定。CGI(Common Gateway Interface)通用网关接口,是外部扩展应用程序与 Web 服务器交互的一个标准接口。
CGI特点
CGI是Web服务器和一个独立的进程之间的协议,通过环境变量以及标准输入、标准输出和服务器进行数据交互。
- 通过环境变量可以获取到网页的请求方式、地址等。
- 通过标准输入可以获取网页的消息正文。
- 通过标准输出可以发送网页请求的数据。
常见的环境变量
REQUEST_URI:访问此页面需要的URL,比如:“/index.html”
REQUEST_METHOD:获取客户端请求数据的方式:POST或GET
CONTENT_LENGTH:获取用户数据的长度
CONTENT_TYPE:网页中存在的 Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件
CGI工作原理
当浏览器向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里面的内容。
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特点
- http是短连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户请求并收到客户端应答后即断开连接,采用这种方法可以节省传输时间。
- http是媒体独立的:即只要客户端和服务器知道如何处理数据内容,任何类型的数据都可以通过http发送,客户端以及服务器指定使用适合的MIME-type内容类型。
- http是无状态的:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它应答的较快。
http协议格式
1. 客户端请求消息格式
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行、请求头部、空行和请求数据四个部分。一般格式如下:
请求行:
请求行是由请求方法字段、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表示请求已经成功)。
html语法
html简介
HTML(Hyper Text Markup Language)是超文本标记语言,用来描述网页的一种语言,所谓超文本就是可以加入图片、动画、声音、多媒体等内容,还可以从一个文件跳转到另一文件,与其他主机的文件连接。HTML不是一种编程语言而是一种标记语言。
Web浏览器的作用是读取HTML文档,并且以网页的形式显示,但是浏览器不会显示HTML标签,而是使用标签来解释页面的内容。
html标签
标签格式
- 有尖括号包围的关键字,例如:<html>
- 通常成对存在,并且后面的一个在尖括号里的内容第一个是 " / " ,例如:<body></body>
- 一对标签中前面的是开始标签,后面的是结束标签
标签分类
- 单标签,也称为空标签,其格式为:<标签名/> 例如:<br/>
- 双标签,都是成对存在,其格式为:<标签名>内容</标签名> 例如:<body>请输入...</body>
常用标签
1. h1-h6标题标签
格式为:<hn>标题文本</hn>
h1是一级标签,依次后推,h6是六级标签
2. p段落标签
一个段落中会根据浏览器窗口的大小自动换行
格式为:<p>文本内容</p>
3. br换行标签(强制换行)
是一个块级元素,可以把文档分割为独立的、不同的部分,可以在div标签中嵌套标签
格式为:<br/>
例如:
<div class="news">
<h2>News headline 1</h2>
<p>some text. some text. some text...</p>
</div>
注意:div标签可以设置class或id,通过选择器设置属性,则内部成员具有相同属性。
4. Input表单标签
表示输入的意思,是单标签
格式为:<input type="" name="" value="" id="" ......>
属性有多种:
其中type属性的text和radio较为重要。
当type为text时,表示的是文本输入框。
用法为:<input type="text" value="文本框默认值">
当type为radio时,表示的是单选框
用法为:<input type="radio" name="控件名称" value="提交的数值" οnclick="处理函数" checked="checked">
其中,name为控件名称,同一组单选框设置相同的名字,一组内只能选一个
value值必须要填写,是当点击时会提交的数据
onclick:点击时会执行双引号中的处理函数
checked:默认选中,同一组中只选中一个即可
5. label标签
label标签为input元素定义标注(标签),其作用是用于绑定一个表单元素,当点击label标签的时候,被绑定的表单元素就会获得输入焦点。
例如:
<label for="male">Male</label>
<input type="radio" name="sex" id="male" value="male">
注:这里for要跟input中的id一致
项目要求:
编程实现采集传感器数据和控制硬件设备(传感器和硬件通过slave模拟)
传感器两个:光线传感器、加速度传感器(x\y\z)
硬件设备两个:led灯、蜂鸣器
要求:
- 多任务编程:多线程
- 循环1s采集一次数据,并将数据打印到网页
- 同时从网页输入指令控制硬件设备
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 <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <errno.h>
#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,.$@.d -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 <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <sys/msg.h>
#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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdarg.h>
#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网页部分代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="js/xhr.js"></script>
<script>
function fun(obj) {
XHR.post('/cgi-bin/web.cgi', obj, function (x, info) { })
if (obj == "set=0 1") {
console.log("led灯打开\n");
}
else if (obj == "set=0 0") {
console.log("led灯关闭\n");
}
else if (obj == "set=1 1") {
console.log("蜂鸣器打开\n");
}
else if (obj == "set=1 0") {
console.log("蜂鸣器关闭\n");
}
}
function get_info() {
var v = document.getElementsByName("usrname");
//v[0].value="hello";
XHR.post('/cgi-bin/web.cgi', "get", function (x, info) {
console.log(info);
var v1 = info.split(" ");
// split()函数作用:使用一个指定的分隔符把一个字符串分割存储到数组
v[0].value = v1[0];
v[1].value = v1[1];
v[2].value = v1[2];
v[3].value = v1[3];
});
}
</script>
</head>
<body background="/2.jpg" style="background-size:100% 100%;background-attachment: fixed; ">
<h1 align="center">
<font color="white"><big>基于WebServer的工业数据采集项目</big></font>
</h1>
<h2 align="center">
<font color="white"><big>------信息采集------</big></font>
</h2>
<br>
<div align="center">
<!-- 块级元素,可以把文档分割为独立的、不同的部分 -->
<font color="white">
<strong>
<input type="button" name="flash" value="数据刷新" onclick="get_info()"
style="background-color: rgb(236, 223, 206)242, 231)">
<br>
<big><strong>光线传感器</strong></big>
<br>
光线传感器:  <input type="text" name="usrname" value="aaa">
<br>
<br>
<br>
<big><strong>加速度传感器</strong></big>
<br>
加速度传感器x:<input type="text" name="usrname" value="bbb"><br>
加速度传感器y:<input type="text" name="usrname" value="ccc"><br>
加速度传感器z:<input type="text" name="usrname" value="ddd"><br>
<!-- 用户密码:<input type="password" name="password" value="111">
<br> -->
<br>
<br>
<big>Modbus设备</big>
<br>
<br>
led灯状态:
开<input type="radio" name="status1" id="set=0 1" onclick="fun(id)">
关<input type="radio" name="status1" id="set=0 0" checked="checked" onclick="fun(id)">
<br>
蜂鸣器状态:
开<input type="radio" name="status2" id="set=1 1" onclick="fun(id)">
关<input type="radio" name="status2" id="set=1 0" checked="checked" onclick="fun(id)">
</strong>
</font>
</div>
</body>
</html>