Chemmy's Blog

chengming0916@outlook.com

npm-check

 npm-check 是一个检查依赖包是否存在过期、不正确、未使用等情况的工具。

 全局安装:

npm  install  -g  npm-check

 使用:

npm-check

上述指令会自动检查当前目录下的依赖包情况。

 这里我们重点关注下未使用的依赖包。npm-check 在检查依赖包是否使用时判断的依据是文件中是否存在 require(package) 这条语句,例如:

const lodash = require(‘lodash’);

只要存在这条语句,即使我并未在其它任何地方使用(也就是说这是个无用的包),但是 npm-check 是不会将其判定为未使用的。

 ESLint

为了解决上述存在的这种情况,我们可以借助 ESLint 先去检查代码是否存在未使用的变量(no-unused-vars),这样就可以检查某个包 require 了但并未在后续使用的情况。

全局安装:

npm install -g eslint

编写 .eslintrc.js 配置文件:

 

eslint  –config  .eslintrc.js  ./

执行上述指令便会检查当前目录下的所有代码是否存在定义了但未使用的变量。删除掉未使用的变量(包含对依赖包的引用)之后,再运行 npm-check 便能正确的找出那些在项目中已不再使用的依赖包了。

通过远程桌面连接windows server服务器时,经常是直接关闭远程桌面程序,而没有注销远程登录的用户,这样导致有很多远程桌面启动的进程依然运行在服务器上,对服务器产生了不必要的开销,其实作为server版操作系统,是可以通过策略控制关闭后自动注销的,步骤如下:

1.运行gpedit.msc

2.找到:本地计算机策略-计算机配置-管理模板-windows组件

3.找到:远程桌面服务-远程桌面会话主机-会话时间限制,右侧,点击设置已中断会话的时间限制

4.选择已启用,在结束已断开连接的会话,选择1分钟

配置用户信息

配置 Git 用户信息是使用 Git 的第一步,这有助于在提交记录中标识作者身份。

1
2
3
# 配置用户名和邮箱
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"

配置编码

为避免中文路径和文件名显示乱码,以及确保提交信息的编码正确,可以进行以下配置:

1
2
3
4
5
6
# 解决中文路径和文件名乱码问题
git config --global core.quotePath false

# 设置提交和日志输出的编码为 UTF-8
git config --global i18n.commitEncoding utf-8
git config --global i18n.logOutputEncoding utf-8

配置内网域名证书

当访问使用自签名证书的内网 Git 仓库时,需要配置 Git 信任该证书。

1
2
3
4
5
# 配置特定内网域名的 SSL 证书路径
git config --global http."内网域名".sslCAInfo "证书所在路径"

# 示例:配置 example.io 域名的自签名根证书
git config --global http."https://example.io".sslCAInfo ~/.certs/selfsigned-root-ca.crt

一、计算机视觉库

1. OpenCV

OpenCV 是最古老、最受欢迎的开源计算机视觉库,为计算机视觉应用提供通用底层算法。

特性

  • 跨平台支持(Windows、Linux、Android、macOS)
  • 支持 Python、Java、C++ 等语言
  • 支持 GPU CUDA 加速
  • 包含可转换为 TensorFlow 模型的预训练模型

主要功能

  • 2D/3D 图像工具包
  • 人脸识别、手势识别
  • 运动检测、人机交互
  • 对象检测、图像分割

2. Scikit-Image

Python 视觉库,是 Scikit-Learn 的扩展,支持将 NumPy 数组作为图像对象处理。

1
2
3
4
5
import skimage as ski
image = ski.data.coins()
edges = ski.filters.sobel(image)
ski.io.imshow(edges)
ski.io.show()

3. Pillow (PIL Fork)

Python 图像处理库,支持多种格式图像读写,包含旋转、合并、缩放等基础变换。

4. TorchVision

PyTorch 扩展库,提供常见图像转换功能、数据集和模型架构,支持 Python 和 C++。

5. MMDetection

基于 PyTorch 的目标检测工具箱,支持多种检测模型(Faster R-CNN、YOLO、SSD 等)。


二、深度学习框架

6. TensorFlow

Google Brain 团队开发(2015年11月发布),支持图像分类、人脸识别、目标检测等任务。

扩展组件

  • TensorFlow.js:浏览器和 Node.js
  • TensorFlow Lite:终端设备
  • TensorFlow Hub:模型复用平台

编程接口:Python、C、C++、Java、JavaScript、Go、R

7. PyTorch

Facebook AI 研究小组开发,Python 优先框架,灵活易用,支持 GPU 加速和动态计算图。

应用场景:图像评估、图像分割、图像分类

8. Keras

