上一篇记录了Hi3861的无线网络连接的模式,包括AP热点构建局域网以及使用STA模式接入其他路由器或者热点进行组网。使用这两种方式解决了各个终端的物理连接问题,但是没有具体说明网络中的各个成员是如何通信的。借用计算机网络的知识来理解,前面的AP以及STA解决了网络通信中网络层(IPV4)、数据链路层以及物理层的连接问题,但要进行通信还需要完成运输层和应用层。
运输层提供应用进程之间的逻辑通信,它完成了应用层中进程的数据通信。而前面提到的STA与AP解决的是通信双方主机间的物理连接及数据传输方式。
站在编程的角度来看,运输层解决了通信过程成中数据以什么样的格式传输的问题,类似笔者前面介绍Modbus通信的类比,计算机中所有的信息都是01二进制组成的,在通信过程中,通信的各方需要有一个统一的断句、位定义和字定义,让通信双方都能明白数据包中每个0和1的意义,这样才能实现通信。
而在运输层中的协议主流就是UDP和TCP,本文介绍UDP的接收、发送以及群发,TCP的内容放到下一篇介绍。
UDP是用户数据报协议,通过名称就可以知道他是面向用户的数据报文的,它在传输过程中不需要和通信另一方进行连接,整个传输过程中它不会去检测对方的状态,也不会管对方有没有接收到;它的工作思路就是上面(应用层)让我发啥,我发了就完事儿了,也不用去管对方有没有接收到。这类似日常生活中的发短信,整个发送过程不需要和对方实时沟通。也正是因为这个传输方式,UDP的传输速度会比较快,但是存在丢包风险,提供的是不可靠交付,但是它胜在传输速率,而且得益于UDP传输不需要在软件方面构建通信双方的连接,所以其可用来实现一对一、一对多、多对一、多对多的功能。
UDP传输过程中的数据包格式如下图所示,大体上说分为两个字段,数据字段和首部字段。
这里还有一个伪首部,这个 伪首部只是用来做校验和的计算的,并不参与传输,只是在计算校验和时会添加。
用一个数据包来举个栗子,看看UDP的数据包到底长啥样以及UDP的校验和如何计算。
上面的数据包的首部中提到了一个端口号,这个端口号是什么东西呢。
应用层中的进程一般都是不唯一的,会有多个应用进程,应用层所有的应用进程都是通过运输层再传送到IP层(网络层),运输层从IP层收到发送给各个不同应用进程的数据后,必须分别交付指明的各应用进程才行,那么运输层是怎么识别各个应用进程的呢,这就需要使用到端口号,可以理解为一个端口号就对应一个进程,当我们要传输数据去对应进程时只需要绑定该进程对应的端口号即可。
端口号是用两个字节(16位的二进制数)表示的,它的取值范围是0-65535,其中,0-1023之间的端口号用于一些知名的网络服务和应用,用户的普通应用程序需要使用1024以上的端口号,从而避免端口号被另外一个应用或服务所占用。
来看一个其他博主的例子:原文链接。
接下来通过一个图例来描述IP地址和端口号的作用,如下图所示。
在了解了上面的这些内容后,就已经可以开始使用Hi3861进行UDP的传输了,先来捋一下思路。
首先,要想进行UDP传输必须组建一个局域网,使用Hi3861组建局域网的方式在上一篇中已经介绍了,可以使用AP模式,让Hi3861作为路由来实现,也可以使用STA,将Hi3861接入已有局域网形成组网,这里笔者使用的是后者,使用Hi3861连接家里的wifi进而实现组网,使用STA接入路由器后,路由器会使用DHCP为Hi3861分配一个IP地址,至此物理层和IP层的设置就已经完成;
然后,参考上面的数据包格式,还需要获取和绑定通信另外一端的IP地址及其应用的端口;
最后就是通信了,将所需传输的内容按照UDP数据包的格式进行封装,包括源端口,目的端口,以及源IP和目的IP,虽然IP不包含着数据包内,但是在校验和的计算中也是需要使用的。
有了上面的编程思路后,是不是发现整个打包发送以及接收解析的代码如果纯手撕的话,还是有难度的,而且代码量不会小,涉及的内容比较多也不利于查询错误,那有没有类似其他接口的API函数呢,答案是肯定的。到这就可以请出本文的又一个重量级的东西了——套接字(Socket)。
套接字(Socket),是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。1这就类似前面提到的CMSIS以及鸿蒙的内核抽象层,它的作用就是将网络通信中的底层处理给屏蔽了,预留了接口供编程者使用,在网络通信时不需要再去纠结TCP/IP,UDP这些数据包的打包,校验计算以及解析数据包这些处理。借用大佬的博客来描述一下,原文链接——Socket原理讲解。
套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的lP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。例如:如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)2。
1.流套接字(SOCK_STREAM)
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议 。
2.数据报套接字(SOCK_DGRAM)
数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理 。
3.原始套接字(SOCK_RAW)
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接 。
要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket]。
以上有关套接字的内容来自百度百科对套接字的解释,原文链接——套接字。
计算机网络告诉我们UDP是不分客户端和服务端的,网络中的任何一个设备都既可以做客户端又可以做服务端,只需要对应IP和端口即可发送数据。显然要使用套接字来实现UDP就有一些矛盾,这里可以将UDP通信双方一个假设为客户端,另外一个假设为服务端,然后运用套接字即可,顺着这个思路,就可以得到下面的一个工作流程图。
了解了上面的知识点后,用Hi3861实现UDP通信的代码的思路就有了,接下来分成两部分来编写即可,一个是假设的UDP客户端,一个是假设的UDP服务端。
笔者此处使用的是电脑作为服务端,家用的WIFI作为物理连接,使得电脑和Hi3861在同一局域网下。首先需要知道电脑的IP地址和服务的端口号,以便于Hi3861的客户端可以将数据发送过来。
打开电脑的命令行,输入“ipconfiig”回车即可查询到电脑的IP;
然后是端口号,这个可以自行设置的,笔者采用的是8888。
这里笔者采用的是小熊派的代码,代码中增加了一些注释,代码流程和上面的工作流程一样。部分Socket的函数简介如下图:
详细讲解可以去他们的开源社区查看——开源社区
已下是代码部分:
#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "wifi_device.h"
#include "lwip/netifapi.h"
#include "lwip/api_shell.h"
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
#include "lwip/sockets.h"//套接字的头文件
#include "wifi_connect.h"
#define _PROT_ 8888//端口对应上面的“8888”
//在sock_fd 进行监听,在 new_fd 接收新的链接
int sock_fd;
int addr_length;
static const char *send_data = "Hello! I'm UDP Test!\r\n";
static void UDPClientTask(void)
{
//初始化服务器的地址信息的结构体
struct sockaddr_in send_addr;
socklen_t addr_length = sizeof(send_addr);
char recvBuf[512];
//连接Wifi
WifiConnect("CU_AXUC", "12345678");//修改成和自己电脑一致的SSID与password
//创建本机的套接字socket(一对中的其一)
if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("create socket failed!\r\n");
exit(1);
}
//初始化预连接的服务端地址,绑定服务端的IP以及应用进程端口
//IPV4
send_addr.sin_family = AF_INET;
//通信另一端的应用进程端口
send_addr.sin_port = htons(_PROT_);
//通信另一端的IP地址
send_addr.sin_addr.s_addr = inet_addr("192.168.1.7");
addr_length = sizeof(send_addr);
//总计发送 count 次数据
while (1)
{
bzero(recvBuf, sizeof(recvBuf));
//发送数据到服务远端
sendto(sock_fd, send_data, strlen(send_data), 0, (struct sockaddr *)&send_addr, addr_length);
//线程休眠一段时间
sleep(10);
//接收服务端返回的字符串
recvfrom(sock_fd, recvBuf, sizeof(recvBuf), 0, (struct sockaddr *)&send_addr, &addr_length);
printf("%s:%d=>%s\n", inet_ntoa(send_addr.sin_addr), ntohs(send_addr.sin_port), recvBuf);
}
//关闭这个 socket
closesocket(sock_fd);
}
static void UDPClientDemo(void)
{
osThreadAttr_t attr;
attr.name = "UDPClientTask";
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((osThreadFunc_t)UDPClientTask, NULL, &attr) == NULL)
{
printf("[UDPClientDemo] Falied to create UDPClientTask!\n");
}
}
APP_FEATURE_INIT(UDPClientDemo);
根据之前的工作流程图,可以发现UDP的服务端,只是在客户端的基础上增加了bind绑定,以及更换了发送接收的顺序,这里笔者借用传智的元气派来实现,原文链接。
代码如下:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "genki_wifi_sta.h"
#include "lwip/sockets.h"
#define WIFI_SSID "itheima"
#define WIFI_PASSWORD "12345678"
#define HOSTNAME "itcast"
static void udp_task(void) {
wifi_sta_connect(WIFI_SSID, WIFI_PASSWORD, HOSTNAME);//链接WIFI
// udp create创建套接字
int sock_fd;
int ret;
sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd < 0) {
perror("sock_fd create error\r\n");
return;
}
// config receive addr初始化服务端配置
struct sockaddr_in recvfrom_addr;
socklen_t recvfrom_addr_len = sizeof(recvfrom_addr);
memset((void *) &recvfrom_addr, 0, recvfrom_addr_len);
// 写IP与端口
recvfrom_addr.sin_family = AF_INET;
recvfrom_addr.sin_addr.s_addr = htonl(INADDR_ANY);
recvfrom_addr.sin_port = htons(8080);
// bind receive addr
// bind
ret = bind(sock_fd, (struct sockaddr *) &recvfrom_addr, recvfrom_addr_len);
if (ret == -1) {
perror("bind error\r\n");
return;
}
char recv_buf[1024];
int recv_len;
while (1) {
struct sockaddr_in sender_addr;
int sender_addr_len;
recv_len = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *) &sender_addr,
sender_addr_len);
if (recv_len <= 0) {
continue;
}
char recv_data[recv_len];
memcpy(recv_data, recv_buf, recv_len);
printf("len: %d data: %s\r\n", recv_len, recv_data);
}
}
static void start(void) {
osThreadAttr_t attr;
attr.name = "udp_recv";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 1024 * 4;
attr.priority = 25;
if (osThreadNew((osThreadFunc_t) udp_task, NULL, &attr) == NULL) {
printf("Create udp recv task Failed!\r\n");
}
}
APP_FEATURE_INIT(start);
前面提到过,UDP是可以进行广播的,这里可以参考元气派的教程,使用UDP群发信息,可以实现同步控制多个终端的进程。原文链接——UDP广播。
有关Hi3861的UDP通信介绍就记录至此,文章如有不妥之处欢迎批评指正。
OpenHarmony学习笔记——南向开发环境搭建
OpenHarmony学习笔记——编辑器访问Linux服务器进行编译
OpenHarmony学习笔记——点亮你的LED
OpenHarmony学习笔记——多线程的创建
OpenHarmony学习笔记——I2C驱动0.96OLED屏幕
OpenHarmony学习笔记——Hi3861使用DHT11获取温湿度
OpenHarmony学习笔记——Hi3861接入OneNET
手把手教你OneNET数据可视化
OpenHarmony学习笔记——Hi386+ASR-01的语音识别助手
王雷,TCP/IP网络编程基础教程,北京理工大学出版社,2017.02,第4页 ↩︎
潘伟编著,计算机网络 理论与实验,厦门大学出版社,2013.12,第145页 ↩︎
创客诚品,刘慧欣,孟令一编著,C语言从入门到精通 全新精华版,北京希望电子出版社,2017.10,第377页~第378页 ↩︎