CS 144 Lab Seven -- putting it all together

news2024/12/26 21:51:34

CS 144 Lab Seven -- putting it all together

  • 引言
  • 测试
  • lab7.cc
    • UDPSocket
    • NetworkInterfaceAdapter
    • TCPSocketLab7
    • main方法
    • 子线程
  • 小结


对应课程视频: 【计算机网络】 斯坦福大学CS144课程

Lab Six 对应的PDF: Checkpoint 6: putting it all together


引言

本实验无需进行任何编码操作,同时我们还可以在这个实验中,将之前6个实验里所有实现的内容全部粘合在一起,并与真实网络进行通信。

在这里插入图片描述


测试

在两个终端分别执行以下两个命令:

./apps/lab7 server cs144.keithw.org 3000
./apps/lab7 client cs144.keithw.org 3001

便可以看到两个服务成功相互连接:

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


lab7.cc

lab seven的测试文件为lab7.cc,借助该测试文件,我们来看一下如何将lab six实现的Router也加入进来共同运作的。

首先我们先来看一下相关工具类和工具方法的实现:

  • random_host_ethernet_address: 为主机生成随机的MAC地址
EthernetAddress random_host_ethernet_address() {
    EthernetAddress addr;
    for (auto &byte : addr) {
        byte = rd();  // use a random local Ethernet address
    }
    addr.at(0) |= 0x02;  // "10" in last two binary digits marks a private Ethernet address
    addr.at(0) &= 0xfe;

    return addr;
}
  • random_router_ethernet_address: 为路由器生成随机的MAC地址
EthernetAddress random_router_ethernet_address() {
    EthernetAddress addr;
    for (auto &byte : addr) {
        byte = rd();  // use a random local Ethernet address
    }
    addr.at(0) = 0x02;  // "10" in last two binary digits marks a private Ethernet address
    addr.at(1) = 0;
    addr.at(2) = 0;

    return addr;
}

MAC地址确实有私有和全球唯一(公有)之分:

  1. 全球唯一MAC地址(全局唯一地址): 这是由IEEE(Institute of Electrical and Electronics Engineers)组织分配的唯一MAC地址,通常称为全球唯一MAC地址或全局唯一地址。全球唯一MAC地址由厂商分配给网络设备,确保在全球范围内没有两个设备使用相同的MAC地址。前三个字节表示厂商识别码(OUI),后三个字节由厂商自行分配。全球唯一MAC地址用于在互联网和广域网等大范围的网络中唯一标识设备。

  2. 本地MAC地址(私有地址): 本地MAC地址也称为私有MAC地址,是用于在局域网(LAN)内部使用的MAC地址。本地MAC地址的第一个字节通常是02060A0E,这些开头的地址被称为本地管理员地址(Locally Administered Addresses)。本地MAC地址通常不需要在全球范围内唯一,因为它们只在局域网内部使用。局域网内的设备可以自行分配本地MAC地址,只要确保在局域网内不会产生冲突即可。

全球唯一MAC地址和本地MAC地址之间的区别在于其范围和分配方式。全球唯一MAC地址由IEEE控制分配,确保在全球范围内唯一,用于在大范围的网络中进行全球性标识。而本地MAC地址是在局域网内部使用的,可以由设备自行分配,只需要在局域网内部保持唯一即可。


UDPSocket

在这里插入图片描述

  • LocalStreamSocket: 这个类在Lab four和Lab five中都间接涉及到了,该Socket子类用于本地两个进程间的通信处理,借助socketpair这个系统调用创建的一对相互连接的套接字完成
  • UDPSocket: 对本机Linux网络子系统提供的UDP socket进行的包装
  • TCPSocket: 对本机Linux网络子系统提供的TCP socket进行的包装

这里我们来看一下UDPSocket的实现:

socket.hh:

//! A wrapper around [UDP sockets](\ref man7::udp)
// 对本机Linux网络子系统提供的UDP socket进行的包装
class UDPSocket : public Socket {
  protected:
    //  Construct from FileDescriptor (used by TCPOverUDPSocketAdapter)
    //  fd is the FileDescriptor from which to construct
    explicit UDPSocket(FileDescriptor &&fd) : Socket(std::move(fd), AF_INET, SOCK_DGRAM) {}