基于 Python 的开源库,对初学者友好,底层使用 TensorFlow,有强大的社区支持。

应用场景:图像分割分类、手写识别、3D图像分类、语义图像聚类

9. Caffe

加州大学伯克利分校开发,使用 C++ 编写,支持多语言,用于图像分类、分割模型开发。

适用领域:视觉、语音、多媒体

10. MXNet

亚马逊 AWS 默认框架,支持混合编程模型,支持 Python、C++、R、Scala、Julia 等语言。


三、推理部署工具

11. NVIDIA CUDA-X

GPU 加速库集合,包含数学库、并行算法库、图像视频库、深度学习库。

12. OpenVINO

英特尔开发的跨平台框架,功能包括对象检测、人脸识别、图像彩色化、运动识别。

13. NVIDIA TensorRT

NVIDIA 高性能深度学习推理引擎,优化模型推理性能。


四、目标检测专用

14. YOLO 系列

实时目标检测模型,Joseph Redmon 和 Ali Farhadi 于 2016 年开发。

版本:YOLOv1 → YOLOv8

特性:将图像划分为网格,同时预测各网格目标,高效实时

应用:目标检测、实例分割、图像分类、姿态估计

15. Detectron2

Facebook AI Research 开发的 PyTorch 目标检测库。

包含模型:Faster R-CNN、Mask R-CNN、RetinaNet、DensePose、Cascade R-CNN、Panoptic FPN、TensorMask


五、轻量级框架

16. SimpleCV

开源机器视觉框架,轻松访问 OpenCV 等高性能库,无需深入了解底层概念。


六、框架选型建议

场景 推荐框架
入门学习 Keras、PyTorch
生产部署 TensorFlow、Caffe2、OpenVINO
目标检测 YOLO、Detectron2、MMDetection
边缘设备 OpenVINO、TensorRT Lite
研究实验 PyTorch、MXNet

cmd

方式1:

搜索CMD Ctrl+Shift+Enter

方式2:

打开CMD,输入

1
runas /noprofile /user:Administrator cmd

输入Administrator账户的密码

runas 允许用户用其他权限运行指定的工具和程序

/noprofile 指定不加载用户的配置文件

/user:UserAccountName 指定在其下运行程序的账户

常见问题

运行runas 指令输入密码报错“无法启动服务,原因可能是已被禁用或与其关联的设备没有启动。”

这是因为“Secondary Logo”服务没有启动,这个服务是”在不同凭据下启用启动过程“。直接在cmd中输入services.msc,将服务从禁用改为手动就好了,之后再次输入runas命令就可以使用administrator账户运行。

一、系统要求与准备工作

1.1 系统要求

  • 操作系统:Windows 10 64位系统
  • 磁盘分区:不能安装在FAT/FAT32分区(建议使用NTFS分区)
  • 系统限制:不支持Windows XP系统

1.2 下载安装包

官方下载地址

推荐镜像站点

本次测试版本

  • 安装包:msys2-x86_64-20180531.exe
  • 注意:请下载最新版本以获得更好的兼容性和功能

二、安装步骤

2.1 运行安装程序

  1. 双击下载的安装包
  2. 选择安装路径(建议安装在非系统盘,如D:\msys64)
  3. 一路点击”下一步”完成安装

2.2 重要提示

安装完成后,不要立即运行MSYS2,需要先配置国内镜像源以提高下载速度。

安装完成界面

三、配置国内镜像源

3.1 配置文件位置

所有配置文件位于安装目录下的etc\pacman.d文件夹中:

  • mirrorlist.msys - MSYS2系统源配置
  • mirrorlist.mingw32 - 32位MinGW-w64源配置
  • mirrorlist.mingw64 - 64位MinGW-w64源配置

3.2 配置方法(清华大学镜像)

mirrorlist.msys 文件配置

1
2
3
4
5
6
7
8
9
10
11
##
## MSYS2 repository mirrorlist
##

## Primary
## msys2.org
## Server = http://repo.msys2.org/msys/$arch
## Server = http://downloads.sourceforge.net/project/msys2/REPOS/MSYS2/$arch
## Server = http://www2.futureware.at/~nickoe/msys2-mirror/msys/$arch/

Server = https://mirrors.tuna.tsinghua.edu.cn/msys2/msys/$arch

mirrorlist.mingw32 文件配置

1
2
3
4
5
6
7
8
9
10
11
##
## 32-bit Mingw-w64 repository mirrorlist
##

## Primary
## msys2.org
## Server = http://repo.msys2.org/mingw/i686
## Server = http://downloads.sourceforge.net/project/msys2/REPOS/MINGW/i686
## Server = http://www2.futureware.at/~nickoe/msys2-mirror/i686/

