学习目标

  • 理解TCP通讯协议
  • 理解UDP通讯协议
  • 能够编写TCP客户端
  • 能够编写TCP服务端
  • 能够编写UDP的发送端
  • 能够编写UDP的接收端
  • 能够使用UDP广播

    学习内容

    Socket通讯

    socket(简称 套接字) ,是支持TCP/IP网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。 网络通讯 - 图1
    它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的
    例如我们每天浏览网页、QQ 聊天、收发 email 等等

TCP协议

TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP) 是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
TCP通信需要经过创建连接、数据传送、终止连接三个步骤。
网络通讯 - 图2
TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中,”打电话””
网络通讯 - 图3
网络通讯 - 图4
TCP的特点:

  1. 面向连接。
    1. 通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态和连接上的传输。
    2. 双方间的数据传输都可以通过这一个连接进行。
    3. 完成数据交换后,双方必须断开此连接,以释放系统资源。
  2. 传输可靠
    1. TCP采用发送应答机制。
    2. 超时重传。发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。
    3. 错误校验。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
    4. 流量控制和阻塞管理。流量控制用来避免主机发送得过快而使接收方来不及完全收下。

UDP协议

UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。
UDP传输的每个数据包被限制在64K以内(数据包大小由一个16位的无符号整数记录)。
网络通讯 - 图5
UDP (User Datagram Protocol )不提供复杂的控制机制, 如果传输过程中出现丢包, UDP 也不负责重发. 甚至当出现包到达顺序乱掉时候也没有纠正的功能. 由于 UDP 面向无连接, 它可以随时发送数据. 再加上 UDP 本身的处理既简单又高效, 因此常用于以下几个方面:

  • 包总量较少的通信(DNS).
  • 视频、音频等多媒体通信(即时通信).
  • 限定于 LAN 等特定网络中的应用通信.
  • 广播通信(广播、多播)

UDP的特点:

  • 需要资源少
  • 不保证接收
  • 无连接
  • UDP和TCP的区别

    | UDP | TCP | | —- | —- | | 面向无连接 | 面向有连接 | | 支持一对一、一对多、多对一、和多对多的通信 | 只能有两个端点,实现一对一的通信 | | 不保证数据传输的可靠性 | 传输数据无差错,不丢失,不重复,且按时序到达 | | 占用资源较 | 占用资源较 |

TCP编程

开发流程图

115.png

客户端开发

需求说明
  1. 确保设备能够连接网络
  2. 当前开发的是客户端,需要有一个服务端配合,实现两者通讯
  3. 确保服务端和客户端在同一个网络环境中(在同一个局域网)
  4. 客户端循环给服务端发送消息

    开发流程

    来到应用开发的根目录。
    我们编写代码的根目录device/board/itcast/genkipi/app

  5. 根目录下新建tcp_client文件夹,此为项目目录

  6. 项目目录下新建main.c文件
  7. 项目目录下新建BUILD.gn文件
  8. 修改根目录下的BUILD.gn文件
    代码部分
    ```c

    include

    include

    include

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; }

  1. // receive addr config
  2. struct sockaddr_in server_addr;
  3. socklen_t server_addr_len = sizeof(server_addr);
  4. memset((void *) &server_addr, 0, server_addr_len);
  5. server_addr.sin_family = AF_INET;
  6. server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
  7. server_addr.sin_port = htons(SERVER_PORT);
  8. ret = connect(sock_fd, (struct sockaddr *) &server_addr, server_addr_len);
  9. if (ret == -1) {
  10. perror("connect error\r\n");
  11. return;
  12. }
  13. char msg[1024];
  14. int cnt = 0;
  15. while (1) {
  16. sprintf(msg, "hello %d\r\n", cnt++);
  17. ret = send(sock_fd, msg, strlen(msg), 0);
  18. if (ret == -1) {
  19. perror("send error\r\n");
  20. break;
  21. }
  22. usleep(1 * 1000 * 1000);
  23. }

}

static void startSta(void) { wifi_sta_connect(SSID, PASSWORD, HOSTNAME); start_tcp_client(); }

static void main(void) { osThreadAttr_t attr;

  1. attr.name = "tcp_client";
  2. attr.attr_bits = 0U;
  3. attr.cb_mem = NULL;
  4. attr.cb_size = 0U;
  5. attr.stack_mem = NULL;
  6. attr.stack_size = 1024 * 4;
  7. attr.priority = 25;
  8. // Create the Thread1 task
  9. if (osThreadNew((osThreadFunc_t)startSta, NULL, &attr) == NULL) {
  10. printf("Failed to create say hello Thread!\n");
  11. }

}

APP_FEATURE_INIT(main);

  1. ```c
  2. static_library("tcp_client") {
  3. sources = [ "main.c" ]
  4. include_dirs = [
  5. "//base/iothardware/peripheral/interfaces/inner_api",
  6. "../../services/wifi/include",
  7. ]
  8. deps = [
  9. "../../services:genkipi_services",
  10. ]
  11. }
  1. import("//build/lite/config/component/lite_component.gni")
  2. lite_component("app") {
  3. features = [
  4. "tcp_client"
  5. ]
  6. }