  public:
    //! Default: construct an unbound, unconnected UDP socket
    // 无参构造,默认创建出来的是UDP socket
    UDPSocket() : Socket(AF_INET, SOCK_DGRAM) {}

    //! Returned by UDPSocket::recv; carries received data and information about the sender
    // recv接收UDP数据报方法返回结果
    struct received_datagram {
        Address source_address;  // Address from which this datagram was received
        std::string payload;     // UDP datagram payload
    };

    //! Receive a datagram and the Address of its sender
    received_datagram recv(const size_t mtu = 65536);

    //! Receive a datagram and the Address of its sender (caller can allocate storage)
    void recv(received_datagram &datagram, const size_t mtu = 65536);

    //! Send a datagram to specified Address
    void sendto(const Address &destination, const BufferViewList &payload);

    //! Send datagram to the socket's connected address (must call connect() first)
    void send(const BufferViewList &payload);
};
  • 如果UDPSocket使用无参构造初始化,那么最终会调用父类Socket对象的构造函数初始化一个使用IPV4协议和UDP协议的Socket:
// default constructor for socket of (subclassed) domain and type
//! \param[in] domain is as described in [socket(7)](\ref man7::socket), probably `AF_INET` or `AF_UNIX`
//! \param[in] type is as described in [socket(7)](\ref man7::socket)
Socket::Socket(const int domain, const int type) : FileDescriptor(SystemCall("socket", socket(domain, type, 0))) {}

socket 系统调用用于创建一个新的套接字,下面是对每个参数的解释:

  • domain: 套接字的协议域(也称为地址族),指定了套接字的通信范围和协议类型。例如,AF_INET 表示 IPv4 地址族,AF_UNIX 表示本地套接字(Unix 域套接字)。这个参数决定了套接字将在哪种网络层协议上工作。
  • type: 套接字的类型,指定了套接字的通信方式。例如,SOCK_STREAM 表示流式套接字(用于 TCP),SOCK_DGRAM 表示数据报套接字(用于 UDP)。这个参数决定了套接字将如何进行数据传输。
  • 0: 这是套接字的选项标志,通常设置为 0,表示不使用任何特定的选项。

如果socket构造函数指明了fd , 并且fd实际指向一个tun设备 ,那么在构造函数中,代码会验证 TUN 设备的协议域和类型是否与预期的值一致。这是因为 TUN 设备在内核中被实现为一个虚拟网络设备,有关于其属性的信息可以通过套接字选项来获取。

在这个情境下,getsockopt 调用用于获取 TUN 设备的协议域和类型。如果 TUN 设备的实际协议域或类型与期望的不匹配,那么将抛出异常,表示套接字不满足所需的属性。

Socket::Socket(FileDescriptor &&fd, const int domain, const int type) : FileDescriptor(move(fd)) {
    int actual_value;
    socklen_t len;

    // verify domain
    len = sizeof(actual_value);
    SystemCall("getsockopt", getsockopt(fd_num(), SOL_SOCKET, SO_DOMAIN, &actual_value, &len));
    if ((len != sizeof(actual_value)) or (actual_value != domain)) {
        throw runtime_error("socket domain mismatch");
    }

    // verify type
    len = sizeof(actual_value);
    SystemCall("getsockopt", getsockopt(fd_num(), SOL_SOCKET, SO_TYPE, &actual_value, &len));
    if ((len != sizeof(actual_value)) or (actual_value != type)) {
        throw runtime_error("socket type mismatch");
    }
}

socket.cc:

  • recv: 调用udp socket的recvfrom接收外网传入的udp数据报(此处说的是Linux网络子系统中提供的udp socket)