Server = https://mirrors.tuna.tsinghua.edu.cn/msys2/mingw/i686

mirrorlist.mingw64 文件配置

1
2
3
4
5
6
7
8
9
10
11
##
## 64-bit Mingw-w64 repository mirrorlist
##

## Primary
## msys2.org
## Server = http://repo.msys2.org/mingw/x86_64
## Server = http://downloads.sourceforge.net/project/msys2/REPOS/MINGW/x86_64
## Server = http://www2.futureware.at/~nickoe/msys2-mirror/x86_64/

Server = https://mirrors.tuna.tsinghua.edu.cn/msys2/mingw/x86_64

3.3 启用彩色输出

编辑pacman.conf文件(位于etc目录),取消#Color行的注释:

1
2
3
4
5
# 修改前:
#Color

# 修改后:
Color

pacman.conf配置

四、启动与系统更新

4.1 启动MSYS2

从开始菜单启动”MSYS2 MinGW 64-bit”(建议始终使用此版本)

启动界面

4.2 系统更新步骤

第一步:同步包数据库

1
pacman -Sy

第二步:全面更新系统

1
pacman -Syu

第三步:更新剩余包

1
pacman -Su

4.3 更新注意事项

  1. 更新过程中如果出现警告窗口,点击右上角的”×”关闭
  2. 关闭时会弹出确认对话框,点击”OK”
  3. 重要:更新后需要重新配置镜像源,因为配置文件会被覆盖

更新过程

五、安装编译环境

5.1 安装GCC编译器

对于64位系统,安装64位版本的GCC:

1
pacman -S mingw-w64-x86_64-gcc

安装GCC

5.2 安装Make工具

1
pacman -S make

安装Make

5.3 安装完整工具链(可选)

1
2
3
4
5
# 32位工具链(64位系统通常不需要)
pacman -S mingw-w64-i686-toolchain

# 64位工具链(推荐安装)
pacman -S mingw-w64-x86_64-toolchain

六、安装常用开发工具

6.1 基础开发工具

1
pacman -S base-devel git wget perl ruby python2

6.2 GUI开发工具(可选)

安装GTK3

1
2
3
4
5
# 32位版本(64位系统通常不需要)
pacman -S mingw-w64-i686-gtk3

# 64位版本
pacman -S mingw-w64-x86_64-gtk3

安装Glade界面设计器

1
2
3
4
5
# 32位版本
pacman -S mingw-w64-i686-glade

# 64位版本
pacman -S mingw-w64-x86_64-glade

七、环境变量配置

7.1 系统Path变量配置

将以下路径添加到系统环境变量Path中:

对于64位系统

1
2
C:\msys64\mingw64\bin
C:\msys64\usr\bin

7.2 自定义环境变量(可选)

设置MINGW_HOME变量

  • 变量名:MINGW_HOME
  • 变量值:C:\msys64\mingw64

设置C_INCLUDE_PATH变量

  • 变量名:C_INCLUDE_PATH
  • 变量值:%MINGW_HOME%\include

设置LIBRARY_PATH变量

  • 变量名:LIBRARY_PATH
  • 变量值:%MINGW_HOME%\lib

八、验证安装

8.1 检查GCC版本

1
gcc --version

8.2 检查Make版本

1
make --version

8.3 测试简单程序

创建测试文件test.c

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("Hello, MSYS2!\n");
return 0;
}

编译并运行:

1
2
gcc test.c -o test.exe
./test.exe

九、常见问题与解决方案

9.1 安装失败问题

  • 问题:无法安装在FAT32分区
  • 解决:将安装目录改为NTFS分区

9.2 更新失败问题

  • 问题:更新过程中出现错误
  • 解决:重新配置镜像源后再次尝试更新

9.3 环境变量问题

  • 问题:命令在cmd中无法识别
  • 解决:检查环境变量配置是否正确,重启命令行窗口

9.4 权限问题

  • 问题:安装包时提示权限不足
  • 解决:以管理员身份运行MSYS2终端

十、使用建议

10.1 日常使用

  • 始终使用”MSYS2 MinGW 64-bit”进行开发
  • 定期使用pacman -Syu更新系统
  • 使用pacman -Ss 包名搜索可用包

10.2 包管理技巧

1
2
3
4
5
6
7
8
9
10
11
# 搜索包
pacman -Ss gcc

# 安装包
pacman -S 包名

# 删除包
pacman -R 包名

# 查看已安装包
pacman -Q

10.3 开发建议

  • 对于Windows程序开发,建议使用静态编译
  • 利用MSYS2的包管理器安装依赖库
  • 保持开发环境的一致性

总结

