ESP32在STA模式下创建TCP Sever允许多个Client建立连接TCP 通信并在客户端异常断开时关闭连接

前言

ESP32是乐鑫现在主推的一款WIFI模块,价格比较合适,文档也比较清晰。IDF框架下开发也比较容易。在网上之前没有搜索到TCP服务器允许多TCP 连接的例程。这里简单的做一个笔记,希望能给各位带来启发!

例程详情

ESP32通过STA模式连接路由器(smartconfig方式连接)后开启TCP Server 最多可允许 2 个Client 连接成功并通信,并且可以 listen 队列中挂起一个连接队列,被挂起的队列未被accept既不能通信,但不同的调试助手会有不一样的反馈,有的会显示连接成功,并在断开两个accpet允许的连接其中一个后(这个时候既释放了一个空余资源)之前被挂起的队列可以被accept 且其接收到的内容也会被listen维护在队列中 内容大于取决于相应的堆栈大小。还有一种助手就如果连接未被accept助手便会提示失败。

你需要做哪些准备工作?

首先你必须已安装了乐鑫的IDF和相应的工具链等。

根据官方链接中 设置工具链, 下载对应的 toolchain. 这里建议详细看完官网上的九步。

本例程是用 eclipseIDE 来编写的,根据Eclipse IDE 的创建和烧录指南 指示的步骤来安装和配置。 eclipse可以方便的编写和烧录程序。但观察日志输出建议还是通过工具链 $ make monitor 来观察,更直观!

当然你最好得有一块乐鑫 esp32的开发板,至少你得有个esp32的模块。此例程在 ESP32-MeshKit-Sense 上运行,但在其它的ESP32环境中应该也可以正常运行的,这里是用 ESP-Prog烧录的

如何让该例程运行起来

1.编译:你可以通过工具链 ‘cd’ 到相应目录后 make all 你也可以通过 eclipse 导入工程再进行相应的配置后 右击工程目录 Build project.
2.烧录:你可以通过工具链 ‘cd’ 到相应目录后 make flash 你也可以通过 eclipse 右击工程目录 Build Target -> Create -> 在Target name中填入 ‘flash’ 点击 'OK’后
你可以直接按Shift+F9 来烧录程序
3.调试:你可以通过网上下载“串口调试助手” 手机上也可以下载对应版本的“串口调试助手”可以开启多个TCP Client来连接到我们的TCP Server
这里需要注意的是我们TCP Server的IP地址是连接路由器后分配的。在日志中 hello_world: GOT IP :172.27.35.26 可看到IP地址 此地址也是我们TCP Server的IP地址 端口固定是7681
4.日志:你可以通过工具链 'make flash monitor '来下载并在下载成功后自动开启日志。如果已通过eclipseIDE Shift+F9下载 可直接 ‘make monitor’

程序的流程

本例程做得比较粗糙有一些都是直接从 esp-idf/examples中复制过来的

app_main 初始化一些东西 执行后会返回delte_task();如果你不希望删除默认任务app_main中不可返回 此任务的堆栈大小可以 make menuconifg相应位置里更改
|
|
调用wifi_init_sta 初始化wifi 判断是否需要smartconfig联网 在回调函数中如果多次连接失败也会开启smartconifg
|
|
联网 got ip 后创建一个socket
|
|
bind 这个socket到 7681 端口
|
|
开启listen监听监听此设备上的 7681 端口 这里注意listen (int socket, int n) 第一个参数是我们创建的套接字描述符第二个参数代表可挂机的队列个数超过了后客户端连接返回失败
|
|
如果还有任务可建就可以 accpet listen队列 注意这里也会返回一个套接字描述符,注意此返回的和listen参数描述符是有区别的
此返回的套接字描述符是对应的相应的TCP连接我们要通过这个描述符出通信

建立TCP Server部分

此任务当获取IP后通过int socket (int namespace, int style, int protocol) 获取套接字描述符
通过 int bind (int socket, struct sockaddr *addr, socklen_t length) 来绑定端口
通过 int listen (int socket, int n)开启监听
此函数返回一个套接字描述符 要注意和socket()获取的描述符区分开来。
通过计数信号量来获取剩余资源(本例程有两个可以连接的资源可连接2个Client)
当有可用资源的时候才 accept 相应的连接,并建立对应的任务来处理通信

