TCP_SOCKET编程实现
文章目录
- 与UDP_SOCKET的区别
- 第一代Tcp_Server
- Tcp_Client
- 第二代Tcp_Server
- 第三代Tcp_server
- 多线程版本Tcp_Server
- 线程池版的Tcp_Server
- 使用inet_ntop来解决线程安全问题
- 业务逻辑编写
- 总结
- 补充说明&&业务代码完成
- ping的真实作用
- Translate编写
- Transform业务代码
- 整体总结
与UDP_SOCKET的区别
与udp大同小异
多了一些步骤
udp是不可靠的, 不连接的协议
tcp是面向连接的
c/s
谁来建立这个链接?
是client主动建立链接
如手机上的抖音打开, 淘宝打开等
服务器是在一直等待链接的到来, 好比餐厅老板等待客人到来
所以与udp的区别
- 设置socket为监听状态
函数接口:
参数2后续解释
第一代Tcp_Server
tcp初始化操作, 都是固定套路
初始化代码实现:
void Init(){// 1. 创建socket, file fd, 本质是文件_sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建tcp套接字, 第一个参数不变, 第二个参数是tcp, 面向字节流if (_sockfd < 0){lg.LogMessage(Fatal, "create socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Fatal);}lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _sockfd);// 2. 填充网络信息并绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_addr.s_addr = htonl(INADDR_ANY);//必须是0, 表示任意local.sin_family = AF_INET;//IPv4的网络套接字local.sin_port = htons(_port);local.sin_zero[0] = 0;//填充剩余部分, 必须置零if(bind(_sockfd, CONV(&local), sizeof(local) != 0)){lg.LogMessage(Fatal, "bind socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Bind_Err);}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _sockfd);// 3.设置socket为监听状态if(listen(_sockfd, default_backlog) != 0){lg.LogMessage(Fatal, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Listen_Err);}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _sockfd);
- tcp_server获取链接
后两个参数是输入输出型, 更强调输出,
这个参数相当于udp的recvfrom
他的返回值成功会返回一个非0的文件描述符
udp里面, sockfd只有一个
-
但是在tcp这里, 会新增一个文件描述符
eg: 门口拉客的 = 旧的sockfd
店内传菜的 = 新创建的sockfd
使用旧的sockfd和client进行获取链接
使用新的sockfd和client进行通信
这样看来, 旧的sockfd只用于listen
新的sockfd才是真的sockfd -
udp是面向数据报, 而tcp是面向字节流, 这和文件, 管道的特性一模一样
Start启动完成
void Start(){_isrunning = true;while(_isrunning){// 4. 获取链接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, CONV(&peer), &len);// 进行获取链接if(sockfd < 0){lg.LogMessage(Warning, "accept socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue;// 获取失败则继续获取}lg.LogMessage(Debug, "accept socket success, get a new sockfd: %d\n", sockfd);// 5. 提供服务Service(sockfd);close(sockfd);} }// 这个sockfd表示连接是全双工的void Service(int sockfd){char buffer[1024];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if(n > 0){buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo_string = "server echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0)// read如果为0, 文件中表示读到了结尾, 这边表示对端关闭了连接(与管道的情况一模一样){lg.LogMessage(Info, "client quit...\n");break;}else{lg.LogMessage(Error, "read error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}}
- 查看结果
tcp_client编写
与udp的差别, tcp在创建了sockfd之后, 只需要建立连接
Tcp_Client
与udp的差别, tcp在创建了sockfd之后, 只需要建立连接
-
connect连接
-
文件流进行消息读写
// 2. client必须要ip和port, 但是不需要显示绑定, client 系统随机端口// tcp发起链接的时候, 被OS自动进行本地绑定// 建立连接connectstruct 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());// 换一个接口 ipv4转二进制 p当成process n是网络 -- 不太准确, 但是好记inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // 1. IPV4--> 四字节ip 2. 进程转网络序列int n = connect(sockfd, CONV(&server), sizeof(server)); // 这里自动进行bindif (n < 0){cerr << "connect error" << endl;return 2;}// 3. 发送数据// 与服务器进行持续通信的循环while (true){// 用户输入的消息string message;// 提示用户输入信息cout << "Please Enter# ";// 读取用户输入的整行文本getline(cin, message);// 尝试向服务器发送消息ssize_t n = write(sockfd, message.c_str(), message.size());// 检查消息是否成功发送if (n > 0){// 用于接收服务器响应的缓冲区char buffer[1024];// 从服务器读取响应, sockfd读写都用, 更加说明是全双工ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);// 检查是否接收到响应if (m > 0){// 确保响应字符串以空字符结尾buffer[m] = 0;// 打印服务器的响应std::cout << "get a echo message# " << buffer << std::endl;}// 检查服务器是否关闭连接else if (m == 0){// 服务器关闭时打印信息并退出循环std::cout << "server close!" << std::endl;break;}}// 发送消息时发生错误else{// 打印错误信息并退出循环cerr << "write error" << endl;break;}}close(sockfd);
进行测试:
结果说明
1. 运行后查看netstat -ntp 有两个服务, 不是一个吗?
原因是, 现在client和server在一台机器上, 所有有两个, 将来如果是在一台机器上, 那就只有一个
2. 这样的新创建的client因为是单进程, 所以新建的client 会阻塞, 发的消息也会阻塞, 当旧的client关闭, 新的client会瞬间显示阻塞的信息, 这个阻塞的极限是多少, 由我们服务器的缓冲区决定
可以看到, 消息被输出后, 一部分成功输出, 另一部分write失败, 导致溢出到shell中输出
3. 服务器断线重连实现:
a. 将上述的工作放到visitServer函数中
然后, 进行断线重连的操作
int count = 1;while(count <= ReTry_count){bool result = visitServer(serverip, serverport);if(result) break;else{// 重连操作sleep(1);cout << "reconnecting..., count: " << count++ << endl;}}if(count > ReTry_count) {cout << "reconnect failed" << endl;}
b. 有时client端会在创建后, server退出, 此时client重连, 让他实现从1次重连开始
像这样,就需要修改代码
只需让他多加一个参数, 在这个函数内返回false时, 让count = 1即可, 这个意义不太大, 有了解即可, 因为现实情况是远比当前复杂, 有可能是客户端断线…等等
- tcp_server启动有时候会出现绑定失败的情况
原理先不解释, 先说解决方式
setsockopt
写在server创建listensockfd那里
第二代Tcp_Server
多进程版本的相互通信
对于一个父进程, 他有他自己的文件描述符表, 且表中的每个信息指向它对应的struct file, 创建子进程之后, 会有多个表中的信息指向同一个struct file, 所以可以像管道那样, 对于父子进程关闭不需要的文件fd
第三代Tcp_server
信号版本的Tcp_server
在linux 中, 如果对SIGCHLD设置为SIG_IGN, 则会自动回收子进程, 不会wait阻塞住
所以开局设置signal
然后创建子进程方面默认执行
因为设置了信号机制, 所以也不用wait
子进程的退出会自动OS回收, 如果不设置信号, 单纯使用上述的代码, 会引起僵尸进程问题
每次创建进程都会消耗时间, 推荐使用进程池来使用, 这边就不进行讲述
问题:
先创建的子进程, 再进行的链接, 进程看不到sockfd,怎么办?
在父子进程之间传递文件描述符的实现, 一个比较老的功能, 可以通过这个解决问题
多线程版本Tcp_Server
相比多进程, 更加的方便, 也不需要文件描述符的传递, 也不需要关闭所谓的fd, 主子线程共享文件描述符表
完成之后, 为什么有报错?
主要是因为类型不匹配导致的
可以考虑使用std::bind来适配成员函数的调用格式,但是更推荐重构成员函数为静态函数或全局函数
传参
定义实现
封装
结果演示
每加一个线程, 文件描述符表也会增加
链接来了, 才创建线程, 还是慢, 所以使用线程池, 且加上client的信息, 通过accept函数获取, 后两个参数也有输出功能
线程池版的Tcp_Server
先加client信息的处理
使用InetAddr.hpp获取属性
相关代码改动
完成之后, 进行验证
相关问题
使用inet_ntop来解决线程安全问题
这个转IP的函数返回值是一个char*, 未来的操作可能会有线程安全问题
他会在函数内部申请一个静态的内存地址, 然后返回, 所以他是一个可重入函数, 存在线程安全问题
应当减少使用, 推荐使用inet_ntop, 这个函数允许我们自己维护一个缓冲区, 而不是函数内部自己去申请
可以有效避免线程安全问题
多线程修改:
防止命名冲突, 将threadpool放在ThreadNs里面, 然后再TcpServer的构造函数里面
下面进行修改代码, 让server能被所有线程就进行访问
往后的任务都是线程池中的线程来接受任务, 进行处理, 而不是创建新的线程
下面实现代码让线程来执行任务
这个任务可以使用Task类来进行处理, 但是也可以使用function来进行处理
这样的方式比较简单
下面用这个包装器来完成任务
未来获取新的文件文件描述符信息等后, 可以直接进行如下构建
再这样修改完成之后, 因为线程池中的线程数目是固定的, 所以我们的client链接数目有限, 当超过限制的client数目时, 就会出现, 连不上的情况. 同时, 为别人提供的服务是死循环式对我们的server端的压力是有点大的, 所以在一般我们很少去这样写代码
现在对他进行调整, 让它变得实际有用一点, 而不是像现在这样进行通信
也就是说, 不能让我们的服务像是死循环这样一直执行, 而是实现一个业务逻辑
这边的策略是实现一个英译汉的功能,要利用I/O, 同时引入unordered_map
定义回调函数(后期可能进行修改)
现在只需要Tcp_server进行IO就可以了
然后构建业务逻辑
业务逻辑编写
在这里存放所有的任务
接下来给Tcp_Server一个register功能
Read方式是进行读取用户第一次输入必须是输入他想要的功能
在 ping Translate Transform之间三选一
read看是读取了哪个结果
进行一个Routine 的路由功能, 实现read到对应的内容给type, 好实现对应的功能, 对应的功能是放在funcs中的
然后, 线程在启动执行的时候, 将对应的sockfd 的功能和Routine进行绑定
然后, 服务器创建出来之后(这一步在Main.cc文件中), 执行注册服务
注册之后, 执行对应的服务, 以便调用时执行对应的回调函数
这个register进行添加对应的name和方法到map
然后在进行Start的时候, 将要执行的方法和对应的用户输入名称之间的匹配调用, 此时Ping之类的函数才算是被真正的执行
Ping函数要做的事就是之前在Tcp_server里面, server方法要做的事
然后在修改Tcp_Client
完成代码进行执行查看, 可以正常执行
server-log
client-log
现在补充其他的路由信息,进行基本的完善
具体功能后续进行补齐
现在ping的作用事是想实现一下一次ping之后的结果, 所以还要进行修改代码
然后因为之前是将Sockfd的关闭放在tcp_server的ThreadData类的析构函数中的, 但是现在已经不需要这个类了, 所以还要进行文件描述符的关闭操作, 这个操作可以放在这个回调函数执行完毕的后面
下面继续执行代码
可以看到在一次运行结束之后, 这个client会进行退出, 主要是因为当期的ping服务已经变成了短服务,只需要执行一次, 而不是像之前那样的死循环
现在进行总结一下
总结
未来服务可能部署在云服务器上面, 如何在未来的某一时刻知道这个服务器是否健康的呢?
可以定期(30s)给服务器发送一个最小服务请求, 如果得到回复, 那么这个服务就是正常的
这个机制, 我们称为心跳机制, 我们可以通过client->服务器, 同时也可以反向的得到server->client
而这个编写的Ping, 其实就是对心跳的一个响应机制, 用于检测服务器是否是正常的
补充说明&&业务代码完成
ping的真实作用
Interact是进行交互功能, 它可以进行心跳机制的检测, 读消息是out, 发消息是in, 以此完成交互
下面进行Translate
Translate编写
首先, 我们到此为止,所有的代码都是网络
下面是业务代码, 顺便完成Translate
为了方便, 可以将单词加载在resource的dict.txt文件内, 进行文件操作
然后构建Translate.hpp业务逻辑代码
定义正常的常变量
字典路径, 当前字典, 当前字典读取的内容
然后是构造函数和加载字典的方法
然后是内部函数
现在可以DEBUG一下, 看看有没有读取成功
推荐使用条件编译
但是这个是.hpp文件, 所以不能这样进行条件编译操作
可以创建一个test.cc来进行debug
说明我们在Translate的构造函数, 读取字典函数, private变量都没有问题
然后进行分割功能
做完分割, 那么可以对这个Debug进行修改
未来的加载词库, 只需要往dict.txt中加就行了. 当然更推荐将这个Translate类改为单例模式, 这边就不做处理
那么如何将这个Translate服务于网络进行结合呢?
那就到了Translate调用
首先定义全局变量(改为单例最好)
所以未来服务首先都是进行注册的
Transform业务代码
将1按照OP的格式转化为result存到1里面
代码完成, 对于defaultserver的部分修改就不演示
整体总结
- IO类函数, write/read其实他们已经做了转网络序列的处理
- udp :数据包 tcp: 面向字节流
a. 区别:
1). TCp代码中我们使用read/write目前是有BUG的(下节讲解), TCP要进行正确读写, 必须结合用户协议
2). udp-----数据和数据是有边界的, sendto只发了一次, 对端对应对端, recvfrom一次----面向数据报
3). tcp------write->1,10, 100->接收方可能一次收完, 也可能是很多次, 但无论是多少次,都和对方发送无关—面向字节流
b.udp就好比是一封信件, 只能一封一封去读
c. tcp是全读, 然后进行手动分割 - 网络服务在真正运行时, 必须在Linux中以守护进程(精灵进程)的形式进行运行
要完成上述工作, 应当利用守护进程, 下篇讲解~~~~~
相关文章:

TCP_SOCKET编程实现
文章目录 与UDP_SOCKET的区别第一代Tcp_ServerTcp_Client第二代Tcp_Server第三代Tcp_server多线程版本Tcp_Server线程池版的Tcp_Server使用inet_ntop来解决线程安全问题 业务逻辑编写总结补充说明&&业务代码完成ping的真实作用Translate编写Transform业务代码 整体总结…...

螺蛳壳里做道场:老破机搭建的私人数据中心---Centos下Docker学习07(基于docker容器的防火墙及NAT企业实战)
7.1 网络准备 7.2 网络规划 1)虚拟网络编辑器 点击右下方“更改设置”,点击“添加网络”假如vmnet3和vmnet4,然后分别选择vmnet3和vmnet4,设置为“仅主机模式”,按③处处理,去掉“使用DHCP”,…...

②EtherNet/IP转ModbusTCP, EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关
EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关https://item.taobao.com/item.htm?ftt&id822721028899 协议转换通信网关 EtherNet/IP 转 Modbus TCP (接上一章) GW系列型号 配置使用 与 EtherNet/IP 主站进行组态说明 这里介…...
Java 集合(Collection)
1.什么是集合? 对象的容器,定义了对多个对象进行操作的常用方法,属于接口类型。 2.集合和数组的区别 (1)数组长度固定,集合长度不固定 (2)数组可以存储基本类型和引用类型&#…...

Windows系统编程(三)线程并发
进程与线程 进程:直观的说就是任务管理器中各种正在运行的程序。对于操作系统来说,进程仅仅是一个数据结构,并不会真实的执行代码 线程:通常被称作但并不真的是轻量级进程或实际工作中的进程,它会真实的执行代码。每…...

【Qt】控件概述(2)—— 按钮类控件
控件概述(2) 1. PushButton2. RadioButton——单选按钮2.1 使用2.2 区分信号 clicked,clicked(bool),pressed,released,toggled(bool)2.3 QButtonGroup分组 3. CheckBox——复选按钮 1. PushButton QPushB…...
Java访问器方法和更改器方法
一.访问器方法 1.访问器方法的定义和用途 访问器方法,通常也称为getter方法,是一种在面向对象编程中用于从类的外部访问私有字段值的特殊方法。这些方法的设计目的是为了提供对类内部状态的受限访问,同时保持类的封装性。通过使用访问器方法&…...
CAN协议帧结构
一、数据帧的整体结构 ┌───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐ │ SOF │ ID[11]│ RTR │ IDE │ DLC │ Data …...
valgrind 单例模式的自动释放(多线程)
单例模式,其中对象是由_pInstance指针来保存的,而在使用单例设计模式的过程中,也难免会遇到内存泄漏的问题。那么是否有一个方法,可以让对象自动释放,而不需要程序员自己手动去释放呢? ——嵌套类 5.1、内…...

OpenFegin
文章目录 一、OpenFegin是什么?二、基本使用三、超时重试机制4.自定义超时重传机制五、底层实现 一、OpenFegin是什么? OpenFeign的全称为Spring Cloud OpenFeign(下文简称OpenFeign),是Spring Cloud团队开发的一款基于 Feign的框架,声明式W…...

LeetCode-2608. 图中的最短环【广度优先搜索 图,腾讯面试真题】
LeetCode-2608. 图中的最短环【广度优先搜索 图,腾讯面试真题】 题目描述:解题思路一:【一图秒懂】枚举起点跑 BFS解题思路二:背诵版解题思路三: 题目描述: 现有一个含 n 个顶点的 双向 图,每个…...

IDEA 编译报错 “java: 常量字符串过长” 的解决办法
目录 一、问题描述二、问题原因2.1 理论角度2.2 源码角度 三、解决方案解决方案①:StringBuilder 拼接解决方案②:读取文件内容 四、方案验证 在线文本换行工具: https://lzltool.cn/Toolkit/WrapWordsInText 一、问题描述 今天在开发过程中…...

RK3568平台开发系列讲解(I2C篇)I2C 总线实现 client 设备方法
🚀返回专栏总目录 文章目录 一、非设备树实现 i2c client1.1、i2c_new_device1.2、i2c client二、设备树实现 i2c2.1、i2c_client 结构体的生成2.2、i2c_driver 驱动2.2.1、module_i2c_driver2.2.2、fan53555_regulator_probe沉淀、分享、成长,让自己和他人都能有所收获!�…...

K8S安装和部署
环境部署说明 主机IPmaster172.25.254.100node10172.25.254.10node20172.25.254.20harbor172.25.254.233 所有节点禁用selinux和防火墙 所有节点同步时间和解析 所有节点安装docker-ce 所有节点禁用swap,注意注释掉/etc/fstab文件中的定义 解析配置(…...
Singleton(单例模式)
1. 意图 在开发中,若某些模块或功能只需要一个类实例,所有调用地方通过着一个类对象访问功能,单例模式符合这种类实例创建模式,并且通过提供统一类实例接口访问类对象。 2. 适用性 《Gof 设计模式-可复用面向对象软件的基础》中对…...

【Linux报错】“-bash: cd: too many arguments“
问题描述 今天使用 cd 想要调整某个文件目录时,发现以下报错 原因分析: arguments 是参数的意思,该报错提示参数过多,意味着系统识别到了多余参数 本质原因:你的命令中输入了多余的 ”空格“ ,检查一…...
C# WebService返回参数为DataTable报错“XML文档有错误”
该问题由于DataTable列存在自定义类型。 解决该报错需要以下几步: 1、自定义类型增加xml序列化 2、由于C#从 XML 反序列化 DataSet 或 DataTable 时的默认限制,所以需要先把调用方的项目开放限制,如果是.netframework项目,需要…...

[paddle]paddleseg快速开始
快速开始 为了让大家快速了解PaddleSeg,本文档使用一个简单示例进行演示。在实际业务中,建议大家根据实际情况进行调整适配。 在开始下面示例之前,请大家确保已经安装好PaddleSeg开发环境(安装说明)。 1 准备数据 …...

UNIAPP popper气泡弹层【unibest框架下】vue3+typescript
看了下市场的代码,要么写的不怎么好,要么过于复杂。于是把市场的代码下下来了自己改。200行代码撸了个弹出层组件。兼容H5和APP。 功能: 1)只支持上下左右4个方向的弹层不支持侧边靠齐 2)不对屏幕边界适配 3)支持弹层外边点击自动隐藏 4)支持…...
launcher.py: error: the following arguments are required: --output_dir
记录一个LLaMA-Factroy配置过程。 安装 git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factory pip install -e ".[torch,metrics]"训练 CUDA_VISIBLE_DEVICES0 llamafactory-cli train example/train_lora/.yaml按理说配置好文件应…...
基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架,支持"一次开发,多端部署",可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务,为旅游应用带来…...
Python爬虫(二):爬虫完整流程
爬虫完整流程详解(7大核心步骤实战技巧) 一、爬虫完整工作流程 以下是爬虫开发的完整流程,我将结合具体技术点和实战经验展开说明: 1. 目标分析与前期准备 网站技术分析: 使用浏览器开发者工具(F12&…...

相机从app启动流程
一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...

用docker来安装部署freeswitch记录
今天刚才测试一个callcenter的项目,所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...
Redis:现代应用开发的高效内存数据存储利器
一、Redis的起源与发展 Redis最初由意大利程序员Salvatore Sanfilippo在2009年开发,其初衷是为了满足他自己的一个项目需求,即需要一个高性能的键值存储系统来解决传统数据库在高并发场景下的性能瓶颈。随着项目的开源,Redis凭借其简单易用、…...

【无标题】湖北理元理律师事务所:债务优化中的生活保障与法律平衡之道
文/法律实务观察组 在债务重组领域,专业机构的核心价值不仅在于减轻债务数字,更在于帮助债务人在履行义务的同时维持基本生活尊严。湖北理元理律师事务所的服务实践表明,合法债务优化需同步实现三重平衡: 法律刚性(债…...
在golang中如何将已安装的依赖降级处理,比如:将 go-ansible/v2@v2.2.0 更换为 go-ansible/@v1.1.7
在 Go 项目中降级 go-ansible 从 v2.2.0 到 v1.1.7 具体步骤: 第一步: 修改 go.mod 文件 // 原 v2 版本声明 require github.com/apenella/go-ansible/v2 v2.2.0 替换为: // 改为 v…...

如何在Windows本机安装Python并确保与Python.NET兼容
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...
[特殊字符] 手撸 Redis 互斥锁那些坑
📖 手撸 Redis 互斥锁那些坑 最近搞业务遇到高并发下同一个 key 的互斥操作,想实现分布式环境下的互斥锁。于是私下顺手手撸了个基于 Redis 的简单互斥锁,也顺便跟 Redisson 的 RLock 机制对比了下,记录一波,别踩我踩过…...