MSYS2为Windows用户提供了一个功能完整的类Unix开发环境,通过本文的安装和配置指南,您可以快速搭建起高效的开发环境。关键步骤包括:正确安装、配置国内镜像源、系统更新、安装必要的开发工具以及配置环境变量。

MSYS2的优势在于:

  1. 完整的包管理系统
  2. 与Windows系统的良好集成
  3. 丰富的开发工具链
  4. 活跃的社区支持

通过合理配置和使用,MSYS2将成为您在Windows平台上进行C/C++等开发的强大工具。

识别接口名称

1
2
# 需要 net-tools
ifconfig

如果使用标准的ifconfig命令没有显示出接口,尝试使用带有-a选项的相同的命令。这个选项强制这个工具去显示系统检测到的所有的网络接口,不管他们是up或down状态。如果ifconfig -a没有提供结果,则硬件有错误或者接口驱动没有加载到内核中。

1
2
# 新版本系统大部分支持
ip addr

dhcp

DHCP(动态主机配置协议)使自动接受网络信息(IP地址、掩码、广播地址、网关、名称服务器等)变得容易。这只在网络中有DHCP服务器(或者如果ISP提供商提供一个DHCP服务)时有用.

1
dhcpcd eth0 # eth0 为网口名称,根据上一步识别出的接口名称修改

ifconfig命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 启用/禁用网卡
ifconfig eth0 up/down

# 设置IP地址及掩码
ifconfig eth0 {IP地址} netmask {掩码} up

# 设置默认网关
route add default gw {网关}

# 配置DNS
nano -w /etc.resolv.conf

#使用下边模板填充
nameserver {名称服务器}

花括号中内容使用具体的地址填充

ip命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 启用/禁用网卡
ip link set dev eth0 up/down

# 设置Ip地址及掩码,掩码一般用 24 相当于255.255.255.0
ip addr add {IP地址}/{掩码} dev eth0

# 删除
ip addr del dev eth0 {IP}/{掩码}

# 刷新接口IP(删除所有)
ip addr flush eth0

# 设置默认网关
ip route add default via {网关}

网关的配置参考

ip route命令

Linux上添加路由,删除路由,修改路由配置(route add, route del, 路由表项基本知识)

| 子网掩码用来划分网络区域
| 子网掩码非0的位对应的ip上的数字表示这个ip的网络位
| 子网掩码0位对应的数字是ip的主机位
| 网络位表示网络区域
| 主机位表示网络区域里某台主机
|
| 11111111.11111111.11111111.00000000 = 255.255.255.0 = 24
| —————————————— —————
| 网络位 主机位

| 网络位一致,主机位不一致的2个IP可以直接通讯
|
| 172.25.254.10/24 #24=255.255.255.0
|
| 172.25.254.20/24
|
| 172.25.0.1/16 #16=255.255.0.0
| 前两个可以直接通讯,最后一个与其他俩个不能直接通讯

无线网连接

当使用一块无线(802.11)网卡,在继续之前需要先配置无线设置。要查看当前无线网卡的设置,你可以使用iw

1
2
3
4
5
6
7
8
# 查看连接信息
iw dev wlan0 info

# 检查连接状态
iw dev wlan0 link

# 连接网络 (确保接口处于活动状态)
iw dev wlan0 connect -w {网络名称} key 0:d:{密码}

如果无线网络配置为WPA或WPA2,则需要使用wpa_supplicant

1
2
3
4
5
6
7
8
9
10
11
12
# 查找附近热点
wpa_cli -i wlan0 scan

# 生成连接配置文件
wpa_passphrase {网络名称} {密码} > /etc/wpa_supplicant.conf

# 连接网络
# -D 驱动程序名称(可以是多个驱动程序:nl80211,wext)
# -i 接口名称
# -c 配置文件
# -B 在后台运行守护进程
wpa_supplicant -D nl80211 -i wlan0 -c /etc/wpa_supplicant.conf -B

SSH配置

1
2
3
4
5
6
7
8
9
nano -w /etc/ssh/sshd_config

# 放开注释
PasswordAuthentication yes
PermitRootLogin yes

# 启用SSH密钥对登录,取消如下行的注释符
PubkeyAuthentication yes
AuthorizeKeysFile .ssh/authorized_keys

启动SSHD

1
2
# 启动SSH服务(需要有可登录的账户)
/etc/init.d/sshd start

先决条件
本教程假定 RabbitMQ 已经安装,并运行在localhost 标准端口(5672)。如果你使用不同的主机、端口或证书,则需要调整连接设置。

从哪里获得帮助
如果您在阅读本教程时遇到困难,可以通过邮件列表 联系我们

在第 教程[2] 中,我们学习了如何使用工作队列在多个工作单元之间分配耗时任务。

