网络编程2(套接字编程)

套接字编程

  • UDP协议通信:
  • TCP通信:

套接字编程:如何编写一个网络通信程序
1.网络通信的数据中都会包含一个完整的五元组:
sip,sport,dip,dport,protocol(源IP,源端口,对端IP,对端端口,协议)
五元组完整的描述了数据从哪来,到哪去,用什么数据格式
2.网络通信–两个主机进程之间的通信:客户端&服务端
客户端:用户使用,发起请求
服务端:网络应用提供商提供服务的程序(后台开发)
在这里插入图片描述
在这里插入图片描述

UDP协议通信:

服务端一方要提前启动,保证有数据到来时,一定能够接收;并且服务端永远都是先接收数据,因为服务端这一方并没有保存客户端的地址,没有地址绑定,也就没有数据来源,也不知道数据要发送什么,要发给谁
客户端:创建套接字,端口绑定(不推荐),发送数据,接收数据,关闭套接字
在这里插入图片描述
操作接口:
socket–创建套接字
在这里插入图片描述
domain—地址域类型(域间通信、IPv4通信、IPv6通信)AF_INET—IPv4网络协议
在这里插入图片描述
type–套接字类型
SOCK_STREAM ; SOCK_DGRAM
在这里插入图片描述

在这里插入图片描述
bind–为套接字绑定地址信息
int bind(int sockfd, struct sockaddr *addr, socklen_t address_len)
三个参数:套接字描述符,要绑定的地址(不同地址域,有不同的地址结构),sockaddr结构体的长度
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
sendto:发送数据
参数:sockfd—返回的套接字描述符
buf–要发送的数据空间起始地址
len–要发送的数据长度(从buf地址开始,发送len长度的数据)
flags–默认0-阻塞发送(发送缓冲区满了就等着)
dest_addr–对端地址信息,数据要发送给谁,目的地
addrlen–对端地址信息长度

在这里插入图片描述
recvfrom:接收数据
对于recvfrom的src_addr参数,是为了获取数据是谁发送的,相当于用指针接收返回值,addrlen也一样(可以看到这里的addrlen是指针)

在这里插入图片描述

int close(int fd)
关闭套接字,释放资源

字节序相关接口:
在这里插入图片描述
htonl(32位),htons(16位),ntohl,ntohs ; l–32位 ; s–16位
这几个接口已经进行了主机字节序的判断,因此无需担心自己的主机字节序
32位数据转换接口与16位不能混用,会出现数据截断,就像汉字用两个字节表示,而如果按字节输出就会乱码,并且再大小端转换时,数据截断会造成数据的错误
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
sockaddr结构体
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

udp_srv.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>//字节序转换接口头文件
#include <netinet/in.h>//地址结构类型以及协议类型宏头文件
#include <sys/socket.h>//套接字接口头文件

int main(int agrc, char *argv[])
{
    if (agrc != 3)
    {
        printf("/unp_srv 192.168.2.2 9000\n");
        return -1;
    }

    uint16_t port = atoi(argv[2]); // 输入的参数都是按字符串存的,这里将端口port转int
    char *ip = argv[1];

    // 创建套接字  int socket(int domain, int type ,int protocol)
    int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // ipv4域,数据报,udp协议
    if (socket < 0)
    {
        perror("socket error");
        return -1;
    }

    // 为套接字绑定地址信息  int bind(int sockfd, struct sockaddr* addr,socklen_t len)
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port); // 16位,存储用16位解析也用16位
    addr.sin_addr.s_addr = inet_addr(ip);
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = bind(sockfd, (struct sockaddr *)&addr, len);
    if (ret == -1)
    {
        perror("bind error");
        return -1;
    }

    // 循环接收发送数据
    while (1)
    {
        char buf[1024] = {0};
        struct sockaddr_in peer; // 地址由系统设置,数据谁发的,设置的就是谁
        socklen_t len = sizeof(struct sockaddr_in);
        ssize_t ret = recvfrom(sockfd, buf, 1023, 0, (struct sockaddr *)&peer, &len);
        if (ret < 0)
        {
            perror("recvfrom error");
            return -1;
        }

        // const char* inet_ntoa(struct in_addr addr);
        char *peerip = inet_ntoa(peer.sin_addr); // 转成字符串
        uint16_t peerport = ntohs(peer.sin_port);
        printf("client[%s:%d] say: %s\n", peerip, peerport, buf);

        // 发送数据
        // ssize_t sendto(int sockfd, void* buf, int len, int flag, struct sockaddr* peer, socklen_t len)
        char data[1024] = {0};
        printf("server say:");
        fflush(stdout);
        scanf("%s", data);
        ret = sendto(sockfd, data, strlen(data), 0, (struct sockaddr *)&peer, len);
        if (ret < 0)
        {
            perror("sendto error");
            return -1;
        }

        
    }
    // 关闭套接字
        close(sockfd);
    return 0;
}


