我不信你看完这帖子还不知道怎么玩TCP

[复制链接]
查看2358 | 回复13 | 2023-10-12 15:26:52 | 显示全部楼层 |阅读模式

本帖最后由 起个名字好难啊 于 2023-10-12 15:26 编辑

本帖最后由 起个名字好难啊 于 2023-10-12 12:03 编辑

本帖最后由 起个名字好难啊 于 2023-10-12 09:44 编辑

本帖最后由 起个名字好难啊 于 2023-10-12 09:30 编辑

本帖最后由 起个名字好难啊 于 2023-10-11 21:44 编辑

本帖最后由 起个名字好难啊 于 2023-10-11 21:41 编辑

本帖最后由 起个名字好难啊 于 2023-10-11 21:38 编辑

一、提前放盐(前言)

这次帖子要介绍的TCP编程属于网络编程的范畴,所以就不得不提一下网络模型。网络模型一共分为七层:

20210118152645470.png

图中最底下的三层绿色所指属于网络传输的基础,可以理解连接网络的过程,设备具备网络连接的能力就需要完成这三层工作,通常在连接WiFi的时候已经完成了。 上面的蓝色四层才是真正的网络数据传输的过程。而本贴要介绍TCP,位于模型中的传输层:是会话层表示层应用层的传输基础。比如:

  • 会话层SSH连接;
  • 表示层SMB文件协议;
  • 应用层HTTPMQTT

它们都是基于传输层来传输数据。而我们需要进行TCP通讯的话,是需要会话层来建立通讯接口,而建立会话层的接口叫socket

1.1 Socket 套接字

度娘对于Socket套接字的描述是这样的:

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口

懵了吧!

简单来说:想和小美聊天,就需要扫码加小美好友。Socket就相当于扫码加好友的过程,给你一个和小美聊天的借(接)口。🚀️

1.2 TCP协议

TCP协议属于TCP/IP协议栈中的一种传输协议,此外还有UDP协议。TCP协议连接需要经过三次握手才能建立稳定的连接,相对UDP而言,TCP连接更加牢靠稳固。TCP和UDP的区别就不展开说了,可以自行学习,本贴重点放在TCP编程中。 TCP通讯有两个必要的身份接口:

  • TCP服务器:提供客户端连接,不能主动建立连接(高冷到极点)
  • TCP客户端:主动发起对服务器的发起连接(主动才有故事)

二、小安派的TCP通讯

2.1 WiFi的连接

开始TCP编程之前,肯定是需要完成WiFi的连接,在代码中,我使用了小安派SDK的Project_basic标准工程,里面已经写好了WiFi连接的函数,简单的调用一下就能实现WiFi的连接了。

image.png

只需要在main.c 中引用wifi_enent.h 头文件,并在main函数中调用wifi_start_firmware_task,即可初始化WiFi 任务,初始化成功之后,不能马上连接,需要等到初始化结束并且WiFi成功启动才能发起连接。

int main(void)
{
board_init();
tcpip_init(NULL, NULL);
wifi_start_firmware_task(); 
wifi_connect("AIOT@FAE", "fae12345678");
vTaskStartScheduler();
}

本贴的wifi_connect函数被我修改了,它目前成为了一个可以初始化之后能够马上连接WiFi的函数,只要创建一个任务等待初始化完成就能发起连接了。

static void wifi_connect_task(void* arg)
{
    int ret = 255;
    // struct fhost_vif_ip_addr_cfg ip_cfg = { 0 };
    uint32_t ipv4_addr = 0;

    while (1)
    {
        if (NULL==SSID || 0==strlen(SSID)) {
            goto  __suTsk;
        }
        if (wifi_mgmr_sta_state_get() == 1) {
            wifi_sta_disconnect();
        }
        if (wifi_sta_connect(SSID, PASS, NULL, NULL, 0, 0, 0, 1)) {
            goto  __suTsk;
        }

        //等待连接成功
        sta_ConnectStatus = 0;
        for (int i = 0;i<10*30;i++) {

            vTaskDelay(100/portTICK_PERIOD_MS);
            switch (sta_ConnectStatus) {
                case CODE_WIFI_ON_MGMR_DONE:
                    goto  __suTsk;
                case CODE_WIFI_ON_SCAN_DONE:

                    goto  __suTsk;
                case CODE_WIFI_ON_DISCONNECT:   //连接失败(超过了重连次数还没有连接成功的状态)

                    goto  __suTsk;
                case CODE_WIFI_ON_CONNECTED:    //连接成功(表示wifi sta状态的时候表示同时获取IP(DHCP)成功,或者使用静态IP)
                    LOG_I("Wating wifi connet OK");
                    break;
                case CODE_WIFI_ON_GOT_IP:
                    goto  __suTsk;
                default:
                    //等待连接成功
                    break;
            }
        }
    __suTsk:
        vTaskSuspend(wifi_con_task);
    }

}