但是如果我们想要运行一个在远程计算机上的函数并等待其结果呢?这将是另外一回事了。这种模式通常被称为 远程过程调用RPC

在本篇教程中,我们将使用 RabbitMQ 构建一个 RPC 系统:一个客户端和一个可扩展的 RPC 服务器。由于我们没有什么耗时任务值得分发,那干脆就创建一个返回斐波那契数列的虚拟 RPC 服务吧。

客户端接口#

为了说明如何使用 RPC 服务,我们将创建一个简单的客户端类。该类将暴露一个名为Call的方法,用来发送 RPC 请求并且保持阻塞状态,直到接收到应答为止。

var rpcClient = new RPCClient();

Console.WriteLine(" [x] Requesting fib(30)");
var response = rpcClient.Call("30");
Console.WriteLine(" [.] Got '{0}'", response);

rpcClient.Close();

关于 RPC 的说明

尽管 RPC 在计算机中是一种很常见的模式,但它经常受到批评。问题出现在当程序员不知道一个函数是本地调用还是一个耗时的 RPC 请求。这样的混淆,会导致系统不可预测,以及给调试增加不必要的复杂性。误用 RPC 可能会导致不可维护的混乱代码,而不是简化软件。

牢记这些限制,请考虑如下建议:

  • 确保可以明显区分哪些函数是本地调用,哪些是远程调用。
  • 为您的系统编写文档,明确组件之间的依赖关系。
  • 捕获异常,当 RPC 服务长时间宕机时客户端该如何应对。

当有疑问的时候可以先避免使用 RPC。如果可以的话,考虑使用异步管道 - 而不是类似 RPC 的阻塞,其会将结果以异步的方式推送到下一个计算阶段。

回调队列#

一般来讲,基于 RabbitMQ 进行 RPC 通信是非常简单的,客户端发送一个请求消息,然后服务端用一个响应消息作为应答。为了能接收到响应,我们需要在发送请求过程中指定一个’callback’队列地址。

var props = channel.CreateBasicProperties();
props.ReplyTo = replyQueueName;

var messageBytes = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: "",
                     routingKey: "rpc_queue",
                     basicProperties: props,
                     body: messageBytes);


消息属性

AMQP 0-9-1 协议在消息中预定义了一个包含 14 个属性的集合,大多数属性很少使用,但以下情况除外:
Persistent:将消息标记为持久的(值为2)或者瞬时的(其他值),可以参考 教程[2]
DeliveryMode:熟悉 AMQP 协议的人可以选择此属性而不是熟悉协议的人可以选择使用此属性而不是Persistent,它们控制的东西是一样的。
ContentType:用于描述编码的 mime 类型。例如,对于经常使用的 JSON 编码,将此属性设置为:application/json是一种很好的做法。
ReplyTo:通常用于命名回调队列。
CorrelationId:用于将 RPC 响应与请求相关联。

关联ID#

在上面介绍的方法中,我们建议为每个 RPC 请求创建一个回调队列,但是这种方式效率低。幸运的是我们有一种更好的方式,那就是为每个客户端创建一个独立的回调队列。

这种方式会引出一个新的问题,在收到响应的回调队列中,它无法区分响应属于哪一个请求,此时便是CorrelationId属性的所用之处。我们将为每个请求的CorrelationId设置一个唯一值。之后当我们在回调队列接收到响应的时候,再去检查下这个属性是否和请求中的值匹配,如此一来,我们就可以把响应和请求关联起来了。如果出现一个未知的CorrelationId值,我们可以安全的销毁这个消息,因为这个消息不属于我们的请求。

你可能会问,为什么我们应该忽略回调队列中的未知的消息,而不是用错误来标识失败呢?这是因为于服务器端可能存在竞争条件。虽然不太可能,但是 RPC 服务器可能在仅发送了响应消息而未发送消息确认的情况下挂掉,如果出现这种情况,RPC 服务器重启之后将会重新处理该请求。这就是为什么在客户端上我们必须优雅地处理重复的响应,并且理想情况下 RPC 应该是幂等的。

总结#

我们的 RPC 会是这样工作:

  • 客户端启动时,会创建一个匿名的独占回调队列。
  • 对于 RPC 请求,客户端发送带有两个属性的消息:ReplyTo(设置为回调队列)和CorrelationId(为每个请求设置唯一值)。
  • 请求被发送到rpc_queue队列。
  • RPC 工作线程(或者叫:服务器)正在等待该队列上的请求。当出现请求时,它会执行该作业,并使用ReplyTo属性设置的队列将带有结果的消息发送回客户端。
  • 客户端等待回调队列上的数据。出现消息时,它会检查CorrelationId属性。如果它与请求中的值匹配,则返回对应用程序的响应。