//! \note If `mtu` is too small to hold the received datagram, this method throws a std::runtime_error
// 将接收到的UDP数据报存储到datagram中
void UDPSocket::recv(received_datagram &datagram, const size_t mtu) {
    // receive source address and payload
    // 用于接收数据报来源地址
    Address::Raw datagram_source_address;
    datagram.payload.resize(mtu);

    socklen_t fromlen = sizeof(datagram_source_address);
    
    // 通过系统调用,调用本机Linux网络子系统中socket提供的recvfrom接口
    const ssize_t recv_len = SystemCall(
        "recvfrom",
        ::recvfrom(
            // 哪个socket,接收的数据存储到哪里,接收缓冲区的大小,接收标志,表示如果数据报过大会截断,并返回截断后的数据。如果不指定这个标志,过大的数据报会被丢弃
            // 用于存储源地址的缓冲区,源地址缓冲区的大小 
            fd_num(), datagram.payload.data(), datagram.payload.size(), MSG_TRUNC, datagram_source_address, &fromlen));
    
    // 如果接收到的数据大小超过了mtu,则抛出异常
    if (recv_len > ssize_t(mtu)) {
        throw runtime_error("recvfrom (oversized datagram)");
    }

    register_read();
    // 记录数据包来源地址
    datagram.source_address = {datagram_source_address, fromlen};
    // 调整payload缓冲区大小为实际接收到的数据量
    datagram.payload.resize(recv_len);
}

UDPSocket::received_datagram UDPSocket::recv(const size_t mtu) {
    received_datagram ret{{nullptr, 0}, ""};
    recv(ret, mtu);
    return ret;
}
  • sendmsg_helper: 调用udp socket的sednmsg将准备好的UDP数据包发送出去(此处说的是Linux网络子系统中提供的udp socket)
// 发送UDP数据报: socket描述符,存放目的地址的缓冲区,缓冲区大小,要发送的数据载荷
void sendmsg_helper(const int fd_num,
                    const sockaddr *destination_address,
                    const socklen_t destination_address_len,
                    const BufferViewList &payload) {
    auto iovecs = payload.as_iovecs();
    // 构建数据包
    msghdr message{};
    message.msg_name = const_cast<sockaddr *>(destination_address);
    message.msg_namelen = destination_address_len;
    message.msg_iov = iovecs.data();
    message.msg_iovlen = iovecs.size();
    // 通过系统调用sendmsg完成数据包的发送
    const ssize_t bytes_sent = SystemCall("sendmsg", ::sendmsg(fd_num, &message, 0));
    // 检验成功发送的字节数和payload大小是否一致,也就是数据包是否成功发送 
    if (size_t(bytes_sent) != payload.size()) {
        throw runtime_error("datagram payload too big for sendmsg()");
    }
}

// 发送时指明目的地址
void UDPSocket::sendto(const Address &destination, const BufferViewList &payload) {
    sendmsg_helper(fd_num(), destination, destination.size(), payload);
    register_write();
}

// 发送时不指定目的地址
void UDPSocket::send(const BufferViewList &payload) {
    sendmsg_helper(fd_num(), nullptr, 0, payload);
    register_write();
}

NetworkInterfaceAdapter

  • 为了适配从通道读写IP数据报
  • lab five中通道被写死为了Tap设备,但是为了解耦,这里采用了双向通道,这样方便切换底层网络驱动实现
// 为了适配从通道读写IP数据报
class NetworkInterfaceAdapter : public TCPOverIPv4Adapter {
  private:
    // 网络接口
    NetworkInterface _interface;
    // 下一条IP地址
    Address _next_hop;
    // socket_pair系统调用创建出来的本地套接字双向通信通道 --> lab five的测试文件中,这里是写死为Tap设备
    // 但是此处我们利用双向通道进行解耦,这样数据可以来源于Tap设备,也可以来源于其他地方 -- 解耦
    pair<FileDescriptor, FileDescriptor> _data_socket_pair = socket_pair_helper(SOCK_DGRAM);
    
    // 将网络接口输出队列中等待输出的以太网帧取出,然后写入双向通信通道中
    void send_pending() {
        while (not _interface.frames_out().empty()) {
            _data_socket_pair.first.write(_interface.frames_out().front().serialize());
            _interface.frames_out().pop();
        }
    }

  public:
    NetworkInterfaceAdapter(const Address &ip_address, const Address &next_hop)
        :
        // 当前网络接口MAC地址采用随机生成,ip地址采用传入的 
        _interface(random_host_ethernet_address(), ip_address), 
        // 下一条IP地址也是采用传入的
        _next_hop(next_hop) {}
    
