【Linux网络编程】第四弹---构建UDP服务器与字典翻译系统:源码结构与关键组件解析

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】

目录

1、UdpServer.hpp

1.1、函数对象声明

1.2、Server类基本结构

1.3、构造函数

1.4、Start()

2、Dict.hpp

2.1、基本结构

2.2、加载字典文件

2.3、构造函数

2.4、翻译函数

2.5、dict.txt

3、UdpServerMain.cc

4、完整源码

4.1、Dict.hpp

4.2、dict.txt

4.3、InetAddr.hpp

4.4、LockGuard.hpp

4.5、Log.hpp

4.6、Makefile

4.7、nocopy.hpp

4.8、UdpClientMain.cc

4.9、UdpServerMain.cc


上一弹我们能够完成客户端与服务端的正常通信,但是我们在实际生活中不仅仅是要进行通信,还需要完成某种功能,此弹实现一个英文翻译成中文版服务端!!

注意:此弹内容的实现是在上一弹的原始代码基础上修改的!!

1、UdpServer.hpp

Server类的基本实现没变,但是要增加实现中英文翻译成中文功能的执行函数(即函数类型成员变量),并适当调整构造函数和启动函数!!!

1.1、函数对象声明

此处需要实现一个英文翻译成中文的执行函数,因此函数参数是一个字符串,返回值也是一个字符串!!!

// 声明函数对象
using func_t = std::function<std::string(std::string)>;

1.2、Server类基本结构

基本结构与上一弹的Server类基本一致,只增加了函数对象类型!!!

class UdpServer : public nocopy
{
public:UdpServer(func_t func,uint16_t localport = glocalport);void InitServer();void Start();~UdpServer();
private:int _sockfd;          // 文件描述符uint16_t _localport;  // 端口号bool _isrunning;func_t _func; // 执行相应函数
};

1.3、构造函数

 构造函数只需增加一个函数对象参数,初始化列表初始化变量即可!!!

UdpServer(func_t func,uint16_t localport = glocalport): _func(func), _sockfd(gsockfd), _localport(localport), _isrunning(false)
{
}

1.4、Start()

上一弹的Start()函数是先接收客户端的消息,然后将客户端的消息发送回去此弹接收客户端的英文单词,然后服务端翻译成中文,然后将中文发送回去!!!