g++ -std=c++11 -o udp_cli udp_cli.cpp
udp_cli.cpp

#include "udp_socket.hpp"
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "usage: ./udp_cli 192.168.2.2 9000\n";
        return -1;
    }
    std::string srv_ip = argv[1];
    uint16_t srv_port = std::stoi(argv[2]);

    UdpSocket cli_sock;
    assert(cli_sock.Socket()==true);

    while (1)
    {
        std::string data;
        std::cout << "clinet say: ";
        fflush(stdout);
        std::cin >> data;
        assert(cli_sock.Send(data, srv_ip, srv_port)==true);

        data.clear();
        assert(cli_sock.Recv(&data)==true);
        std::cout << "server say:" << data << std::endl;
    }

    cli_sock.Close();
    return 0;
}

在这里插入图片描述

udp_socket.hpp //封装socket接口

#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <cstdio>
#include <cassert>
#include <string>
class UdpSocket
{
private:
    int _sockfd;

public:
    UdpSocket():_sockfd(-1){}
    ~UdpSocket(){Close();}
    bool Socket()
    {
        _sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
        if (_sockfd < 0)
        {
            perror("socket error");
            return false;
        }
        return true;
    }
    bool Bind(const std::string &ip, uint16_t port)
    {
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);
        int ret = bind(_sockfd, (struct sockaddr *)&addr, len);
        if (ret < 0)
        {
            perror("bind error");
            return false;
        }
        return true;
    }
    bool Recv(std::string *body, std::string *peer_ip = NULL, uint16_t *peer_port = NULL)
    {
        // ssize_t recvfrom(int _fd, void *_restrict_ _buf, size_t _n, int _flags, sockaddr *_restrict_ _addr, socklen_t *_restrict_ _addr_len)
        struct sockaddr_in peer;
        socklen_t len = sizeof(struct sockaddr_in);
        char tmp[4096] = {0};
        ssize_t ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr *)&peer, &len);
        if (ret < 0)
        {
            perror("recvfrom error");
            return false;
        }
        if(peer_ip!=NULL) *peer_ip = inet_ntoa(peer.sin_addr);
        if(peer_port!=NULL) *peer_port = ntohs(peer.sin_port);
        body->assign(tmp, ret); // 从tmp中取出ret长度的数据,放到body
        return true;
    }

    bool Send(const std::string &body, const std::string &peer_ip, uint16_t peer_port)
    {
        struct sockaddr_in addr;
        addr.sin_family=AF_INET;
        addr.sin_port = htons(peer_port);
        addr.sin_addr.s_addr = inet_addr(peer_ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);
        // ssize_t sendto(int __fd, const void *__buf, size_t __n, int __flags, const sockaddr *__addr, socklen_t __addr_len)
        ssize_t ret = sendto(_sockfd, body.c_str(), body.size(), 0, (struct sockaddr *)&addr, len);
        if (ret < 0)
        {
            perror("sendto error");
            return false;
        }
        return true;
    }
    bool Close()
    {
        if (_sockfd != -1)
        {
            close(_sockfd);
            _sockfd = -1;
        }
        return true;
    }
};