校验方式

打开网络调试助手,当前编写的代码为tcp 客户端,那么我们需要将调试助手配置为tcp 服务端。配置如图:
116.png
观察输出结果:
117.png

服务端开发

需求说明
  1. 确保设备能够连接网络
  2. 当前开发的是服务端,需要有一个客户端配合,实现两者通讯
  3. 确保服务端和客户端在同一个网络环境中(在同一个局域网)
  4. 服务端等待客户端发送消息

    开发流程

    来到应用开发的根目录。
    我们编写代码的根目录device/board/itcast/genkipi/app

  5. 根目录下新建tcp_server文件夹,此为项目目录

  6. 项目目录下新建main.c文件
  7. 项目目录下新建BUILD.gn文件
  8. 修改根目录下的BUILD.gn文件
    代码部分
    ```c

    include

    include

    include

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);

  1. memset((void *) &recvfrom_addr, 0, recvfrom_addr_len);
  2. recvfrom_addr.sin_family = AF_INET;
  3. recvfrom_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  4. recvfrom_addr.sin_port = htons(SERVER_PORT);
  5. // bind
  6. ret = bind(sock_fd, (struct sockaddr *) &recvfrom_addr, recvfrom_addr_len);
  7. if (ret == -1) {
  8. perror("bind error\r\n");
  9. return;
  10. }
  11. // listen, just listen 1 connect
  12. ret = listen(sock_fd, 1);
  13. if (ret == -1) {
  14. perror("[actuator_service]listen error\r\n");
  15. return;
  16. }
  17. while (1) {
  18. int client_fd;
  19. struct sockaddr_in client_addr;
  20. socklen_t client_addr_len = sizeof(client_addr);
  21. client_fd = accept(sock_fd, (struct sockaddr *) &client_addr, &client_addr_len);
  22. if (client_fd < 0) {
  23. perror("accept client error\r\n");
  24. continue;
  25. }
  26. printf("client connect: %s\r\n", inet_ntoa(((struct sockaddr_in*)(&client_addr))->sin_addr));
  27. char recv_buf[1024];
  28. int recv_len;
  29. while (1) {
  30. recv_len = recv(client_fd, recv_buf, sizeof(recv_buf), 0);
  31. if (recv_len <= 0) {
  32. break;
  33. }
  34. char recv_data[recv_len];
  35. memcpy(recv_data, recv_buf, recv_len);
  36. recv_data[recv_len] = '\0';
  37. printf("len: %d data: %s\r\n", recv_len, recv_data);
  38. }
  39. }

}

static void startSta(void) { wifi_sta_connect(SSID, PASSWORD, HOSTNAME); start_tcp_server(); }

static void main(void) { osThreadAttr_t attr;

  1. attr.name = "tcp_server";
  2. attr.attr_bits = 0U;
  3. attr.cb_mem = NULL;
  4. attr.cb_size = 0U;
  5. attr.stack_mem = NULL;
  6. attr.stack_size = 1024 * 4;
  7. attr.priority = 25;
  8. // Create the Thread1 task
  9. if (osThreadNew((osThreadFunc_t)startSta, NULL, &attr) == NULL) {
  10. printf("Failed to create say hello Thread!\n");
  11. }

}

APP_FEATURE_INIT(main);

  1. ```c
  2. static_library("tcp_server") {
  3. sources = [ "main.c" ]
  4. include_dirs = [
  5. "//base/iothardware/peripheral/interfaces/inner_api",
  6. "../../services/wifi/include",
  7. ]
  8. deps = [
  9. "../../services:genkipi_services",
  10. ]
  11. }
  1. import("//build/lite/config/component/lite_component.gni")
  2. lite_component("app") {
  3. features = [
  4. "tcp_server"
  5. ]
  6. }