void Start()
{_isrunning = true;char inbuffer[1024];while (_isrunning){// sleep(1);struct sockaddr_in peer;socklen_t len = sizeof(peer);// 接收客户端的英文单词ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){InetAddr addr(peer);inbuffer[n] = 0;// 一个一个的单词std::cout << "[" << addr.Ip() << ":" << addr.Port() << "]# " << inbuffer << std::endl;std::string result = _func(inbuffer); // 执行翻译功能// 将翻译的中文结果发回客户端sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr *)&peer,len);}}
}

2、Dict.hpp

Dict类执行加载字典文件和执行翻译的功能!!

2.1、基本结构

Dict成员包含一个存储字典的KV结构和一个文件路径

class Dict
{
private:// 加载字典文件void LoadDict(const std::string& path);
public:// 构造Dict(const std::string& dict_path);// 英语翻译为中文std::string Translate(std::string word);~Dict(){}
private:std::unordered_map<std::string, std::string> _dict; // 字典结构std::string _dict_path;                             // 文件路径
};

2.2、加载字典文件

注意:此处的字典文件是以冒号 + 空格来分割英文与中文的!

加载字典文件的本质是以KV的形式将英文单词和中文翻译插入到_dict哈希表中

 加载文件包含3个大的步骤:

  • 1、读方式打开文件
  • 2、按行读取内容[需要考虑中间有空格情况,一行中没找到分隔符情况]
  • 3、关闭文件
const static std::string sep = ": "; // 分隔符 冒号+空格// 加载字典文件
void LoadDict(const std::string &path)
{// 1.读方式打开文件std::ifstream in(path);// 判断是否打开成功if (!in.is_open()){LOG(FATAL, "open %s failed\n", path.c_str());exit(1);}std::string line;// 2.按行读取内容while (std::getline(in, line)){LOG(DEBUG, "load info : %s ,success\n", line.c_str());if (line.empty())continue; // 中间有空格情况auto pos = line.find(sep); // 使用find找到分割符位置,返回迭代器位置if (pos == std::string::npos)continue; // 一行中没找到分隔符// apple: 苹果std::string key = line.substr(0, pos); // [) 前闭后开if (key.empty())continue;std::string value = line.substr(pos + sep.size()); // 从pos + 分隔符长度开始到结尾if (value.empty())continue;_dict.insert(std::make_pair(key, value));}LOG(INFO, "load %s done\n", path.c_str());in.close(); // 3.关闭文件
}

2.3、构造函数

构造函数初始化字典文件和加载字典文件(将字典文件以KV格式插入到_dict中)。

// 构造
Dict(const std::string &dict_path) : _dict_path(dict_path)
{LoadDict(_dict_path);
}

2.4、翻译函数

翻译函数在_dict中查找是否有该单词,有该单词则返回_dict的value值(没找到返回None)

// 英语翻译为中文
std::string Translate(std::string word)
{if (word.empty())return "None";auto iter = _dict.find(word);if (iter == _dict.end())return "None";elsereturn iter->second;
}

2.5、dict.txt

dict.txt文件存储对应的英文单词和翻译结果

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

3、UdpServerMain.cc

服务端主函数基本结构没变,但是构造指针对象时需要传递翻译函数对象,但是这个函数对象的参数是字符串类型,返回值是字符串类型,而Dict类中的翻译函数有this指针,因此我们需要使用bind()绑定函数

// .udp_client local-port
// .udp_client 8888
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << "  server-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);EnableScreen();Dict dict("./dict.txt");                                                      // 构造字典类func_t translate = std::bind(&Dict::Translate, &dict, std::placeholders::_1); // 绑定翻译函数std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(translate, port); // C++14标准usvr->InitServer();usvr->Start();return 0;
}

运行结果 

4、完整源码

4.1、Dict.hpp

#pragma once#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"using namespace log_ns;const static std::string sep = ": "; // 分隔符 冒号+空格class Dict
{
private:// 加载字典文件void LoadDict(const std::string &path){// 1.读方式打开文件std::ifstream in(path);// 判断是否打开成功if (!in.is_open()){LOG(FATAL, "open %s failed\n", path.c_str());exit(1);}std::string line;// 2.按行读取内容while (std::getline(in, line)){LOG(DEBUG, "load info : %s ,success\n", line.c_str());if (line.empty())continue; // 中间有空格情况auto pos = line.find(sep); // 使用find找到分割符位置,返回迭代器位置if (pos == std::string::npos)continue; // 一行中没找到分隔符// apple: 苹果std::string key = line.substr(0, pos); // [) 前闭后开if (key.empty())continue;std::string value = line.substr(pos + sep.size()); // 从pos + 分隔符长度开始到结尾if (value.empty())continue;_dict.insert(std::make_pair(key, value));}LOG(INFO, "load %s done\n", path.c_str());in.close(); // 3.关闭文件}public:// 构造Dict(const std::string &dict_path) : _dict_path(dict_path){LoadDict(_dict_path);}// 英语翻译为中文std::string Translate(std::string word){if (word.empty())return "None";auto iter = _dict.find(word);if (iter == _dict.end())return "None";elsereturn iter->second;}~Dict(){}private:std::unordered_map<std::string, std::string> _dict; // 字典结构std::string _dict_path;                             // 文件路径
};

4.2、dict.txt

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

4.3、InetAddr.hpp

#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>class InetAddr
{
private:// 网络地址转本地地址 void ToHost(const struct sockaddr_in& addr) {_port = ntohs(addr.sin_port); // 网络转主机_ip = inet_ntoa(addr.sin_addr); // 结构化转字符串}
public:InetAddr(const struct sockaddr_in& addr):_addr(addr){ToHost(addr);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}~InetAddr(){}
private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};

4.4、LockGuard.hpp

#pragma once 
#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t* mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t* _mutex;
};

4.5、Log.hpp

