基于udp协议的网络通信(windows客户端版+简易聊天室版),重定向到终端
目录
和windows通信
引入
思路
WSADATA
代码
运行情况
简单的聊天室
思路
重定向
代码
terminal.hpp -- 重定向函数
服务端
客户端
运行情况
和windows通信
引入
linux和windows都需要联网,虽然他们系统设计不同,但网络部分一定是相同的,所以套接字也是一样的
- 这里我们只需要写出windows风格的客户端即可,服务端仍然在linux上跑
- 当然,除去套接字的部分,他们使用的接口和规则肯定是有区别的
思路
套接字的部分不变,处理一下头尾即可
首先要引入winsock2.h头文件,并引入库文件
定义一个WSADATA结构并初始化(不同版本,看到的接口+底层代码也不同)
WSADATA
- 用于在 Windows 操作系统上开发网络应用程序时管理套接字(sockets)库的初始化和配置
- 包含了关于 Winsock 环境的信息,例如 Winsock 版本、所支持的特性等
- 使用WSAStartup初始化,WSACleanup来释放资源并并终止 Winsock 环境
修改完之后,就可以让linux和windows通信了
代码
这里用的是vs2019,加了两个define,防止报错(vs太安全了,汗)
可以看出来,中间的socket+收发数据绝大部分都是一样的,只有那么一两个类型的命名不同:
#define _CRT_SECURE_NO_WARNINGS #define _WINSOCK_DEPRECATED_NO_WARNINGS#include <stdio.h> #include <winsock2.h> #include <Windows.h>#include<iostream> #include<string>#pragma comment(lib,"ws2_32.lib") //引入库文件int main() {//初始化网络环境WSADATA wsa;if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0){printf("WSAStartup failed\n");return -1;}//建立一个udp的socketSOCKET socked = socket(AF_INET, SOCK_DGRAM, 0);if (socked == INVALID_SOCKET){printf("create socket failed\n");return -1;}int port = 8080;std::string ip = "47.108.135.233";//创建结构体sockaddr_in addr = { 0 };addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.S_un.S_addr = inet_addr(ip.c_str());std::string info;char buffer[1024];memset(buffer, 0, sizeof(buffer));//收发数据while (true) {std::cout << "Please enter:";std::getline(std::cin, info);//发送数据int n = sendto(socked, info.c_str(), info.size(), 0, (SOCKADDR*)&addr, sizeof(SOCKADDR));if (n == 0){printf("send failed\n");return -1;}sockaddr_in t = { 0 };int len = sizeof(sockaddr_in);// 接收数据n = recvfrom(socked, buffer, sizeof(buffer) - 1, 0, (SOCKADDR*)&t, &len);buffer[n] = 0;std::cout << buffer << std::endl;memset(buffer, 0, sizeof(buffer));}//关闭SOCKET连接closesocket(socked);//清理网络环境WSACleanup();return 0; }
运行情况
我们成功在windows终端上与在linux下的服务端进行通信:
简单的聊天室
前面写的echo版就已经有聊天室的影子了,聊天软件的服务器的作用也就是向用户转发消息
这里我们修改一下就差不多了
思路
这里以ip地址和端口号作为每个人的标识(类似于用户名的作用),在echo版里其实就已经实现过了
但是echo版每个客户端的消息都是独立的
- 聊天室的话,每个人在自己的客户端上都可以看见彼此发出的消息
- 就需要我们将每条消息发送给所有运行起来的客户端
- 可以考虑创建一个在线用户表(ip,结构体对象) -- 每收到一个消息,就转发给所有注册在表中的用户
- 如果有登录功能的话,应该是登录后转发
- 这里稍微模拟一下登录过程 -- 当客户端运行起来后,有一句打印,且直接将该条打印语句发送给服务器,并且直接注册在表中(简易版嘛)
服务器修改好后,客户端就出现问题了
- 还记得我们的客户端吗,它的第一个函数就是getline
- 如果不发送消息的话,就会卡在那里不会往下走,也就无法调用下面的recvfrom函数,也就无法看见其他用户发送的数据
- 而udp协议是全双工的(它支持边读边写)
- 所以我们可以将客户端修改为多线程,一个读,一个写,这样就互不干扰了
虽然解决了收发消息的问题,但是客户端仅有一个窗口,这样直接打印的话,会导致输入消息和输出的消息混在一块
- 而聊天室一般是分为上下两部分,上面是所有人发送的消息,下面是自己的输入框
- 综合我们是在终端上显示,可以开俩终端,拼接在一起作为我们的界面,输入和输出在不同终端上工作
- 实现的话 -- /dev/pts里是终端文件
- 当我们打开xshell:
- 如果再开一个会话:
重定向
我们如果试着将数据重定向(dup2函数)到终端文件里,就可以看见自己的终端显示出了数据:
这样,我们就可以通过重定向,先确定当前终端属于哪个文件
然后就可以利用这个(也就是将数据重定向到终端文件里,而不是显示器),实现聊天室的分块显示
但如果输出数据时将fd=1的显示器重定向到终端文件1里,那么输入数据时/其他时候的打印,都会到那个终端文件里,而不会像我们预想的那样分成两个模块
所以,我们将标准错误重定向到其中一个终端文件里,另一个终端运行客户端,这样cout时会默认打印到当前终端里,就不会互相影响了
重定向既可以在代码中使用dup2函数,也可以直接在命令行中重定向(a>b)
代码
terminal.hpp -- 重定向函数
#include <iostream> #include <string> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>std::string terminal = "/dev/pts/2";void my_dup() {int fd = open(terminal.c_str(), O_WRONLY);if (fd < 0){perror("open");exit(1);}dup2(fd, 2);close(fd); }
服务端
这里增加了用户表和chat函数(聊天室专用启动函数)
#include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <strings.h> #include <cstring>#include <string> #include <functional> #include <map> #include <iostream>#include "Log.hpp"extern std::string get_time();Log lg;const int buff_size = 1024; using func_t = std::function<std::string(const std::string &)>;enum {SOCKET_ERR = 1,BIND_ERR = 2 };// 启动服务器时,传入ip地址和端口号 // 手动启动class udp_server { public:udp_server(const uint16_t port = 8080, const std::string ip = "0.0.0.0"): ip_(ip), port_(port), sockfd_(0){}void run(func_t func){init();// 开始收发数据char buffer[buff_size];std::string message;while (true){memset(buffer, 0, sizeof(buffer));struct sockaddr_in src_addr;socklen_t src_len = sizeof(src_addr);// 获取数据ssize_t n = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&src_addr), &src_len);if (n < 0){lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}buffer[n] = 0;std::string id = generate_id(inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port));message = id + "sever recvfrom success";lg(INFO, message.c_str());// 处理数据std::string echo_info = func(buffer);// 响应给发送端sendto(sockfd_, echo_info.c_str(), echo_info.size(), 0, reinterpret_cast<const struct sockaddr *>(&src_addr), src_len);message = id + "sever sendto success";lg(INFO, message.c_str());}}void chat(){init();// 开始收发数据char buffer[buff_size];memset(buffer, 0, sizeof(buffer));std::string message;while (true){struct sockaddr_in src_addr;socklen_t src_len = sizeof(src_addr);// 获取数据ssize_t n = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&src_addr), &src_len);char ip[30];// std::cout << inet_ntop(AF_INET, &(src_addr.sin_addr), ip, sizeof(ip) - 1)<<std::endl;if (n < 0){lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}buffer[n] = 0;// std::cout << buffer << std::endl;usr_[src_addr.sin_addr.s_addr] = src_addr; // 注册用户表// for (auto it : usr_)// {// std::cout << inet_ntop(AF_INET, &((it.second).sin_addr), ip, sizeof(ip) - 1) << std::endl;// }std::string id = generate_id(inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port));message = id + "sever recvfrom success";lg(INFO, message.c_str());// 处理数据std::string time_stamp = get_time();std::string echo_info = id + time_stamp + buffer;memset(buffer, 0, sizeof(buffer));// 响应给所有用户端send_all(echo_info);message = id + "sever sendto success";lg(INFO, message.c_str());}}~udp_server(){if (sockfd_ > 0){close(sockfd_);}}static std::string get_id(){udp_server obj;return obj.generate_id(obj.ip_, obj.port_);}private:std::string generate_id(const std::string ip, const uint16_t port){return "[" + ip + ":" + std::to_string(port) + "]";}void init(){// 创建套接字文件sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){lg(FATAL, "socket create error, sockfd : %d,%s", sockfd_, strerror(errno));exit(SOCKET_ERR);}// 创建sockaddr结构struct sockaddr_in addr;socklen_t len = sizeof(addr);bzero(&addr, len);addr.sin_addr.s_addr = inet_addr(ip_.c_str());addr.sin_family = AF_INET;addr.sin_port = htons(port_);// 绑定套接字信息int res = bind(sockfd_, reinterpret_cast<const struct sockaddr *>(&addr), len);if (res < 0){lg(FATAL, "bind error, sockfd : %d,%s", sockfd_, strerror(errno));exit(BIND_ERR);}lg(INFO, "bind success, sockfd : %d", sockfd_);}void send_all(const std::string &echo_info){char ip[30];for (auto it : usr_){// std::cout << inet_ntop(AF_INET, &((it.second)->sin_addr), ip, sizeof(ip) - 1)<<std::endl;sendto(sockfd_, echo_info.c_str(), echo_info.size(), 0, reinterpret_cast<const struct sockaddr *>(&(it.second)), sizeof(it.second));}}private:int sockfd_;std::string ip_;uint16_t port_;std::map<in_addr_t, struct sockaddr_in> usr_; //不能是指针,这样下次循环时,指针就换成新的客户端了 };
客户端
分出了写函数和读函数,chat函数中创建两个线程,让他们运行
#include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <functional> #include <strings.h> #include <cstring>#include <string> #include <iostream>#include <pthread.h>#include "Log.hpp" #include "terminal.hpp"const int buff_size = 1024;Log lg;enum {SOCKET_ERR = 1,BIND_ERR = 2 };// 客户端需要提前知道服务端的套接字地址信息 // 日常生活中,我们一般直接通过网址进入,网址就是ip地址,且它会直接和端口号绑定 // 所以,这里我们只能自己手动提供服务端的ip和端口号// 客户端不需要手动创建套接字,os会自动为我们提供(在首次发送数据时) struct data {int sockfd_;struct sockaddr_in *paddr_;socklen_t len_; };class udp_client { public:udp_client(const uint16_t port = 8080, const std::string ip = "47.108.135.233"): ip_(ip), port_(port), sockfd_(0){}void run(){data *d = init();std::string info;char buffer[buff_size];memset(buffer, 0, sizeof(buffer));while (true){std::cout << "Please enter:";std::getline(std::cin, info);// 将消息发送给服务器sendto(d->sockfd_, info.c_str(), info.size(), 0, reinterpret_cast<const struct sockaddr *>(d->paddr_), d->len_);info.clear();struct sockaddr_in addr; // 仅用于填充参数,拿到自己的地址信息没啥意义socklen_t len = sizeof(addr);// 获取数据ssize_t n = recvfrom(d->sockfd_, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&addr), &len);if (n < 0){lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}buffer[n] = 0;std::cout << buffer << std::endl;memset(buffer, 0, sizeof(buffer));}}void chat(){data *d = init();pthread_t r = 0, w = 0;pthread_create(&r, nullptr, input, d);pthread_create(&w, nullptr, output, d);pthread_join(r, nullptr);pthread_join(w, nullptr);}private:data *init(){// 创建套接字文件int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){lg(FATAL, "socket create error, sockfd : %d", sockfd);exit(SOCKET_ERR);}// 创建sockaddr结构struct sockaddr_in *svr_paddr = new sockaddr_in;socklen_t svr_len = sizeof(*svr_paddr);bzero(svr_paddr, svr_len);inet_aton(ip_.c_str(), &(svr_paddr->sin_addr));svr_paddr->sin_family = AF_INET;svr_paddr->sin_port = htons(port_);return new data({sockfd, svr_paddr, svr_len});}static void *input(void *args){data *d = reinterpret_cast<data *>(args);char ip[30];inet_ntop(AF_INET, &((d->paddr_)->sin_addr), ip, sizeof(ip) - 1);std::string welcome = "comming...";sendto(d->sockfd_, welcome.c_str(), welcome.size(), 0, reinterpret_cast<const struct sockaddr *>(d->paddr_), d->len_);std::string info;while (true){std::cout << "Please enter:";std::getline(std::cin, info);// 将消息发送给服务器sendto(d->sockfd_, info.c_str(), info.size(), 0, reinterpret_cast<const struct sockaddr *>(d->paddr_), d->len_);info.clear();}return nullptr;}static void *output(void *args){data *d = reinterpret_cast<data *>(args);// my_dup();char buffer[buff_size];memset(buffer, 0, sizeof(buffer));while (true){struct sockaddr_in addr; // 仅用于填充参数,拿到自己的地址信息没啥意义socklen_t len = sizeof(addr);// 获取数据(所有用户的消息都会获取)ssize_t n = recvfrom(d->sockfd_, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&addr), &len);if (n < 0){lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}buffer[n] = 0;std::cerr << buffer << std::endl;memset(buffer, 0, sizeof(buffer));}return nullptr;}private:int sockfd_;std::string ip_;uint16_t port_; };
两个cpp函数之间构建相应cs的对象+调用chat函数即可
运行情况
手动重定向(这个适合在其他主机上运行客户端,因为每个人打开的终端不一定正好有2,测试后进行手动重定向最好)
在代码内重定向:
下图是两个云服务器之间进行通信:
大家也可以下载文件试试,只要有两个执行文件+client文件执行时进行手动重定向(分好两个终端屏幕,确定好各自的编号),就能通信
(也就是说总共需要运行三个终端)
相关文章:

