前言
网络通讯是嵌入式开发、物联网设备开发的核心基础能力,而Socket套接字作为TCP/IP协议簇的核心通讯单元,是实现跨设备、跨主机进程双向数据传输的核心载体。日常网络浏览、即时通讯、音视频传输、设备物联网数据上报等绝大多数网络服务,均基于Socket通讯机制实现。
在嵌入式鸿蒙设备开发场景中,熟练掌握Socket通讯原理,区分TCP、UDP协议的核心特性与适用场景,能够独立完成客户端、服务端及广播通讯的开发调试,是物联网设备联网交互开发的必备技能。本文将从核心通讯原理入手,详细讲解Socket、TCP、UDP的基础概念与核心区别,并结合实际开发案例,完整演示TCP点对点通讯、UDP点对点通讯、UDP广播通讯的开发流程、代码实现与调试校验方式,为嵌入式网络通讯开发提供完整的实战参考。
一、核心通讯基础原理
Socket通讯
socket(简称 套接字) ,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
编辑
它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的
例如我们每天浏览网页、QQ 聊天、收发 email 等等
TCP协议
TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP) 是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
TCP通信需要经过创建连接、数据传送、终止连接三个步骤。
编辑
TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中,"打电话""
编辑
编辑
TCP的特点:
- 面向连接。
-
- 通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态和连接上的传输。
- 双方间的数据传输都可以通过这一个连接进行。
- 完成数据交换后,双方必须断开此连接,以释放系统资源。
- 传输可靠
-
- TCP采用发送应答机制。
- 超时重传。发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。
- 错误校验。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
- 流量控制和阻塞管理。流量控制用来避免主机发送得过快而使接收方来不及完全收下。
UDP协议
UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。
UDP传输的每个数据包被限制在64K以内(数据包大小由一个16位的无符号整数记录)。
编辑
UDP (User Datagram Protocol )不提供复杂的控制机制, 如果传输过程中出现丢包, UDP 也不负责重发. 甚至当出现包到达顺序乱掉时候也没有纠正的功能. 由于 UDP 面向无连接, 它可以随时发送数据. 再加上 UDP 本身的处理既简单又高效, 因此常用于以下几个方面:
- 包总量较少的通信(DNS).
- 视频、音频等多媒体通信(即时通信).
- 限定于 LAN 等特定网络中的应用通信.
- 广播通信(广播、多播)
UDP的特点:
- 需要资源少
- 不保证接收
- 无连接
- 快
UDP和TCP的区别
| UDP | TCP |
| 面向无连接 | 面向有连接 |
| 支持一对一、一对多、多对一、和多对多的通信 | 只能有两个端点,实现一对一的通信 |
| 不保证数据传输的可靠性 | 传输数据无差错,不丢失,不重复,且按时序到达 |
| 占用资源较少 | 占用资源较多 |
TCP编程
开发流程图
编辑
客户端开发
需求说明
- 确保设备能够连接网络
- 当前开发的是客户端,需要有一个服务端配合,实现两者通讯
- 确保服务端和客户端在同一个网络环境中(在同一个局域网)
- 客户端循环给服务端发送消息
开发流程
来到应用开发的根目录。
我们编写代码的根目录 为device/board/itcast/genkipi/app。
- 根目录 下新建
tcp_client文件夹,此为项目目录。 - 此项目目录 下新建
main.c文件 - 此项目目录 下新建
BUILD.gn文件 - 修改根目录 下的
BUILD.gn文件
代码部分
ini
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "cmsis_os2.h"
#include "ohos_init.h"
#include "wifi_sta.h"
#include "lwip/sockets.h"
#define SSID "xq"
#define PASSWORD "qwer1234"
#define HOSTNAME "itcast"
#define SERVER_IP "192.168.137.1"
#define SERVER_PORT 8080
static void start_tcp_client(void) {
int sock_fd;
int ret;
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
perror("[actuator_service]sock_fd create error\r\n");
return;
}
// receive addr config
struct sockaddr_in server_addr;
socklen_t server_addr_len = sizeof(server_addr);
memset((void *) &server_addr, 0, server_addr_len);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(SERVER_PORT);
ret = connect(sock_fd, (struct sockaddr *) &server_addr, server_addr_len);
if (ret == -1) {
perror("connect error\r\n");
return;
}
char msg[1024];
int cnt = 0;
while (1) {
sprintf(msg, "hello %d\r\n", cnt++);
ret = send(sock_fd, msg, strlen(msg), 0);
if (ret == -1) {
perror("send error\r\n");
break;
}
usleep(1 * 1000 * 1000);
}
}
static void startSta(void)
{
wifi_sta_connect(SSID, PASSWORD, HOSTNAME);
start_tcp_client();
}
static void main(void)
{
osThreadAttr_t attr;
attr.name = "tcp_client";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 1024 * 4;
attr.priority = 25;
// Create the Thread1 task
if (osThreadNew((osThreadFunc_t)startSta, NULL, &attr) == NULL) {
printf("Failed to create say hello Thread!\n");
}
}
APP_FEATURE_INIT(main);
ini
static_library("tcp_client") {
sources = [ "main.c" ]
include_dirs = [
"//base/iothardware/peripheral/interfaces/inner_api",
"../../services/wifi/include",
]
deps = [
"../../services:genkipi_services",
]
}
javascript
import("//build/lite/config/component/lite_component.gni")
lite_component("app") {
features = [
"tcp_client"
]
}
校验方式
打开网络调试助手,当前编写的代码为tcp 客户端,那么我们需要将调试助手配置为tcp 服务端。配置如图:
编辑
观察输出结果:
编辑
服务端开发
需求说明
- 确保设备能够连接网络
- 当前开发的是服务端 ,需要有一个客户端配合,实现两者通讯
- 确保服务端和客户端在同一个网络环境中(在同一个局域网)
- 服务端等待客户端发送消息
开发流程
来到应用开发的根目录。
我们编写代码的根目录 为device/board/itcast/genkipi/app。
- 根目录 下新建
tcp_server文件夹,此为项目目录。 - 此项目目录 下新建
main.c文件 - 此项目目录 下新建
BUILD.gn文件 - 修改根目录 下的
BUILD.gn文件
代码部分
ini
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "cmsis_os2.h"
#include "ohos_init.h"
#include "wifi_sta.h"
#include "lwip/sockets.h"
#define SSID "xq"
#define PASSWORD "qwer1234"
#define HOSTNAME "itcast"
#define SERVER_PORT 8080
static void start_tcp_server(void) {
int sock_fd;
int ret;
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
perror("[actuator_service]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);
recvfrom_addr.sin_family = AF_INET;
recvfrom_addr.sin_addr.s_addr = htonl(INADDR_ANY);
recvfrom_addr.sin_port = htons(SERVER_PORT);
// bind
ret = bind(sock_fd, (struct sockaddr *) &recvfrom_addr, recvfrom_addr_len);
if (ret == -1) {
perror("bind error\r\n");
return;
}
// listen, just listen 1 connect
ret = listen(sock_fd, 1);
if (ret == -1) {
perror("[actuator_service]listen error\r\n");
return;
}
while (1) {
int client_fd;
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
client_fd = accept(sock_fd, (struct sockaddr *) &client_addr, &client_addr_len);
if (client_fd < 0) {
perror("accept client error\r\n");
continue;
}
printf("client connect: %s\r\n", inet_ntoa(((struct sockaddr_in*)(&client_addr))->sin_addr));
char recv_buf[1024];
int recv_len;
while (1) {
recv_len = recv(client_fd, recv_buf, sizeof(recv_buf), 0);
if (recv_len <= 0) {
break;
}
char recv_data[recv_len];
memcpy(recv_data, recv_buf, recv_len);
recv_data[recv_len] = '\0';
printf("len: %d data: %s\r\n", recv_len, recv_data);
}
}
}
static void startSta(void)
{
wifi_sta_connect(SSID, PASSWORD, HOSTNAME);
start_tcp_server();
}
static void main(void)
{
osThreadAttr_t attr;
attr.name = "tcp_server";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 1024 * 4;
attr.priority = 25;
// Create the Thread1 task
if (osThreadNew((osThreadFunc_t)startSta, NULL, &attr) == NULL) {
printf("Failed to create say hello Thread!\n");
}
}
APP_FEATURE_INIT(main);
ini
static_library("tcp_server") {
sources = [ "main.c" ]
include_dirs = [
"//base/iothardware/peripheral/interfaces/inner_api",
"../../services/wifi/include",
]
deps = [
"../../services:genkipi_services",
]
}
javascript
import("//build/lite/config/component/lite_component.gni")
lite_component("app") {
features = [
"tcp_server"
]
}
校验方式
打开网络调试助手,当前编写的代码为tcp 服务端,那么我们需要将调试助手配置为tcp 客户端。配置如图:
编辑
服务器IP的获取方式需要通过路由器获得,我使用的是PC机作为热点,可以通过热点中进行设备ip的查询:
编辑
点击连接网络,查看串口打印:
编辑
客户端已经连接到自己编写的服务器上了。
在网络调试工具中输入要发送的内容,点击发送,查看服务端的打印结果:
编辑
编辑
UDP编程
编辑
客户端开发
需求说明
- 确保设备能够连接网络
- 当前开发的是客户端,需要有一个服务端配合,实现两者通讯
- 确保服务端和客户端在同一个网络环境中(在同一个局域网)
- 客户端循环给服务端发送消息
开发流程
来到应用开发的根目录。
我们编写代码的根目录 为device/board/itcast/genkipi/app。
- 根目录 下新建
udp_client文件夹,此为项目目录。 - 此项目目录 下新建
main.c文件 - 此项目目录 下新建
BUILD.gn文件 - 修改根目录 下的
BUILD.gn文件
代码部分
arduino
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "cmsis_os2.h"
#include "ohos_init.h"
#include "wifi_sta.h"
#include "lwip/sockets.h"
#define SSID "xq"
#define PASSWORD "qwer1234"
#define HOSTNAME "itcast"
#define SERVER_IP "192.168.137.1"
#define SERVER_PORT 8080
static void start_udp_client(void) {
// 创建udp
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 send addr
struct sockaddr_in send_addr;
socklen_t send_addr_len = sizeof(send_addr);
// 初始化发送地址
memset((void *) &send_addr, 0, send_addr_len);
send_addr.sin_family = AF_INET;
//这个位置是否是inet_addr_t
send_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
send_addr.sin_port = htons(SERVER_PORT);
char msg[1024];
int cnt = 0;
while (1) {
sprintf(msg, "hello %d\r\n", cnt++);
ret = sendto(sock_fd, msg, strlen(msg), 0, (struct sockaddr *) &send_addr, send_addr_len);
usleep(1 * 1000 * 1000);
}
}
static void startSta(void)
{
wifi_sta_connect(SSID, PASSWORD, HOSTNAME);
start_udp_client();
}
static void main(void)
{
osThreadAttr_t attr;
attr.name = "udp_client";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 1024 * 4;
attr.priority = 25;
// Create the Thread1 task
if (osThreadNew((osThreadFunc_t)startSta, NULL, &attr) == NULL) {
printf("Failed to create say hello Thread!\n");
}
}
APP_FEATURE_INIT(main);
ini
static_library("udp_client") {
sources = [ "main.c" ]
include_dirs = [
"//base/iothardware/peripheral/interfaces/inner_api",
"../../services/wifi/include",
]
deps = [
"../../services:genkipi_services",
]
}
javascript
import("//build/lite/config/component/lite_component.gni")
lite_component("app") {
features = [
"udp_client"
]
}
校验方式
打开网络调试助手,当前编写的代码为udp客户端,那么我们需要将调试助手配置为UDP服务端。配置如图:
编辑
服务器IP也是本机IP地址,我们可以通过命令行查询本机的IP:
ipconfig
结果如图:
编辑
我查看的是无线局域网适配器,因为我的PC作为热点和我们的开发板进行连接的。查询结果有两个,两个都可以使用。
点击连接网络后,观察输出结果:
编辑
服务端开发
需求说明
- 确保设备能够连接网络
- 当前开发的是服务端 ,需要有一个客户端配合,实现两者通讯
- 确保服务端和客户端在同一个网络环境中(在同一个局域网)
- 服务端等待客户端发送消息
开发流程
来到应用开发的根目录。
我们编写代码的根目录 为device/board/itcast/genkipi/app。
- 根目录 下新建
udp_server文件夹,此为项目目录。 - 此项目目录 下新建
main.c文件 - 此项目目录 下新建
BUILD.gn文件 - 修改根目录 下的
BUILD.gn文件
代码部分
c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "cmsis_os2.h"
#include "ohos_init.h"
#include "wifi_sta.h"
#include "lwip/sockets.h"
#define SSID "xq"
#define PASSWORD "qwer1234"
#define HOSTNAME "itcast"
#define SERVER_PORT 8080
static void start_udp_server(void) {
// 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);
recvfrom_addr.sin_family = AF_INET;
recvfrom_addr.sin_addr.s_addr = htonl(INADDR_ANY);
recvfrom_addr.sin_port = htons(SERVER_PORT);
// 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 startSta(void)
{
wifi_sta_connect(SSID, PASSWORD, HOSTNAME);
start_udp_server();
}
static void main(void)
{
osThreadAttr_t attr;
attr.name = "udp_server";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 1024 * 4;
attr.priority = 25;
// Create the Thread1 task
if (osThreadNew((osThreadFunc_t)startSta, NULL, &attr) == NULL) {
printf("Failed to create say hello Thread!\n");
}
}
APP_FEATURE_INIT(main);
ini
static_library("tcp_server") {
sources = [ "main.c" ]
include_dirs = [
"//base/iothardware/peripheral/interfaces/inner_api",
"../../services/wifi/include",
]
deps = [
"../../services:genkipi_services",
]
}
javascript
import("//build/lite/config/component/lite_component.gni")
lite_component("app") {
features = [
"tcp_server"
]
}
校验方式
打开网络调试助手,当前编写的代码为tcp 服务端,那么我们需要将调试助手配置为tcp 客户端。配置如图:
编辑
服务器IP的获取方式需要通过路由器获得,我使用的是PC机作为热点,可以通过热点中进行设备ip的查询:
编辑
点击连接网络,查看串口打印:
编辑
广播开发
需求说明
- 确保设备能够连接网络
- 当前开发的是广播端,需要多个接收端配合,实现一对多通讯
- 确保服务端和客户端在同一个网络环境中(在同一个局域网)
- 广播端循环给接收端发送消息
开发流程
来到应用开发的根目录。
我们编写代码的根目录 为device/board/itcast/genkipi/app。
- 根目录 下新建
udp_broadcast文件夹,此为项目目录。 - 此项目目录 下新建
main.c文件 - 此项目目录 下新建
BUILD.gn文件 - 修改根目录 下的
BUILD.gn文件
代码部分
ini
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "cmsis_os2.h"
#include "ohos_init.h"
#include "wifi_sta.h"
#include "lwip/sockets.h"
#define SSID "xq"
#define PASSWORD "qwer1234"
#define HOSTNAME "itcast"
#define SERVER_PORT 8080
static void start_udp_broadcast(void) {
// 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;
}
// set broadcast mode
int yes = 1;
ret = setsockopt(sock_fd, SOL_SOCKET, SO_BROADCAST, (char *) &yes, sizeof(yes));
if (ret == -1) {
perror("setsockopt error\r\n");
return;
}
// config broadcast addr
struct sockaddr_in broadcast_addr;
socklen_t broadcast_addr_len = sizeof(broadcast_addr);
memset((void *) &broadcast_addr, 0, broadcast_addr_len);
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_addr.s_addr = htonl(INADDR_BROADCAST);
broadcast_addr.sin_port = htons(SERVER_PORT);
char msg[1024];
int cnt = 0;
while (1) {
sprintf(msg, "hello %d\r\n", cnt++);
ret = sendto(sock_fd, msg, strlen(msg), 0, (struct sockaddr *) &broadcast_addr, broadcast_addr_len);
usleep(1 * 1000 * 1000);
}
}
static void startSta(void)
{
wifi_sta_connect(SSID, PASSWORD, HOSTNAME);
start_udp_broadcast();
}
static void main(void)
{
osThreadAttr_t attr;
attr.name = "tcp_client";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 1024 * 4;
attr.priority = 25;
// Create the Thread1 task
if (osThreadNew((osThreadFunc_t)startSta, NULL, &attr) == NULL) {
printf("Failed to create say hello Thread!\n");
}
}
APP_FEATURE_INIT(main);
ini
static_library("udp_broadcast") {
sources = [ "main.c" ]
include_dirs = [
"//base/iothardware/peripheral/interfaces/inner_api",
"../../services/wifi/include",
]
deps = [
"../../services:genkipi_services",
]
}
javascript
import("//build/lite/config/component/lite_component.gni")
lite_component("app") {
features = [
"udp_broadcast"
]
}
校验方式
打开网络调试助手,当前编写的代码为UDP广播端,那么我们需要将调试助手配置为UDP接收端,而且不止一个UDP接收端,这个时候我们需要多台电脑,每台电脑都打开网络调试助手。网络调试助手配置如图:
编辑
点击连接网络,观察打印输出结果。
编辑
在这里需要特别强调的是,多台接收电脑和广播端必须在同一个局域网内。
总结
本文系统阐述了Socket通讯的核心原理,清晰对比了TCP与UDP两大传输协议的特性、区别与适用场景,并基于鸿蒙嵌入式设备,完整实现了TCP点对点通讯、UDP点对点通讯、UDP局域网广播通讯三大核心开发案例。
TCP协议凭借面向连接、可靠传输的特性,适用于文件传输、指令交互、数据上报等对完整性、有序性要求高的场景;而UDP协议凭借无连接、低延迟、低资源消耗的优势,广泛应用于音视频传输、实时广播、轻量化数据交互场景,其中UDP广播更是实现局域网一对多通讯的核心方案。
本次所有案例均包含完整的项目搭建、代码编写、编译配置与调试校验流程,可直接落地应用。熟练掌握以上开发内容,能够满足嵌入式物联网设备基础网络通讯的开发需求,为后续复杂网络交互、多设备组网、物联网数据传输等进阶开发奠定坚实的技术基础。