本帖最后由 起个名字好难啊 于 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编程属于网络编程的范畴,所以就不得不提一下网络模型。网络模型一共分为七层:
图中最底下的三层绿色所指属于网络传输的基础,可以理解连接网络的过程,设备具备网络连接的能力就需要完成这三层工作,通常在连接WiFi的时候已经完成了。
上面的蓝色四层才是真正的网络数据传输的过程。而本贴要介绍TCP,位于模型中的传输层:是会话层
、表示层
、应用层
的传输基础。比如:
会话层
的SSH连接;
表示层
的SMB文件协议;
应用层
的HTTP和MQTT;
它们都是基于传输层来传输数据。而我们需要进行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的连接了。
只需要在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.c
和tcp_client.h
:
当然,这个文件夹也需要在CMakeList.txt
中声明头文件来源,不然编译不到固件里面去的。
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)
{
}
然后把所有的函数在头文件中声明:
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_task
的CODE_WIFI_ON_GOT_IP
事件中调用初始化和连接函数就可以了,注意:连接的参数需要根据实际的IP地址和端口号填入:
case CODE_WIFI_ON_GOT_IP:
// tcp_client_init();
tcpClientInit();
tcpClientConentStart("122.114.122.174", 44800);
goto __suTsk;
运行结果
服务器连接情况
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;
}
在连接成功之后调用这个函数,测一下:
运行结果
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);
}
- 运行结果:
每次收到数据都会打印一次,而不是持续地打印。
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();
}
}
2.3 总结
总而言之,TCP客户端的编程是有套流程的:
跟着这个流程都可以实现客户端的连接;
2.3.1 注意事项
- 安信可透传云每3分钟需要刷新一次端口号,所以建议用串口实现服务器的配置连接
- 本贴有些编程技巧值得学习的,比如说TCP读取数据使用的回调。
- 服务器的编程会尽快出,实现两个小安派的通讯
三、本贴源码地址