组合在一起#

斐波纳契 任务:

private static int fib(int n)
{
    if (n == 0 || n == 1) return n;
    return fib(n - 1) + fib(n - 2);
}

我们宣布我们的斐波那契函数。并假定只允许有效的正整数输入。 (不要期望这个适用于大数字,它可能是最慢的递归实现)。

我们的 RPC 服务端代码 RPCServer.cs 看起来如下所示:

using System;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;

class RPCServer
{
    public static void Main()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using (var connection = factory.CreateConnection())
        using (var channel = connection.CreateModel())
        {
            channel.QueueDeclare(queue: "rpc_queue", durable: false,
              exclusive: false, autoDelete: false, arguments: null);
            channel.BasicQos(0, 1, false);
            var consumer = new EventingBasicConsumer(channel);
            channel.BasicConsume(queue: "rpc_queue",
              autoAck: false, consumer: consumer);
            Console.WriteLine(" [x] Awaiting RPC requests");

            consumer.Received += (model, ea) =>
            {
                string response = null;

                var body = ea.Body;
                var props = ea.BasicProperties;
                var replyProps = channel.CreateBasicProperties();
                replyProps.CorrelationId = props.CorrelationId;

                try
                {
                    var message = Encoding.UTF8.GetString(body);
                    int n = int.Parse(message);
                    Console.WriteLine(" [.] fib({0})", message);
                    response = fib(n).ToString();
                }
                catch (Exception e)
                {
                    Console.WriteLine(" [.] " + e.Message);
                    response = "";
                }
                finally
                {
                    var responseBytes = Encoding.UTF8.GetBytes(response);
                    channel.BasicPublish(exchange: "", routingKey: props.ReplyTo,
                      basicProperties: replyProps, body: responseBytes);
                    channel.BasicAck(deliveryTag: ea.DeliveryTag,
                      multiple: false);
                }
            };

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }







​ private static int fib(int n)
​ {
​ if (n == 0 || n == 1)
​ {
​ return n;
​ }

​ return fib(n - 1) + fib(n - 2);
​ }
​ }

服务端代码非常简单:

  • 像往常一样,首先建立连接,通道和声明队列。
  • 我们可能希望运行多个服务器进程。为了在多个服务器上平均分配负载,我们需要设置channel.BasicQos中的prefetchCount值。
  • 使用BasicConsume访问队列,然后注册一个交付处理程序,并在其中完成工作并发回响应。

我们的 RPC 客户端 RPCClient.cs 代码:

using System;
using System.Collections.Concurrent;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

public class RpcClient
{
    private readonly IConnection connection;
    private readonly IModel channel;
    private readonly string replyQueueName;
    private readonly EventingBasicConsumer consumer;
    private readonly BlockingCollection<string> respQueue = new BlockingCollection<string>();
    private readonly IBasicProperties props;

public RpcClient()
{
        var factory = new ConnectionFactory() { HostName = "localhost" };

        connection = factory.CreateConnection();
        channel = connection.CreateModel();
        replyQueueName = channel.QueueDeclare().QueueName;
        consumer = new EventingBasicConsumer(channel);

        props = channel.CreateBasicProperties();
        var correlationId = Guid.NewGuid().ToString();
        props.CorrelationId = correlationId;
        props.ReplyTo = replyQueueName;

        consumer.Received += (model, ea) =>
        {
            var body = ea.Body;
            var response = Encoding.UTF8.GetString(body);
            if (ea.BasicProperties.CorrelationId == correlationId)
            {
                respQueue.Add(response);
            }
        };
    }

    public string Call(string message)
    {
        var messageBytes = Encoding.UTF8.GetBytes(message);
        channel.BasicPublish(
            exchange: "",
            routingKey: "rpc_queue",
            basicProperties: props,
            body: messageBytes);

        channel.BasicConsume(
            consumer: consumer,
            queue: replyQueueName,
            autoAck: true);

        return respQueue.Take(); ;
    }

    public void Close()
    {
        connection.Close();
    }
}

public class Rpc
{
    public static void Main()
    {
        var rpcClient = new RpcClient();

        Console.WriteLine(" [x] Requesting fib(30)");
        var response = rpcClient.Call("30");

        Console.WriteLine(" [.] Got '{0}'", response);
        rpcClient.Close();
    }
}