基于udp协议的网络通信(windows客户端版+简易聊天室版),重定向到终端
目录 和windows通信 引入 思路 WSADATA 代码 运行情况 简单的聊天室 思路 重定向 代码 terminal.hpp -- 重定向函数 服务端 客户端 运行情况 和windows通信 引入 linux和windows都需要联网,虽然他们系统设计不同,但网络部分一定是相同的,所以套接字也是一样的 这…...

Qt+FFmpeg+opengl从零制作视频播放器-7.OpenGL播放视频
在上一节Qt+FFmpeg+opengl从零制作视频播放器-6.视频解码中,我们学到了如何将视频数据解码成YUV原始数据,并且保存到本地,最后使用工具来播放YUV文件。 本节使用QOpenGLWidget来渲染解码后的YUV视频数据。 首先简单介绍QOpenGLWidget的使用。 QOpenGLWidget类是用于渲染O…...

用两个栈实现简单的四则运算
题目要求:给定一个字符串如“12*3”,没有括号,要求利用栈的知识来处理结果算出答案 我的思路:建立两个栈,一个存放数据,一个存放符号,再定义一个结构体做为操作的主体,然后制作几个函数&#x…...

<个人笔记>数论
1.快速幂 (1)求解问题: 给定 n组 ai,bi,pi求 aibi mod pi 的值。 (2)主要思想:任何一个数(b),可以被 n 个 2k 相加获得。 即 b 2k1 2k2 2k3 … 2logb。 快速幂模板: typedef long long LL;LL qmi(int a,int b,int p){LL re…...

CMS垃圾收集
初始标记 需要暂停所有的其他线程,但这个阶段会很快完成。它的目的是标记所有的根对象,以及被根对象直接引用的对象,以及年轻代指向老年代的对象,不会遍历对象关系,单线程执行。 并发标记阶段 不需要暂停应用线程&a…...

Incorrect DECIMAL value: ‘0‘ for column ‘‘ at row -1
用mysql插入数据的时候,报了上面的错误。 语句类似:INSERT INTO t_aa(c1,c2,c3,a1,a2,a3) SELECT t1,t2,t3,b1,b2,b3 FROM ( SELECT, t1,t2,t3 cast(ifnull(d1,0)as decimal(8,1) b1, cast(ifnull(d2,0) as decimal(8,1) b2, …...

Vue3组件通信的方式
1、父给子传 — props 父组件 <template><h1>父</h1><Son :value"number" /><button click"add">点我加1</button> </template><script setup> import Son from ./son.vue;import { ref } from vue; le…...

双场板功率型GaN HEMT中用于精确开关行为的电容建模
来源:Capacitance Modeling in Dual Field-Plate Power GaN HEMT for Accurate Switching Behavior (TED 16年) 摘要 本文提出了一种基于表面电势的紧凑模型,用于描述具有栅极和源极场板(FP)结构的AlGaN/GaN高电子迁移率晶体管(…...

UE4_AI_行为树_行为树快速入门指南
声明:学习笔记。 在 行为树快速入门指南 中,你将学会如何创建一个敌方AI,该AI看到玩家后会做出反应并展开追逐。当玩家离开视线后,AI将在几秒钟后(这可根据你的需求进行调整)放弃追逐,并在场景中…...

c++ 面试100个题目中的编程题目
88、下列程序的运行结果是? #include <stdlib.h> #include <stdio.h> #include <string.h> #include <iostream> const char* str = "vermeer"; using namespace std; int main(){ const char* pstr = str;cout << "The add…...

C++初阶:类与对象(尾篇)
目录 1. 构造函数与初始化列表1.1 对象的创建与构造函数的初始化1.2 初始化列表及构造函数存在的意义1.3 explicit关键字与构造函数的类型转换 2. static成员变量与static成员函数2.1 static成员变量2.2 static成员函数 3. 日期类流插入操作符的重载与友元3.1 友元3.2 友元函数…...

Spring状态机简单实现
一、什么是状态机 状态机,又称有限状态自动机,是表示有限个状态以及在这些状态之间的转移和动作等行为的计算模型。状态机的概念其实可以应用的各种领域,包括电子工程、语言学、哲学、生物学、数学和逻辑学等,例如日常生活中的电…...

WebServer -- 面试题(下)
👂 夏风 - Gifty - 单曲 - 网易云音乐 目录 🌼前言 🎂面试题(下) 4)HTTP报文解析 为什么要用状态机 状态转移图画一下 https 协议为什么安全 https 的 ssl 连接过程 GET 和 POST 的区别 5)数据库注册登录 登…...

企业微信如何接入第三方应用?
1.登录企业微信管理后台:https://work.weixin.qq.com/wework_admin 2.点击创建应用; 3. 此时可以看到已经创建好的应用,并且生成应用的唯一id(agentId) 4. 第三方应用申请域名 (举例&…...

JAVA后端编码的主键字段存储为什么倾向于使用雪花算法
1.背景 最近有人问,什么是雪花算法,为什么使用雪花算法不使用数据库UUID,基于此,写一个说明。 2.简介 (1)雪花算法,英文名为snowflake,翻译过来就是是雪花,所以叫雪花…...

Rust 深度学习库 Burn
一、概述 Burn 它是一个新的综合动态深度学习框架,使用 Rust 构建的,以极高的灵活性、计算效率和可移植性作为其主要目标。 Rust Burn 是一个以灵活性、高性能和易用性为核心设计原则工具,主打就是灵活性 、高性能 及易用性。 二、Rust B…...

C语言-存储期2.0
静态存储期 在数据段中分配的变量,统统拥有静态存储期,因此也都被称为静态变量。这里静态的含义,指的是这些变量的不会因为程序的运行而发生临时性的分配和释放,它们的生命周期是恒定的,跟整个程序一致。 静态变量包含…...

计算机网络面经八股-HTTP请求报文和响应报文的格式?
请求报文格式: 请求行(请求方法URI协议版本)请求头部空行请求主体 请求行:GET /sample.jsp HTTP/1.1 表示使用 GET 方法请求 /sample.jsp 资源,并使用 HTTP/1.1 协议。请求头部:包含多个字段,…...

Ubuntu 18.04安装最新版Visual Studio Code(VS Code)报依赖库版本过低错误
Ubuntu 18.04安装最新版Visual Studio Code(VS Code)报依赖库版本过低错误 1. 问题描述2. 解决方案2.1 修复之前安装的错误2.2 安装VS Code 1.85.2 3. 原因分析 1. 问题描述 在Ubuntu 18.04系统上安装VS Code ≥ v1.86.2(测试到v1.87.1&…...

Android NDK入门:在应用中加入C和C++的力量
目录 编辑 引 NDK的设计目的 与Java/Kotlin的结合 使用场景 开发流程 设置项目以支持NDK 编写本地代码 使用JNI连接本地代码和Java/Kotlin代码 编译和运行你的应用 附 引 自诩方向是android方向的移动端开发工程师,却从来没有真正仔细了解过NDK&#…...

2024年华为OD机试真题-田忌赛马-Java-OD统一考试(C卷)
题目描述: 给定两个只包含数字的数组a,b,调整数组 a 里面数字的顺序,使得尽可能多的 a[i] >b[i]。数组 a和 b 中的数字各不相同。 输出所有可以达到最优结果的 a 数组的数量 输入描述: 输入的第一行是数组 a 中的数字,其中只包含数字,每两个数字之间相隔一个空格,a…...

C++ 网络编程学习五
C网络编程学习五 网络结构的更新单例模式懒汉单例模式饿汉单例模式懒汉式指针智能指针设计单例类 服务器优雅退出asio的多线程模型IOServiceasio多线程IOThreadPoolepoll 和 iocp的一些知识点 网络结构的更新 asio网络层,会使用io_context进行数据封装,…...

案例分析篇05:数据库设计相关28个考点(9~16)(2024年软考高级系统架构设计师冲刺知识点总结系列文章)
专栏系列文章推荐: 2024高级系统架构设计师备考资料(高频考点&真题&经验)https://blog.csdn.net/seeker1994/category_12593400.html 【历年案例分析真题考点汇总】与【专栏文章案例分析高频考点目录】(2024年软考高级系统架构设计师冲刺知识点总结-案例分析篇-…...

pip 和conda 更换镜像源介绍
1、前言 很多深度学习的项目免不了安装库文件、配置环境等等,如果利用官方提供的连接,网速很慢,而且很容易download掉。 所以配置好了虚拟环境,将pip换源属实重要 常见的国内镜像源有清华、中科大、阿里等等... 这里建议用中科…...

Git概述及安装步骤
一、Git简介 Git是一个免费的、开源的分布式版本控制系统,可以快速高效地处理从小型到大型的各种项目。Git 易于学习,占地面积小,性能极快。它具有廉价的本地库,方便的暂存区域和多个工作流分支等特性。其性能优于Subversion、CV…...

北京保险服务中心携手镜舟科技,助推新能源车险市场规范化
2022 年,一辆新能源汽车在泥泞的小路上不慎拖底,动力电池底壳受损,电池电量低。车主向保险公司报案,希望能够得到赔偿。然而,在定损过程中,保司发现这辆车的电池故障并非由拖底事件引起,而是由于…...

给女朋友的浪漫微信消息推送超详细版
1. 下载代码 地址: 链接: https://pan.baidu.com/s/1lESgRoWn8bXyE0jsSVCHqQ?pwdimr6 提取码: imr6 根据 resources/db 下sql文件创建表 修改yml文件中数据库连接 2. 进入微信测试平台 地址:微信公众平台 扫码登录获取测试号信息 修改代…...

Android开发 Activity启动模式、ViewModel与LiveData,及Kotlin Coroutines
目录 Activity启动模式 onNewIntent解释 Activity启动模式的考虑时机 Service启动模式 ContentProvider的作用 Broadcast的注册方式 AsyncTask的作用 ViewModel LiveData Kotlin Coroutines 结合使用 Activity启动模式 Android中Activity的启动模式有四种࿰…...

MQL语言实现抽象工厂模式
文章目录 一、定义抽象产品接口二、定义抽象工厂接口三、定义具体产品四、定义具体工厂五、定义工厂客户端六、客户端调用工厂客户端七、抽象工厂模式的结构 一、定义抽象产品接口 //------------------------------------------------------------------ //| participants …...

UE4开个头-简易小汽车
跟着谌嘉诚学的小Demo,记录一下 主要涉及到小白人上下车和镜头切换操作 1、动态演示效果 2、静态展示图片 3、蓝图-上下车...