static void test_task(void* args)
{

	UBaseType_t taskCount 	= 0;
	char addr_str[128];
	EventBits_t uxBits;
	char taskName[20];
	int addr_family;
	int ip_protocol;
	struct hostent *hostP = NULL;

	ESP_LOGI(TAG,"test_task is running");
	while(1){
		taskCount++;
		ESP_LOGW(TAG,"test_task is running  %d",taskCount);

		if(wifi_EventHandle != NULL){
			uxBits = xEventGroupWaitBits(wifi_EventHandle,CONNECTED_BIT,false,true,portMAX_DELAY);
			 if((uxBits & CONNECTED_BIT) != 0){
				ESP_LOGI(TAG,"Test task Event CONNECTED_BIT is received!");

				addr_family 			 = AF_INET;
				ip_protocol 			 = IPPROTO_IP;
				// 本地IP设为0 应该底层会自动设置本地IP 端口固定到7681
				struct sockaddr_in localAddr;
				localAddr.sin_addr.s_addr 	= htonl(INADDR_ANY);
				localAddr.sin_family		= AF_INET;
				localAddr.sin_port			=htons(7681);


				//新建一个 socket
				int listen_sock = socket(addr_family, SOCK_STREAM, ip_protocol);
				if (listen_sock < 0) {
					ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
					break;
				}
				//把socket绑定到7681端口 地址设为0表示此设备上的所有的IP的7681端口
				int err = bind(listen_sock, (struct sockaddr *)&localAddr, sizeof(localAddr));
				if (err < 0) {
					ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
				}
				ESP_LOGI(TAG, "Socket created");
				
				//开启监听 监听7681端口
				err = listen(listen_sock,0);
				if(err != 0){
					ESP_LOGI(TAG,"Socket unable to connect: errno %d", errno);
				}
				ESP_LOGI(TAG,"Socket is listening");
				//为accpet连接传入参数初始化
				struct sockaddr_in6 sourceAddr;
				uint addrLen = sizeof(sourceAddr);

				while (1) {
					//获取信号量,这里先阻滞portMAX_DELAY
					if(CountHandle != NULL){
						xSemaphoreTake(CountHandle,portMAX_DELAY);
						UBaseType_t semapCount = uxSemaphoreGetCount(CountHandle);
						ESP_LOGI(TAG,"Semaphore take success semapCount is:%d",semapCount);
					}else ESP_LOGW(TAG,"SemaphoreHandle is NULL");

					//accept是会阻滞任务的  如果SemaphorTake 也一直阻滞不知道行不行。
					int sock = accept(listen_sock, (struct sockaddr *)&sourceAddr, &addrLen);
					if (sock < 0) {
						ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
						break;
					}
					ESP_LOGI(TAG, "Socket accepted sock is %d",sock);
					//获取到accept的IP sock 端口信息保存
					struct sockinfo remoteInfo;
					remoteInfo.sock = sock;
					if(sourceAddr.sin6_family == PF_INET){
						remoteInfo.remoteIp = inet_ntoa_r(((struct sockaddr_in *)&sourceAddr)->sin_addr.s_addr,addr_str,sizeof(addr_str) - 1);
						remoteInfo.sa_familyType = PF_INET;

					}else if(sourceAddr.sin6_family == PF_INET6){
						remoteInfo.remoteIp = inet6_ntoa_r(sourceAddr.sin6_addr,addr_str,sizeof(addr_str) - 1);
						remoteInfo.sa_familyType = PF_INET6;
					}
					remoteInfo.remotePort = ntohs(sourceAddr.sin6_port);
					//等待获取空余资源(空余的可建立任务)
					uxBits = xEventGroupWaitBits(wifi_EventHandle,TASK1_BIT|TASK2_BIT,false,false,portMAX_DELAY);
					ESP_LOGW(TAG,"Test task wait TASK_BIT ok!!");
					for(int i = 0; i < MAX_SOC_COUNT; i++){
						if((uxBits & (1 << (i + 1))) != 0){ //这里i + 1是因为 事件标志组的最后一位是由CONNECT_BIT所占用 TASK2_BIT是从第BIT1开始
							sprintf(taskName,"Tcp_Client%d",i+1);
							//打印remoteInfo的内容然后再建立任务
							ESP_LOGI(TAG,"Currently socket NO:%d IP is:%s PORT is:%d",sock,remoteInfo.remoteIp,remoteInfo.remotePort);
							portBASE_TYPE res1 = xTaskCreate(taskList[i], taskName,
																2048, (void *)&remoteInfo,
																7, NULL);
							assert(res1 == pdTRUE);
							break; //如果成功的创建了一个任务就应该结束本次查找了
						}
					}
					vTaskDelay(200 / portTICK_PERIOD_MS);
				}
				if (listen_sock != -1) {
					ESP_LOGE(TAG, "Shutting down listen_socket and restarting...");
					shutdown(listen_sock, 0);
					close(listen_sock);

				}
			}
		}else{
			ESP_LOGW(TAG,"wifi_EventHandle is NULL!");
		}
		vTaskDelay(5000 / portTICK_PERIOD_MS); //5秒钟后再重新执行
		//taskYIELD();
	}
	ESP_LOGE(TAG,"test_task something ERROR inside");
	vTaskDelete(NULL);
}