客户端代码稍微复杂一些:

  • 建立连接和通道,并为响应声明一个独有的 ‘callback’ 队列。
  • 订阅这个 ‘callback’ 队列,以便可以接收到 RPC 响应。
  • Call方法用来生成实际的 RPC 请求。
  • 在这里,我们首先生成一个唯一的CorrelationId编号并保存它,while 循环会使用该值来捕获匹配的响应。
  • 接下来,我们发布请求消息,其中包含两个属性:ReplyToCorrelationId
  • 此时,我们可以坐下来稍微一等,直到指定的响应到来。
  • while 循环做的工作非常简单,对于每个响应消息,它都会检查CorrelationId是否是我们正在寻找的那一个。如果是这样,它就会保存该响应。
  • 最后,我们将响应返回给用户。

客户发出请求:

var rpcClient = new RPCClient();

Console.WriteLine(" [x] Requesting fib(30)");
var response = rpcClient.Call("30");
Console.WriteLine(" [.] Got '{0}'", response);

rpcClient.Close();

现在是查看 RPCClient.csRPCServer.cs 的完整示例源代码(包括基本异常处理)的好时机哦。

像往常一样设置(请参见 教程[1]):

我们的 RPC 服务现已准备就绪,现在可以启动服务端:

cd RPCServer
dotnet run

要请求斐波纳契数,请运行客户端:

cd RPCClient
dotnet run

这里介绍的设计并不是 RPC 服务的唯一可能实现,但它仍具有一些重要优势:

  • 如果 RPC 服务器太慢,您可以通过运行另一个服务器来扩展。尝试在新开一个控制台,运行第二个 RPCServer。
  • 在客户端,RPC 只需要发送和接收一条消息。不需要像QueueDeclare一样同步调用。因此,对于单个 RPC 请求,RPC 客户端只需要一次网络往返。

我们的代码很简单,也并没有尝试去解决更复杂(但很重要)的问题,比如就像:

  • 如果服务端没有运行,客户端应该如何反应?
  • 客户端是否应该为 RPC 设置某种超时机制?
  • 如果服务端出现故障并引发异常,是否应将其转发给客户端?
  • 在处理之前防止无效的传入消息(例如:检查边界、类型)。

如果您想进行实验,您可能会发现 管理 UI 对于查看队列非常有用。

写在最后#

本文翻译自 RabbitMQ 官方教程 C# 版本。如本文介绍内容与官方有所出入,请以官方最新内容为准。水平有限,翻译的不好请见谅,如有翻译错误还请指正。

先决条件
本教程假定 RabbitMQ 已经安装,并运行在localhost 标准端口(5672)。如果你使用不同的主机、端口或证书,则需要调整连接设置。

从哪里获得帮助
如果您在阅读本教程时遇到困难,可以通过邮件列表 联系我们

路由#

(使用.NET客户端)

教程[3] 中,我们构建了一个简单的日志系统,可以向多个接收者广播消息。

在本教程中,我们会为日志系统再添加一个特性,使其可以只订阅消息的一个子集。例如,将所有日志消息打印到
控制台,同时只会将严重错误消息写入日志文件(保存到磁盘空间)。

绑定#

在前面的例子中,我们创建过_绑定_。不知道您是否还记得下面的代码:

channel.QueueBind(queue: queueName,
                  exchange: "logs",
                  routingKey: "");

绑定是指交换器和队列之间的关联关系。可以简单地理解为:某个队列对来自此交换器的消息感兴趣。

绑定可以采用额外的routingKey参数,为了避免与BasicPublish方法中相同参数混淆,我们将其称为binding key(这里是指路由键从声明角度的一种别称,绑定键)。下面即是如何使用绑定键 建立一个绑定:

channel.QueueBind(queue: queueName,
                  exchange: "direct_logs",
                  routingKey: "black");

绑定键的含义取决于交换器类型。像我们前面使用的fanout 交换器,忽略了它的值(依据fanout交换器的特性,它会把消息广播到所有订阅的队列,所以就算指定routingKey也不会根据其过滤消息)。

Direct交换器#

在上篇教程中,我们的日志系统会把所有消息广播给所有消费者,现在我们想要扩展使其可以根据消息的严重性过滤消息。例如,我们希望将日志消息写入磁盘的脚本仅接收严重错误的消息,而不是在警告或者信息类型的消息上浪费磁盘空间。

之前我们使用的是fanout交换器,它没有给我们足够的灵活性 - 它只能进行无意识的广播。

现在我们要用direct交换器替换它,direct交换器背后的路由算法很简单 - 消息会进入其binding key恰好与routing key相匹配的队列。
为了说明这一点,请参考以下设置:

在上面的设置中,我们可以看到direct交换器X与两个队列绑定。第一个队列通过键orange绑定,第二个队列有两个绑定,一个通过键black绑定、另外一个通过键green绑定。

如此设置,发布使用路由键orange的消息到交换器最终会被路由到队列Q1,路由键为blackgreen的消息会去向队列Q2,而其他所有的消息会被丢弃。