    // 从通道读取数据
    optional<TCPSegment> read() {
        EthernetFrame frame;
        // 解析从通道读取得到的以太网帧
        if (frame.parse(_data_socket_pair.first.read()) != ParseResult::NoError) {
            return {};
        }

        // Give the frame to the NetworkInterface. Get back an Internet datagram if frame was carrying one.
        // 交给网络接口处理,得到IP数据报
        optional<InternetDatagram> ip_dgram = _interface.recv_frame(frame);

        // The incoming frame may have caused the NetworkInterface to send a frame
        // 将网络接口中待发送的以太网帧一股脑发送到通道中
        send_pending();

        // Try to interpret IPv4 datagram as TCP
        // 如果是IP数据报,那么剥离得到TCP segment,然后返回
        if (ip_dgram) {
            return unwrap_tcp_in_ip(ip_dgram.value());
        }

        return {};
    }
    
    // 向网络接口写入TCP数据段,网络接口将处理完毕的数据段写入_segment_out输出队列
    // 然后调用send_pending将队列中带输出的以太网帧写入通道
    void write(TCPSegment &seg) {
        // tcp段加上IP头
        _interface.send_datagram(wrap_tcp_in_ip(seg), _next_hop);
        send_pending();
    }

    // _tcp_loop会不间断调用当前适配器的tick
    void tick(const size_t ms_since_last_tick) {
        _interface.tick(ms_since_last_tick);
        send_pending();
    }
    
    NetworkInterface &interface() { return _interface; }
    queue<EthernetFrame> frames_out() { return _interface.frames_out(); }
    // 运算符重载,用于事件循环判获取first,从而判断当前NetworkInterfaceAdapter是否可读可写
    operator FileDescriptor &() { return _data_socket_pair.first; }
    FileDescriptor &frame_fd() { return _data_socket_pair.second; }
};

在这里插入图片描述


TCPSocketLab7

  • 对NetworkInterfaceAdapter的适配,同时在父类TCPSpongeSocket基础上增加一些参数合法检测和方法,使其更符合标准Socket接口
class TCPSocketLab7 : public TCPSpongeSocket<NetworkInterfaceAdapter> {
    Address _local_address;

  public:
    TCPSocketLab7(const Address &ip_address, const Address &next_hop)
        : TCPSpongeSocket<NetworkInterfaceAdapter>(NetworkInterfaceAdapter(ip_address, next_hop))
        , _local_address(ip_address) {}

    // client建立连接--参数: 连接的server的地址
    void connect(const Address &address) {
        FdAdapterConfig multiplexer_config;
        // 客户端的启动端口随机采用
        _local_address = Address{_local_address.ip(), uint16_t(random_device()())};
        cerr << "DEBUG: Connecting from " << _local_address.to_string() << "...\n";
        // multiplexer_config保存源地址和目的地址
        multiplexer_config.source = _local_address;
        multiplexer_config.destination = address;
        // 调用父类的Connect方法
        TCPSpongeSocket<NetworkInterfaceAdapter>::connect({}, multiplexer_config);
    }
    
    // server绑定端口
    void bind(const Address &address) {
        // 我们只能指定port,ip是固定的
        if (address.ip() != _local_address.ip()) {
            throw runtime_error("Cannot bind to " + address.to_string());
        }
        _local_address = Address{_local_address.ip(), address.port()};
    }
    
    // server监听端口
    void listen_and_accept() {
        FdAdapterConfig multiplexer_config;
        multiplexer_config.source = _local_address;
        // 调用父类listen_and_accept方法
        TCPSpongeSocket<NetworkInterfaceAdapter>::listen_and_accept({}, multiplexer_config);
    }

    NetworkInterfaceAdapter &adapter() { return _datagram_adapter; }
};

main方法

