目录
[Linux 多线程服务器](#Linux 多线程服务器)
一、前言
这个是前段时间做的一个项目,综合性很强,还是值得学习的。技术栈有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的代码,到时候更新一篇博客把源码展示出来。
需要源码的点个免费的关注,评论邮箱直接发!