多重绑定#

使用相同的绑定键绑定多个队列是完全合法的。在示例中,我们可以在XQ1之间添加一个键为black的绑定。这种情况下,direct交换器会像fanout交换器一样,把消息广播到所有匹配的队列,路由键为black的消息会被分别发送到队列Q1Q2

发送日志#

我们将在日志系统中使用上述消息模型,在发送消息时使用direct交换机来替换fanout交换器。同时我们会把日志的严重性作为路由键,这样的话,接收脚本就可以选择性地接收它期望严重性的消息。首先我们来关注如何发送日志。

同样地,我们需要先创建一个交换器:

channel.ExchangeDeclare(exchange: "direct_logs", type: ExchangeType.Direct);

准备好发送消息:

var body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: "direct_logs",
                     routingKey: severity,
                     basicProperties: null,
                     body: body);

简单起见,我们先假定severity可以是infowarningerror任意一值。

订阅#

马上就可以像前面的教程接收消息了,但有一点不同, 我们需要为我们感兴趣的每种日志严重性级别的消息建立一个新的绑定。

var queueName = channel.QueueDeclare().QueueName;

foreach(var severity in args)
{
    channel.QueueBind(queue: queueName,
                      exchange: "direct_logs",
                      routingKey: severity);
}

组合在一起#

EmitLogDirect.cs类的代码:

using System;
using System.Linq;
using RabbitMQ.Client;
using System.Text;

class EmitLogDirect
{
    public static void Main(string[] args)
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using(var connection = factory.CreateConnection())
        using(var channel = connection.CreateModel())
        {
            channel.ExchangeDeclare(exchange: "direct_logs",
                                    type: "direct");

            var severity = (args.Length > 0) ? args[0] : "info";
            var message = (args.Length > 1)
                          ? string.Join(" ", args.Skip(1).ToArray())
                          : "Hello World!";
            var body = Encoding.UTF8.GetBytes(message);
            channel.BasicPublish(exchange: "direct_logs",
                                 routingKey: severity,
                                 basicProperties: null,
                                 body: body);
            Console.WriteLine(" [x] Sent '{0}':'{1}'", severity, message);
        }

        Console.WriteLine(" Press [enter] to exit.");
        Console.ReadLine();
    }
}

ReceiveLogsDirect.cs类的代码:

using System;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;

class ReceiveLogsDirect
{
    public static void Main(string[] args)
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using(var connection = factory.CreateConnection())
        using(var channel = connection.CreateModel())
        {
            channel.ExchangeDeclare(exchange: "direct_logs",
                                    type: "direct");
            var queueName = channel.QueueDeclare().QueueName;

            if(args.Length < 1)
            {
                Console.Error.WriteLine("Usage: {0} [info] [warning] [error]",
                                        Environment.GetCommandLineArgs()[0]);
                Console.WriteLine(" Press [enter] to exit.");
                Console.ReadLine();
                Environment.ExitCode = 1;
                return;
            }

            foreach(var severity in args)
            {
                channel.QueueBind(queue: queueName,
                                  exchange: "direct_logs",
                                  routingKey: severity);
            }

            Console.WriteLine(" [*] Waiting for messages.");

            var consumer = new EventingBasicConsumer(channel);
            consumer.Received += (model, ea) =>
            {
                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);
                var routingKey = ea.RoutingKey;
                Console.WriteLine(" [x] Received '{0}':'{1}'",
                                  routingKey, message);
            };
            channel.BasicConsume(queue: queueName,
                                 autoAck: true,
                                 consumer: consumer);

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }
}

请像往常一样创建项目(请参阅 教程[1])。

如果您想将warningerror(不包括info)日志消息保存到文件,只需打开控制台并输入:

cd ReceiveLogsDirect
dotnet run warning error > logs_from_rabbit.log

如果您想在屏幕上看到所有日志消息,请打开一个新终端并执行以下操作:

cd ReceiveLogsDirect
dotnet run info warning error

例如,想要发出error日志消息,只需要输入:

cd EmitLogDirect
dotnet run error "Run. Run. Or it will explode."

EmitLogDirect.csReceiveLogsDirect.cs 的完整源代码。

跳转到 教程[5],了解如何基于模式监听消息。

写在最后#

本文翻译自 RabbitMQ 官方教程 C# 版本。如本文介绍内容与官方有所出入,请以官方最新内容为准。水平有限,翻译的不好请见谅,如有翻译错误还请指正。

  • 原文链接:RabbitMQ tutorial - Routing
  • 实验环境:RabbitMQ 3.7.4 、.NET Core 2.1.3、Visual Studio Code
  • 最后更新:2018-08-31
0%