连接通信处理部分

每一个客户端被允许进来都创建一个任务来处理通信(由于recv()是阻滞的),当然还有更好的做法,我这里抛砖引玉引出大神来指点一二了。

static void Tcp_Client1(void *args)
{

	char rx_buffer[128];
	struct sockinfo remoteInfo;
	// 给remoteInfo.remoteIp 开辟一定的空间 且注意释放
	remoteInfo.remoteIp = (char *)heap_caps_malloc(32,MALLOC_CAP_8BIT);
	memset(remoteInfo.remoteIp,0,32);

	remoteInfo.sock			= ((struct sockinfo *)args)->sock;
	remoteInfo.remotePort 	= ((struct sockinfo *)args)->remotePort;
//	ESP_LOGE(TAG,"remoteIP len is %d",strlen(((struct sockinfo *)args)->remoteIp));

	memcpy(remoteInfo.remoteIp,((struct sockinfo *)args)->remoteIp,strlen(((struct sockinfo *)args)->remoteIp));
	//打印出来
	ESP_LOGI(TAG,"Tcp_Client1 args is %d",remoteInfo.sock);
	ESP_LOGI(TAG,"Tcp_Client1 was created and task name is:%s",pcTaskGetTaskName(NULL));
	ESP_LOGI(TAG,"Tcp_Client1 socket NO:%d IP is:%s PORT is:%d",remoteInfo.sock,remoteInfo.remoteIp,remoteInfo.remotePort);

	EventBits_t res = xEventGroupClearBits(wifi_EventHandle,TASK1_BIT);
	if((res & TASK1_BIT) != 0) ESP_LOGI(TAG,"TASK1_BIT cleared successfully");
	else{ 
			ESP_LOGE(TAG,"TASK1_BIT clear failed"); //如果再执行10次都不成功那么就日志失败
	}

	int keepAlive = 1; // 开启keepalive属性

	int keepIdle = 20; // 如该连接在60秒内没有任何数据往来,则进行探测

	int keepInterval = 5; // 探测时发包的时间间隔为5 秒

	int keepCount = 3; // 探测尝试的次数.如果第1次探测包就收到响应了,则后2次的不再发.

	setsockopt(remoteInfo.sock,SOL_SOCKET,SO_KEEPALIVE,	(void *)&keepAlive,		sizeof(keepAlive));
	setsockopt(remoteInfo.sock,IPPROTO_TCP,TCP_KEEPIDLE,	(void *)&keepIdle,		sizeof(keepIdle));
	setsockopt(remoteInfo.sock,IPPROTO_TCP,TCP_KEEPINTVL,(void *)&keepInterval, 	sizeof(keepInterval));
	setsockopt(remoteInfo.sock,IPPROTO_TCP,TCP_KEEPCNT,	(void *)&keepCount, 	sizeof(keepCount));

	previousSock = 0;
	while(1)
	{
		int len = recv(remoteInfo.sock,rx_buffer,sizeof(rx_buffer) - 1,0);
		//连接错误
		if(len < 0){
			ESP_LOGE(TAG,"Tcp_Client1 Recv failed errno :%d",errno);
			break;
		}
		//连接断开
		else if(len == 0){
			ESP_LOGW(TAG,"Tcp_Client1 Connection closed Tcp_Client1");
			break;
		}
		//收到数据
		else{

			rx_buffer[len] = 0; //结束指向空,不管我们接收到什么我们都把它视为一个数组
			if(previousSock != remoteInfo.sock){
				ESP_LOGW(TAG,"Tcp_Client1 Received %d bytes form %s:%d",len,remoteInfo.remoteIp,remoteInfo.remotePort); //打印出我们获得的数组长度和来源地址端口等
				previousSock = remoteInfo.sock;
			}
			ESP_LOGI(TAG,"%s",rx_buffer);

			int err = send(remoteInfo.sock, rx_buffer, len, 0);
			if (err < 0) {
				ESP_LOGE(TAG, "Tcp_Client1 Error occured during sending: errno %d", errno);
				break;
			}
		}
//		vTaskDelay(100 / portTICK_PERIOD_MS);
		taskYIELD();
	}
	if (remoteInfo.sock != -1) {
		ESP_LOGW(TAG, "Tcp_Client1 Shutting down socket");
		shutdown(remoteInfo.sock, 0);
		close(remoteInfo.sock);
	}

	if(CountHandle != NULL){
		if(xSemaphoreGive(CountHandle) != pdTRUE){
			ESP_LOGE(TAG,"Tcp_Client1 Try to Give semaphore and failed!");
		}else ESP_LOGI(TAG,"Give semaphore success!");
	}
	if(wifi_EventHandle != NULL){

		EventBits_t uxBits = xEventGroupSetBits(wifi_EventHandle,TASK1_BIT);
		if((uxBits & TASK1_BIT) != 0)  	ESP_LOGI(TAG,"Tcp_Client1 set event bit ok");
		else							ESP_LOGI(TAG,"Tcp_Client1 set event bit failed");
	}else ESP_LOGE(TAG,"Tcp_Client1 wifi_EventHandle is NULL");
	ESP_LOGE(TAG,"Tcp_Client1: Something error occurred or connect close,Ready to clear EventGroup and delete Task");
	previousSock = 0;
	vTaskDelete(NULL);
}