uint8_t wifi_connect(char* ssid, char* passwd)
{
    SSID = ssid;
    PASS = passwd;
    LOG_I("Wating wifi connet");
    if (wifi_con_task==NULL)
        xTaskCreate(wifi_connect_task, "wifi_con_tak", 1024, 2, NULL, &wifi_con_task);
    else
        vTaskResume(wifi_con_task);
}

2.2 TCP客户端编程

2.2.1 源文件创建及声明头文件来源

众所周知,我是一个不喜欢把功能代码放在main函数里实现的人,所以照旧TCP客户端的代码需要新建一组文件夹,用VS code直接新建文件夹并且命名为TCP_client,并在TCP_client文件夹中新建两个文件:tcp_client.ctcp_client.h

image.png

当然,这个文件夹也需要在CMakeList.txt中声明头文件来源,不然编译不到固件里面去的。 image.png

sdk_add_include_directories(main config components/wifi components/TCP_client)

2.2.2 tcp_client.h 的预编译

头文件的常规操作:

#ifndef TCP_CLIENT_H
#define TCP_CLIENT_H



#endif

2.2.3 TCP客户端功能编程

先引用一些需要用到的头文件:

#include <stdio.h>
#include <string.h>
#include "FreeRTOS.h"
#include "task.h"
#include "log.h"
//tcp 
#include <lwip/tcpip.h>
#include <lwip/sockets.h>
#include <lwip/netdb.h>

我的习惯是编写封装函数,比如初始化,会单独写一个初始化的函数进行,方便以后查看逻辑。所以根据功能,应该有五个函数:

  • TCP初始化函数
  • TCP连接函数
  • TCP客户端发送数据函数
  • TCP客户端接收数据函数
  • TCP客户端断开连接函数

提前把函数给创建好,方便功能的开发,不够再加嘛。

/**
 * @brief TCP初始化函数
 *
 * @return int
*/
int tcpClientInit(void)
{

}
/**
 * @brief TCP连接函数
 *
 * @param addr
 * @param port
 * @return int
*/
int tcpClientConentStart(char* addr, uint16_t port)
{

}
/**
 * @brief TCP 发送函数
 *
 * @param data
 * @return int
*/
int tcpClientSend(char* data)
{

}
/**
 * @brief TCP读取函数
 *
 * @param buff
 * @return int
*/
int tcpClientRead(tcp_read_handler_t read_handler)
{

}
/**
 * @brief TCP断开连接
 *
 * @return int
*/
int tcpClientClose(void)
{

}

然后把所有的函数在头文件中声明: image.png

2.2.3.1 TCP初始化函数

TCP的初始化功能就是创建socket 客户端,客户端创建成功之后,会返回一个网络描述符,代表本次客户端的ID。往后的连接和数据交互都需要通过这个ID来实现。于是初始化函数就得到以下内容:

int tcpClientInit(void)
{
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);//创建socket
    if (socket_fd<0) return socket_fd;//创建失败返回
    else return 0; //创建成功返回 0
}

2.2.3.2 TCP连接函数

TCP 客户端连接是TCP客户端编程当中最复杂的了,因为在连接之前,需要配置好需要连接的服务器IP地址和端口号。然后就得到了以下代码:

int tcpClientConentStart(char* addr, uint16_t port)
{
    //判断socket 是否创建成功
    if (socket_fd<0) {
        LOG_E("socket is no creat");
        return -1;
    }
    struct sockaddr_in remote_addr = {
        .sin_addr.s_addr = inet_addr(addr), //配置IP地址
        .sin_port = htons(port),//配置端口号
        .sin_family = AF_INET,//配置连接类型为TCP
    };

    memset(&(remote_addr.sin_zero), 0, sizeof(remote_addr.sin_zero));

    if (connect(socket_fd, (struct sockaddr*)&remote_addr, sizeof(struct sockaddr)) != 0) {//发起连接
        LOG_E("TCP client connect server falied!"); //连接失败显示
        closesocket(socket_fd);//关闭socket
        return -1;//返回错误
    }
    LOG_I("TCP client connect server success");//连接成功显示
    return 0; //返回OK
}

连接函数会通过参数传入需要连接的IP地址和端口。写到这里,就能测试客户端的连接情况了。因为客户端是需要连接服务器的,再没有编写服务器之前,建议使用安信可透传云: http://tt.ai-thinker.com:8000/ttcloud

因为TCP需要在WiFi连接成功之后才能建立连接,所以我直接在WiFi 连接成功的时间里做连接,在wifi_event.c 中引用tcp_client.h

#include "board.h"
#include "log.h"
//tcp
#include "tcp_client.h"