#pragma once#include <iostream>
#include <fstream>
#include <ctime>
#include <cstring>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>
#include <pthread.h>
#include "LockGuard.hpp"namespace log_ns
{// 日志等级enum{DEBUG = 1,INFO,WARNING,ERROR,FATAL};std::string LevelToString(int level){switch (level){case DEBUG:return "DEBUG";case INFO:return "INFO";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "UNKNOW";}}std::string GetCurrTime(){time_t now = time(nullptr); // 时间戳struct tm *curr_time = localtime(&now);char buffer[128];snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",curr_time->tm_year + 1900,curr_time->tm_mon + 1,curr_time->tm_mday,curr_time->tm_hour,curr_time->tm_min,curr_time->tm_sec);return buffer;}class logmessage{public:std::string _level;        // 日志等级pid_t _id;                 // pidstd::string _filename;     // 文件名int _filenumber;           // 文件行号std::string _curr_time;    // 当前时间std::string _message_info; // 日志内容};#define SCREEN_TYPE 1
#define FILE_TYPE 2const std::string glogfile = "./log.txt";pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;// log.logMessage(""/*文件名*/,12/*文件行号*/,INFO/*日志等级*/,"this is a %d message,%f,%s,hello world"/*日志内容*/,x,,);class Log{public:// 默认向显示器打印Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE){}// 打印方式void Enable(int type){_type = type;}// 向屏幕打印void FlushToScreen(const logmessage &lg){printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());}// 向文件打印void FlushToFile(const logmessage &lg){std::ofstream out(_logfile, std::ios::app); // 追加打开文件if (!out.is_open())return; // 打开失败直接返回char logtxt[2048];snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());out.write(logtxt, strlen(logtxt)); // 写文件out.close();                       // 关闭文件}// 刷新日志void FlushLog(const logmessage &lg){// 加过滤逻辑 --- TODO// ...LockGuard lockguard(&glock); // RAII锁switch (_type){case SCREEN_TYPE:FlushToScreen(lg);break;case FILE_TYPE:FlushToFile(lg);break;}}// ... 可变参数(C语言)// 初始化日志信息void logMessage(std::string filename, int filenumber, int level, const char *format, ...){logmessage lg;lg._level = LevelToString(level);lg._id = getpid();lg._filename = filename;lg._filenumber = filenumber;lg._curr_time = GetCurrTime();va_list ap;           // va_list-> char*指针va_start(ap, format); // 初始化一个va_list类型的变量char log_info[1024];vsnprintf(log_info, sizeof(log_info), format, ap);va_end(ap); // 释放由va_start宏初始化的va_list资源lg._message_info = log_info;// std::cout << lg._message_info << std::endl; // 测试// 日志打印出来(显示器/文件)FlushLog(lg);}~Log(){}private:int _type;            // 打印方式std::string _logfile; // 文件名};Log lg;
// 打印日志封装成宏,使用函数方式调用
#define LOG(Level, Format, ...)                                          \do                                                                   \{                                                                    \lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \} while (0)
// 设置打印方式,使用函数方式调用
#define EnableScreen()          \do                          \{                           \lg.Enable(SCREEN_TYPE); \} while (0)
// 设置打印方式,使用函数方式调用
#define EnableFile()          \do                        \{                         \lg.Enable(FILE_TYPE); \} while (0)}

4.6、Makefile

.PHONY:all
all:udpserver udpclientudpserver:UdpServerMain.ccg++ -o $@ $^ -std=c++14udpclient:UdpClientMain.ccg++ -o $@ $^ -std=c++14.PHONY:clean 
clean:rm -rf udpserver udpclient

4.7、nocopy.hpp

#pragma onceclass nocopy
{
public:nocopy(){}~nocopy(){}nocopy(const nocopy&) = delete;const nocopy& operator=(const nocopy&) = delete;
};

4.8、UdpClientMain.cc

#include "UdpServer.hpp"
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// 客户端在未来一定要知道服务器的IP地址和端口号
// .udp_client server-ip server-port
// .udp_client 127.0.0.1 8888
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1.创建套接字int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "create socket eror\n"<< std::endl;exit(1);}// client的端口号,一般不让用户自己设定,而是让client 所在OS随机选择?怎么选择?什么时候?// client 需要bind它自己的IP和端口,但是client 不需要 "显示" bind它自己的IP和端口// client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); // 转换重要!!!server.sin_addr.s_addr = inet_addr(serverip.c_str());while (true){std::string line;std::cout << "Please Enter# ";std::getline(std::cin, line); // 以行读取消息// 发消息,你要知道发送给谁int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));if(n > 0){// 收消息struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];int m = recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);if(m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}else{break;}}else{break;}}// 关闭套接字::close(sockfd);return 0;
}