TCP通信:

客户端向服务器发送一个请求,服务段处于listen监听状态,则会对这个连接请求进行处理:
1.为这个新链接请求,创建一个套接字结构体socket
2.为这个新的socket,描述完整的五元组信息(sip,sport,dip,dport,protocol)
往后的数据通信都是由这个新的套接字进行通信
一个服务器上有多少客户端想要简历连接,服务端就要创建多少个套接字
最早服务端创建的监听套接字–只负责新连接请求处理,不负责数据通信
服务端会为每个客户端都创建一个新的套接字,负责与这个客户端进行数据通信,但是想要通过这个套接字与客户顿进行通信,就要拿到这个套接字的描述符sockfd
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
相较于UDP套接字通信,TCP通信多了listen,connect,accept,
用recv,send简化了数据收发,因为地址信息都在sockfd描述的socket结构体中五元组完全包含,并且TCP通信时有状态的status
recv
这些调用返回接收到的字节数,如果发生错误,则返回-1。在如果发生错误,则设置errno以指示错误。
返回值将为0,说明没有数据,实际是对方已经执行有序关闭时,连接断开了

在客户端要注意监听套接字listenfd,通信套接字connfd的区别,listenfd只负责连接的建立在listen和accept时使用,当要发送数据是用accept返回的套接字connfd进行recv数据发送

tcpsocket.hpp(封装系统调用接口)

#ifndef __M_TCP_H__
#define __M_TCP_H__
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <cstdio>
#include <cassert>
#include <string>

#define MAX_LISTEN 1024
class TcpSocket
{
private:
    /* data */
    int _sockfd; // 这是监听套接字

public:
    TcpSocket() : _sockfd(-1) {}
    ~TcpSocket()
    {
        Close();
        _sockfd = -1;
    }
    bool Socket()
    {
        // int socket(int domain, int type, int protocol)
        _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (_sockfd < 0)
        {
            perror("socket error");
            return false;
        }
        return true;
    }
    bool Bind(const std::string &ip, uint16_t port)
    {
        // int bind(int sockfd, struct sockaddr* addr, socklen_t len)
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);                  // port是2字节,要注意字节序问题,使用htons
        addr.sin_addr.s_addr = inet_addr(ip.c_str()); // 192.168.154.131 ===> 转换成网络字节序的整形ip
        // 客户端bind的sockaddr要进行内容填充,将协议族,ip,端口都要确定
        socklen_t addrlen = sizeof(struct sockaddr_in);
        // struct sockaddr: Structure describing a generic socket address.
        int ret = bind(_sockfd, (struct sockaddr *)&addr, addrlen);
        if (ret < 0)
        {
            perror("bind error");
            return false;
        }
        return true;
    }
    bool Listen(int backlog = MAX_LISTEN)
    {
        // int listen(int backlog)
        int ret = listen(_sockfd, backlog);
        if (ret < 0)
        {
            perror("listen error");
            return false;
        }
        return true;
    }

    bool Accept(TcpSocket &newsock) // 这里传入的newsock引用的是外部定义的新socket对象,用来保存通信套接字connfd,accept系统函数传入的是listenfd
    {
        // int accpet(int sockfd, struct sockaddr* peer,sock_len* len)
        int newfd = accept(_sockfd, (struct sockaddr *)NULL, NULL); // addr设置为NULL时,表示不关心客户端的地址,addrlen也应该设置为NULL
        if (newfd < 0)
        {
            perror("accept error");
            return false;
        }
        newsock._sockfd = newfd; // 将获取的新建连接描述符,赋值给外部传入的TcpSocket对象

        return true;
    }

    bool Recv(TcpSocket &sock, std::string &body)
    {
        // ssize_t recv(int sockfd, void* buf, int len, int flag);//收发数据的sockfd是accept获取的新建连接的描述符,不是监听Socket套接字
        char tmp[1024] = {0};
        // recv返回值Returns the number read or -1 for errors.为0时说明连接断开,所以也就没有数据
        ssize_t ret = recv(sock._sockfd, tmp, 1023, 0); // flag=0,表示默认阻塞接收(接受缓冲区没有数据就阻塞)
        if (ret < 0)
        {
            perror("recv error");
            return false;
        }
        else if (ret == 0)
        {
            std::cout << "connect broken";
            return false;
        }
        body.assign(tmp, ret); // 从temp中截取ret大小数据放到body这个string对象中
        return true;
    }

    bool Send(TcpSocket &sock, const std::string &body)
    {
        // ssize_t send(int sockfd, void* data, int len, int flag)
        ssize_t ret = send(sock._sockfd, body.c_str(), body.size(), 0);
        if (ret < 0)
        {
            perror("send error");
            return false;
        }
        return true;
    }

    bool Connect(TcpSocket &sock, const std::string &ip, uint16_t port) // 客户端使用connect
    {
        // int connect(int sockfd, struct sockaddr* srvaddr, socklen_t len);
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(10001);
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);
        int ret = connect(sock._sockfd, (struct sockaddr *)&addr, len);
        if (ret < 0)
        {
            perror("connect error");
            return false;
        }
        return true;
    }

    bool Close()
    {
        if (_sockfd != 1)
        {
            close(_sockfd);
            _sockfd = -1;
        }
        return true;
    }
};

