基于Linux C语言多线程服务器+Qt客户端+STM32客户端实现的无人超市项目

目录

一、前言

二、项目演示

三、项目概述

3.1项目流程图

3.2项目简介

四、项目难点

[Linux 多线程服务器](#Linux 多线程服务器)

Qt管理员端

Qt用户端

STM32客户端


一、前言

这个是前段时间做的一个项目,综合性很强,还是值得学习的。技术栈有Linux系统编程(线程编程、进程间通信、网络编程作为socket服务端)、MySQL数据库(服务端和数据库进行交互,存储和读取数据)、Qt上位机开发(作为socket客户端)、STM32使用8266WIFI模块也作为socket客户端。这些内容我之前都有写过博客,可以去看相关博文:Linux网络编程Qt网络调试助手MySQL数据库开发

二、项目演示

无人超市项目

三、项目概述

3.1项目流程图

3.2项目简介

其中Qt我写了两个程序都作为socket的客户端(client),其中Qt管理员端主要负责商品和会员信息的注册和数据的修改工作。Qt用户端就是用户的结算界面,通过识别商品卡完成商品加入购物车这一动作,并计算总价格,结算的时候识别会员卡进行结算操作,并给Linux服务端发送数据以执行下一步操作,同时MySQL数据库的数据实时更新。STM32通过8266WiFi模块也作为socket的客户端(client),主要是给Linux服务端发送唯一的卡号,并接收开柜门的信息控制舵机旋转模拟商品出柜。

Linux服务端其实相当于数据的中转站,服务端程序创建三个线程对应三个客户端程序。MySQL数据库就是存储会员和商品的数据**(其实也可以用文件来代替,通过读取文件来操作数据;但是用文件管理数据会比较麻烦,如果会员或商品数据一多,就不知道要从哪里读,一口气全部读出来再查找数据费时费力)**。

四、项目难点

大佬的源码是用C++写的服务器,对于字符串的管理都是由std命名空间里的string来管理的,std::string 是一个封装了动态内存管理的类,通常不需要手动释放内存。我用C语言实现服务端代码就需要严格对内存进行管理,避免出现段错误(由于不会用gdb,因此只会在可能出现段错误的地方加上打印信息,然后又要重新将程序上传至虚拟机再编译执行,十分麻烦)。不过写完这么一个大型项目,能大大锻炼自己的调试能力以及学习面向对象的编程思想。

还有一个难点是单片机只负责上传卡号,我要怎么分辨这个卡号要执行什么操作?是已经注册的商品要加入购物车?还是要注册的会员或商品的ID?亦或者是结算时刷的会员卡?这里就需要一个全局变量,用到类似状态机 的方法,通过进程间通信里的信号Linux进程间通信:信号,通过改变要发送的信号执行对应的信号响应函数就好了。什么时候改变要发送的信号?例如Qt管理员端,点击商品注册页面的请求资源按键,会向Linux服务端发送数据,进而改变要发送的信号(全局变量),通过这种方式就能实现单片机只刷卡,但会执行该执行的操作。至于难点主要就这两个,其他都是一些细节的操作,比如客户端会有很多操作,要采用什么方式来处理这些数据?我采用了类似于数据报 的格式,就是"操作码+数据",服务端接收到数据后先提取操作码,再将数据分发到对应的函数,每个线程都是类似的操作。

在main函数的while循环里,会阻塞在accept这里等待客户端的连接请求,但是刷卡的时候会触发软件中断,导致accept函数退出,有可能会导致程序崩溃,我是用goto语句,一旦跳出accept函数就退出立马跳回来,代码如下:

cpp 复制代码
// 接受客户端连接
ret_client myserver_get_client_socket(Myserver* server) 
{
    ret_client ret;
reboot:
	
    server->client_socket = accept(server->server_socket, (struct sockaddr*)&ret.client_struct, &server->len);
    if (server->client_socket == -1) 
	{
        perror("accept interrupted by signal, retrying...\n-----------------\n");
        goto reboot; // 关键:立即重试
    }

    ret.client_socket = server->client_socket;
    return ret;
}

Linux 多线程服务器

学过网络编程的对于socket服务端的编写应该很熟悉了,我只是把它封装了一层,再结合Linux线程编程,每当有新的客户端连接请求,就创建一个新的线程与它接应,并传入必需的操作句柄,如何传入句柄这些数据?。我定义了一个Mythread结构体,模拟C++中的基类,里面只有一个函数指针,如下:

cpp 复制代码
#ifndef __MYTHREAD_H_
#define __MYTHREAD_H_

#include <stdio.h>
#include "myserver.h"

// 定义Mythread结构体,模拟C++中的基类
typedef struct {
    void (*thread_start)(ret_client *param);
} Mythread;


#endif

在每个线程的处理函数中都定义对应的线程结构体,其中就定义了Mythread的对象,这些线程结构体就模拟C++中的派生类;在main函数里,接收到了客户端的连接请求,就调用对应线程的初始化函数,传入线程结构体的指针,将结构体里定义的Mythread函数指针指向实现好的线程启动函数,再在main函数里通过传入的线程结构体指针调用线程启动函数即可。演示如下:

总之服务端就像一个消息的中转站,通过和MySQL服务器进行连接,服务器通过客户端发送的数据的操作码来执行对应的函数,main.c如下:

cpp 复制代码
#include "myserver.h"
#include "mythread.h"
#include "managerthread.h"
#include "customerthread.h"
#include "mcuthread.h"

#define SERVER_IP "192.168.254.128"
#define SERVER_PORT 8888

int RES = 34;

int main() 
{
	/*--------------------------*/
	/*数据库初始化相关操作*/
    // 初始化数据库连接对象
    SQLifconfig *MySQL_Handler = SQLifconfig_init();
    if (!MySQL_Handler) {
        printf("初始化数据库失败\n");
        return -1;
    }
    // 连接数据库
    if (!SQLifconfig_SQL_init(MySQL_Handler, "127.0.0.1", "root", "123456", "supermarket")) {
        printf("连接数据库失败\n");
        SQLifconfig_destroy(MySQL_Handler);
        return -1;
    }
	/*--------------------------*/

	/*--------------------------*/
	/*服务器初始化相关操作*/
	//初始化服务器
	Myserver *server = myserver_init(AF_INET, SOCK_STREAM, 0);
	// 启动服务器
    myserver_start(server, SERVER_IP, SERVER_PORT, AF_INET);
	/*--------------------------*/

	while(1)
	{
		// 接受客户端连接
	    ret_client client_ret = myserver_get_client_socket(server);
	    printf("New client connected: client socket fd = %d\n", client_ret.client_socket);
		client_ret.MySQL_Handler = MySQL_Handler;

		// 读取客户端数据
	    char* buf = myserver_readbuf(server);
		unsigned long num = -1;
	    if (buf)
		{
	        printf("Received data: %s\n", buf);
			num = atoi(buf);  // 将字符串转换为整数
    		printf("Converted number: %ld\n", num);
	        free(buf);
	    } 
		else
		{
	        printf("Failed to read data\n");
	    }

		switch (num)
		{
			case 100101:
			{
				Managerthread *manager_thread = (Managerthread *)malloc(sizeof(Managerthread));
				managerthread_init(manager_thread);
				manager_thread->base.thread_start(&client_ret);
				printf("Qt管理员端连接成功: %ld\n", num);
				break;
			}
			case 100111:
			{
				Customerthread *customer_thread = (Customerthread *)malloc(sizeof(Customerthread));
				customerthread_init(customer_thread);
				customer_thread->base.thread_start(&client_ret);
				printf("Qt用户端连接成功: %ld\n", num);
				break;
			}
			case 101001:
			{
				Mcuthread *mcu_thread = (Mcuthread *)malloc(sizeof(Mcuthread));
				mcuthread_init(mcu_thread);
				mcu_thread->base.thread_start(&client_ret);
				printf("STM32客户端连接成功: %ld\n", num);
				break;
			}
			default:
         		printf("Unknown case: %ld\n", num);   
         	break;
		}
	}

	// 销毁服务器
    myserver_destroy(server);
    
    //释放数据库连接对象
    SQLifconfig_destroy(MySQL_Handler);
    return 0;
}

Qt管理员端

界面:

功能:会员注册、查询、充值、注销操作;商品的添加、删除操作;查看销售记录(以文本的形式记录在Linux服务端);查看操作日志(当管理员端停止运行后消失,只记录启动后的会员和商品的操作)

启动后每两秒尝试连接到服务器端,连接失败继续尝试重连。连接成功后先向服务器发送一段数据,创建与其对接的线程,当点击会员卡管理和添加新商品页面的请求资源按键时,单片机刷卡后会将卡号填充到对应的lineEdit 文本框内(上面说到的发送**"信号"**),后续可以给销售记录文本做一个大小的限制,避免占用过多的内存;还可以在STM32下位机添加一个GPS模块,得到经纬度信息后在Qt管理员端显示地图,查看是哪一家店完成了销售。

Qt用户端

界面:

功能:识别商品卡进行商品入购物车的操作;结算;删除不要的商品;从服务器获得STM32客户端上传的温湿度信息。

客户端就比较简单了,就是平时展示给消费者的页面,点击结算案件后等待用户刷会员卡,然后将购买信息上传到服务端,服务端从数据库中读取数据,进行余额的比较后再进行下一步操作;可能会因为余额不足结算失败,这时候就要去管理员端充值后再来结算;结算成功会打印用户消费信息。

STM32客户端

这个项目我一开始是用的网络调试助手来模拟STM32的,因为关键步骤就是刷卡嘛,就提前写好几个卡号然后发给服务器就好了。因此STM32的代码我还没写,这里就展示一下部分代码,DHT11温湿度模块和舵机的操作就很简单了,就是while循环读,RFID刷卡模块也是放在while循环里面读取卡号。

main.c的while循环如下:

cpp 复制代码
while(1) 
{			
    if(!RC522_cardScan(cardID))
    {
        send_idcard(cardID);
    } 
		
    Delay_ms(250);
		
    if(GPS_Flag==1)  
    {     
        send_gps();
    }
		 
    Delay_ms(350);
		 
    if(Read_DHT11(&DHT11_Data) == SUCCESS)
    {
        send_wunshidu();
    }
		
    Delay_ms(250);
		
    if(Serial_Flag==1)
    {
        My_Servo_SetAngle();
    }
}

8266WiFi模块配置如下,直接连接到Tcp服务端:

cpp 复制代码
#include "8266wifi.h"
#include <stdio.h>

void Wifi_TCP_Init(void)
{
    Serial_Init();
		
	Serial_SendString("AT+RST\r\n");
	Delay_s(1);
	
	Serial_SendString("AT+CWMODE=3\r\n");
	Delay_s(1);
	
	Serial_SendString("AT+CWJAP=\"sakabu\",\"12345678\"\r\n");//连接热点
	Delay_s(5);

	Serial_SendString("AT+CIPSTART=\"TCP\",\"192.168.254.128\",8888\r\n");
	Delay_s(4); 
	
	Serial_SendString("AT+CIPMODE=1\r\n");
	Delay_s(1); 
	
	Serial_SendString("AT+CIPSEND\r\n");
	Delay_s(1); 
	
	Serial_SendString("101001");
	Delay_s(1); 
}

后续我会写一下STM32的代码,到时候更新一篇博客把源码展示出来。

需要源码的点个免费的关注,评论邮箱直接发!

相关推荐
二进制人工智能1 小时前
【QT5 网络编程示例】TCP 通信
网络·c++·qt·tcp/ip
柒月玖.2 小时前
基于AT89C52单片机的轮胎压力监测系统
单片机·嵌入式硬件·mongodb
opentrending2 小时前
Github 热点项目 awesome-mcp-servers MCP 服务器合集,3分钟实现AI模型自由操控万物!
服务器·人工智能·github
多多*3 小时前
Java设计模式 简单工厂模式 工厂方法模式 抽象工厂模式 模版工厂模式 模式对比
java·linux·运维·服务器·stm32·单片机·嵌入式硬件
Android洋芋3 小时前
C语言深度解析:从零到系统级开发的完整指南
c语言·开发语言·stm32·条件语句·循环语句·结构体与联合体·指针基础
bjxiaxueliang3 小时前
一文详解QT环境搭建:Windows使用CLion配置QT开发环境
开发语言·windows·qt
此刻我在家里喂猪呢4 小时前
qt介绍tcp通信
qt·tcp/ip
Run_Teenage4 小时前
C语言 【初始指针】【指针一】
c语言·开发语言
Guarding and trust4 小时前
python系统之综合案例:用python打造智能诗词生成助手
服务器·数据库·python
南鸳6104 小时前
Linux常见操作命令(2)
linux·运维·服务器