4.9、UdpServerMain.cc

#include "UdpServer.hpp"
#include "Dict.hpp"
#include <memory>// .udp_client local-port
// .udp_client 8888
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << "  server-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);EnableScreen();Dict dict("./dict.txt");                                                      // 构造字典类func_t translate = std::bind(&Dict::Translate, &dict, std::placeholders::_1); // 绑定翻译函数std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(translate, port); // C++14标准usvr->InitServer();usvr->Start();return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DBA面试题-1

面临失业&#xff0c;整理一下面试题&#xff0c;找下家继续搬砖 主要参考&#xff1a;https://www.csdn.net/?spm1001.2101.3001.4476 略有修改 一、mysql有哪些数据类型 1&#xff0c; 整形 tinyint,smallint,medumint,int,bigint&#xff1b;分别占用1字节、2字节、3字节…

vxe-table 树形表格序号的使用

vxe-table 树形结构支持多种方式的序号&#xff0c;可以及时带层级的序号&#xff0c;也可以是自增的序号。 官网&#xff1a;https://vxetable.cn 带层级序号 <template><div><vxe-grid v-bind"gridOptions"></vxe-grid></div> <…

精通.NET鉴权与授权

授权在.NET 中是指确定经过身份验证的用户是否有权访问特定资源或执行特定操作的过程。这就好比一个公司&#xff0c;身份验证(鉴权)是检查你是不是公司的员工&#xff0c;而授权则是看你这个员工有没有权限进入某个特定的办公室或者使用某台设备。 两个非常容易混淆的单词 鉴…

Spring Task和WebSocket使用

在现代 Web 应用中&#xff0c;WebSocket 作为一种全双工通信协议&#xff0c;为实时数据传输提供了强大的支持。若要确保 WebSocket 在生产环境中的稳定性和性能&#xff0c;使用 Nginx 作为反向代理服务器是一个明智的选择。本篇文章将带你了解如何在 Nginx 中配置 WebSocket…

机器学习任务功略

loss如果大&#xff0c;训练资料没有学好&#xff0c;此时有两个可能&#xff1a; 1.model bias太过简单&#xff08;找不到loss低的function&#xff09;。 解决办法&#xff1a;增加输入的feacture&#xff0c;设一个更大的model&#xff0c;也可以用deep learning增加弹性…

STL:相同Size大小的vector和list哪个占用空间多?

在C中&#xff0c;vector和list是两种不同的序列容器。vector底层是连续的内存&#xff0c;而list是非连续的&#xff0c;分散存储的。因此&#xff0c;vector占用的空间更多&#xff0c;因为它需要为存储的元素分配连续的内存空间。 具体占用多少空间&#xff0c;取决于它们分…

Windows 10电脑无声问题的全面解决方案

Windows 10操作系统以其强大的功能和用户友好的界面赢得了广大用户的青睐&#xff0c;但在使用过程中&#xff0c;有时会遇到电脑突然没有声音的问题。这一问题可能由多种原因引起&#xff0c;包括音频驱动程序问题、音频设置错误、系统更新冲突等。本文将详细介绍Windows 10无…

6.824/6.5840 Lab 1: Lab 3: Raft

漆昼中温柔的不像话 静守着他的遗憾啊 旧的摇椅吱吱呀呀停不下 风卷走了满院的落叶落花 ——暮色回响 完整代码见&#xff1a; https://github.com/SnowLegend-star/6.824 在完成Lab之前&#xff0c;务必把论文多读几遍&#xff0c;力求完全理解Leader选举、log日志等过程。 …

小程序-基于java+SpringBoot+Vue的养老院管理系统设计与实现