#endif

服务端s1.cpp

#include "tcpsocket.hpp"
using namespace std;
int main(int argc, char **argv)
{
    TcpSocket tcpsocket;
    tcpsocket.Socket();
    // uint16_t port = htons(stoi(argv[1]));
    string ip = string("192.168.154.131");
    tcpsocket.Bind(ip, 10001);
    tcpsocket.Listen();
    TcpSocket newsock; // 新建对象来保存connfd通信套接字(accept函数返回的)
    newsock.Socket();
    tcpsocket.Accept(newsock);
    string buf;
    while (1)
    {
        tcpsocket.Recv(newsock, buf);
        cout << "client say:" << buf << endl;
        buf.clear();
        cout << "server say:";
        cin >> buf;
        tcpsocket.Send(newsock, buf);
    }
    tcpsocket.Close();
    return 0;
}

g++ -std=c++11 -o c1 c1.cpp
客户端c1.cpp

#include "tcpsocket.hpp"
using namespace std;
int main(int argc, char **argv)
{
    TcpSocket tcp_cli;
    tcp_cli.Socket();
    tcp_cli.Connect(tcp_cli, "192.168.154.131", (uint16_t)10001);
    string buf;
    while (1)
    {
        cout << "client say:";
        cin >> buf;
        tcp_cli.Send(tcp_cli, buf);
        buf.clear();
        tcp_cli.Recv(tcp_cli, buf);
        cout << "server say:" << buf << endl;
    }
    tcp_cli.Close();
}

在这里插入图片描述
多线程实现
在这里插入图片描述
多线程实现tcp通信:
在单进程实现的问题,服务器只能与一个客户端进行通信
原因:因为不知道什么时候有数据到来和新连接到来,因此流程只能固定为获取新连接,然后进行通信,但是这两部都有可能会阻塞,一个执行流中要完成的事情太多了
解决:多执行流并发处理–获取新建连接后创建一个执行流专门负责与客户端的通信
多进程注意事项:1.僵尸子进程处理(SIGCHLD)2.描述符资源的释放(父子进程都有sockfd,父进程需要创建子进程后释放)
多线程注意事项:1.线程之间共享大部分进程数据资源(文件描述符表),因此通信套接字需要让负责通信的线程进行释放