int main(int argc, char *argv[]) {
    try {
        if (argc <= 0) {
            abort();  // For sticklers: don't try to access argv[0] if argc <= 0.
        }

        if (argc != 4 and argc != 5) {
            print_usage(argv[0]);
            return EXIT_FAILURE;
        }

        if (argv[1] != "client"s and argv[1] != "server"s) {
            print_usage(argv[0]);
            return EXIT_FAILURE;
        }
        // 启动程序主体 
        program_body(argv[1] == "client"s, argv[2], argv[3], argc == 5);
    } catch (const exception &e) {
        cerr << e.what() << "\n";
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

lab7测试程序的主体:

// lab7测试程序的主体
// 参数: 当前启动的是客户端和服务端,
void program_body(bool is_client, const string &bounce_host, const string &bounce_port, const bool debug) {
    // 连接外网的udp socket(通过本地linux网络子系统构建得到的udp socket)
    UDPSocket internet_socket;
    // 外部帮忙中转数据包的server
    Address bounce_address{bounce_host, bounce_port};

    /* let bouncer know where we are */
    // 让bouncer知道我们是谁
    internet_socket.sendto(bounce_address, "");
    internet_socket.sendto(bounce_address, "");
    internet_socket.sendto(bounce_address, "");

    /* set up the router */
    // client和server各自都有一个默认路由器 -- lab six我们实现的路由器
    Router router;
    
    // host_side代表的网络接口为连接主机所在内网的一端
    // internet_side代表的网络接口为连接外网的一端
    unsigned int host_side, internet_side;
    // 启动的是客户端
    if (is_client) {
        // 向路由器中添加两个网络接口
        // 1.该网络接口处于client主机所在子网
        host_side = router.add_interface({random_router_ethernet_address(), {"192.168.0.1"}});
        // 2.该网络接口连接广域网
        internet_side = router.add_interface({random_router_ethernet_address(), {"10.0.0.192"}});
        // 向路由器添加路由条目: 路由前缀,前缀长度,下一条IP地址,网络接口索引
        // 如果路由前缀所在子网与当前传入的网络接口处在同一个网络,则目的IP地址为空
        router.add_route(Address{"192.168.0.0"}.ipv4_numeric(), 16, {}, host_side);
        router.add_route(Address{"10.0.0.0"}.ipv4_numeric(), 8, {}, internet_side);
        router.add_route(Address{"172.16.0.0"}.ipv4_numeric(), 12, Address{"10.0.0.172"}, internet_side);
    } else {
    // 启动的是服务端    
        host_side = router.add_interface({random_router_ethernet_address(), {"172.16.0.1"}});
        internet_side = router.add_interface({random_router_ethernet_address(), {"10.0.0.172"}});
        router.add_route(Address{"172.16.0.0"}.ipv4_numeric(), 12, {}, host_side);
        router.add_route(Address{"10.0.0.0"}.ipv4_numeric(), 8, {}, internet_side);
        router.add_route(Address{"192.168.0.0"}.ipv4_numeric(), 16, Address{"10.0.0.192"}, internet_side);
    }

    /* set up the client */
    TCPSocketLab7 sock =
        is_client ? 
        // 客户端主机IP地址和下一条的IP地址(默认路由)
        TCPSocketLab7{{"192.168.0.50"}, {"192.168.0.1"}} : 
        // 服务端主机IP地址和下一条的IP地址(默认路由)
        TCPSocketLab7{{"172.16.0.100"}, {"172.16.0.1"}};

    atomic<bool> exit_flag{};

    /* set up the network */
    // 启动一个子线程
    thread network_thread([&]() {
         // 子线程需要干的事情
         ...
    });
    
    try {
        // 如果当前启动的是客户端,则调用sock的connect
        if (is_client) {
            sock.connect({"172.16.0.100", 1234});
        } else {
        // 如果启动的是服务端,向绑定ip和端口,然后开启监听    
            sock.bind({"172.16.0.100", 1234});
            sock.listen_and_accept();
        }
        // 开启标准输入,标准输出与socket之间的双向复制
        bidirectional_stream_copy(sock);
        sock.wait_until_closed();
    } catch (const exception &e) {
        cerr << "Exception: " << e.what() << "\n";
    }

    cerr << "Exiting... ";
    exit_flag = true;
    network_thread.join();
    cerr << "done.\n";
}

子线程

上面将program_body函数中子线程需要干的事情注释掉了,因为子线程干的事情是重点,所以这里单独拎出来看:

// 启动一个子线程
    thread network_thread([&]() {
        try {
            // 初始化事件循环
            EventLoop event_loop;
            // Frames from host to router
            // _data_socket_pair通道可读事件
            event_loop.add_rule(sock.adapter().frame_fd(), Direction::In, [&] {
                EthernetFrame frame;
                // 从通道读取以太网帧
                if (frame.parse(sock.adapter().frame_fd().read()) != ParseResult::NoError) {
                    return;
                }
                if (debug) {
                    cerr << "     Host->router:     " << summary(frame) << "\n";
                }
                // 交给对应路由器进行路由
                // 1.先找到当前主机端对应的网络接口,让其接收以太网帧,处理后暂存队列
                router.interface(host_side).recv_frame(frame);
                // 2.进行路由
                router.route();
            });

            // Frames from router to host
            // _data_socket_pair通道可写事件
            event_loop.add_rule(
                sock.adapter().frame_fd(),
                Direction::Out,
                [&] {
                    // 从路由器取出当前主机端的网络接口,获取其待输出队列
                    // 该输出队列暂存待发送以太网帧
                    // 由于lab seven构造的每个主机所在的局域网只有他自己,所以这里的以太网帧就是发送给当前主机的
                    auto &f = router.interface(host_side).frames_out();
                    if (debug) {
                        cerr << "     Router->host:     " << summary(f.front()) << "\n";
                    }
                    // 将数据包写入通道,即发送以太网帧给当前主机自己
                    sock.adapter().frame_fd().write(f.front().serialize());
                    f.pop();
                },
                [&] { return not router.interface(host_side).frames_out().empty(); });

            // Frames from router to Internet
            // internet_socket可写事件
            event_loop.add_rule(
                internet_socket,
                Direction::Out,
                [&] {
                    // 从当前路由器取出连接广域网的网络接口,然后获取它的待输出数据包队列
                    auto &f = router.interface(internet_side).frames_out();
                    if (debug) {
                        cerr << "     Router->Internet: " << summary(f.front()) << "\n";
                    }
                    // 将数据包发送到外部server服务器
                    internet_socket.sendto(bounce_address, f.front().serialize());
                    f.pop();
                },
                [&] { return not router.interface(internet_side).frames_out().empty(); });

            // Frames from Internet to router
            // internet_socket可读事件
            event_loop.add_rule(internet_socket, Direction::In, [&] {
                EthernetFrame frame;
                // 从internet_socket读取出以太网数据包
                if (frame.parse(internet_socket.read()) != ParseResult::NoError) {
                    return;
                }
                if (debug) {
                    cerr << "     Internet->router: " << summary(frame) << "\n";
                }
                // 然后将数据包交给连接广域网的网络接口进行接收,处理完后暂存队列
                router.interface(internet_side).recv_frame(frame);
                // 将队列中待路由的数据包取出进行路由发送
                router.route();
            });
            
            // 开启事件循环,并且一直轮询
            while (true) {
                // 每次最多等待50毫秒
                if (EventLoop::Result::Exit == event_loop.wait_next_event(50)) {
                    cerr << "Exiting...\n";
                    return;
                }
                // 定时调用tick方法
                router.interface(host_side).tick(50);
                router.interface(internet_side).tick(50);
                if (exit_flag) {
                    return;
                }
            }
        } catch (const exception &e) {
            cerr << "Thread ending from exception: " << e.what() << "\n";
        }
    });

此处的事件循环相较于lab four而言更加复杂,数据读写过程共涉及三个事件循环公共协作完成,首先我们来看一下键盘输入数据发送的整个流程:
在这里插入图片描述

从网络接收到udp数据包,并从udp数据包的payload中获取以太网帧,然后将以太网帧传送给Router,经过一系列步骤后,最终回显到屏幕的整个过程如下:
在这里插入图片描述
有一点需要注意,我们最终是借助本机linux操作系统提供的udp socket完成数据包的发送,并且该udp数据包的payload载荷是我们封装好的以太网帧:
在这里插入图片描述

此时,我们在来回看一开始给出的这张协作图,或许就没有那么难以理解了:
在这里插入图片描述


小结

本节作为cs144课程lab终章,给出了笔者个人对于整个组合过程的理解,肯定存在理解偏差之处,欢迎各位大佬在评论区指出错误或给予补充。

CS144作为计算机网络的入门课程,下面是一些CS计网相关的进阶课程:

  • CS155(计算机与网络安全)
  • CS244(网络高级主题)
  • CS249i(现代互联网)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HubSpot网页设计神器,你想要的Landing Page都在这里!

在当今数字化时代&#xff0c;Landing Page&#xff08;落地页&#xff09;已成为营销策略中不可或缺的一环。它是与潜在客户首次接触的窗口&#xff0c;能够直接影响转化率和销售结果。而作为一款领先的营销自动化工具&#xff0c;HubSpot为企业提供了打造引人注目的Landing P…

uniapp微信小程序 401时重复弹出登录弹框问题

APP.vue 登陆成功后&#xff0c;保存登陆信息 if (res.code 200) {uni.setStorageSync(loginResult, res)uni.setStorageSync(token, res.token);uni.setStorageSync(login,false);uni.navigateTo({url: "/pages/learning/learning"}) }退出登录 toLogout: func…

Spring Cloud 的版本和SpringBoot的版本

Spring Cloud 的版本选择 Spring Cloud 和SpringBoot的版本存在对应关系 Spring Cloud 的版本和SpringBoot的版本&#xff0c;存在对应关系。最新的SpringCloud版本&#xff08;发布文章时为2022.0.3&#xff09;&#xff0c;需要SpringBoot&#xff08;3.0.9&#xff09; 的…

爬虫获取电影数据----以沈腾参演电影为例

数据可视化&分析实战 1.1 沈腾参演电影数据获取 文章目录 数据可视化&分析实战前言1. 网页分析2. 构建数据获取函数2.1 网页数据获取函数2.2 网页照片获取函数 3. 获取参演影视作品基本数据4. 电影详细数据获取4.1 导演、演员、描述、类型、投票人数、评分信息、电影海…

【云原生】Docker-compose中所有模块学习

compose模块 模板文件是使用 Compose 的核心&#xff0c;涉及到的指令关键字也比较多。但大家不用担心&#xff0c;这里面大部分指令跟 docker run 相关参数的含义都是类似的。 默认的模板文件名称为 docker-compose.yml&#xff0c;格式为 YAML 格式。 version: "3&quo…

STM32基础入门学习笔记:内部高级功能应用

文章目录&#xff1a; 一&#xff1a;低功耗模式 1.睡眠模式测试程序 NVIC.h NVIC.c key.h key.c main.c 2.停机模式测试程序 main.c 3.待机模式测试程序 main.c 二&#xff1a;看门狗 1.独立看门狗测试程序 iwdg.h iwdg.c main.c 2.窗口看门狗测试程序 wwdg…

Unity面板究极优化

首先对于大项目来说UI首选一定的UGUI&#xff0c;目前没有啥可选的余地。多一点都是对性能的负担&#xff0c;UGUI底层基于多线程技术&#xff0c;可以有效分担压力&#xff0c;对于一些不是那么重的面板几乎无感。 无论其他面板只是在此基础上修改的&#xff0c;但每多一层&am…

机器学习---监督学习和非监督学习

根据训练期间接受的监督数量和监督类型&#xff0c;可以将机器学习分为以下四种类型&#xff1a;监督学习、非监督学习、半监督学习和强化学习。 监督学习 在监督学习中&#xff0c;提供给算法的包含所需解决方案的训练数据&#xff0c;成为标签或标记。 简单地说&#xff0c;…

40.利用欧拉法求解微分方程组(matlab程序)

1.简述 求解微分方程的时候&#xff0c;如果不能将求出结果的表达式&#xff0c;则可以对利用数值积分对微分方程求解&#xff0c;获取数值解。欧拉方法是最简单的一种数值解法。前面介绍过MATLAB实例讲解欧拉法求解微分方程&#xff0c;今天实例讲解欧拉法求解一阶微分方程组。…

简单认识ELK日志分析系统

一. ELK日志分析系统概述 1.ELK 简介 ELK平台是一套完整的日志集中处理解决方案&#xff0c;将 ElasticSearch、Logstash 和 Kiabana 三个开源工具配合使用&#xff0c; 完成更强大的用户对日志的查询、排序、统计需求。 好处&#xff1a; &#xff08;1&#xff09;提高安全…

OnlyOffice社区版破解最大连接限制部署

onlyoffice社区版部署并且破解最大连接数 docker镜像 docker pull onlyoffice/documentserver:5.3.1.265.4或更高的版本已经解决了此方法的Bug 运行镜像 docker run -d --name onlyoffice --restartalways -p 暴露端口号:80 onlyoffice/documentserver:5.3.1.26进入容器内部…

windows系统安装ElasticSearch7.9.3笔记

windows系统安装ElasticSearch7.9.3笔记 从es中文社区 或elastic官网下载安装包 ES中文社区-浏览器地址https://elasticsearch.cn/download/ 下载7.9.3版本的相关安装包 下载的安装包清单如下 开始配置使用带ik分词器和拼音分词器的ES7.9.3 分别解压这3个zip 拷贝ik分词器…

48.C++ vector容器(模板类)

今天学习了C中的vector容器&#xff0c;它提供动态数组功能&#xff0c;方便管理和存储数据。支持动态调整大小&#xff0c;简化了数组操作&#xff0c;使代码更灵活、高效。实在是太牛了&#xff0c;后悔没有早点学习容器。 目录 一、vector简介 二、头文件和命名空间 三…

该你出手了!华为云PaaS服务 8月线上开发者活动指南——技术提升、合作交流、创新无限!

天高云阔、风朗气清 亲爱的开发者们&#xff0c;8月已经来临 你们准备好迎接一个充满激情和挑战的月份了吗&#xff1f; 如果你对编程充满热情&#xff0c;对技术前沿有所追求 那么华为云PaaS为各位开发者准备的这些8月活动&#xff0c;你一定不能错过。 为了让开发者们更好…

集睿致远/CS5269设计Typec转HDMI带PD100w快充方案

集睿致远ASL推出的CS5269AN是一款高性能Typec/DP1.4转HDMI 4k_60Hz带PD100w快充转换器方案芯片 DP接口包括4条主通道、辅助通道和HPD信号。支持5.4Gbps(HBR2&#xff09;的数据速率。DP接收机兼容HDCP1.4和HDCP2.3内容保护方案具有嵌入式密钥&#xff0c;用于数字音视频内容的…

【源码分析】Nacos如何使用AP协议完成服务端之间的数据同步?

AP节点的同步使用的是异步任务消息队列的方式来实现的。 取出任务之后将会放入到一个List集合中。 然后会发现任务的执行是由条件的。 首先是当前集群的节点数量等于1000&#xff0c;那么此时会直接开始同步&#xff0c;当然这个条件在小项目中不会成立&#xff0c;所以还有…

BIO,NIO,AIO总结

文章目录 1. BIO (Blocking I/O)1.1 传统 BIO1.2 伪异步 IO1.3 代码示例 1.4 总结2. NIO (New I/O)2.1 NIO 简介2.2 NIO的特性/NIO与IO区别1)Non-blocking IO&#xff08;非阻塞IO&#xff09;2)Buffer(缓冲区)3)Channel (通道)4)Selector (选择器) 2.3 NIO 读数据和写数据方式…

python-opencv对极几何 StereoRectify

OpenCV如何正确使用stereoRectify函数 函数介绍 用于双目相机的立体校正环节中&#xff0c;这里只谈谈这个函数怎么使用&#xff0c;参数具体指哪些函数参数 随便去网上一搜或者看官方手册就能得到参数信息&#xff0c;但是&#xff01;&#xff01;相对关系非常容易出错&…

MySQL体系结构

连接层&#xff1a; 最上层是一些客户端和链接服务&#xff0c;主要完成一些类似于连接处理、授权认证、及相关的安全方案。服务器也会为安全接入的每个客户端验证它所具有的操作权限。服务层&#xff1a; 第二层架构主要完成大多数的核心服务功能&#xff0c;如…

【从零学习python 】02. 开发工具介绍

文章目录 编写Python代码一、常见的代码编辑工具二、运行Python程序三、Pycharm的下载和安装PyCharm的主要功能区域进阶案例 编写Python代码 根据我们之前介绍的知识&#xff0c;我们知道&#xff0c;所谓代码其实就是将一段普通文本按照一定的规范编写&#xff0c;然后交给电…