校验方式

打开网络调试助手,当前编写的代码为tcp 服务端,那么我们需要将调试助手配置为tcp 客户端。配置如图:
119.png
服务器IP的获取方式需要通过路由器获得,我使用的是PC机作为热点,可以通过热点中进行设备ip的查询:
118.png
点击连接网络,查看串口打印:
120.png
客户端已经连接到自己编写的服务器上了。
在网络调试工具中输入要发送的内容,点击发送,查看服务端的打印结果:
121.png
122.png

UDP编程

网络通讯 - 图14

客户端开发

需求说明
  1. 确保设备能够连接网络
  2. 当前开发的是客户端,需要有一个服务端配合,实现两者通讯
  3. 确保服务端和客户端在同一个网络环境中(在同一个局域网)
  4. 客户端循环给服务端发送消息

    开发流程

    来到应用开发的根目录。
    我们编写代码的根目录device/board/itcast/genkipi/app

  5. 根目录下新建udp_client文件夹,此为项目目录

  6. 项目目录下新建main.c文件
  7. 项目目录下新建BUILD.gn文件
  8. 修改根目录下的BUILD.gn文件
    代码部分
    ```c

    include

    include

    include

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; }

  1. // config send addr
  2. struct sockaddr_in send_addr;
  3. socklen_t send_addr_len = sizeof(send_addr);
  4. // 初始化发送地址
  5. memset((void *) &send_addr, 0, send_addr_len);
  6. send_addr.sin_family = AF_INET;
  7. //这个位置是否是inet_addr_t
  8. send_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
  9. send_addr.sin_port = htons(SERVER_PORT);
  10. char msg[1024];
  11. int cnt = 0;
  12. while (1) {
  13. sprintf(msg, "hello %d\r\n", cnt++);
  14. ret = sendto(sock_fd, msg, strlen(msg), 0, (struct sockaddr *) &send_addr, send_addr_len);
  15. usleep(1 * 1000 * 1000);
  16. }

}

static void startSta(void) { wifi_sta_connect(SSID, PASSWORD, HOSTNAME); start_udp_client(); }

static void main(void) { osThreadAttr_t attr;

  1. attr.name = "udp_client";
  2. attr.attr_bits = 0U;
  3. attr.cb_mem = NULL;
  4. attr.cb_size = 0U;
  5. attr.stack_mem = NULL;
  6. attr.stack_size = 1024 * 4;
  7. attr.priority = 25;
  8. // Create the Thread1 task
  9. if (osThreadNew((osThreadFunc_t)startSta, NULL, &attr) == NULL) {
  10. printf("Failed to create say hello Thread!\n");
  11. }

}

APP_FEATURE_INIT(main);

  1. ```c
  2. static_library("udp_client") {
  3. sources = [ "main.c" ]
  4. include_dirs = [
  5. "//base/iothardware/peripheral/interfaces/inner_api",
  6. "../../services/wifi/include",
  7. ]
  8. deps = [
  9. "../../services:genkipi_services",
  10. ]
  11. }
  1. import("//build/lite/config/component/lite_component.gni")
  2. lite_component("app") {
  3. features = [
  4. "udp_client"
  5. ]
  6. }

