引言:

Socket(即套接字)编程是网络编程的基础,对于具有出色联网功能的ESP系列开发板,学会网络编程能帮助我们写出更好玩的程序。目前,ESP8266 RTOS-SDK v3.3以上的版本给出了socket编程的示例(即example),虽然socket编程已经有了很多教程,但想在ESP系列芯片上玩转其支持的Socket接口,我们还是需要自己挖掘熟悉一下其具体实现与应用。

最新的ESP8266 RTOS-SDK中关于socket编程的实现包含了TCP/UDP的实现,我们主要以TCP的Sever\Client为例,说明一下如何理解以及应用其example的示例完成TCP的Sever\Client为测试:(开发板是ESP8266 DevKitc,使用ESP32进行实验的步骤是一样的)

1.TCP server 测试

关于TCP server 的实现代码如下:

/* BSD Socket API Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include <sys/param.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "protocol_examples_common.h"
#include "nvs.h"
#include "nvs_flash.h"

#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include <lwip/netdb.h>

#define PORT CONFIG_EXAMPLE_PORT

static const char *TAG = "example";

static void tcp_server_task(void *pvParameters)
{
    char rx_buffer[128];
    char addr_str[128];
    int addr_family;
    int ip_protocol;

    while (1) {

#ifdef CONFIG_EXAMPLE_IPV4
        struct sockaddr_in destAddr;
        destAddr.sin_addr.s_addr = htonl(INADDR_ANY);
        destAddr.sin_family = AF_INET;
        destAddr.sin_port = htons(PORT);
        addr_family = AF_INET;
        ip_protocol = IPPROTO_IP;
        inet_ntoa_r(destAddr.sin_addr, addr_str, sizeof(addr_str) - 1);
#else // IPV6
        struct sockaddr_in6 destAddr;
        bzero(&destAddr.sin6_addr.un, sizeof(destAddr.sin6_addr.un));
        destAddr.sin6_family = AF_INET6;
        destAddr.sin6_port = htons(PORT);
        addr_family = AF_INET6;
        ip_protocol = IPPROTO_IPV6;
        inet6_ntoa_r(destAddr.sin6_addr, addr_str, sizeof(addr_str) - 1);
#endif

        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;
        }
        ESP_LOGI(TAG, "Socket created");

        int err = bind(listen_sock, (struct sockaddr *)&destAddr, sizeof(destAddr));
        if (err != 0) {
            ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
            break;
        }
        ESP_LOGI(TAG, "Socket binded");

        err = listen(listen_sock, 1);
        if (err != 0) {
            ESP_LOGE(TAG, "Error occured during listen: errno %d", errno);
            break;
        }
        ESP_LOGI(TAG, "Socket listening");

#ifdef CONFIG_EXAMPLE_IPV6
        struct sockaddr_in6 sourceAddr; // Large enough for both IPv4 or IPv6
#else
        struct sockaddr_in sourceAddr;
#endif
        uint addrLen = sizeof(sourceAddr);
        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");

        while (1) {
            int len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
            // Error occured during receiving
            if (len < 0) {
                ESP_LOGE(TAG, "recv failed: errno %d", errno);
                break;
            }
            // Connection closed
            else if (len == 0) {
                ESP_LOGI(TAG, "Connection closed");
                break;
            }
            // Data received
            else {
#ifdef CONFIG_EXAMPLE_IPV6
                // Get the sender's ip address as string
                if (sourceAddr.sin6_family == PF_INET) {
                    inet_ntoa_r(((struct sockaddr_in *)&sourceAddr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
                } else if (sourceAddr.sin6_family == PF_INET6) {
                    inet6_ntoa_r(sourceAddr.sin6_addr, addr_str, sizeof(addr_str) - 1);
                }
#else
                inet_ntoa_r(((struct sockaddr_in *)&sourceAddr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
#endif

                rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string
                ESP_LOGI(TAG, "Received %d bytes from %s:", len, addr_str);
                ESP_LOGI(TAG, "%s", rx_buffer);

                int err = send(sock, rx_buffer, len, 0);
                if (err < 0) {
                    ESP_LOGE(TAG, "Error occured during sending: errno %d", errno);
                    break;
                }
            }
        }

        if (sock != -1) {
            ESP_LOGE(TAG, "Shutting down socket and restarting...");
            shutdown(sock, 0);
            close(sock);
        }
    }
    vTaskDelete(NULL);
}

void app_main()
{
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    ESP_ERROR_CHECK(example_connect());

    xTaskCreate(tcp_server_task, "tcp_server", 4096, NULL, 5, NULL);
}

在编译时(make menuconfig 时),如图所示,进入connection 配置选项,要填入正确的WiFi名与密码:

新大陆通过socket给esp8266拓展 esp8266 socket编程_网络通信

然后编译烧录即可。

接着,运行make monitor命令,连接成功的话,就会在串口生成如下信息:

新大陆通过socket给esp8266拓展 esp8266 socket编程_socket_02

现在,TCP服务器已经建立好了,通过打印信息我们知道他的IP为:192.168.47.100。打开新的Terminal终端,用如下命令

nc  192.168.47.100  3333(注意将命令中的IP替换为你的串口打印的IP地址)

建立一个TCP Client去连接上述建立的TCP Server,然后发送字符串:you got me,开发板上运行的服务器就会回应一个同样的字符串作为回应,就像这样:

新大陆通过socket给esp8266拓展 esp8266 socket编程_网络通信_03

以上就是TCP server程序测试的基本步骤,值得注意的是,务必确保开发板和PC端的Terminal终端在同一局域网,否则无法通信。(运行CTRL+C命令可以终止终端中用于nc命令发起的TCP进程)

2.TCP Client 测试

代码我就不贴了,还是上述SDK中自包含的TCP Client代码:

基本步骤是:

(1)终端下运行ifconfig命令,查看本机IP:

新大陆通过socket给esp8266拓展 esp8266 socket编程_socket_04

(2)配置TCP Client的编译选项,运行make menuconfig

首先,配置连网选项:进入connection 配置选项,要填入正确的WiFi名与密码,这点与1中的联网操作类似。

接着,配置TCP Server的信息,主要是PC端运行的TCP Server的IP,将上述(1)中查看的本机IP填入:

新大陆通过socket给esp8266拓展 esp8266 socket编程_tcpip_05

新大陆通过socket给esp8266拓展 esp8266 socket编程_网络通信_06

如此,开发板在联网后就会尝试连接到IP为192.168.47.103的服务器上去。

(3)配置成功后,编译烧录程序到到开发板。

(4)PC的终端下新建一个窗口,运行命令:nc -l 192.168.47.103 -p 3333启动TCP Server(注意,根据(1)中的IP信息,替换命令中的IP,此外,开发板的串口打印在一个窗口,需要打开别的新窗口启动TCP,不要在一个窗口下操作)

  (5) 开发板的对应的命令窗口中运行make monitor命令,启动开发板中的TCP Client,开发板连接成功后便打印如下信息:

新大陆通过socket给esp8266拓展 esp8266 socket编程_socket_07

(6)同时 ,开发板将向PC端的TCP Server发送"Message from ESP32"消息,我们接着收到的消息写“you got me",向ESP8266上的TCP Client发送消息,ESP8266上的TCP Client收到消息后,会再回一条"Message from ESP32"消息。

新大陆通过socket给esp8266拓展 esp8266 socket编程_socket_08

上述给出了TCP Client/TCP Server的使用案例及步骤,ESP8266的SDK中还提供了UDP Client,UCP Server的代码,实验步骤与上类似,不过在使用nc命令时,要加上-u 选项,比如1中的nc  192.168.47.100  3333命令换为nc  -u 192.168.47.100  3333,2中的nc -l 192.168.47.103 -p 3333命令换为nc  -u -l 192.168.47.103 -p 3333即可。

另外,作为补充,说明下nc命令,nc命令是Netcat网络工具提供的拥有强大TCP/UDP测试功能的命令行工具,想了解它的可以看这:

https://zhuanlan.zhihu.com/p/32637570

https://zhuanlan.zhihu.com/p/83959309

总结:

上诉介绍了ESP8266下的Socket TCP的基本测试,下一节,我将结合TCP Server的代码,就Socket常见配置选项(option)的查询与修改进行介绍,了解了Socket的常见配置选项,你就能写出更灵活实用的网络编程程序了。