基于 TCP 的IOT物联网云端服务端和设备客户端通信架构设计与实现

一、项目背景与设计目标

在典型的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

相关推荐
创界工坊工作室6 小时前
DPJ-137 基于单片机的公交车自动报站系统设计(源代码+proteus仿真)
stm32·单片机·嵌入式硬件·51单片机·proteus
TDengine (老段)7 小时前
开放生态破局工业大数据困局:TDengine 的迭代升级与全链路数据自由流动
大数据·数据库·物联网·时序数据库·tdengine·涛思数据
濊繵7 小时前
Linux网络--IP 分片和组装的具体过程
linux·网络·tcp/ip
while(1){yan}7 小时前
HTTP的加密过程
java·开发语言·网络·网络协议·http·青少年编程
重生之我是Java开发战士7 小时前
【计算机网络】数据链路层:从帧传输到高速以太网的完整梳理
网络·网络协议·计算机网络
啃硬骨头7 小时前
Aurix TC387 Ethernet代码解析之六_MAC的LwIP初始化3
单片机·嵌入式硬件
尼罗河女娲7 小时前
【获取WebSocket】使用 Playwright 监听 Selenium 自动化测试中的 WebSocket 消息(二)
websocket·网络协议·selenium
福尔摩斯张7 小时前
【实战】C/C++ 实现 PC 热点(手动开启)+ 手机 UDP 自动发现 + TCP 通信全流程(超详细)
linux·c语言·c++·tcp/ip·算法·智能手机·udp
了一梨7 小时前
网络编程:UDP Socket
linux·网络协议·udp