校验方式

打开网络调试助手,当前编写的代码为udp客户端,那么我们需要将调试助手配置为UDP服务端。配置如图:
123.png
服务器IP也是本机IP地址,我们可以通过命令行查询本机的IP:

  1. ipconfig

结果如图:
124.png
我查看的是无线局域网适配器,因为我的PC作为热点和我们的开发板进行连接的。查询结果有两个,两个都可以使用。

点击连接网络后,观察输出结果:
125.png

服务端开发

需求说明
  1. 确保设备能够连接网络
  2. 当前开发的是服务端,需要有一个客户端配合,实现两者通讯
  3. 确保服务端和客户端在同一个网络环境中(在同一个局域网)
  4. 服务端等待客户端发送消息

    开发流程

    来到应用开发的根目录。
    我们编写代码的根目录device/board/itcast/genkipi/app

  5. 根目录下新建udp_server文件夹,此为项目目录

  6. 项目目录下新建main.c文件
  7. 项目目录下新建BUILD.gn文件
  8. 修改根目录下的BUILD.gn文件
    代码部分
    ```c

    include

    include

    include

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; }

  1. // config receive addr
  2. struct sockaddr_in recvfrom_addr;
  3. socklen_t recvfrom_addr_len = sizeof(recvfrom_addr);
  4. memset((void *) &recvfrom_addr, 0, recvfrom_addr_len);
  5. recvfrom_addr.sin_family = AF_INET;
  6. recvfrom_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  7. recvfrom_addr.sin_port = htons(SERVER_PORT);
  8. // bind receive addr
  9. // bind
  10. ret = bind(sock_fd, (struct sockaddr *) &recvfrom_addr, recvfrom_addr_len);
  11. if (ret == -1) {
  12. perror("bind error\r\n");
  13. return;
  14. }
  15. char recv_buf[1024];
  16. int recv_len;
  17. while (1) {
  18. struct sockaddr_in sender_addr;
  19. int sender_addr_len;
  20. recv_len = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *) &sender_addr,
  21. sender_addr_len);
  22. if (recv_len <= 0) {
  23. continue;
  24. }
  25. char recv_data[recv_len];
  26. memcpy(recv_data, recv_buf, recv_len);
  27. printf("len: %d data: %s\r\n", recv_len, recv_data);
  28. }

}

static void startSta(void) { wifi_sta_connect(SSID, PASSWORD, HOSTNAME); start_udp_server(); }

static void main(void) { osThreadAttr_t attr;

  1. attr.name = "udp_server";
  2. attr.attr_bits = 0U;
  3. attr.cb_mem = NULL;
  4. attr.cb_size = 0U;
  5. attr.stack_mem = NULL;
  6. attr.stack_size = 1024 * 4;
  7. attr.priority = 25;
  8. // Create the Thread1 task
  9. if (osThreadNew((osThreadFunc_t)startSta, NULL, &attr) == NULL) {
  10. printf("Failed to create say hello Thread!\n");
  11. }

}

APP_FEATURE_INIT(main);

  1. ```c
  2. static_library("tcp_server") {
  3. sources = [ "main.c" ]
  4. include_dirs = [
  5. "//base/iothardware/peripheral/interfaces/inner_api",
  6. "../../services/wifi/include",
  7. ]
  8. deps = [
  9. "../../services:genkipi_services",
  10. ]
  11. }
  1. import("//build/lite/config/component/lite_component.gni")
  2. lite_component("app") {
  3. features = [
  4. "tcp_server"
  5. ]
  6. }

校验方式

