传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 79 定义。
想了解更多关于开源的内容,请访问:
51CTO 开源基础软件社区
https://ost.51cto.com
一、前言
学习OpenHarmony南向设备开发中的网络通信,它可以将底层开发板获得的数据传输到上层的服务器,服务器亦可通过网络通信控制底层开发板。
二、TCP简介
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 79 定义。
TCP旨在适应支持多网络应用的分层协议层次结构。 连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务。TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。
网络编程开发绕不开socket(套接字)的使用,socket就是整合好TCP/IP协议的一个工具。让我们无需过度关注于底层协议的实现,直接用封装好的socket就行了.
TCP服务器端与TCP客户端进行通信的流程👇
三、分析代码
本次实验使用的是OpenHarmony1.0.0的源码:源码压缩包地址参考HiSpark WiFi-IoT 鸿蒙套件样例开发–网络编程(tcpclient)
1.导入样例
将润和提供的21_tcpclient开发样例文件夹复制到源码applications/sample/wifi-iot/app路径下:
在app路径下的BUILD.gn添加需要编译的静态库名称:tcpclient:net_demo。
import("//build/lite/config/component/lite_component.gni")
lite_component("app") {
features = [
"startup",
"tcpclient:net_demo",
]
}
静态库名称可在21_tcpclient文件夹下的BUILD.gn里查看。
踩坑:一开始直接写静态库名net_demo是会报错的!
报错内容👇一般都是BUILD.gn文件出现问题:
2、分析代码
- demo_entry_cmsis.c : 鸿蒙liteos-m程序入口,支持Hi3861。
- demo_entry_posix.c :鸿蒙liteos-a和Unix系统程序入口,Hi3516、Hi3518、PC。
- net_common.h :系统网络接口头文件。
- net_demo.h :demo脚手架头文件。
- net_params.h :网络参数,包括WiFi热点信息,服务器IP、端口信息。
- tcp_client_test.c :TCP客户端。
- wifi_connecter.c :鸿蒙WiFi STA模式API的封装实现文件,比鸿蒙原始接口更容易使用。
- wifi_connecter.h :鸿蒙WiFi STA模式API的封装头文件,比鸿蒙原始接口更容易使用。
事先在net_params.h文件里修改WiFi的配置。
程序入口:demo_entry_cmsis.c文件。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "iot_gpio.h"
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "net_demo.h"
#include "net_params.h"
#include "wifi_connecter.h"
#define LED_TASK_GPIO 9
static void NetDemoTask(void *arg) //一开始线程入口函数
{
(void)arg;
WifiDeviceConfig config = {0}; //表示用于连接到指定 Wi-Fi 设备的 Wi-Fi 站配置。
IoTGpioInit(LED_TASK_GPIO); //初始化IO口,为后文点灯做准备
IoTGpioSetDir(LED_TASK_GPIO, IOT_GPIO_DIR_OUT); //设置GPIO为输出模式
// 准备AP的配置参数
strcpy(config.ssid, PARAM_HOTSPOT_SSID); //从net_params.h拷贝WiFi的参数
strcpy(config.preSharedKey, PARAM_HOTSPOT_PSK);
config.securityType = PARAM_HOTSPOT_TYPE; //配置WiFi的安全模式
osDelay(10);
int netId = ConnectToHotspot(&config); //连接热点
int timeout = 10;
while (timeout--) //等待10秒后开始执行NetDemoTest
{
printf("After %d seconds, I will start %s test!\r\n", timeout, GetNetDemoName());
osDelay(100);
}
while (1)
{
NetDemoTest(PARAM_SERVER_PORT, PARAM_SERVER_ADDR); //开始TCP连接,输入端口号,ip地址
}
printf("disconnect to AP ...\r\n");
// DisconnectWithHotspot(netId);
printf("disconnect to AP done!\r\n");
}
static void NetDemoEntry(void)
{
osThreadAttr_t attr;
attr.name = "NetDemoTask";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 10240;
attr.priority = osPriorityNormal;
if (osThreadNew(NetDemoTask, NULL, &attr) == NULL)
{
printf("[NetDemoEntry] Falied to create NetDemoTask!\n");
}
}
SYS_RUN(NetDemoEntry);
①成功连接wifi后,接下来就是创建socket套接字准备进行TCP连接。
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET:IP 协议系列。SOCK_STREAM=1:TCP协议
跳转到socket的定义。
- domain:协议族(family),常用的协议族有 AFL INET(ipv4 )、AF INET6、AF LOCAL(或称AF UNIX, Unix成socket) AF ROUTE 等。协议族决定了 socket 的地址类型,在通信中必须采用对应的地址。
- type:指定 Socket 类型。
- 流式 socket (SOCK STREAM)是一种面向连接的 Socket, 针对于面向连接的 TCP 服务应用。数据报式 socket(SOCK DGRAM) 是一种无连接的 Socket,对应于 无连接的 UDP 服务应用。
- protocol: 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。protocol 的值设为 0,系统会自动推演出应该使用什么协议。
②配置
struct sockaddr_in serverAddr = {0}; //描述互联网套接字地址的结构体
serverAddr.sin_family = AF_INET; // AF_INET表示IPv4协议
serverAddr.sin_port = htons(port); // 端口号,从主机字节序转为网络字节序
if (inet_pton(AF_INET, host, &serverAddr.sin_addr) <= 0)
{ // 将主机IP地址从“点分十进制”字符串 转化为 标准格式(32位整数)
printf("inet_pton failed!\r\n");
goto do_cleanup;
}
③与主机连接。
// 尝试和目标主机建立连接,连接成功会返回0 ,失败返回 -1
if (connect(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0)
{
printf("connect failed!\r\n");
goto do_cleanup;
}
printf("connect to server %s success!\r\n", host);
④连接成功后,发送数据给目标主机测试是否发送成功。
// 建立连接成功之后,这个TCP socket描述符 —— sockfd 就具有了 “连接状态”,发送、接收 对端都是 connect 参数指定的目标主机和端口
retval = send(sockfd, request, sizeof(request), 0); //发送request给目标主机,成功会返回字符串长度 ,失败返回 -1
if (retval < 0)
{
printf("send request failed!\r\n");
goto do_cleanup;
}
printf("send request{%s} %ld to server done!\r\n", request, retval);
⑤接收服务器发送过来的数据。
retval = recv(sockfd, &response, sizeof(response), 0);//接收目标主机的消息存入response,成功会返回字符串长度 ,失败返回 -1
if (retval <= 0) {
printf("send response from server failed or done, %ld!\r\n", retval);
goto do_cleanup;
}
response[retval] = '\0';
printf("recv response{%s} %ld from server done!\r\n", response, retval);
3、修改代码,实现开关灯操作
①在入口demo_entry_cmsis.c 文件中初始化LED灯的io口。
代码在上文已贴出
②tcp_client_test.c文件。
由上文分析原始的代码可知:开发板(客户端)与主机(服务器)完成一次消息交互后就会关闭socket套接字,再关闭WiFi。
所以可以把关闭套接字的函数(close(sockfd))注释掉,再加个while死循环即可。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "net_demo.h"
#include "net_common.h"
#define LED_TASK_GPIO 9
static char request[] = "Hello";
static char response[128] = "";
void TcpClientTest(const char *host, unsigned short port)
{
ssize_t retval = 0;
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET:IP 协议系列。SOCK_STREAM=1:TCP协议
struct sockaddr_in serverAddr = {0}; //描述互联网套接字地址的结构体
serverAddr.sin_family = AF_INET; // AF_INET表示IPv4协议
serverAddr.sin_port = htons(port); // 端口号,从主机字节序转为网络字节序
if (inet_pton(AF_INET, host, &serverAddr.sin_addr) <= 0)
{ // 将主机IP地址从“点分十进制”字符串 转化为 标准格式(32位整数)
printf("inet_pton failed!\r\n");
goto do_cleanup;
}
// 尝试和目标主机建立连接,连接成功会返回0 ,失败返回 -1
if (connect(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0)
{
printf("connect failed!\r\n");
goto do_cleanup;
}
printf("connect to server %s success!\r\n", host);
// 建立连接成功之后,这个TCP socket描述符 —— sockfd 就具有了 “连接状态”,发送、接收 对端都是 connect 参数指定的目标主机和端口
retval = send(sockfd, request, sizeof(request), 0); //发送request给目标主机,成功会返回字符串长度 ,失败返回 -1
if (retval < 0)
{
printf("send request failed!\r\n");
goto do_cleanup;
}
printf("send request{%s} %ld to server done!\r\n", request, retval);
while (1)
{
retval = recv(sockfd, &response, sizeof(response), 0); //接收目标主机的消息存入response,成功会返回字符串长度 ,失败返回 -1
if (retval <= 0)
{
printf("send response from server failed or done, %ld!\r\n", retval);
goto do_cleanup;
}
response[retval] = '\0';
printf("recv response{%s} %ld from server done!\r\n", response, retval);
if (response[0] == 'o' && response[1] == 'n')
{
IoTGpioSetOutputVal(LED_TASK_GPIO, 0); //开灯
printf("The led is on\n");
}
if (response[0] == 'o' && response[1] == 'f' && response[2] == 'f')
{
IoTGpioSetOutputVal(LED_TASK_GPIO, 1); //关灯
printf("The led is off\n");
}
}
do_cleanup:
printf("do_cleanup...\r\n");
// close(sockfd);//关闭套接字
}
CLIENT_TEST_DEMO(TcpClientTest);
四、测试
1.安装netcat(一个非常强大的网络实用工具,可以用它来调试TCP/UDP应用程序)
二选一:
- Linux上:sudo apt-get install netcat。
- Windows上:Windows版netcat。
将解压出来的文件全部复制到C:\Windows\System32的文件夹下。
Windows+R cmd 打开命令行。输入nc 命令即可。
2.开始测试
先是PC机开启TCP服务端监听(我选择的是Windows启动netcat)。
-l: 开始监听。
-p:指定端口 (端口号必须保持一致,可在net_params.h文件配置)。
开发板烧录新的固件后rest启动后可观察到服务端接收到了客户端传输过来的数据"hello"。
开发板👇一开始灯是亮的状态。
PC服务端。
服务端输入"off",可让开发板关灯,完成交互。
继续开灯。
五、总结
这次实践中还有一些地方不能完全理解,在net_demo.h文件中。
为什么有这么多斜杠?
testFun是什么?它又是怎样跳转到tcp_client_test.c文件执行TcpClientTest()函数的呢?
想了解更多关于开源的内容,请访问:
51CTO 开源基础软件社区
https://ost.51cto.com。