结语。

由于个人能力有限难免会出现各种各样的错误,如果您发现哪里的错误,敬请告知万分感谢,我会立马更改!
本文中关于TCP客户端在异常断开连接时关闭对应socket参考于这里
本文代码参考于乐鑫examples
如需查看本文的详细代码请 移步于github

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要使用ESP8266的STA模式将ADC采集到的值发送给TCP服务器,你可以按照以下步骤进行操作: 1. 配置ESP8266的STA模式连接到你的Wi-Fi网络。你可以使用ESP8266的AT指令或者ESP8266的API库来实现这一步骤。确保ESP8266成功连接到Wi-Fi网络。 2. 使用适当的ADC库或者代码获取ADC的采样值。这取决于你使用的具体硬件和编程语言。确保你可以获取到正确的ADC采样值。 3. 使用TCP客户端代码将ADC采样值发送到TCP服务器。以下是一个示例代码,使用ESP8266的AT指令将数据发送到TCP服务器: ```c #include <SoftwareSerial.h> SoftwareSerial esp(10, 11); // 设置ESP8266的串口引脚 void setup() { Serial.begin(9600); // 设置串口波特率 esp.begin(9600); // 初始化ESP8266的串口通信 delay(1000); // 等待ESP8266启动 // 连接到Wi-Fi网络 esp.println("AT+CWJAP=\"你的WiFi名称\",\"你的WiFi密码\""); delay(5000); // 等待连接成功 // 建立TCP连接 esp.println("AT+CIPSTART=\"TCP\",\"服务器IP地址\",服务器端口"); delay(5000); // 等待建立连接 } void loop() { // 获取ADC采样值 int adcValue = analogRead(A0); // 发送ADC采样值到TCP服务器 esp.print("AT+CIPSEND="); esp.println(adcValue); delay(1000); // 等待发送完成 // 断开TCP连接 esp.println("AT+CIPCLOSE"); delay(5000); // 等待断开连接 delay(5000); // 等待一段间再进行下一次采样和发送 } ``` 在上述示例代码中,我们使用了SoftwareSerial库来实现与ESP8266的串口通信。你需要根据实际连接的引脚进行修改。 在`setup`函数中,我们首先连接到Wi-Fi网络,并等待连接成功。然后,建立TCP连接,并等待连接建立完成。 在`loop`函数中,我们获取ADC采样值,并使用AT指令将其发送到服务器。然后,断开TCP连接,并等待一段间再进行下一次采样和发送。 请注意,你需要将代码中的以下部分替换为你的实际值: - `"你的WiFi名称"`:你的Wi-Fi网络名称 - `"你的WiFi密码"`:你的Wi-Fi密码 - `"服务器IP地址"`:TCP服务器的IP地址 - `"服务器端口"`:TCP服务器的端口号 这是一个基本的示例,你可以根据需要进行修改和扩展。同,建议使用更可靠的方法(如使用ESP8266的API库)来实现与ESP8266的通信,以提高稳定性和灵活性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值