打开网络调试助手,当前编写的代码为tcp 服务端,那么我们需要将调试助手配置为tcp 客户端。配置如图:
126.png
服务器IP的获取方式需要通过路由器获得,我使用的是PC机作为热点,可以通过热点中进行设备ip的查询:
118.png
点击连接网络,查看串口打印:
127.png

广播开发

需求说明
  1. 确保设备能够连接网络
  2. 当前开发的是广播端,需要多个接收端配合,实现一对多通讯
  3. 确保服务端和客户端在同一个网络环境中(在同一个局域网)
  4. 广播端循环给接收端发送消息

    开发流程

    来到应用开发的根目录。
    我们编写代码的根目录device/board/itcast/genkipi/app

  5. 根目录下新建udp_broadcast文件夹,此为项目目录

  6. 项目目录下新建main.c文件
  7. 项目目录下新建BUILD.gn文件
  8. 修改根目录下的BUILD.gn文件
    代码部分
    ```c

    include

    include

    include

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; }

  1. // set broadcast mode
  2. int yes = 1;
  3. ret = setsockopt(sock_fd, SOL_SOCKET, SO_BROADCAST, (char *) &yes, sizeof(yes));
  4. if (ret == -1) {
  5. perror("setsockopt error\r\n");
  6. return;
  7. }
  8. // config broadcast addr
  9. struct sockaddr_in broadcast_addr;
  10. socklen_t broadcast_addr_len = sizeof(broadcast_addr);
  11. memset((void *) &broadcast_addr, 0, broadcast_addr_len);
  12. broadcast_addr.sin_family = AF_INET;
  13. broadcast_addr.sin_addr.s_addr = htonl(INADDR_BROADCAST);
  14. broadcast_addr.sin_port = htons(SERVER_PORT);
  15. char msg[1024];
  16. int cnt = 0;
  17. while (1) {
  18. sprintf(msg, "hello %d\r\n", cnt++);
  19. ret = sendto(sock_fd, msg, strlen(msg), 0, (struct sockaddr *) &broadcast_addr, broadcast_addr_len);
  20. usleep(1 * 1000 * 1000);
  21. }

}

static void startSta(void) { wifi_sta_connect(SSID, PASSWORD, HOSTNAME);

  1. start_udp_broadcast();

}

static void main(void) { osThreadAttr_t attr;

  1. attr.name = "tcp_client";
  2. attr.attr_bits = 0U;
  3. attr.cb_mem = NULL;
  4. attr.cb_size = 0U;
  5. attr.stack_mem = NULL;
  6. attr.stack_size = 1024 * 4;
  7. attr.priority = 25;
  8. // Create the Thread1 task
  9. if (osThreadNew((osThreadFunc_t)startSta, NULL, &attr) == NULL) {
  10. printf("Failed to create say hello Thread!\n");
  11. }

}

APP_FEATURE_INIT(main);

  1. ```c
  2. static_library("udp_broadcast") {
  3. sources = [ "main.c" ]
  4. include_dirs = [
  5. "//base/iothardware/peripheral/interfaces/inner_api",
  6. "../../services/wifi/include",
  7. ]
  8. deps = [
  9. "../../services:genkipi_services",
  10. ]
  11. }
  1. import("//build/lite/config/component/lite_component.gni")
  2. lite_component("app") {
  3. features = [
  4. "udp_broadcast"
  5. ]
  6. }

校验方式

打开网络调试助手,当前编写的代码为UDP广播端,那么我们需要将调试助手配置为UDP接收端,而且不止一个UDP接收端,这个时候我们需要多台电脑,每台电脑都打开网络调试助手。网络调试助手配置如图:
128.png
点击连接网络,观察打印输出结果。
129.png
在这里需要特别强调的是,多台接收电脑和广播端必须在同一个局域网内。

练习题

  1. 实现TCP 客户端的编写,进行调试。
  2. 实现TCP 服务端的编写,进行调试。
  3. 实现UDP 客户端的编写,进行调试。
  4. 实现UDP 服务端的编写,进行调试。
  5. 实现UDP 广播端的编写,进行调试。