一、项目背景与设计目标
在典型的IOT物联网应用中,嵌入式硬件设备(如 ESP8266 / ESP32)往往部署在内网或复杂网络环境中,而控制端(PC / 手机 / 上位机)需要通过云端服务器与这些设备进行远程通信。
IOT物联网通常都是要实现以下功能:
-
支持多个硬件客户端同时在线
-
支持软件控制端与指定硬件设备通信
-
支持云端服务器对客户端进行统一管理与数据转发
-
协议简单、可扩展、适合 MCU 侧实现
二、通信协议设计
为了降低 MCU 端解析复杂度,协议采用 定长二进制结构体 ,并通过 #pragma pack(1) 保证字节对齐。
1.定义存放客户端传输数据的结构体
#pragma pack(1) //以下结构体以一个字节对齐
//定义存放客户端传输数据的结构体
struct SocketRxTxData
{
unsigned char FrameHead[4]; //存放帧头数据, 固定为: 0xA1 0xA2 0xA3 0xA4
unsigned char Databuffer[30]; //存放传输的字符串数据
unsigned int id[3]; //存放96位ID号
unsigned char clientmode; //0:表示硬件客户端 1:表示软件控制客户端
unsigned int CheckSum; //存放数据位的校验和
};
协议设计要点:
固定帧头:用于快速丢弃非法数据
96 位 ID:唯一标识一组通信对象
clientmode:区分控制端与被控端
简单累加校验和:适合 MCU 实时计算
二、IOT物联网服务端实现
1. 服务器核心任务
(1)监听指定TCP端口,接受客户端连接。
(2)为每个TCP客户端创建独立线程。
(3)保存客户端的信息(ID / fd / 类型)。
(4)根据协议规则转发数据。
2. 多客户端并发连接架构实现
服务器采用:一个主线程 + 多个子线程 的架构
一个主线程 负责TCP服务端的创建、监听和接收客户端。
多个子线程 负责每一个客户端的数据收发处理。
/*TCP服务器代码*/
int main(int argc,char *argv[])
{
if(argc != 2)
{
printf("传参格式:./app <端口号>\n");
return -1;
}
//相关变量定义
pthread_t thread_id; //存放线程的标识符
struct sockaddr_in client_address; //存放客户端的信息
socklen_t address_len; //存放客户端结构体信息的长度
int socketfd; //保存TCP服务器的网络套接字
int *clientfd; //保存TCP客户端的网络套接字
struct sockaddr_in server_address; //存放服务器的IP地址信息
memset(&server_address,0,sizeof(struct sockaddr_in)); //初始化内存空间
memset(&client_address,0,sizeof(struct sockaddr_in)); //初始化内存空间
server_address.sin_family=PF_INET; //IPV4协议
server_address.sin_port=htons(atoi(argv[1])); //端口号赋值
server_address.sin_addr.s_addr=INADDR_ANY; //本地IP地址
/*初始化信号量*/
sem_init(&lock,0,1); //信号量的值初始为 1,表示资源可用
//创建链表头
TCP_ClientInfoListHead=CreateListHead(TCP_ClientInfoListHead);
/*1 .创建套接字*/
socketfd=socket(PF_INET,SOCK_STREAM,0);
if(socketfd<0)
{
printf("服务器网络套接字创建失败!\n");
return -1;
}
int on = 1;
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
/*2. 绑定端口,创建服务器*/
if(bind(socketfd,(const struct sockaddr *)&server_address,sizeof(struct sockaddr))!=0)
{
printf("服务器绑定端口失败!\n");
return -1;
}
/*3. 设监听的端口数量*/
if(listen(socketfd,100)!=0)
{
printf("服务器端口监听失败!\n");
return -1;
}
int i;
while(1)
{
address_len=sizeof(struct sockaddr); //计算结构体大小 20
//申请空间,线程并发执行不能使用变量地址传参数
clientfd=malloc(sizeof(int));
/*4. 等待客户端连接*/
if((*clientfd=accept(socketfd,(struct sockaddr *)&client_address,&address_len))<0)
{
printf("等待客户端连接失败!\n");
break;
}
/*打印一些客户端的信息*/
printf("成功连接的客户端端口号:%d\n",ntohs(client_address.sin_port));
printf("成功连接的客户端IP地址:%s\n",inet_ntoa(client_address.sin_addr));
//创建线程
if(pthread_create(&thread_id,NULL,pthread_func,(void*)clientfd)!=0)
{
printf("线程_%d_创建失败!\n",i);
}
pthread_detach(thread_id); //设置分离属性,自己回收资源
}
sem_destroy(&lock); //注销信号量
return 0;
}
3. 客户端信息管理(链表)
服务器通过 单向链表 维护所有在线客户端:
/*------------------------------------------------------------------------------*/
#pragma pack(1) //以下结构体以一个字节对齐
//定义存放客户端信息的结构体
struct SocketTcpClient
{
unsigned int id[3]; //存放96位ID号
int clientfd; //存放客户端文件描述符
unsigned char clientmode; //存放客户端区分标志 0:表示硬件客户端 1:表示软件客户端
struct SocketTcpClient *next; //定义存放下一个地址的成员
};
-
客户端首次发送合法数据包后加入链表
/*
函数功能:链表结尾添加新的节点
函数参数:
head:链表头
NewData:要添加进去的结构体数据
*/
void AddNewListNode(struct SocketTcpClient *head,struct SocketTcpClient NewData)
{
struct SocketTcpClient *p=head; //保存链表头
struct SocketTcpClient *tmp=NULL; //新链表节点
/1. 找到链表结尾/
while(p->next!=NULL)
{
p=p->next;
}
/2. 申请新节点/
tmp=malloc(sizeof(struct SocketTcpClient)); //申请新的节点空间
memcpy(tmp,&NewData,sizeof(struct SocketTcpClient)); //拷贝结构体数据
tmp->next=NULL; //结尾指向空
/3. 添加新节点到链表结尾/
p->next=tmp;
} -
客户端断开连接时,从链表中删除
/*
函数功能: 根据文件描述符删除指定的链表节点
函数参数:
head :链表头
clientfd:文件描述符
*/
void DeleteListNode(struct SocketTcpClient *head,int clientfd)
{
struct SocketTcpClient *p=head; //保存链表头
struct SocketTcpClient *tmp=NULL; //保存链表地址节点
/1. 查找要删除的链表节点/
while(p->next!=NULL)
{
tmp=p; //保存上一个节点的地址
p=p->next;
if(p->clientfd==clientfd) //查找到节点
{
/2. 删除节点/
tmp->next=tmp->next->next; //连接节点
free(p); //释放节点空间
break;
}
}
}
4. 数据转发逻辑(核心)
服务器并不关心数据内容,仅根据规则转发:ID相同的两个客户端但TCP套接字不一样。
这使得:
-
一个软件客户端可以控制指定硬件设备
-
多组设备之间互不干扰
-
服务器逻辑高度通用
/---------------------------------------遍历链表向符合条件客户端发送数据-----------------------------/
int SendDataToClient(struct SocketRxTxData data, int clientfd)
{
int cnt = 0;
struct SocketTcpClient p = TCP_ClientInfoListHead;
while(p != NULL)
{
if(p->id[0]==data.id[0]&&p->id[1]==data.id[1]&&p->id[2]==data.id[2]&&p->clientfd!=clientfd&&p->clientmode!=data.clientmode) //查找到节点
{
//存在就转发数据
write(p->clientfd,(void)&data,sizeof(struct SocketRxTxData));
cnt++;
}
p = p->next;
}
return cnt;
}
三、IOT物联网客户端实现
1. 客户端核心任务
TCP 客户端主要用于:
(1)连接云端服务器
(2)定期发送数据帧
(3)接收并解析服务器转发的数据
2. 客户端实现
简单来说就是:创建TCP客户端→连接TCP服务端→收发数据→校验数据→解析数据
/*
TCP客户端创建
*/
int main(int argc,char **argv)
{
struct SocketRxTxData RxTxData;
int tcp_client_fd; //客户端套接字描述符
int Server_Port; //服务器端口号
struct sockaddr_in tcp_server; //存放服务器的IP地址信息
int rx_len;
struct SocketRxTxData socket_data;
struct SocketRxTxData rx_data;
fd_set readfds;
struct timeval timeout;
if(argc!=7)
{
printf("TCP客户端形参格式:./tcp_client <服务器IP地址> <端口号> <stringData> <id1> <id2> <id3>\n");
return -1;
}
/*1. 创建网络套接字*/
tcp_client_fd=socket(AF_INET,SOCK_STREAM,0);
if(tcp_client_fd<0)
{
printf("TCP服务器端套接字创建失败!\n");
return -1;
}
/*2. 连接到指定的服务器*/
tcp_server.sin_family=AF_INET; //IPV4协议类型
tcp_server.sin_port=htons(atoi(argv[2]));//端口号赋值,将本地字节序转为网络字节序
tcp_server.sin_addr.s_addr=inet_addr(argv[1]); //IP地址赋值
if(connect(tcp_client_fd,(const struct sockaddr*)&tcp_server,sizeof(const struct sockaddr))<0)
{
printf("TCP客户端: 连接服务器失败!\n");
return -1;
}
//封装结构体
SetPackageData(&socket_data,argv[3],atoi(argv[4]),atoi(argv[5]),atoi(argv[6]));
while(1)
{
FD_ZERO(&readfds); //清除文件描述符集合
FD_SET(tcp_client_fd,&readfds); //设置监听的文件描述符
timeout.tv_sec=1;
timeout.tv_usec=100;
/*监控是否有对应的事件发生*/
rx_len=select(tcp_client_fd+1,&readfds,NULL,NULL,&timeout);
if(rx_len>0) //有数据
{
rx_len=read(tcp_client_fd,&rx_data,sizeof(struct SocketRxTxData));
if(rx_len==sizeof(struct SocketRxTxData))
{
if(CheckPackageData(rx_data)==0)
{
printf("rx=%s,%d,%d,%d\n",rx_data.Databuffer,rx_data.id[0],rx_data.id[1],rx_data.id[2]);
}
}
if(rx_len==0)break; //客户端断开连接
}
if(rx_len<0)break; //出现错误
if(rx_len==0) //没有事件产生,等待超时
{
//向服务器发送数据
write(tcp_client_fd,&socket_data,sizeof(struct SocketRxTxData));
}
}
/*4. 关闭连接*/
close(tcp_client_fd);
}
数据封包和校验:
/*
函数功能: 数据封包
*/
void SetPackageData(struct SocketRxTxData *p,char *str,int id1,int id2,int id3)
{
int i;
p->FrameHead[0]=0xA1;
p->FrameHead[1]=0xA2;
p->FrameHead[2]=0xA3;
p->FrameHead[3]=0xA4;
memcpy(p->Databuffer,str,30);
p->CheckSum=0;
p->id[0]=id1;
p->id[1]=id2;
p->id[2]=id3;
p->clientmode = 1;//标志为软件客户端
//赋值校验和
for(i=0;i<30;i++)
{
p->CheckSum+=p->Databuffer[i];
}
}
/*
函数功能:判断传输的数据是否正确
函数返回值: 0正确 其他值错误
*/
int CheckPackageData(struct SocketRxTxData ClientRxTxData)
{
int i;
unsigned int CheckSum=0; //存放校验和
/*1. 判断帧头是否正确*/
if(ClientRxTxData.FrameHead[0]!=0xA1||
ClientRxTxData.FrameHead[1]!=0xA2||
ClientRxTxData.FrameHead[2]!=0xA3||
ClientRxTxData.FrameHead[3]!=0xA4)
{
return -1;
}
/*2. 计算校验和*/
for(i=0;i<30;i++)
{
CheckSum+=ClientRxTxData.Databuffer[i];
}
if(CheckSum!=ClientRxTxData.CheckSum) //校验失败
{
return -1;
}
return 0; //数据校验成功
}
四、IOT服务端客户端源码附件
【免费】IOT物联网服务端和客户端搭建代码资源-CSDN下载
https://download.csdn.net/download/qq_34885669/92470649
