
??想了解更多關于開源的內容,請訪問:???
??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("http://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??。