如何在代码中知道连接断开了?连接断开后,在代码中如何体现的?

当recv函数接受数据时,返回值为0,代表的不仅仅是没有接收到数据,更多是为了表示连接断开了!!!
当send函数发送数据是,程序直接异常(SIGPIPE)退出,会显示坏管道
因此,如果网络通信中,不想让程序因为连接断开而导致发送数据的时候程序异常退出就对SIGPIPE信号进行signal处理
在这里插入图片描述
netstat -anptu

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/3566.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【linux】多线程控制详述

文章目录一、进程控制1.1 POSIX线程库1.2 创建线程pthread_create1.2.1 创建一批线程1.3 终止线程pthread_exit1.4 线程等待pthread_jion1.4.1 线程的返回值&#xff08;退出码&#xff09;1.5 取消线程pthread_cancel1.6 C多线程1.7 分离线程pthread_detach二、线程ID值三、线…

C/C++内存管理

内存管理在C中无处不在&#xff0c;内存泄漏几乎在每个C程序中都会发生。因此&#xff0c;要学好C&#xff0c;内存管理这一关势在必得&#xff01; 目录 1.C/C内存分布 2.C语言中动态内存管理方式 3.C内存管理方式 3.1.new和delete操作内置类型 3.2.new和delete操作自定义类型…

SQL注入之HTTP请求头注入

Ps&#xff1a; 先做实验&#xff0c;在有操作的基础上理解原理会更清晰更深入。 一、实验 sqli-lab 1. User-Agent注入 特点&#xff1a;登陆后返回用户的 User-Agent --> 服务器端可能记录用户User-Agent 输入不合法数据报错 payload: and updatexml(1,concat("~&…

异或相关算法

文章目录1. 异或的性质2. 题目一3. 题目二4. 题目三5. 题目四1. 异或的性质 我们知道&#xff0c;异或的定义是&#xff1a;相同为0&#xff0c;相异为1。所以也被称为无进位相加&#xff0c;根据这定义&#xff0c;我们可以得出三个性质&#xff1a; 1. N ^ N0。2. N ^ 0N。3…

13-C++面向对象(纯虚函数(抽象类)、多继承、多继承-虚函数、菱形继承、虚继承、静态成员)

虚析构函数 存在父类指针指向子类对象的情况&#xff0c;应该将析构函数声明为虚函数&#xff08;虚析构函数&#xff09; 纯虚函数 纯虚函数&#xff1a;没有函数体且初始化为0的虚函数&#xff0c;用来定义接口规范 抽象类&#xff1a; 含有纯虚函数的类&#xff0c;不可以实…

Prometheus监控实战系列十七:探针监控

目前对于应用程序的监控主要有两种方式&#xff0c;一种被称为白盒监控&#xff0c;它通过获取目标的内部信息指标&#xff0c;来监控目标的状态情况&#xff0c;我们前面介绍的主机监控、容器监控都属于此类监控。另一种则是“黑盒监控”&#xff0c;它指在程序外部通过探针的…

【Linux】Linux下权限的理解

前言&#xff1a;在之前我们已经对基本的指令进行了深入的学习&#xff0c;接下来我将带领大家学习的是关于权限的相关问题。在之前&#xff0c;我们一直是使用的【root】用户&#xff0c;即为“超级用户”&#xff0c;通过对权限的学习之后&#xff0c;我们就会慢慢的切换到普…

【数据结构】双向链表实现

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 一、什么是双向链表 二、双向链表的实现 一、什么是双向链表 双向链表也叫双链表&#xff0c;是链表的一种&#xff0c;它的每个数据节点中都有两个指针&#xff0c;分别指向直接后…

【数据结构初阶】单链表

目录一、思路>>>>>>>>>>>>过程<<<<<<<<<<<<<<<1.打印2.尾插3.尾删4.头插5.头删6.查找7.指定位置后插入8.指定位置后删除9.链表的销毁二、整个程序1.SLTlist.c2.SLTlist.c一、思路 #define …

