ESP32在STA模式下创建TCP Sever允许多个Client建立连接TCP 通信并在客户端异常断开时关闭连接
TCP Sever创建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
- live555 testrtspclient客户端建立rtp over tcp 异常问题
- (1) linux 3.x - client - TCP连接建立系列 — 客户端发送SYN段
- (2) linux 3.x - client - TCP连接建立系列 — 客户端的端口选取和重用
- (3) linux 3.x - client - TCP连接建立系列 — 客户端接收SYNACK和发送ACK
- HttpClient出现TCP连接异常关闭发送RST包
- ESP8266 station模式下建立client、server TCP连接
- 【通信】TCP建立连接协议是三次握手,而关闭连接却是四次握手
- 利用asio实现了一个服务器,多个客户端连接,并异常断开连接,发现后面再也连接不上服务器了,不能建立新连接了。原因分析
- How to release the port of TCP Client immediately when the connection is disconnect with the TCP server. - TCP 客户端与 TCP 服务器断开连接后
- navicat 客户端连接oracle数据库 异常处理 can't load oci dll 和 Instant Client package...
- TCP的三次握手(建立连接)和四次挥手(关闭连接)
- TCP的三次握手(建立连接)和四次挥手(关闭连接)
- [网络原理]TCP连接是由客户端主动建立的吗?
- TCP连接建立断开
- TCP的三次握手(建立连接)和四次挥手(关闭连接)
- Python网络通信_tcp_client(客户端)
- TCP建立连接时三次握手,断开时为何4次握手
- TCP协议连接建立与连接断开过程(含断开时的TCP状态图)
- TCP的三次握手(建立连接)和四次挥手(关闭连接)
- Tcp 创建与断开连接过程