然后在wifi_connect_taskCODE_WIFI_ON_GOT_IP 事件中调用初始化和连接函数就可以了,注意:连接的参数需要根据实际的IP地址和端口号填入

case CODE_WIFI_ON_GOT_IP:
                    // tcp_client_init();
                    tcpClientInit();
                    tcpClientConentStart("122.114.122.174", 44800);
                    goto  __suTsk;

运行结果 image.png 服务器连接情况 image.png

2.2.3.3 TCP发送函数

TCP 发送很简单,只是一个write函数,只需要传入相应的值就行了,做一下判断看看是否发送成功,发送成功之后,会返回发送数据的数量:

int tcpClientSend(char* data)
{
    size_t ret;
    ret = write(socket_fd, data, strlen(data));
    if (ret<0) {
        LOG_E("TCP client send falied!");
        return -1;
    }
    return ret;
}

在连接成功之后调用这个函数,测一下: image.png

运行结果 image.png

2.2.3.4 TCP读取函数

发布也是只有一个函数而已,但是需要不停地读取,所以需要创建读取任务,然后做了一个读取回调函数,当有数据来的时候,就调用回调函数,方便外部读取。 因此,在使用读取函数的时候,需要创建一个回调函数:

  • 读取函数实现:
static void read_task(void* arg)
{
    char* buff = pvPortMalloc(1024);
    uint64_t ret = 0;
    while (1) {
        memset(buff, 0, 1024);
        ret = recv(socket_fd, buff, sizeof(buff), 0);
        if (ret<=0) {
            vTaskDelay(pdMS_TO_TICKS(5));
            continue;
        }
        *read_handle_cb(buff, ret);
        LOG_I("%s", buff);
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}
/**
 * @brief TCP读取函数
 *
 * @param buff
 * @return int
*/
int tcpClientRead(tcp_read_handler_t read_handler)
{
    if (tcp_read==NULL)
    {
        xTaskCreate(read_task, "tcp read", 1024*2, NULL, 3, &tcp_read);
        read_handle_cb = read_handler;
    }
    else {
        LOG_F("tcp_read runing");
    }

    return 0;
}
  • 调用方式:
/**
 * @brief 读取回调
 *
 * @param buff
 * @param len
*/
static void tcp_read_cb(char* buff, signed int len)
{
    LOG_I("TCP client read %d:%s", len, buff);
}

image.png

  • 运行结果: image.png

每次收到数据都会打印一次,而不是持续地打印。

2.2.3.5 断开连接函数

断开连接就也只有一个函数closesocket ,但是因为在TCP通讯中还有任务在运行,所以断开之前先把读取任务给关掉,所以就有:

int tcpClientClose(void)
{
    if (tcp_read != NULL)vTaskDelete(tcp_read);
    closesocket(socket_fd);
}

做个简单的功能,在读取回调函数中识别close 字符关闭连接:

static void tcp_read_cb(char* buff, signed int len)
{
    LOG_I("TCP client read %d:%s", len, buff);
    if (memcmp(buff, "close", len)==0) {
        tcpClientClose();
    }
}
  • 运行结果:

image.png

2.3 总结

总而言之,TCP客户端的编程是有套流程的: image.png

跟着这个流程都可以实现客户端的连接;

2.3.1 注意事项

  • 安信可透传云每3分钟需要刷新一次端口号,所以建议用串口实现服务器的配置连接
  • 本贴有些编程技巧值得学习的,比如说TCP读取数据使用的回调。
  • 服务器的编程会尽快出,实现两个小安派的通讯

三、本贴源码地址

image.png
image.png

本帖被以下淘专辑推荐:

回复

使用道具 举报

ai_mcu | 2023-10-12 15:33:42 | 显示全部楼层
莫工辛苦了
明天总会更好
回复 支持 反对

使用道具 举报

wangxiangtan | 2023-10-12 15:51:17 | 显示全部楼层
先收藏后实践
回复 支持 反对

使用道具 举报

496199544 | 2023-10-12 16:56:30 | 显示全部楼层
6666
回复

使用道具 举报

WangChong | 2023-10-12 21:07:05 | 显示全部楼层
我多半是废了
回复 支持 反对

使用道具 举报

jkernet | 2023-10-12 22:01:33 | 显示全部楼层
打卡学习
回复

使用道具 举报

iiv | 2023-10-12 23:37:39 | 显示全部楼层
莫哥出马了
回复 支持 反对

使用道具 举报

爱笑 | 2023-10-13 08:35:43 | 显示全部楼层
莫工辛苦了!
用心做好保姆工作
回复 支持 反对

使用道具 举报

Ject | 2023-10-14 09:29:56 | 显示全部楼层
学习进度30%
回复 支持 反对

使用道具 举报

123456 | 2023-11-8 14:53:47 | 显示全部楼层
好帖子  收藏了
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则