点云可视化:使用open3d实现点云连续播放

模型训练完成后除了看ap等定量的指标是否变好外,还需要将结果可视化出来,直接观察模型的输出结果,往往我们的数据会比较多,如果单帧的看的话会比较麻烦,需要频繁的关闭窗口,最好是能直接连续的播放数据和模型的推理结果。有三种方法: clear_geomotry()和update_render()…

SpringBoot 解决id使用字符串类型可以解决精度问题

1. 问题引入 当主键超过19位长度的数值型的属性值后三位会被四舍五入 2. 使用雪花算法解决 雪花算法长度最大只有19位的10进制&#xff0c;所以不会丢失精度问题&#xff01;SpringBoot 解决主键雪花算法配置https://liush.blog.csdn.net/article/details/129779627 ① appli…

Linux的基础知识

根目录和家目录根目录&#xff1a;是Linux中最底层的目录&#xff0c;用"/"表示家目录&#xff1a;当前用户所在的路径&#xff0c;用“~”表示&#xff0c;root用户的家目录和普通用户的家目录不一样&#xff0c;普通用户的家目录在/home路径下&#xff0c;每一个用…

eNSP 网络地址转换配置实验

关于本实验当使用私有IP地址的内部主机访问外网时&#xff0c;需要使用NAT将其私有IP地址转换为公有IP地址&#xff0c;此时需要在网关路由器上配置NAT来提供相应的地址转换服务。当网关路由器连接ISP的接口上未使用固定IP地址&#xff0c;而是动态地从ISP获取IP地址时&#xf…

沁恒CH32V307使用记录:SPI基础使用

文章目录目的基础说明使用演示其它补充总结目的 SPI是单片机中比较常用的一个功能。这篇文章将对CH32V307中相关内容进行说明。 本文使用沁恒官方的开发板 &#xff08;CH32V307-EVT-R1沁恒RISC-V模块MCU赤兔评估板&#xff09; 进行演示。 基础说明 SPI的基础概念见下面文…

【Docker】之docker-compose的介绍与命令的使用

&#x1f341;博主简介 &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01; 文章目录docker-compose简介docker-compose基础…

C++中的list类【详细分析及模拟实现】

list类 目录list类一、list的介绍及使用1、构造器及其它重点①遍历②插入删除操作③insert和erase④resize2、Operations接口①remove②sort③merge3、vector与list排序性能比较二、list的深度剖析及模拟实现1、结点的定义2、创建list类3、list类方法的实现3.1 迭代器类的实现*…

【机器学习面试总结】————特征工程

【机器学习面试总结】————特征工程一、特征归一化为什么需要对数值类型的特征做归一化?二、类别型特征在对数据进行预处理时,应该怎样处理类别型特征?三、高维组合特征的处理什么是组合特征?如何处理高维组合特征?四、组合特征怎样有效地找到组合特征?五、文本表示模型…

STM32 10个工程篇:1.IAP远程升级(二)

一直提醒自己要更新CSDN博客&#xff0c;但是确实这段时间到了一个项目的关键节点&#xff0c;杂七杂八的事情突然就一涌而至。STM32、FPGA下位机代码和对应Labview的IAP升级助手、波形设置助手上位机代码笔者已经调试通过&#xff0c;因为不想去水博客、凑数量&#xff0c;复制…

基于51单片机的室内湿度加湿温度声光报警智能自动控制装置设计

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;单片机湿度 获取完整无水印论文报告&#xff08;内含电路原理图和源程序代码&#xff09; 在日常生活中加湿器得到了广泛的应用&#xff0c;但是现有的加湿器都需要手工控制开启和关闭并且不具备对室内空气温湿度的监测&am…

【微信小程序】-- 页面导航 -- 编程式导航(二十三)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…
最新文章