项目运行 1.运行环境&#xff1a;最好是java jdk 1.8&#xff0c;我们在这个平台上运行的。其他版本理论上也可以。 2.IDE环境&#xff1a;IDEA&#xff0c;Eclipse,Myeclipse都可以。推荐IDEA; 3.tomcat环境&#xff1a;Tomcat 7.x,8.x,9.x版本均可 4.硬件环境&#xff1a…

YOLOv11 NCNN安卓部署

YOLOv11 NCNN安卓部署 之前自己在验证更换relu激活函数重新训练部署模型的时候&#xff0c;在使用ncnn代码推理验证效果很好&#xff0c;但是部署到安卓上cpu模式会出现大量的错误检测框&#xff0c;现已更换会官方默认的权重 前言 YOLOv11 NCNN安卓部署 目前的帧率可以稳定…

WPF_3

x名称空间的由来和作用 WPF程序中有这样的代码&#xff1a; x:Class"WpfControlLibrary1.UserControl1"<!--这是对x的使用-->xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/…

使用flex布局实现一行固定展示n个元素

前言&#xff1a; 最近在公司中完成小程序的UI设计稿时&#xff0c;遇到了布局一个问题&#xff1a;UI设计稿想实现的布局是这样的&#xff1a; 笔者第一反应就是使用flex中的justify-content: space-between;属性&#xff0c;但是使用之后发现&#xff0c;justify-content: …

Angular v19 (三):增量水合特性详解 - 什么是水合过程?有哪些应用场景?与 Qwik 相比谁更胜一筹?- 哪个技术好我就学哪个,这就是吸心大法吧

Angular在其最新版本 v19 中引入了增量水合&#xff08;Incremental Hydration&#xff09;这一特性。这一更新引发了开发者们广泛的讨论&#xff0c;特别是在优化首屏加载速度和改善用户体验方面。本文将详解水合过程的概念、增量水合的应用场景&#xff0c;以及它与类似框架如…

各类 AI API获取方法,GPT | Claude | Midjourney等

前言 在当今数字化转型的浪潮中&#xff0c;企业和开发者都面临着前所未有的技术挑战与机遇。随着ChatGPT等大语言模型的崛起&#xff0c;AI应用开发已从可选项变成了必选项。在AI应用开发中&#xff0c;成本控制是一个普遍的痛点。单是API调用费用就包含了多个维度&#xff1…

Linux:进程间通信之system V

一、共享内存 进程间通信的本质是让不同的进程看到同一份代码。 1.1 原理 第一步&#xff1a;申请公共内存 为了让不同的进程看到同一份资源&#xff0c;首先我们需要由操作系统为我们提供一个公共的内存块。 第二步&#xff1a;挂接到要通信进程的地址空间中 &#xff…

Python数组拆分(array_split())

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

微信小程序——文档下载功能分享(含代码)

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

LabVIEW氢气纯化控制系统

基于LabVIEW的氢气纯化控制系统满足氢气纯化过程中对精确控制的需求&#xff0c;具备参数设置、过程监控、数据记录和报警功能&#xff0c;体现了LabVIEW在复杂工业控制系统中的应用效能。 项目背景 在众多行业中&#xff0c;尤其是石油化工和航天航空领域&#xff0c;氢气作为…

【linux】(23)对象存储服务-MinIo

MinIO 是一个高性能的对象存储服务&#xff0c;兼容 Amazon S3 API。 Docker安装MinIo 前提条件 确保您的系统已经安装了 Docker。如果还没有安装 Docker&#xff0c;可以参考 Docker 官方文档进行安装。 1. 拉取 MinIO Docker 镜像 首先&#xff0c;从 Docker Hub 拉取 Mi…

(超详细图文)PLSQL Developer 配置连接远程 Oracle 服务

1、下载配置文件 &#xff08;超详细图文详情&#xff09;Navicat 配置连接 Oracle-CSDN博客 将下载的文件解压到单独文件夹&#xff0c;如&#xff1a;D:\App\App_Java\Oracle\instantclient-basic-windows.x64-19.25.0.0.0dbru 2、配置 打开 PLSQL Developer&#xff0c;登…