一、Tomcat下载【备注】前提是必须安装好了对应JDK版本。例如OpenJDK 19,或者其他版本。1.1 下载指南【步骤1】下载tomcat,由于我本地用的是JDK19,所以我下载新一点的tomcat版本进行使用,例如Tomcat 10版本。【步骤2】如果不知道自己要下载哪个版本,可以找到一个 README 的链接,点击进去进行查看。【步骤3】检查版本。例如此处10版本要求JDK11或以上版本,那JDK19版本就妥妥够用了。【步骤4】返回回去,下对应系统的版本,例如我本地是64位的,那就下载64位的这个压缩包文件。附件:Tomcat10.1.5版本二、Tomcat安装与运行2.1 安装直接对压缩包解压到自定义的目录即可。2.2 运行与验证【步骤1】找上图中bin目录下面的startup.bat文件双击运行,不报错,正常运行就表示成功。【步骤2】访问tomcat服务器:访问localhost:8080,显示如下页面就代表成功。【说明】tomcat默认会把日志写入到 logs文件夹下,例如启动的日志,也可以看到。控制台由于编码格式问题,所以乱码,文本里面就可以清晰看到具体的显示内容了。【说明】webapps是用于部署静态页面的地方。【步骤3】静态文件验证:如果需要部署静态页面不能直接放到该目录下,必须提供一个新的文件夹进行。例如新建一个html文件。【步骤4】必须放在一个子文件夹下面,例如此处我把它放到wesky文件夹下。【步骤5】重启tomcat服务,进行访问。访问地址为tomcat服务启动的ip:端口号/子文件夹/具体页面名称。【说明】验证直接存放在webapps文件夹下,而不创建子文件夹进行存放的效果。结果是会无法访问。三、IDEA创建JavaWeb项目3.1 创建支持多个项目的解决方案【步骤1】新建一个空项目【步骤2】再创建一个新模块。【步骤3】对新模块进行命名等基础配置。例如名字叫 ServletDemo【说明】新建以后的结构如图所示,是个空的项目,此处仅用于展示使用,等下用来和新建的web项目做对比。【步骤4】新建一个模块,选择外部库,然后再通过新建新模块,选择Java EE或Java Enterprise里面的Web Application项目。不过此处IDEA不存在Java Web项目模板有关,那就先创建一个空的模块备用。此时,就可以在同一个窗口上面,存在两个不同的Java项目了。3.2 IDEA添加对Java EE/Java Enterprise 项目的支持【步骤1】先创建空的模块,这个步骤可以参考上面章节部分。【步骤2】在上面创建的空模块项目的地方,选中右键,添加框架支持(Add Framework Support……)【步骤3】选择Java EE下面的Web Application【说明】创建成功,在项目的路径下自动有Web相关的文件了。【说明】移除不用的模块:为了后面好区分,这儿先把另一个模块移除掉。选中项目->右键->移除模块 即可。四、IDEA集成本地Tomcat服务器【步骤1】把上面测试tomcat的html页面,拷贝到项目的web目录下备用。web目录下用于存放所有的动态或静态页面。这儿用来做后续的测试。【步骤2】选择当前项目,在上方锤子图标旁边,选择编辑配置 进行打开配置页面。或者也可以在 工具栏的 运行-> 编辑配置 进行打开。【步骤3】在配置页面里面,点击 新增(+符号)【步骤4】选择本地的Tomcat服务器【步骤5】添加本地的tomcat服务器目录(选择主目录)【说明】选择目录成功以后,会自动识别出服务器的版本号。【步骤6】修改Jre版本,例如OpenJDK 19;修改端口号,默认的端口号是8080,改为18080(或者其他自定义的端口号);JMX端口,默认是1099,也改为其他的,例如11099.【步骤7】然后切换到部署(Deployment)页面,新建一个工件。【步骤8】修改应用上下文(Application Context)的名称,把自动生成的后缀删掉。【步骤9】配置完毕以后,点击应用,再点击确定。【说明】在工具栏上,锤子图标旁边,现在就有我们上面新增的Tomcat服务器信息了。【步骤10】运行程序,可以看到程序成功运行,并且tomcat服务以18080端口正常启动起来了。如果要使用默认端口,那本地上原先启用的tomcat需要先关闭,否则可能会导致端口冲突。【步骤11】在自动打开的页面上面,给它导个航到hello页面,并且打开了hello页面进行展示,说明程序是OK的。
一、Java环境安装1.1 下载openjdk环境安装包可以进华为镜像站进行下载。参考链接:repo.huaweicloud.com/openjdk/1.2 配置Java环境解压缩openjdk到任意路径,建议路径不要有中文。然后把路径的bin文件,配置到系统的环境变量Path里面去。把解压缩的open jdk的bin目录,添加到【系统变量】的环境变量Path里面去。 打开cmd,输入java --version查看版本,提示版本正确,说明JDK环境配置OK二、IDEA 社区版安装与插件配置2.1 下载IDEA社区版下载地址:www.jetbrains.com.cn/idea/downlo…选择Community版本进行下载2.2 社区版IDEA推荐插件配置IDEA社区版安装时候,默认都是下一步。如果遇到勾选的情况,那就全部勾上,以防万一。打开IDEA编辑器,选择左边的Plugins:或者在Settings里面,选择Plugins:开始搜索一些常用的插件备用。2.2.1 Smart Tomcat这是针对Tomcat服务器的插件。SmartTomcat将从项目和模块中自动加载Webapp类和库,无需将类和库复制到WEB-INF/classes和WEB-INF/lib。 Smart Tomcat插件将自动配置tomcat服务器的类路径。安装方式:搜索插件名称,选择对应的插件,进行安装。如果有第三方插件风险提示,选择接受即可。2.2.2 Database Navigator该产品为IntelliJ IDEA开发环境和相关产品增加了广泛的数据库开发和维护功能。它提供了高级数据库连接管理、脚本执行支持、数据库对象浏览、数据和代码编辑器、数据库编译器操作支持、方法执行和调试支持、数据库对象工厂以及所有组件之间的各种导航功能。2.2.3 汉化包 如果英文水平还可以的,就不需要这个包了。2.2.4 Spring Assistant由于该插件在2022上面暂时不支持,所以我们通过本地引入插件的形式进行导入插件。插件下载地址:github.com/ErickPang/i…下载到本地以后,在插件配置栏,点击工具按钮,选择 从本地磁盘安装插件,如图:引入以后:2.2.5 tabnine前身是Codota 这个插件,用于智能代码补全。2.2.6 Rainbow Brackets可以实现彩虹括号效果。能帮你快速定位到代码块中的上下文,突出显示。2.2.7 Maven Helper查看和管理maven依赖的插件,可以展示pom.xml文件中的依赖以列表或树的形式,该插件可以很直观地帮你分析和排除冲突依赖.2.2.8 RestfulTool一套 Restful 服务开发辅助工具集。2.2.9 Translation一款翻译插件.2.2.10 SequenceDiagramSequenceDiagram 插件可以根据代码调用链路自动生成时序图.2.2.11 MybatisXMybatisX 是一款基于 IDEA 的快速开发插件,为效率而生,支持Java 与 XML 调回跳转和Mapper 方法自动生成 XML.2.2.12 Mybatis Log将SQL语句操作日志转换为可执行SQL语句.2.2.13 Json Helper该插件提供了一个易于使用的工具窗口,可以直接在IDE中执行JSON字符串操作,不用再打开网站格式化JSON.2.2.14 GsonFormatPlusJson转Java实体类,该插件可以加快开发进度.2.2.15 Key Promoter X快捷键提示。在一些你本来可以使用快捷键操作的地方,提醒你用快捷键操作,并有历史记录。2.2.16 Alibaba Cloud ToolkitAlibaba Cloud Toolkit(后文简称Cloud Toolkit)可以帮助开发者更高效地部署、测试、开发和诊断应用。官网地址:www.aliyun.com/product/clo…2.2.17 CodeGlance显示代码缩略图插件2.2.18 SonarLint 代码质量检查插件,帮助我们提升代码质量.2.2.19 Save Actions可以帮忙我们优化包导入,自动给没有修改的变量添加final修饰符,调用方法的时候自动添加this关键字等,使我们的代码更规范统一.2.2.20 重启IDE重启IDE用于插件的生效,效果如图所示,也可以看到中文插件生效了,变成中文。三、IDEA一些可选设置3.1 代码快捷模板只要输入apr ,就能自动提示,并且生成Autowired 语句了。可以根据自己的代码习惯,自定义一些代码模板,帮助我们快速写代码。3.2 取消tab页单行显示3.3 双斜杠注释改成紧跟代码头3.4 取消匹配大小写取消勾选后,输入小写 s ,也能提示出 String3.5 优化版本控制的目录颜色展示3.6 显示行号和方法分割线
前言:前不久微信上大家玩ChatGPT聊天机器人玩的不亦乐乎;不过随着ChatGPT被封杀,所以用微信聊天机器人有可能导致封号的风险。那如果自己不想每次都去OpenAI官网上进行对话【PS:官网上面聊天对话有局限性,例如回复的内容比较长,AI回答是一个一个字写的,就可能导致超过一定时间以后,变成请求超时之类的异常;而通过API直接访问,可以避免这个情况发生】,想要自己搭建一个服务来本地调用,是不是也可以?于是,找了官方的一些资料,就动手咱们自己搭建一个服务端,方便给别人调用来提供服务。 好了,接下来开始我们自己的表演。1、新建一个webapi服务程序 2、我选择的是.NET6,大家也可以自己选择自己喜欢的环境,问题都不大。为了方便阅读,我选择了使用控制器和启用OpenAPI支持(swagger)。 3、创建好以后,在program里面,添加HttpClient服务的注册,用来访问openai的api会用到。4、新建一个控制器,就叫 RobotController,用来提供webapi接口做测试使用。5、在新建的控制器里面,做点最初的准备,例如对IHttpClientFactory注入进来备用。6、官网上有一些资料,例如text-davinci-003模型的最大tokens是4000,所以后面有个请求参数的部分,不能超过这个数。 7、这个是一个参数建议,把temperature设为0.9f,把top_p设为1. 按照文档的解释,temperature的值会影响回答的内容的一些特性,例如可能设计不友好的回复的内容的比重等。8、我们做个通用的请求实体类,用来当作访问我们提供的webapi接口所需的参数信息。三个信息比较重要的,当作参数,可以进行微调,例如文档建议的temp为0.9f,max tokens最大为4000,我们可以设为其他的值进行微调,等等。而message字段就是我们本身的请求参数,用来和机器人对话使用的。9、然后是返回体,这个格式是解析openai的返回值进行配置的,大家也可以随意参考,或者不做解析直接返回字符串也是OK的,反正是一串Json数据,问题不大。10、然后对刚才的Call方法做个完善,大体内容如下所示。其中,openaiKey是我个人的key,所以为了隐私泄露,我稍微打了马赛克,希望理解。其他代码内容,可以直接看截图代码。11、最后,启动服务程序,进入到swagger里面进行调用接口。例如,message字段我传的 “帮我写一个C#版本的Hello World”,得到的返回值体里面,位于choises[]数组的第一条数据,text就是机器人回复的内容。12、以上只是一个简单的写法,大家可以根据自己需要进行拓展或者改造。比如说,用已有的key和规律,写个其他语言的聊天服务,或者写个聊天客户端进行访问,等等,一切皆有可能。或者微信被封杀了,那就可以尝试自己搭建一个服务来间接继续实现某些APP的智能聊天服务等等。
前言:大概一年多前写过一个部署ELK系列的博客文章,前不久刚好在部署一个ELK的解决方案,我顺便就把一些基础的部分拎出来,再整合成一期文章。大概内容包括:搭建ELK集群,以及写一个简单的MQ服务。如果需要看一年多之前写的文章,可以详见下列文章链接(例如部署成Windows服务、配置浏览器插件、logstash接收消费者数据等,该篇文章不再重复描述,可以点击下方链接自行参考):ElasticSearch、head-master、Kibana环境搭建:https://www.cnblogs.com/weskynet/p/14853232.html给ElasticSearch添加SQL插件和浏览器插件:https://www.cnblogs.com/weskynet/p/14864888.html使用Logstash通过Rabbitmq接收Serilog日志到ES:https://www.cnblogs.com/weskynet/p/14952649.html使用nssm工具将ES、Kibana、Logstash或者其他.bat文件部署为Windows后台服务的方法:https://www.cnblogs.com/weskynet/p/14961565.html 安装包目录将有关环境解压备用。如图:说明:集群环境下,使用4 个elasticsearch 数据库。其中三个为master,分别可用于选举主节点、存储数据使用;另外一个为client,仅作为es 的负载均衡使用。如果服务器配置环境太低的情况下,client 也可以不需要,不会产生影响;如果访问会比较频繁,建议可以加上。以下配置操作,均以使用3+1 的集群策略进行配置。各个环境/安装包说明Elasticsearch:主要用来存储数据的一个非关系型文档数据库,基于lucene 内核的搜索引擎,可以实现快速搜索数据的目的。一般用来存储日志数据比较常见。Logstash:用于收集数据的管道, 例如此处用来收集消息队列的数据进行转储到elasticsearch 里面。Kibana:一个可视化的搜索引擎组件,用来查询elasticsearch 的数据和展示使用。OpenJDK:非官方版本的JDK 环境,此处使用的是华为镜像下的定制过的open JDK 19 版本。Elasticsearch-analisis-ik: 一个中文分词工具,用于搜索引擎内搜索词汇时候,可以对词汇进行归集处理,而不至于导致搜索出来的结果是模糊查询。例如: 如果没有分词,输入“大佬”进行查询,会把有“大”和“佬”的结果都查询出来。而实际上我们需要查的是“大佬”这个词。IK 分词的目的就是这个作用。Elasticsearch-sql: 一款用于提供SQL 语句进行查询的工具。Sqlsite:一款浏览器插件,用来集成到谷歌内核的浏览器上,可以通过sql 语句进行查询es 的数据集。Otp_win64_xx : rabbitmq 环境的erlang 语言包环境,安装rabbitmq 服务之前,需要先安装该包Rabbitmq-server: rabbitmq 安装包JDK 环境配置把解压缩的open jdk 的bin 目录,添加到【系统变量】的环境变量Path 里面去。 打开cmd,输入java --version 查看版本,提示版本正确,说明JDK 环境配置OK RabbitMQ 环境配置安装Otp 语言包:全部默认下一步到头,简单粗暴。如果有提示确认的,勾选确认即可。 安装RabbitMQ 服务安装包:同上,直接下一步到底,完成。 备注:安装rabbitmq 对应的服务器主机,服务器【主机的名称】和【用户名称】不能有中文字符,不能有中文字符,不能有中文字符,重要的事情说三遍,我年轻时候踩过坑。配置环境变量。先新增一个ERLANG_HOME,路径是安装erlang的目录: 环境变量的Path 里面新增ERLANG_HOME 的bin 目录: 新增RabbitMQ 服务的变量RABBITMQ_SERVER,值是rabbitmq 服务的安装目录:把rabbitmq 的sbin 目录,加入到path 环境变量里面去: 打开cmd,输入 rabbitmq-plugins enable rabbitmq_management 进行安装RabbitMQ-Plugins 的插件: 设置好以后,打开浏览器,在服务器上输入访问http://127.0.0.1:15672使用guest 账号进行登录 进去以后,找到Admin 菜单,新增一个用户。此处用户默认为admin,密码默认为admin。也可以设为其他的,设为其他的。最后点击set 旁边的Admin,再点击左下角的Add User进行添加一个我们的用户。(不建议使用guest 用户直接做消息队列的处理,可能有安全风险)默认添加完成以后,是没有权限的,需要进一步进行添加权限。点击表格下的Name字段,会自动进入到设置权限页面。 啥也不干,直接点下方第一个按钮【Set permission】即可 返回Admin 菜单下,可以看到admin 用户的黄色底色已经没有了,并且access 权限也变成了/ Elasticsearch 配置文件elasticsearch.yml 说明【以下内容摘自网络,如果不想科普可以跳过该部分】cluster.name: elasticsearch配置的集群名称,默认是elasticsearch,es 服务会通过广播方式自动连接在同一网段下的es服务,通过多播方式进行通信,同一网段下可以有多个集群,通过集群名称这个属性来区分不同的集群。node.name: "node-01"当前配置所在机器的节点名,你不设置就默认随机指定一个name 列表中名字,该name 列表在es 的jar 包中config 文件夹里name.txt 文件中,其中有很多作者添加的有趣名字。当创建ES 集群时,保证同一集群中的cluster.name 名称是相同的,node.name 节点名称是不同的;node.master: true指定该节点是否有资格被选举成为node(注意这里只是设置成有资格, 不代表该node 一定就是master),默认是true,es 是默认集群中的第一台机器为master,如果这台机挂了就会重新选举master。node.data: true指定该节点是否存储索引数据,默认为true。index.number_of_shards: 5设置默认索引分片个数,默认为5 片。index.number_of_replicas: 1设置默认索引副本个数,默认为1 个副本。如果采用默认设置,而你集群只配置了一台机器,那么集群的健康度为yellow,也就是所有的数据都是可用的,但是某些复制没有被分配(健康度可用curl 'localhost:9200/_cat/health?v' 查看, 分为绿色、黄色或红色。绿色代表一切正常,集群功能齐全,黄色意味着所有的数据都是可用的,但是某些复制没有被分配,红色则代表因为某些原因,某些数据不可用)path.conf: /path/to/conf设置配置文件的存储路径,默认是es 根目录下的config 文件夹。path.data: /path/to/data设置索引数据的存储路径,默认是es 根目录下的data 文件夹,可以设置多个存储路径,用逗号隔开,例:path.data: /path/to/data1,/path/to/data2path.work: /path/to/work设置临时文件的存储路径,默认是es 根目录下的work 文件夹。path.logs: /path/to/logs设置日志文件的存储路径,默认是es 根目录下的logs 文件夹path.plugins: /path/to/plugins设置插件的存放路径,默认是es 根目录下的plugins 文件夹, 插件在es 里面普遍使用,用来增强原系统核心功能。bootstrap.mlockall: true设置为true 来锁住内存不进行swapping。因为当jvm 开始swapping 时es 的效率会降低,所以要保证它不swap,可以把ES_MIN_MEM 和ES_MAX_MEM 两个环境变量设置成同一个值,并且保证机器有足够的内存分配给es。同时也要允许elasticsearch 的进程可以锁住内存,linux 下启动es 之前可以通过`ulimit -l unlimited`命令设置。network.bind_host: 192.168.0.1设置绑定的ip 地址,可以是ipv4 或ipv6 的,默认为0.0.0.0,绑定这台机器的任何一个ip。network.publish_host: 192.168.0.1设置其它节点和该节点交互的ip 地址,如果不设置它会自动判断,值必须是个真实的ip 地址。(可以用不配)network.host: 192.168.0.1这个参数是用来同时设置bind_host 和publish_host 上面两个二手手机参数。(低版本时配置0.0.0.0,不然启动会报错。1.7.1 和1.3.1 版本亲测)transport.tcp.port: 9300设置节点之间交互的tcp 端口,默认是9300。如我搭建多节点,我的配置分别是9300、9302、9304transport.tcp.compress: true设置是否压缩tcp 传输时的数据,默认为false,不压缩。http.port: 9200设置对外服务的http 端口,默认为9200。http.max_content_length: 100mb设置内容的最大容量,默认100mbhttp.enabled: false是否使用http 协议对外提供服务,默认为true,开启。gateway.type: localgateway 的类型,默认为local 即为本地文件系统,可以设置为本地文件系统,分布式文件系统,hadoop 的HDFS,和amazon 的s3 服务器等。gateway.recover_after_nodes: 1设置集群中N 个节点启动时进行数据恢复,默认为1。gateway.recover_after_time: 5m设置初始化数据恢复进程的超时时间,默认是5 分钟。gateway.expected_nodes: 2设置这个集群中节点的数量,默认为2,一旦这N 个节点启动,就会立即进行数据恢复。cluster.routing.allocation.node_initial_primaries_recoveries: 4初始化数据恢复时,并发恢复线程的个数,默认为4。cluster.routing.allocation.node_concurrent_recoveries: 2添加删除节点或负载均衡时并发恢复线程的个数,默认为4。indices.recovery.max_size_per_sec: 0设置数据恢复时限制的带宽,如入100mb,默认为0,即无限制。indices.recovery.concurrent_streams: 5设置这个参数来限制从其它分片恢复数据时最大同时打开并发流的个数,默认为5。discovery.zen.minimum_master_nodes: 1设置这个参数来保证集群中的节点可以知道其它N 个有master 资格的节点。默认为1,对于大的集群来说,可以设置大一点的值(2-4)discovery.zen.ping.timeout: 3s设置集群中自动发现其它节点时ping 连接超时时间,默认为3 秒,对于比较差的网络环境可以高点的值来防止自动发现时出错。 discovery.zen.ping.multicast.enabled: false设置是否打开多播发现节点,默认是true。discovery.zen.ping.unicast.hosts: ["host1", "host2:port", "host3[portX-portY]"]设置集群中master 节点的初始列表,可以通过这些节点来自动发现新加入集群的节点。例如:discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300","127.0.0.1:9302","127.0.0.1:9304"] 配置了三个节点 ES 集群配置文件配置集群配置,采用3+1 的部署方案,包括3 个可选主节点,以及1 个客户端节点。其中:cluster.name 的值保持一致node.name 的值,可自定义,此处默认可以成为主节点的节点名称为master1 master2和master3path.data 和path.log 用来存储es 节点存储的数据和自身日志的路径使用。path.data 的值不能一样,不同的节点,存储的地址需要分开,否则会报错。data 路径用于存放我们发送给es 的数据所存储的路径位置。http.port 默认为9200,用于外部访问es 使用;四个节点,端口号此处分别设置为9200、9201、9202、9203。其中9203 端口号用于分配给client 使用。transport.tcp.port 默认为9300,用于内部集群间通信使用;四个节点,端口号此处分别设置为9300,9301,9302,92303。其中9303 用于client 节点使用。discovery.seed_hosts 记录的是这四个集群节点的内部通信地址,我本地局域网内的一台服务器为10.1.11.74,所以我此处为了好分辨,就全部写成10.1.11.74 的地址。生产环境下,可以根据实际情况修改为生产环境的ip 地址。警告:如果es 部署在同一个服务器上,请默认使用127.0.0.1 这个ip,其他ip 可能会受到防火墙限制。例如上面我写的10.1.11.74 的ip,后面就会有集群关连失败的情况。cluster.initial_master_nodes 此处默认主节点设置为master1,当然也可以设为master2或者3,但是不能是client,因为client 不能当做主节点。node.master 和node.data,3+1 集群模式下,【master 节点的值全是true】,【client节点的值全是false】。以下是我先前做的一个master1 的配置文件截图: 以下是master2 的配置文件截图,可以和以上的master1 的配置文件进行比对查看不同点。master3 节点以此类推。 以下是client 节点的配置文件截图,可以和master 节点进行比较差异点。Client 节点不进行主节点的选举,也不进行存储数据,仅用于负载均衡,用于对3 个主要节点进行压力分解的作用使用。 Jvm.options 环境配置Jvm.options 文件在config 文件夹下该路径下用来配置单个ES 的内存分配规则。最低可以配置512m,最高32g 此处以最高和最低内存使用量都是512m 来配置(公司云服务器配置太低,没办法~)。如果是g 为单位,注意都是小写,并且不带b,例如Xms512m Xmx4g 等等。Xms 代表最低分配内存,Xmx 代表最大分配内存。以此类推,把4 个es 的内存都设置一下。警告:以上512m 只是因为我本地的局域网内的服务器上的配置比较低,所以在生产环境下,请配置大一点,例如,一台64GB 内存的服务器,以3+1 部署模式进行部署的情况下,最大可以对每个节点分配(64/2)/5 ≈ 6g 的最大值,其中5 代表的是4 台es 和一台logstash。ELK 的总内存最大占用建议不要超过服务器总内存的一半,留点面子。 通过bat 文件启动es 集群进行初始验证进入到四个es 的根目录下(esxxx/bin),打开cmd,执行elasticsearch.bat 文件:启动以后,浏览器输入localhost:端口号,例如master1 的端口号是9200,启动成功会有一段简单的提示信息,包括节点名称、集群名称,es 版本等等: 四个es 都启动成功以后,浏览器输入http://localhost:9200/_cat/nodes?v可以看到集群内部的活动以及主节点信息。当然,上面的ip 和端口号改成任意集群内的一个节点的地址,都可以查出来。其中master1 为现在的主节点。 做个测试,测试主节点宕机以后的效果。(以下内容仅用于测试观看,可以直接跳过)停止master1 节点,看看情况。 刷新一下页面,可以看到少了一个master1,然后master3 被自动选举为主节点。 重新启动master1 节点,看下效果。如图所示,master1 自动成为了子节点,这个就是集群的魅力,挂了一个一点都不慌。 集成sql 插件和ik 插件在所有的es 的plugins 文件夹下,新建两个文件夹,分别是sql 和ik 把ik 解压后的内容,全部拷贝到各个es 都ik 文件夹下:FromTo:ik 说明:如果有某个关键词汇查不出来,就可以在ik 分词里面进行新增,例如“老吴”。新增说明:在ik 的config 目录下,任意找一个词字典,在最后进行添加有关词汇。把sql 解压后的内容,全部拷贝到各个es 都sql 文件夹下:From: To: 添加完成以后,通过bat 启动时候,可以看到有关的加载信息:logstash 的jvm 配置Logstash 的jvm 配置,同elasticsearch 配置,也需要配置最大内存和最小内存的临界值。此处写的是2g,我改为了512m(自行根据个人服务器配置进行设置)logstash 的config 配置文件设置config 文件用于指定logstash 使用何种方式进行监听数据流的输入和输出功能,当前使用监听RabbitMQ 的方式来监听日志数据流,然后进行转存到ElasticSearch 数据库上。以下截图为预设置的配置文件接收端(config 目录下的rabbitmq.conf 文件),预设三个监听数据来源的队列信息。采用direct 模式,并且指定不同的日志内容走不同的队列,例如我之前项目上使用的WCSLog、DeviceLog、ApiLog 等,配置信息可以当做参考。以下截图配置的是每天新增一个索引,分别以sys、device、api 开头。 注意事项:RabbitMQ 里面必须已经存在以上的队列以后,才可以启动logstash,否则会启动失败。正确做法是,先启动你的程序,初始化一下或者创建一下MQ 有关的队列信息以后,再启动Logstash。 Kinaba 配置打开kibana 根目录下的config 文件夹,下面的kibana.yml 配置文件进行配置。Kibana 配置比较简单,此处只配置三个地方:其中,server.port 是对外开放的端口号,默认是5601,此处改成了15601,防止被人随意登录。然后是es.hosts,有多少es 的节点,就都配上去。还一个是il8n.locale 是默认的语言类型,默认是en 英语,此处我们改为zh-CN 中文。 本博客文章原文地址是:https://www.cnblogs.com/weskynet/p/16890741.html如果在其他地方看到该文章【如果作者不是Wesky或者WeskyNet】,都是盗版链接,请注意~ 快速开发MQ生产者和消费者基础服务新建一个webapi项目进行测试。并添加引用:Wesky.Infrastructure.MQ 其中,这个包是我写的一个比较通用的基于DIRECT模式的简易版MQ生产者和消费者基础功能服务,也可以直接拿来做MQ的业务对接开发。然后,在program.cs里面,添加对WeskyMqService的注册,里面注册了基础的生产者和消费者服务。 咱新增一个消费者消息消费的方法ConsumeMessage以及有关interface接口,该方法后面用来当作回调函数使用,用来传给MQ消费者,当监听到消息以后,会进入到该方法里面。 再然后,回到program里面,对刚才新增的方法进行依赖注入的注册;以及在里面做一个简单的MQ队列的配置信息。 其中,RabbitMqOptions构造函数带有两个参数,分别是RabbitMqConfiguration:它传入MQ的连接RabbitMqMessageQueues[]:路由键以及对应的队列名称数组,有多少个队列,数组元素就是多少个一一对应。以上创建了基础的连接,通过direct模式进行消息订阅和发布;并且创建两个队列,分别是Test1和Test2再然后,新建一个API控制器,并提供有关依赖注入进行实现的验证。 为了方便测试,新增两个api接口,用来检测MQ的连接以及消息发布和订阅。 其中,index此处的作用是咱们配置的消息队列的队列数组里面的下标。连接参数isActive代表是否消费者消费消息,如果不消费消息,那么就可以用来给我们上面的logstash来消费了;如果消费消息,那么消息就会进入到我们刚才创建的ConsumeMessage方法里面去,因为它通过回调函数参数丢进去了:打开MQ面板,可以看到现在是都没有队列存在的。 此时启动程序,先调用连接的API(API里面,包括了生产者连接和消费者连接,平常如果只需要连接一个,也是可以屏蔽其他的,例如与第三方MQ做信息通信的话),再调用发布消息的API,查看效果。咱先做一个不消费(允许消费参数设为false)的例子: 然后再查看MQ面板,可以看到多了两个队列,这两个队列就是代码里面连接以后自动创建的: 调用生产者,发布一条消息,例如发给数组的第一个队列一条消息: 因为不允许消费,所以消息会一直在队列里面没有被消费掉:我们先关闭api程序,然后重新做个连接,做成允许消费的,看下效果: 再切换到MQ面板,可以看到消息立马被消费了: 我们刚才写的一个输出控制台的消费消息的业务方法,此时也被执行了: 以上说明MQ的生产者与消费者服务是OK的了。然后就可以与上面的EKL集群进行配合使用,例如你的程序需要通过MQ的方式给Logstash发送消息,那么就可以使用传入不启用MQ客户端消费的功能来实现;如果需要与其他生产者对接,或者需要做MQ消息消费的业务,就可以通过类似方式,写一个回调函数当作参数丢进MQ消费者服务里面去即可。最后,ELK上面我没做其他的优化,大佬们感兴趣可以自己优化,例如信息的压缩、定时删除等等,这些都可以在Kibana的管理界面里面进行配置。公众号聊天框内回复【ELKQ】可以获取的工具环境、内容等,见下图:
前言:突然想打算把Rust作为将来自己主要的副编程语言。当然,主语言还是C#,毕竟.NET平台这么强大,写起来就是爽。缘起:之前打算一些新的产品或者新的要开发的东西,由于没有历史包袱,就想重新选型一下,在.NET平台(C#语言)、Golang、Rust里面进行选择一个。后面随着多方面的对比,最终打算选择Rust,理由是:卧槽,性能有点6!!!于是,就有了下面这个文章,自己搭建环境时候,以及后续的一些基础的操作、性能比较,写成了这个文章,供大家参考戏谑。废话不多说,直接开撸。以下均是在Windows环境下进行:1、先安装Rust环境。Rust环境下载地址:https://www.rust-lang.org/zh-CN/tools/install然后根据自己的需要,选择下载的版本。2、安装期间,会提示操作选项,我这边选择的是默认 1.安装完成以后,会有对应的提示。 3、rustc --version可以查看rust环境的版本。 4、查看cargo工具版本。rust程序的编译,离不开cargo。 5、如果需要对本地的rust环境进行升级,可以使用命令 rustip update(图片标识错了,大家看命令即可)6、使用命令 rustup doc可以调出教程文档,不过都是英文的,英文好的大佬,用它来学习也是很不错的选择。 7、可以通过 cargo new 项目名称 来创建一个新的项目。创建以后,会有一些初始的内容。 8、cargo build会进行编译,cargo run可以直接运行编译后的程序。默认创建的是一个hello world,所以可以直接输出。 9、编译以后,在target文件夹内,会生成对应的可执行文件。cargo build默认是debug模式进行编译。 10、我们也可以通过VS CODE来进行编辑代码和编译,效率高一点。 11、安装Rust的拓展。目前没这个拓展了,可以选择 rust-analyzer,应该是原先Rust的更新版本。也有一百多万的下载量了,看来玩的人有点多。12、代码debug工具,这个看个人,可以不安装,没啥影响。配图可以不管。它只是提供了一个debug功能。13、VS CODE上面可以直接运行,运行时候默认会编译到代码文件同一个目录下。控制台也可以看到运行以后的输出结果。14、一些基础语法说明,可以直接看下图标识和说明。15、Rust的变量比较神奇,默认的变量都是不可变的,如果需要允许可变,需要添加mut关键字来区分。 16、定义一个方法/函数以及其他基础操作,可见下图标识说明。 17、Rust的第三方包,都在 https://crates.io 上面。类似于,.NET开发上面,对应的Nuget包网址一样。不过Cargo目前没有VS这样的强大编译器集成了直接可视化查询的东西,所以我们需要自己手动查找要的包以及版本。 18、例如我要用一个time包来做获取时间的。我直接选个最新版本,0.3.15然后在 cargo.toml里面,在dependencies里面添加该包的名称和版本号。添加以后,会自动搜索有关版本进行下载引用。 19、然后在代码内进行引入。引入包,使用 extern crate 包名称。例如time。use 类似C#里面的 using,可以using包内的一些功能或者模块。例如我要获取time里面的now()方法,不过这个包看来现在没有这个方法了。Rust这方面的语法和C++还是比较接近,通过 :: 来进行引出下一级;类似.NET里面的 XXX.XXX的这个句点。 20、咱们换一个,换成 chrono这个包21、然后引入有关版本进来,同上面的time操作。因为time包没有我要的,所以就干掉就好了。22、引入包,以及使用它里面的所有功能模块,可以使用*来代替,这点跟Java或Python比较类似。23、然后写一个累加器,用来做性能测试使用。例如,从1累加到10亿。代码可见下图。24、此处我再创建一个.NET 6都控制台程序,也做同样的事情,看看谁更快。有关代码,如下图所示,功能与Rust的代码一模一样。25、为了更加公平,咱们把程序都编译为release版本。使用命令 cargo build --release 可以指定编译为release版本26、VS上对.NET 6开发的程序,也编译成release版本。27、为了方便查看效果,我们都在控制台下面准备好这两个程序。28、两个程序都运行一下看看效果。可以发现各自的大概耗时。Rust大约耗费40~50MS,.NET6编写的大约耗费600~700MS,卧槽,差距有点大。29、那再来个中间的语言,C++来看看效果。写一个功能一模一样的C++程序,也编译为release版本,然后再看效果。由于C++版本输出详细时间,包括毫秒等,写起来比较啰嗦,所以就简单点,直接输出耗时的毫秒数了。有关代码以及说明,见下图。 30、C++的运行输出结果,大约接近200MS,比.NET6快400MS,但是比Rust居然差距也这么多?unbelievable! 31、为了客观一点,咱再打开Rust与.NET6的程序,以及和C++程序,再运行一下看看结果。结果如下图所示,还是差不多的保持上面的结果。 32、总结一下:Rust依靠强大的性能,以及安全性(你写代码的时候就可以感受到了,如果被检测到代码不够安全,波浪线或者错误提醒会一直提示你,直到你换个写法),已经开始被更多的人采纳。一些科普类的,就不继续描述了,大佬们要是感兴趣,可以自行去探索一下。如需转载,请注明文章作者以及出处。作者:Wesky, 原文出处【https://www.cnblogs.com/weskynet/p/16808320.html】 以上就是该文章的全部内容,要是觉得有帮助,欢迎一键三连啊!!!
前言:目前翻译都是在线的,要在C#开发的程序上做一个可以实时翻译的功能,好像不是那么好做。而且大多数处于局域网内,所以访问在线的api也显得比较尴尬。于是,就有了以下这篇文章,自己搭建一套简单的离线翻译系统。以下内容采用python提供基础翻译服务+ C#访问服务的功能,欢迎围观。系统环境: WIN10 开发环境:VS2022 + VS CODE开发语言环境: Python3.8 + .NET 6以下正文:1、由于本地环境没有python,所以先安装python有关环境先。 2、安装好以后,控制台下输入 python,进入如下终端内容,就代表安装成功了。建议安装时候,选择自动添加到环境变量里面,这样不需要自己配置了。 3、由于翻译功能,会使用到一些已有的模型进行计算,所以以下需要安装几个包。第一个是pytorch, 输入 pip install torch 即可安装。如果安装比较慢,在后面设置一个镜像,可以加速,例如此处我使用的清华的加速器:https://pypi.tuna.tsinghua.edu.cn/simple 4、然后安装flask: pip install flask 5、接着需要安装 gevent: pip install gevent 6、接着是 transformers 7、安装transformers时候,有的会自动安装sentencepiece包,有的时候不会。如果上面查找没有,就手动安装一下: 8、以上包安装完毕,打开VS CODE,创建一个python语言文件 9、此处文件命名为 MyTranslate.py 然后引入可能所需要的包 10、接着,上 https://huggingface.co/Helsinki-NLP 上面,查找需要的语言翻译模型。此处使用的离线翻译,使用的该项目下的。 11、Models里面有上千个语言模型,选择自己需要的名称,记住就行。12、此处,我选了四个模型,分别是英汉/汉英 以及德汉/汉德的翻译模型。有关代码实现如下所示。 13、接着定义一个api接口,用于提供给外部访问(毕竟主业不是python,提供api就可以跨语言来访问该服务了)。有关代码如下所示。 14、VS CODE上运行程序,可以看到终端控制台上面打印出一些下载进度。这是因为本地现在还没有模型,我们选择的四个模型,会被下载到本地来,这样下次就不需要再下载模型了。 15、 模型加载完毕,启动服务。此处0.0.0.0代表本机ip都可以被访问,我们正常使用时候,本机就127.0.0.1即可;如果是局域网或者外网,那就提供真实IP即可。 16、下载的模型,会自动下载到当前用户文件夹下,具体效果如下图所示。所以如果某个服务器没有外网,也可以直接拷贝该.cache文件夹到指定服务器下面的某用户下,也可以被识别。 17、打开postman,做个简单的测试。可以看到,服务是可以被成功访问的,说明代码可以跑,问题不是很大。 18、换一种翻译模式再试一下: 19、再试一试另类点的,看看效果: 20、看不懂德文,把德文搞到百度在线翻译上面反翻译回来,看来语意好像差不多。 21、程序这样运行不是事儿,所以我们可以把它打包为exe程序来运行,这样就可以在不安装python环境的电脑也可以跑了。安装 pyinstaller: 22、在MyTranslate.py同文件夹下,新建一个py文件,名称不能改:hook-ctypes.macholib.py 该文件用于提供虚拟环境使用。23、该文件下,需要导入所有可能用到的依赖的包。不然打包可能出错;或者打包完毕以后,运行可能出错。 24、打包应用的内容,根据个人实际情况来选择,pip 下载时候,有一个 Collecting提示,提示后面就是安装的依赖包,不晓得哪些需要的,就全部搞进去,减少错误几率。 25、执行打包命名 pyinstaller -F xxx.py --additional-hooks-dir=. 如果不需要有控制台提示,可以加个 -w 26、打包安装成功了 27、打包成功的exe文件,自动放在 dist文件夹下 28、生成的exe文件,如图。 29、直接运行走一波,看看效果。为了避免看不到错误提示,所以我在控制台内运行,如图30、由于模型被下载过,所以第二次启动,不会重复下载模型。 31、现在再用 Postman 走一波,看看效果。32、直接运行的程序,难免被人不小心误操作给关闭了,所以我们还可以把他丢到Windows服务上面,这样服务器重启也不担心了。使用NSSM工具进行操作。如果想知道如何使用,也可以参考我的另一篇博客。博客地址:https://www.cnblogs.com/weskynet/p/14961565.html 33、设置描述,备注为 离线翻译服务。安装为服务 TranslateService(名字可以随意)34、安装以后,可以看到已经生成一个对应的Windows服务了。35、服务启动,可以等待一小会儿,加载模型要一丢丢时间。一小会儿以后,使用Postman进行测试一下,看看效果。36、接下来,创建一个基于.NET的webapi程序,用来通过代码来访问翻译服务,看看能不能访问到。 37、创建一个控制器,搭建个基础模子先。38、注入IHttpClientFactory(用来访问webapi使用的,实际上就是提供HttpClient)。然后写个简单的测试功能,直接看以下代码: 39、通过自带的swagger,走一波。输入有关参数,走一个看看,嘿,可以使用,bingo~
前言:关于如何制作一个软件安装包的教程,与编程语言无关。以下,请看详情~ 1、下载Inno Setup,下载地址:https://jrsoftware.org/isinfo.php2、下载最新版本即可。 说明:Inno Setup软件没有提示具体开源协议版本,不过通过有关版权说明,类似于BSD开源协议。具体可以也可以参考 Inno Setup的源码,开源项目地址:https://github.com/jrsoftware/issrc 3、安装程序下载成功以后,按照常规套路进行安装即可。 4、安装成功以后,Inno Setup Compiler就是它的真身。5、做个简单测试,自己创建一个WPF项目程序。6、没啥功能,仅提供用于测试的一个按钮,单击弹出hello world提示框的。 7、制作一个程序图标,例如 666 图标保存为 bmp格式8、程序资源里面添加图标信息 9、编译以后,根目录下测试一下效果,可以看到程序可以正常运行。接下来开始制作安装包。10、打开 inno setup安装包制作工具。新手用户建议使用向导来协助制作。 11、下一步 12、编写有关应用信息 13、配置默认安装路径有关14、选择主启动程序15、如果没有主启动程序,例如是B/S的,就可以选择第二个勾勾。16、选择程序所需要的所有文件或文件夹 17、一些信息编写 18、一些配置选项 19、添加版权信息文件 20、系统用户使用权限等 21、选择语言 22、下一步23、下一步 24、完成 25、生成脚本代码,编译脚本 26、选择保存脚本代码 27、保存到个人指定的位置 28、编译成功29、看到编译成功以后,生成的安装包文件。30、双击进行安装,最先打开的是版权说明。 31、选择安装目录,配置的默认地址是C/Program xxx ,所以这儿会看到默认地址。 32、下一步 33、完成。 34、运行看一下效果 35、桌面也会生成图标。图标是我们制作的666图标。36、安装包的一些配置信息,也可以被查看到。 以上就是该文章的全部内容,谢谢观看。
前言:Maui终于在昨天(2022年8月9日)推送出来了。今儿就迫不及待来把玩一下先。A、我本地已有VS2022,不过版本比较老,此处选择更新。工具 -> 获取功能和更新里面,可以获取到新版本更新。 B、最新版本是17.3.0,我本地只有17.1.1,选择 更新。 C、让网络飞一会儿。 1、更新完毕,打开VS,创建一个新的Maui项目(.NET MAUI应用) 2、创建成功以后,右边可以看到如图所示的起始项目。Platforms里面,是该项目支持的平台环境类型,包括安卓、苹果、windows桌面等。 3、直接运行,走一个。默认情况下,启动为Windows桌面客户端APP的形式。 4、项目的一些层级关系,如图所示。App构造函数里面,指定了主页为AppShell;AppShell里面又重定向到MainPage,MainPage设计器与业务交互代码里面,有一个点击事件。 5、VS工具栏有一排工具,可以用来配置模拟运行环境的。 6、比如说,此处我配置了一个默认的模拟器。 7、配置好以后,运行里面就可以选择该模拟器了。 8、右键,默认情况下,可能都勾选了安卓和iOS,没有iOS模拟设备,就去掉该选项。 9、然后直接运行,会看到提示,显示正在部署到 xxx模拟器上。前提可能需要电脑开启虚拟化,没开启的,可以参考我的另一篇文章进行开启虚拟化:https://www.cnblogs.com/weskynet/p/14825081.html 10、运行以后的效果,如图所示。显示内容与桌面端是一样的,相当于一套同样的代码,多端可以同时跑。【注意事项】Maui项目路径必须不能含有中文,否则可能会提示找不到文件 ,或者 APT 2000 错误之类的等等。 11、测试一下点击按钮,搞个断点,走一波,嘿,还真的进来了。12、新建一个Maui页面,此处就叫Wesky,在里面写点小内容。同时把App构造函数里面的主页改成我自己的新建页,然后走一波。13、然后此处新建一个button按钮,搞一个弹窗测试下效果。14、然后,测试一下依赖注入的效果。新建一个Test类以及接口ITest,里面写一个GetString方法,用来返回一个字符串。然后在Program里面,进行依赖注入注册,然后在App构造函数里面进行注入,可以看到进入构造函数里面了,并且可以获取到实例,说明依赖注入是生效的。 15、Maui和WPF可谓几乎是同卵双胞胎,所以肯定也可以支持MVVM模式啦~~ 新建一个WeskyViewModel,同时也在里面进行构造函数注入ITest接口,然后开发一个点击事件的绑定方法ClickCommand,用来测试MVVM的事件的双向绑定。点击以后,进行弹窗,弹出Test里面的获取字符串的提示信息,同时做一波Maui自带的依赖注入+MVVM模式的同时验证的效果。16、Wesky.cs文件里面(设计器代码文件),构造函数添加WeskyViewModel的注入,然后对BindingContext赋值为注入的参数实例,用来提供对MVVM的双向绑定的关联。 17、Wesky.Xaml设计器文件里面,原来的Click事件干掉,改成Command进行绑定刚才定义的ClickCommand方法,用来测试MVVM双向绑定的效果。18、App的构造函数里面,对Wesky页面进行注入,然后把实例赋值给MainPage,用于依赖注入的实例传递。19、最后,在Program里面,添加Wesky页面和ViewModel的注册。注册的生命周期,可以根据自己的实际情况进行选择,包括Singleton、Scoped、Transient,跟传统的.NET 6的注册方式一致。 20、然后,运行一下,程序在模拟器里面运行,然后点击按钮以后,效果如图所示。说明依赖注入+MVVM同时验证都通过了。21、模拟器运行以后,在根目录下,会有对应生成的apk文件,可以拿来安装使用。一个是没有签名版本,一个是签名版本。 22、拷到我的老古董华为鸿蒙系统的手机上,试着安装一下。23、尴尬了,没成功。没成功的可能性,大概率可能是跟华为的麒麟芯片是基于arm架构的,而模拟器是非arm架构的。24、选择一个arm架构的模拟器,再创建一个,走一波25、由于本机系统环境原因,没能启动arm架构的模拟器,所以暂告失败~~ 26、最后,不用模拟器运行,直接当作Windows桌面程序走一波,效果如图所示。27、以上就是该文章的全部内容,大佬们如果觉得有帮助,欢迎推荐、留言。也欢迎大佬们感兴趣的,也可关注我的个人公众号:微信公众号搜索【Dotnet Dancer】即可关注。 如果对.NET技术比较感兴趣,也喜欢吹牛聊天,也可以在该文章【https://www.cnblogs.com/weskynet/p/16573873.html】最下方,点击加入QQ群,一起吹牛一起谈人生~
前言: MQTT广泛应用于工业物联网、智能家居、各类智能制造或各类自动化场景等。MQTT是一个基于客户端-服务器的消息发布/订阅传输协议,在很多受限的环境下,比如说机器与机器通信、机器与物联网通信等。好了,科普的废话不多说,下面直接通过.NET环境来实现一套MQTT通信demo,实现服务端与客户端的双边消息发布与订阅的功能和演示。 开发环境:VS2022 + .NET 6 + Webapi / 控制台 1、新建一个webapi项目,用来后面做测试使用2、新建一个继承自IHostedService的服务,用于随着webapi程序的启动而自动执行。(最终代码在文末)3、引入 MQTTNet 包,该项目提供了.net环境下的MQTT通信协议支持,这款框架很优秀,此处直接引用它来进行使用。4、在上面的MqttHostService类里面,开始方法里面新增初始化MQTT服务端的一些功能,例如 IP、端口号、事件等等。5、mqtt服务端支持的一系列功能很多,大佬们可以自行去尝试一些新发现,此处只使用若干个简单功能。6、添加客户端连接事件、连接关闭事件 7、由于事件要用的可能有点多,此处就不一一例举了,可以直接看以下的代码,以及有关注释来理解。8、事件触发时候,打印输出 9、输出之前,记录一个当前事件名称标记一下,用于可以更加清楚看出是哪个事件输出的。 10、对MqttHostService类进行注册,用于程序启动时候跟随启动。 11、上面貌似设计的不是很友好,所以把mqtt服务实例单独弄出来,写入到单独的类里面做成属性,供方便调用。 12、把先前的一些东西改一下,换成使用上面步骤的属性来直接调用使用。13、运行一下,看看是否可以成功,显示服务已启动,说明服务启动时OK的了. 14、新增一个控制台程序 MqttClient,用于模拟客户端。15、创建客户端启动以及有关配置信息和有关事件,如图。具体使用可以看代码注释,就不过多解释了。 16、在program类里面,调用客户端启动方法,用于测试使用。17、上面客户端对应的三个事件的实现如图,同时进行有关信息的打印输出。 18、启动服务端,然后启动客户端,可以看到服务端有一个连接失败的消息,这个是因为上面配置的客户端用户名是admin,密码是1234567,而服务端配置的规则是,用户名是admin 密码是123456 19、密码改回正常匹配项以后,再重新运行试试看,可以看到客户端与服务端连接上了。 20、如果关闭客户端,也可以看到服务端会进入客户端关闭事件内。 21、把上面主题订阅的内容写到连接成功以后的事件里面,不然客户端连接期间,可能就执行了主题订阅,会存在订阅失败的情况。改为写入到连接成功以后的事件里面,可以保证主题订阅肯定是在客户端连接成功以后才执行的。 22、接下来测试服务端消息推送,在MqttService服务里面,新增一个方法,用来执行mqtt服务端发布主题消息使用。有关配置信息和消息格式,如图所示。 23、新增一个API控制器,用来测试使用。API参数直接拿来进行消息的推送使用。24、运行服务端和客户端,并访问刚刚新增的api接口,手动随意输入一条消息,可以看到客户端订阅的主题消息已经被实时接收到了。 25、接下来对客户端新增一个消息推送的方法,用来测试客户端消息发布的功能。有关消息格式和调用,如图所示,以及注释部分的说明。 26、客户端program类里面,客户端连接以后,通过手动回车,来执行客户端发布消息。27、再次启动服务端和客户端28、然后客户端内按一下回车,执行消息发布功能。可以看到,服务端成功接收到了客户端发过来的主题消息。29、接下来测试客户端与客户端之间的消息发布与订阅,为了模拟多客户端效果,把上面客户端已经编译好的文件拷贝一份出来。30、然后本地的代码进行一些修改,用来当做第二个客户端程序。所以客户端id也进行变更为 testclient0231、对客户端订阅的主题,也改成 topic_0232、启动服务端,以及拷贝出来的客户端1,和上面修改了部分代码的客户端2,保证都已经连接上服务端。33、调用服务端的api接口,由于服务端发布的消息是发布给topic_01的,所以只有客户端1可以接收到消息。 34、客户端1执行回车,用于发布一段消息给主题 topic_02,可以看到客户端01发布的消息,同时被服务端和客户端02接收到了。因为服务端是总指挥,所以客户端发布的消息都会经过服务端,从而服务端都可以接收到连接的客户端发布的所有消息。 35、测试数据保持,下面先对客户端1进行断开,然后再重新连接客户端1,可以看到客户端1直接接收到了它订阅的主题的上一次最新的消息内容,这个就是消息里面,Retain属性设为True的结果,用于让服务端记忆该主题消息使用的。如果设为false,就没有这个效果了,大佬们也可以自己尝试。36、最终的服务端代码:MqttHostService:public class MqttHostService : IHostedService, IDisposable { public void Dispose() { } const string ServerClientId = "SERVER"; public Task StartAsync(CancellationToken cancellationToken) { MqttServerOptionsBuilder optionsBuilder = new MqttServerOptionsBuilder(); optionsBuilder.WithDefaultEndpoint(); optionsBuilder.WithDefaultEndpointPort(10086); // 设置 服务端 端口号 optionsBuilder.WithConnectionBacklog(1000); // 最大连接数 MqttServerOptions options = optionsBuilder.Build(); MqttService._mqttServer = new MqttFactory().CreateMqttServer(options); MqttService._mqttServer.ClientConnectedAsync += _mqttServer_ClientConnectedAsync; //客户端连接事件 MqttService._mqttServer.ClientDisconnectedAsync += _mqttServer_ClientDisconnectedAsync; // 客户端关闭事件 MqttService._mqttServer.ApplicationMessageNotConsumedAsync += _mqttServer_ApplicationMessageNotConsumedAsync; // 消息接收事件 MqttService._mqttServer.ClientSubscribedTopicAsync += _mqttServer_ClientSubscribedTopicAsync; // 客户端订阅主题事件 MqttService._mqttServer.ClientUnsubscribedTopicAsync += _mqttServer_ClientUnsubscribedTopicAsync; // 客户端取消订阅事件 MqttService._mqttServer.StartedAsync += _mqttServer_StartedAsync; // 启动后事件 MqttService._mqttServer.StoppedAsync += _mqttServer_StoppedAsync; // 关闭后事件 MqttService._mqttServer.InterceptingPublishAsync += _mqttServer_InterceptingPublishAsync; // 消息接收事件 MqttService._mqttServer.ValidatingConnectionAsync += _mqttServer_ValidatingConnectionAsync; // 用户名和密码验证有关 MqttService._mqttServer.StartAsync(); return Task.CompletedTask; } /// <summary> /// 客户端订阅主题事件 /// </summary> /// <param name="arg"></param> /// <returns></returns> private Task _mqttServer_ClientSubscribedTopicAsync(ClientSubscribedTopicEventArgs arg) { Console.WriteLine($"ClientSubscribedTopicAsync:客户端ID=【{arg.ClientId}】订阅的主题=【{arg.TopicFilter}】 "); return Task.CompletedTask; } /// <summary> /// 关闭后事件 /// </summary> /// <param name="arg"></param> /// <returns></returns> private Task _mqttServer_StoppedAsync(EventArgs arg) { Console.WriteLine($"StoppedAsync:MQTT服务已关闭……"); return Task.CompletedTask; } /// <summary> /// 用户名和密码验证有关 /// </summary> /// <param name="arg"></param> /// <returns></returns> private Task _mqttServer_ValidatingConnectionAsync(ValidatingConnectionEventArgs arg) { arg.ReasonCode = MqttConnectReasonCode.Success; if ((arg.Username ?? string.Empty)!="admin" || (arg.Password??String.Empty)!="123456") { arg.ReasonCode = MqttConnectReasonCode.Banned; Console.WriteLine($"ValidatingConnectionAsync:客户端ID=【{arg.ClientId}】用户名或密码验证错误 "); } return Task.CompletedTask; } /// <summary> /// 消息接收事件 /// </summary> /// <param name="arg"></param> /// <returns></returns> private Task _mqttServer_InterceptingPublishAsync(InterceptingPublishEventArgs arg) { if (string.Equals(arg.ClientId, ServerClientId)) { return Task.CompletedTask; } Console.WriteLine($"InterceptingPublishAsync:客户端ID=【{arg.ClientId}】 Topic主题=【{arg.ApplicationMessage.Topic}】 消息=【{Encoding.UTF8.GetString(arg.ApplicationMessage.Payload)}】 qos等级=【{arg.ApplicationMessage.QualityOfServiceLevel}】"); return Task.CompletedTask; } /// <summary> /// 启动后事件 /// </summary> /// <param name="arg"></param> /// <returns></returns> private Task _mqttServer_StartedAsync(EventArgs arg) { Console.WriteLine($"StartedAsync:MQTT服务已启动……"); return Task.CompletedTask; } /// <summary> /// 客户端取消订阅事件 /// </summary> /// <param name="arg"></param> /// <returns></returns> private Task _mqttServer_ClientUnsubscribedTopicAsync(ClientUnsubscribedTopicEventArgs arg) { Console.WriteLine($"ClientUnsubscribedTopicAsync:客户端ID=【{arg.ClientId}】已取消订阅的主题=【{arg.TopicFilter}】 "); return Task.CompletedTask; } private Task _mqttServer_ApplicationMessageNotConsumedAsync(ApplicationMessageNotConsumedEventArgs arg) { Console.WriteLine($"ApplicationMessageNotConsumedAsync:发送端ID=【{arg.SenderId}】 Topic主题=【{arg.ApplicationMessage.Topic}】 消息=【{Encoding.UTF8.GetString(arg.ApplicationMessage.Payload)}】 qos等级=【{arg.ApplicationMessage.QualityOfServiceLevel}】"); return Task.CompletedTask; } /// <summary> /// 客户端断开时候触发 /// </summary> /// <param name="arg"></param> /// <returns></returns> /// <exception cref="NotImplementedException"></exception> private Task _mqttServer_ClientDisconnectedAsync(ClientDisconnectedEventArgs arg) { Console.WriteLine($"ClientDisconnectedAsync:客户端ID=【{arg.ClientId}】已断开, 地址=【{arg.Endpoint}】 "); return Task.CompletedTask; } /// <summary> /// 客户端连接时候触发 /// </summary> /// <param name="arg"></param> /// <returns></returns> private Task _mqttServer_ClientConnectedAsync(ClientConnectedEventArgs arg) { Console.WriteLine($"ClientConnectedAsync:客户端ID=【{arg.ClientId}】已连接, 用户名=【{arg.UserName}】地址=【{arg.Endpoint}】 "); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } MqttService: public class MqttService { public static MqttServer _mqttServer { get; set; } public static void PublishData(string data) { var message = new MqttApplicationMessage { Topic = "topic_01", Payload = Encoding.Default.GetBytes(data), QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, Retain = true // 服务端是否保留消息。true为保留,如果有新的订阅者连接,就会立马收到该消息。 }; _mqttServer.InjectApplicationMessage(new InjectedMqttApplicationMessage(message) // 发送消息给有订阅 topic_01的客户端 { SenderClientId = "Server_01" }).GetAwaiter().GetResult(); } }37、最终的客户端代码:MqttClientService:public class MqttClientService { public static IMqttClient _mqttClient; public void MqttClientStart() { var optionsBuilder = new MqttClientOptionsBuilder() .WithTcpServer("127.0.0.1", 10086) // 要访问的mqtt服务端的 ip 和 端口号 .WithCredentials("admin", "123456") // 要访问的mqtt服务端的用户名和密码 .WithClientId("testclient02") // 设置客户端id .WithCleanSession() .WithTls(new MqttClientOptionsBuilderTlsParameters { UseTls = false // 是否使用 tls加密 }); var clientOptions = optionsBuilder.Build(); _mqttClient = new MqttFactory().CreateMqttClient(); _mqttClient.ConnectedAsync += _mqttClient_ConnectedAsync; // 客户端连接成功事件 _mqttClient.DisconnectedAsync += _mqttClient_DisconnectedAsync; // 客户端连接关闭事件 _mqttClient.ApplicationMessageReceivedAsync += _mqttClient_ApplicationMessageReceivedAsync; // 收到消息事件 _mqttClient.ConnectAsync(clientOptions); } /// <summary> /// 客户端连接关闭事件 /// </summary> /// <param name="arg"></param> /// <returns></returns> private Task _mqttClient_DisconnectedAsync(MqttClientDisconnectedEventArgs arg) { Console.WriteLine($"客户端已断开与服务端的连接……"); return Task.CompletedTask; } /// <summary> /// 客户端连接成功事件 /// </summary> /// <param name="arg"></param> /// <returns></returns> private Task _mqttClient_ConnectedAsync(MqttClientConnectedEventArgs arg) { Console.WriteLine($"客户端已连接服务端……"); // 订阅消息主题 // MqttQualityOfServiceLevel: (QoS): 0 最多一次,接收者不确认收到消息,并且消息不被发送者存储和重新发送提供与底层 TCP 协议相同的保证。 // 1: 保证一条消息至少有一次会传递给接收方。发送方存储消息,直到它从接收方收到确认收到消息的数据包。一条消息可以多次发送或传递。 // 2: 保证每条消息仅由预期的收件人接收一次。级别2是最安全和最慢的服务质量级别,保证由发送方和接收方之间的至少两个请求/响应(四次握手)。 _mqttClient.SubscribeAsync("topic_02", MqttQualityOfServiceLevel.AtLeastOnce); return Task.CompletedTask; } /// <summary> /// 收到消息事件 /// </summary> /// <param name="arg"></param> /// <returns></returns> private Task _mqttClient_ApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs arg) { Console.WriteLine($"ApplicationMessageReceivedAsync:客户端ID=【{arg.ClientId}】接收到消息。 Topic主题=【{arg.ApplicationMessage.Topic}】 消息=【{Encoding.UTF8.GetString(arg.ApplicationMessage.Payload)}】 qos等级=【{arg.ApplicationMessage.QualityOfServiceLevel}】"); return Task.CompletedTask; } public void Publish(string data) { var message = new MqttApplicationMessage { Topic = "topic_02", Payload = Encoding.Default.GetBytes(data), QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, Retain = true // 服务端是否保留消息。true为保留,如果有新的订阅者连接,就会立马收到该消息。 }; _mqttClient.PublishAsync(message); } } 38、后记:MQTT以上演示已经完毕,可以看到它的一些特性,跟websocket很接近,但是又比websocket通信更加灵活。其实,实际上MQTT的客户端在现实生产环境场景下,并不需要咱们开发者进行开发,很多硬件设备都支持提供MQTT协议的通信客户端,所以只需要自己搭建一个服务端,就可以实现实时监控各种设备推送过来的各种信号数据。同时客户端支持发布消息给其他客户端,所以就实现了设备与设备之间的一对一信号通信的效果了。如果需要下发信号给硬件设备,MQTT服务端也可以直接下发给某个指定设备来进行实现即可。上面案例只提供入门方案,如果有感兴趣的大佬,可以自己去拓展一下,来达到更好的效果。
前言:如题。直接上手撸,附带各种截图,就不做介绍了。 1、influxDB的官网下载地址 https://portal.influxdata.com/downloads/打开以后,如下图所示,可以选择版本号,以及平台。此处咱们选择windows平台。不过此处没有实际的可以下载的地方,着实比较过分,不过咱们可以另辟蹊径。2、直接下载。具体地址如下,2.3.0是版本号:https://dl.influxdata.com/influxdb/releases/influxdb2-2.3.0-windows-amd64.zip链接说明:该链接是下载windows版本的influxDB的链接,其中 influxdb2-2.3.0-windows-amd64.zip 里面,2.3.0是版本号,可以通过修改这个版本号来下载你所需要的具体版本文件。3、或者通过这个地址进行下载:https://docs.influxdata.com/influxdb/v2.1/install/?t=Windows其中,/v2.1是版本号,把2.1改成2.3就可以下载2.3的版本了。此处仅做个实验,例如下载2.1版本。4、可以对比下真实的下载链接地址,与上面的2.3.0版本地址只差了一个版本号信息,其他都一样。5、此处使用2.3.0版本,解压以后进行使用。 6、CMD到解压的根目录下,直接执行influxdb.exe文件(cmd命令执行,不会闪退,直接点有可能会一闪而过)备注:也可以通过nssm工具进行部署成Windows服务,部署方法可以参考我的其他博客内容,有相关信息,此处不再重复写。 7、启动以后,在cmd窗口也可以看到默认端口号8086,所以在地址栏输入 htp://127.0.0.1:8086/onboarding/ 就可以打开起始监控面板,然后进行一些初始化操作了。 8、打开初始页面,可以用来创建初始用户信息 9、例如,我此处创建一个用户 wesky,以及有关的组和实例,如图备注的信息。然后执行下一步(CONTINUE) 10、选择快速开始 11、创建完成以后,进入到主页。 12、可以看到它支持的客户端,包括C#,以及其他很多别的支持。说明还是比较强大了,支持的方案有很多,以及也可以支持从消息队列、系统日志、其他数据库等地方进行导入或写入数据,有待大佬们自己摸索了。13、找到API TOKENS选项,这里会生成用户的一个唯一token信息,用来写代码时候会用到。 14、点击用户's Token,可以打开具体的token信息 15、找到token信息,可以先拷贝下来备用。或者等下需要的时候,知道在这里寻找也可以。 16、接下来开始写个代码进行演示一下基础操作,当作入个门。创建一个控制台项目,叫InfluxDbTest 17、此处选择.net 6版本环境,当然,大佬们也可以选择其他环境,问题不大的。 18、创建完成以后,引入nuget包 InfluxDB.Client 19、写点代码测试一下(源码会附录在文末),此处先创建客户端,然后定义组织、以及实例(Bucket),然后通过写入一个数据进行进行测试(此处手抖了一下,我运行了两次,所以实际写入了两个数据)备注:写入数据或者读写或者其他操作,也可以参考上面influxdb面板里面提供C#功能的案例里面,点击进去可以看到一些例子。 20、如图,可视化面板里面,可以进行数据查询,以及数据可视化。Bucket就是咱们创建的数据库实例,mem就是对应上面的代码里面写的mem,可以当作是一个表,然后是一些标签、字段等。Field是字段,可以自己拓展其他字段等等。 21、写个循环,累加测试一下,改造一下代码,然后继续运行。22、可以看到数据一直往上飘,因为值是累加的,所以效果和预期一致。 23、来点刺激一点的测试,搞个随机数,可能效果会好玩一点。此处弄个写入0-100的随机数,然后间隔10msx写入一次。 24、让显示的按照10ms为单位进行显示,效果如图,数值都是随机的,所以走势就很花里胡哨了。 25、展示效果选择表格样式,如图,也是可以的。 有关最终的代码:using InfluxDB.Client; using InfluxDB.Client.Api.Domain; using InfluxDB.Client.Writes; Console.WriteLine("Hello, World!"); const string token = "mOGqO3m23KHOAnsByiEAS6rJGEZEl0iuhZNGn0QNbg_vs4P_Rqa9_eWmnuYb_ovS7dy2G19xA-SqR6RMlQ3iXw=="; // influxdb生成的token const string org = "Organization"; const string bucket = "Bucket"; using (var client = InfluxDBClientFactory.Create("http://localhost:8086", token)) // 生产环境下使用,可以使用单例来注册使用同一个客户端,减少创建次数 { using (var writeApi = client.GetWriteApi()) { for (int i = 0; i <= 1000; i++) { var point = PointData.Measurement("mem") .Tag("host", "local") .Field("Field1", new Random().Next(0,100)) .Timestamp(DateTime.UtcNow, WritePrecision.Ns); writeApi.WritePoint(point, bucket, org); Thread.Sleep(10); } } } Console.WriteLine("Hello, World 2 !"); Console.ReadLine(); 以上就是该文章的全部内容,时序数据库可以用于工业物联网环境下,特别是对设备数值进行监控,可以很直观看出每个时间区间的状态图、或者步行图等等。欢迎大佬们自行去拓展更加风骚的玩法,此处仅用于入门教程。
前言:废话不多说了,直接上步骤。 系统环境:win10测试用的开发环境和服务类型:VS2022 + DotNet 6 + WebApi 1、本地先创建一个webapi项目,用于测试使用。 2、新建一个API控制器,里面只提供一个Post请求类型的测试方法。如果不晓得怎么创建webapi项目,可以围观我的其他博客文章来了解,博客地址:【包括gRPC\minimalApi\传统Webapi】https://www.cnblogs.com/weskynet/p/15677719.html 3、下载Nginx,下载地址:http://nginx.org/en/download.html建议选择下载稳定版 (Stable version),上面Mainline version是最新版。4、对程序进行分身,为了验证测试的最终效果,此处直接输出三个不同的值用来区分。5、分别把三个不同的输出结果的程序拷贝出来,先区分一下,用来后面打开三个程序做分布式测试使用。 6、分别启动三个程序,此处给三个程序分别赋予端口号18888、18889、18890 7、启动Nginx,并测试Nginx是否可以使用。正常情况下,启动时候可能会一闪而过,所以可以通过 cmd 命令进行打开。直接在根目录下输入 nginx.exe 即可;或者输入start nginx 命令也是可以的。启动成功以后,浏览器输入 localhost,会有如下图的提示效果,说明Nginx启动成功。但是这样的启动方式,每次都会比较麻烦,都需要手动来启动,比较反人类。所以可以通过nssm工具来把nginx服务部署成windows服务。如果想了解nssm怎么部署成Windows服务,可以参考我的另一篇部署elk服务的文章:https://www.cnblogs.com/weskynet/p/14961565.html 8、先测试一下启动的api服务是不是正常,先通过api测试工具,例如postman调用一下,查看效果。如下,访问了18888端口,即第一个程序,返回了first,说明api是可以访问成功的。 9、在Nginx根目录下,conf文件夹下,有它的配置文件们。nginx.conf配置文件可以用来配置负载均衡的策略有关。此处我用来配置监听10080端口,然后进行反射到18888、18889、18890三个地址。其他介绍,如图内的文字描述所述。 10、让配置生效,通过命令 nginx -s reload 即可生效,无需重启nginx服务。 11、使用postman进行测试,把端口改为nginx监听的10080端口,然后不断点击send进行查看结果,可以看到结果会不断变化,说明可以随机访问nginx反向代理的三个api服务;并且由于配置的权重不一致,所以会有third的结果出现的频率最多的情况。如果要都很平均,可以都设置为一样的值即可。最后:以上就是该文章的全部内容,如有帮助,欢迎点赞、推荐、转发和评论。谢谢各位大佬围观。
前言:Wpf开发过程中,最经常使用的功能之一,就是用户控件(UserControl)了。用户控件可以用于开发用户自己的控件进行使用,甚至可以用于打造一套属于自己的UI框架。依赖属性(DependencyProperty)是为用户控件提供可支持双向绑定的必备技巧之一,同样用处也非常广泛。以下案例,为了图方便,我以之前的博客的基础为模板,直接进行开发。如有遇到疑问的地方,可以查看先前的博客(WPF使用prism框架+Unity IOC容器实现MVVM双向绑定和依赖注入)的文章做个前瞻了解:https://www.cnblogs.com/weskynet/p/15967764.html 以下是正文(代码在文末) 0、配置环境客户端环境:WIN 10 专业版VS开发环境:VS 2022 企业版运行时环境:.NET 6开发语言:C#前端框架:WPF 1、新建了一个用户控件,里面画了一个实心圆,以及一个文本控件的组合,当作我要实验使用的用户控件(TestUserControl)。 2、在主窗体里面进行引用,可以看到引用以后,会在工具箱上显示新增的用户控件 3、为了测试方便,我直接在先前的Lo'gin页面直接进行添加该用户控件,效果如下。 4、运行效果如下。由于该用户控件没有设置过任何属性,所以现在是没有任何事件、也没有办法更改默认文本等信息的。 5、接下来进行设置属性,用于可以直接更改TextName属性的Text值。设置一个MyText属性,用于可以获取和设置用户控件内的TextBlock的Text值。 6、然后可以在Xaml里面直接通过更改MyText的属性,来更新显示的Text值。如下图所示,设置MyText属性后,设置值为666,同步更新成666了。 7、但是如果想要实现双向绑定,其实还不太够,直接Binding会提示错误XDG0062:Object of type 'System.Windows.Data.Binding' cannot be converted to type 'System.String'. 如图。 8、以上问题可以通过自定义依赖属性来解决。在用户控件的设计器交互代码类(TestUserControl)里面,新增以下代码,功能如图所示。 9、现在在xaml里面,设置Binding就不会提示错误了。 10、并且也可以直接设置值,效果同上面设置属性以后直接写值效果一样。 11、在Login页面的ViewModel里面,新增属性提供给双向绑定使用。 12、设置MyText进行Binding到刚刚写的ViewModel的属性TestText上。 13、运行效果如下图所示,说明双向绑定成功了。 14、接下来对用户控件设置单击事件的双向绑定。先设置Command有关的依赖属性。 15、一些有关方法和其他的属性设置,就不做过多介绍了,看图说话。 16、然后是关键的一步,需要设置单机事件与Command属性关联。当然,Command是命名得来的,所以也可以使用其他的命名,也都是OK的,不用在意这些细节,只是默认情况下,单击都喜欢用Command。如果自带的控件也没有双击、右键等双向绑定,也可以通过设置依赖属性来实现。 17、在ViewModel里面定义单击事件以及有关执行的方法。方法为一个弹出消息框。 18、使用Command进行绑定事件名称。 19、运行,并单击实心圆的效果,并弹出提示框,说明单击事件通过依赖属性进行设置成功。 20、接下来测试一下带参数的事件。在viewmodel里面,对刚才无参数的事件,改为带一个string参数的。 21、在xaml里面,传入一个字符串参数,就叫 Hello world 22、运行,并点击实心圆后效果如图所示,说明带参数也是OK的。 23、其他套路如出一辙,大佬们可以自行尝试,例如通过设置背景依赖属性,变更实心圆的背景,而不是整个用户控件(正方形)的背景。这部分本来也要写一个给大佬们压压惊,由于时间关系,大佬们可以自己尝试玩一下。提示:背景 Background是系统自带的,所以需要new。通过属性依赖进行更改圆的颜色,而不是背景色。有兴趣的大佬或者需要学习的,可以动手玩一玩,加深印象。 以上就是该文章的全部内容,如果对你有帮助,欢迎大佬点赞、留言与转发。如需转发,请注明我的博客出处:https://www.cnblogs.com/weskynet/p/16290422.html
前言:随着工业化的发展,目前越来越多的开发,从互联网走向传统行业。其中,工业领域也是其中之一,包括各大厂也都在陆陆续续加入工业4.0的进程当中。工业领域,最核心的基础设施,应该是与下位硬件设备或程序进行通信有关的了,而下位机市场基本上是PLC的天下。而PLC产品就像编程语言一样,类型繁多,协议也多种多样。例如,西门子PLC最常用的S7协议、施耐德PLC最常用的Modbus协议、以及标准工业通信协议CIP协议等等。而多种通信协议里面,基于以太网通信的居多。以太网通信的里面,通用协议除了CIP协议,就属于Modbus TCP协议了。接下来的内容,我会以从头开发一个简单的基于modbus tcp通信的案例,来实现一个基础的通信功能。 有关环境:开发环境: VS 2022企业版运行环境: Win 10 专业版.NET 环境版本: .NET 6 【备注】 源码在文末 1、新建一个基于.NET 6带控制器的webapi项目,以及一个类库项目。如下图所示,新建以后的项目目录结构。 2、由于modbus tcp通信实际上就是一个socket通信,所以在类库项目下,先创建了一个Modbus服务类,并且提供一个基于socket通信连接的方法。socket连接以后,需要返回socket实例拿来使用。 3、为了方便一点,再新增一个通用的返回信息类,用于存储一些返回信息使用。 4、基于以上的返回信息类,咱对连接方法进行稍微改造一下,让它看起来更方便一点。这样可以用来验证连接是否正常,以及返回对应的异常信息,好做进一步处理。 5、Modbus TCP请求的报文规则,一些解析信息如下:站地址:默认0x01, 除非PLC告诉我们其他站地址。功能码:代表读写数据时候指定的读写方法等。例如读取线圈的功能码是0x01。地址和读取长度:地址目前个人在施耐德物理的PLC环境上,不能超过30000。同时,单次读写长度不能超过248个byte,否则PLC可能会飘。当然,也可能将来一些PLC可以支持更长的批量数据读写,目前在施耐德PLC环境下不支持(具体型号忘记了,有点久了,当前身边没得PLC了,等下会使用仿真工具来做环境)。头部校验(消息唯一识别码):0~65535,用于PLC服务端进行区分不同的客户端而使用的一组数据标识,不同的客户端必须保证标识码不重合。例如多个客户端同时存在时候,发起的通信请求,必须保持不一样的识别码,否则Modbus服务端有可能会因为不知是哪个客户端发起的请求而导致信息乱了。无(协议标识):默认0,代表是Modbus协议。数据长度:发送的报文的长度,刚好是6位,所以可以写成固定值0x06。(写入的规则不一样,此处固定值只当作读取时候使用) 6、根据协议的一些具体内容,写一个存储功能码和异常返回码的数据类,用于后期做通信时候传参和通信数据验证使用。有关协议具体内容,如下代码所示。 7、由于异常码是byte数据,直接验证可能会麻烦一点,为了可以直观一些,此处再新增一个用于解析Modbus返回的异常信息的方法,用于备用。 8、根据协议规则,提供一些参数,并先搭建一个简单的方法框架,用来可以进行读取线圈的功能。包含简单的报文数据拆分以及报文发送和接收。由于发送报文长度不能超过248byte(1 bool大小 == 1 byte,如果是其他类型,需要做其他长度换算),所以当长度超过时候,做个简单的算法进行拆分再发送,防止发生不必要的异常。以下做一个读取线圈(Bool类型数据)的简单方法。 9、根据上方提供的协议报文组装规则,进行开发一个通用的报文组织方法。有高低位之分,所以对于占用2byte的数据,需要进行"倒装"。 10、发送报文以后,返回的报文含有校验信息:发送的数据报文的第7位的数据,加上 0x80 以后,跟返回的报文的第7位byte数据如果一致,则代表当前通信上可能有异常。异常码在接收的响应报文的第8位。所以可以继续写一个验证是否成功的校验方法: 11、由于返回的数据也都是byte数据,以上读取的线圈值(布尔值),就需要提供一个数据类型转换的功能用于把byte数组转换为bool数组。 12、对读取线圈的最开始的方法,进行一些完善以后的代码如下。响应报文长度是 发送数据长度*2+9 。 13、接下来做一个简单的测试。准备一下仿真环境,进行本地的测试,看看是否可以连通。先准备两个工具,一个是 modbus poll,另一个是modbus slave。一个用来模拟服务端环境,另一个可用来模拟数据收发验证。【备注】:由于网上存在很多爬虫爬取博客文章到各个地方的,所以如果有需要这俩工具的小伙伴,可以点击该文章的 原文链接:【https://www.cnblogs.com/weskynet/p/16121383.html】的最下方的QQ群号进行加群进行获取,或者在文章最后提供的个人微信号,加我个人微信私发也可以。 14、两边都设置为读写单个线圈的功能,用于测试以上线圈读取的代码的功能。 15、两边都设置为modbus tcp连接方式。Slave站点启动以后,默认为本地,poll工具上的IP地址选择本地即可。如果是真实PLC环境,则填写真实PLC地址。 16、测试两边是否通信上。给任意一个地址写入一个true,可以看到另一边也同步更新,说明通信是通的了。【注意】modbus工具,poll和slave工具默认占用了消息唯一标识码,大概是1~5左右的固定值,所以使用该工具期间,建议程序上的唯一消息识别码设置为5以上,以防止通信干扰。 17、接下来就可以继续完善代码进行验证了。先新增ModbusService的接口IModbusService,用于实现依赖注入。然后在program.cs文件里面进行服务注册。 18、新建一个控制器,用来进行模拟实验。有关代码和注释如图所示。 19、进行读取一个长度试试效果。结果是数据不支持,说明报文有问题。 20、通过断点,找到问题所在,上面的代码里面,length经过简单算法计算以后等于0,此处需要用的应该是newLength变量的值。 21、再次测试,地址从1开始,读取两个地址,结果符合预期。 22、再测试一下,从0开始读取30个,并随即设置若干个是True的值。 23、其他的写入、以及其他类型读写,基本类似。由于篇幅有限,就不继续进行一步一步操作的截图了。读取的,选好类型,报文格式都是一样的,唯一有差别的是写入的报文。下面是写入单个线圈值的报文。线圈当前仅支持一个一个写入。 24、写入寄存器的规则会有些偏差,协议规则如下图。 【备注】以上图的标题,我写错了,应该是 “写入寄存器”报文协议,懒得换图了,大佬们看的时候自己辨别哈~ 读取线圈当作引导,其他类型也都异曲同工,大佬们可以自行尝试。 另外说点,如果是生产环境下使用,建议把客户端连接做成【长连接】,不然重复创建连接比较耗费资源,耗时也会因为新建连接而占用一大半。同时,如果是多线程访问,使用同一个客户端连接,必须加锁,否则会干扰数据;如果是多线程,不同客户端,就要保证每个消息识别码必须不同,如果存在同一个识别码,很容易发生数据异常等情况。 有关源码:ModbusService源码:public class ModbusService: IModbusService { public ResultInformation<Socket> ConnectModbusTcpService( IPAddress ip, int port) { ResultInformation<Socket> client = new(); try { client.Result = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); client.Result.Connect(new IPEndPoint(ip, port)); client.IsSucceed = true; } catch (Exception ex) { client.IsSucceed=false; client.Message = ex.Message; } return client; } /// <summary> /// 读取线圈值(Bool) /// </summary> /// <param name="client">客户端</param> /// <param name="headCode">头部标识</param> /// <param name="station">站地址</param> /// <param name="address">地址</param> /// <param name="length">长度</param> /// <returns></returns> public ResultInformation<bool[]> ReadCoils(Socket client,ushort headCode,byte station, ushort address, ushort length) { ResultInformation<bool[]> result = new(); int resultIndex = 0; ushort newLength = 0; ushort realLength = length; // 存储实际长度 try { List<byte> byteResult = new List<byte>(); // 存储实际读取到的所有有效的byte数据 while (length > 0) { if (length > 248) // 长度限制,不能超过248 { length = (ushort)(length - 248); newLength = 248; } else { newLength = length; length = 0; } resultIndex += newLength; byte[] sendBuffers = BindByteData(headCode,station,FunctionCode.ReadCoil,address,newLength); // 组装报文 client.Send(sendBuffers); byte[] receiveBuffers = new byte[newLength * 2 + 9]; int count = client.Receive(receiveBuffers); // 等待接收报文 var checkResult = CheckReceiveBuffer(sendBuffers, receiveBuffers); // 验证消息发送成功与否 if (checkResult.IsSucceed) { // 成功,如果长度超出单次读取长度,进行继续读取,然后对数据进行拼接 List<byte> byteList = new List<byte>(receiveBuffers); byteList.RemoveRange(0, 9); // 去除前面9个非数据位 byteResult.AddRange(byteList); // 读取到的数据进行添加进集合 address += newLength; // 下一个起始地址 } else { throw new Exception(checkResult.Message); } } result.IsSucceed = true; result.Result = ByteToBoolean(byteResult.ToArray(), realLength); } catch (Exception ex) { result.IsSucceed = false; result.Result = new bool[0]; result.Message = ex.Message; } return result; } private bool[] ByteToBoolean(byte[] data,int length) { if (data == null) { return new bool[0]; } if (length > data.Length * 8) length = data.Length * 8; bool[] result = new bool[length]; for (int i = 0; i < length; i++) { int index = i / 8; int offect = i % 8; byte temp = 0; switch (offect) { case 0: temp = 0x01; break; case 1: temp = 0x02; break; case 2: temp = 0x04; break; case 3: temp = 0x08; break; case 4: temp = 0x10; break; case 5: temp = 0x20; break; case 6: temp = 0x40; break; case 7: temp = 0x80; break; default: break; } if ((data[index] & temp) == temp) { result[i] = true; } } return result; } private byte[] BindByteData(ushort headCode,byte station,byte functionCode,ushort address, ushort length) { byte[] head = new byte[6]; head[0] = station; // 站地址 head[1] = functionCode; // 功能码 head[2] = BitConverter.GetBytes(address)[1]; // 起始地址 head[3] = BitConverter.GetBytes(address)[0]; head[4] = BitConverter.GetBytes(length)[1]; // 长度 head[5] = BitConverter.GetBytes(length)[0]; return GetSocketBytes(headCode,head); } private byte[] GetSocketBytes(ushort headCode,byte[] head) { byte[] buffers = new byte[head.Length+6]; buffers[0] = BitConverter.GetBytes(headCode)[1]; buffers[1] = BitConverter.GetBytes(headCode)[0]; // 2 和 3位置默认,所以不需要赋值 buffers[4] = BitConverter.GetBytes(head.Length)[1]; buffers[5] = BitConverter.GetBytes(head.Length)[0]; head.CopyTo(buffers, 6); return buffers; } private ResultInformation<string> CheckReceiveBuffer(byte[] send,byte[] receive) { ResultInformation<string> result = new(); if ((send[7] + 0x80) == receive[7]) { var str = FunctionCode.GetDescriptionByErrorCode(receive[8]); result.IsSucceed = false; result.Message = str; } else { result.IsSucceed = true; } return result; } }控制器源码:[Route("api/[controller]/[action]")] [ApiController] public class TestModbusController : ControllerBase { IModbusService _service; public TestModbusController(IModbusService modbusService) { _service = modbusService; } [HttpPost] public IActionResult ReadCoil(ushort address, ushort length) { var ip = IPAddress.Parse("127.0.0.1"); // ip地址 int port = 502; // modbus tcp通信,默认端口 byte station = (byte)((short)1); // 站地址为1 var connectResult = _service.ConnectModbusTcpService(ip,port); if (connectResult.IsSucceed) { // socket连接创建成功 var readResult = _service.ReadCoils(connectResult.Result,6,station,address,length); // 唯一消息码设为6(大于5,且不重复即可) if (readResult.IsSucceed) { if (readResult.Result.Any()) { StringBuilder sb = new StringBuilder(); for(int i = 0; i < readResult.Result.Length; i++) { sb.AppendLine($"[{i}]:{readResult.Result[i]}"); } return Ok(sb.ToString()); } } else { return Ok(readResult.Message); } } else { return Ok(connectResult.Message); } return Ok(); } }功能码和异常码:public class FunctionCode { #region 功能码 public const byte ReadCoil = 0x01; // 读取线圈状态 寄存器PLC地址 00001 - 09999 public const byte ReadInputDiscrete = 0x02; // 读取 可输入的离散量 寄存器PLC地址 10001 - 19999 public const byte ReadRegister = 0x03; // 读取 保持寄存器 40001 - 49999 public const byte ReadInputRegister = 0x04; // 读取 可输入寄存器 30001 - 39999 public const byte WriteSingleCoil = 0x05; // 写单个 线圈 00001 - 09999 public const byte WriteSingleRegister = 0x06; // 写单个 保持寄存器 40001 - 49999 public const byte WriteMultiCoil = 0x0F; // 写多个 线圈 00001 - 09999 public const byte WriteMultiRegister = 0x10; // 写多个 保持寄存器 40001 - 49999 public const byte SelectSlave = 0x11; // 查询从站状态信息 (串口通信使用) #endregion #region 异常码 public const byte FunctionCodeNotSupport = 0x01;// 非法功能码 public const byte DataAddressNotSupport = 0x02;// 非法数据地址 public const byte DataValueNotSupport = 0x03;// 非法数据值 public const byte DeviceNotWork = 0x04;// 从站设备异常 public const byte LongTimeResponse = 0x05;// 请求需要更长时间才能进行处理请求 public const byte DeviceBusy = 0x06;// 设备繁忙 public const byte OddEvenError = 0x08;// 奇偶性错误 public const byte GatewayNotSupport = 0x0A;// 网关错误 public const byte GatewayDeviceResponseTimeout = 0x0B;// 网关设备响应失败 #endregion public static string GetDescriptionByErrorCode(byte code) { switch (code) { case FunctionCodeNotSupport: return "FunctionCodeNotSupport"; case DataAddressNotSupport: return "DataAddressNotSupport"; case DataValueNotSupport: return "DataValueNotSupport"; case DeviceNotWork: return "DeviceNotWork"; case LongTimeResponse: return "LongTimeResponse"; case DeviceBusy: return "DeviceBusy"; case OddEvenError: return "OddEvenError"; case GatewayNotSupport: return "GatewayNotSupport"; case GatewayDeviceResponseTimeout: return "GatewayDeviceResponseTimeout"; default: return "UnknownError"; } } }好了,以上就是该文章的全部内容。如果觉得有帮助,欢迎一键三连啊~~ 如果有兴趣,也可以加我私人微信,欢迎大佬来我微信群做客。私人微信:【WeskyNet001】
前言:随着跨平台越来越流行,.net core支持跨平台至今也有好几年的光景了。但是目前基于.net的跨平台,大多数还是在使用B/S架构的跨平台上;至于C/S架构,大部分人可能会选择QT进行开发,或者很早之前还有一款Mono可以支持.NET开发者进行开发跨平台应用。以下内容,我使用Avalonia UI框架来开发支持可以跨平台的应用程序(仿WPF程序)。 前提准备:开发环境:Win10+VS2022企业版运行环境:Win10 & Ubuntu20.04 LTS.NET环境: .NET 6 以下,正文:0、在开始之前,需要添加一个拓展,名称叫 Avalonia for Visual Studio xxxx。安装完成以后,需要关闭所有当前运行的VS,然后会提示安装。选择安装即可。 1、添加拓展成功以后,在创建新项目里面,创建项目时候,会多出两个项目选项。一个是无双向绑定的项目,另一个是基于MVVM双向绑定的项目。此处,我选择基于MVVM双向绑定的项目。 2、配置项目时候,最好对项目名称进行小写。大写可能Linux系统在识别的时候会有某些意想不到的bug(人品好的可能没有,人品不好的可以自行测试)。此处我的项目名称命名为linuxwpf3、新建的项目,初始项目文件以及代码,如下图所示。4、直接运行,运行以后的画面,如下图所示。 5、咱们改造一下,写一个按钮,然后点击弹出提示框,意思一下。此处需要引入nuget包:MessageBox.Avalonia6、引入nuget包以后,对主窗体页面进行改写,提供了个button,并且在对应的VM里面,添加一个Running方法,用于当做点击触发的绑定方法。同时方法里面提供了一个弹出消息提示框的功能。 7、运行,验证一下,结果如图所示。 8、按钮绑定的方法,还可以传输参数,如下图所示。跟往常传统的WPF双向绑定基本一致。 以上代码:VM部分:public class MainWindowViewModel : ViewModelBase { // public string Greeting => "Welcome to Avalonia!"; public void Running(string msg) { var message = MessageBox.Avalonia.MessageBoxManager.GetMessageBoxStandardWindow("标题",msg); message.Show(); } }axaml部分:<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:linuxwpf.ViewModels" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="linuxwpf.Views.MainWindow" Icon="/Assets/avalonia-logo.ico" Title="linuxwpf"> <Design.DataContext> <vm:MainWindowViewModel/> </Design.DataContext> <!--<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>--> <Button Command="{Binding Running}" CommandParameter="Hello World" Content="点我" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Window>9、运行结果,如下图所示。 10、接下来,咱们把它部署到Linux系统上去实验一下。需要先创建个 xxx.desktop文件,用于指定可执行文件路径、快捷图标路径有关。其中,png图片随便搞一个就行。同时,需要对新增的这俩文件,属性设置为“始终复制”。 以上配置代码:[Desktop Entry] Name=linuxwpf Type=Application Exec=/usr/share/ linuxwpf/linuxwpf Icon=/usr/share/icons/linuxwpf.png 11、在项目文件里面,新增上面俩文件的有关配置,如图所示。 新增的配置代码:<ItemGroup> <Content Include="linuxwpf.png" CopyToPublishDirectory="PreserveNewest"> <LinuxPath>/usr/share/icons/linuxwpf.png</LinuxPath> </Content> <Content Include="linuxwpf.desktop" CopyToPublishDirectory="PreserveNewest"> <LinuxPath>/usr/share/applications/linuxwpf.desktop</LinuxPath> </Content> </ItemGroup> 12、然后,在程序包管理器下,或者shell窗口,或者dos窗口,输入 dotnet tool install --global dotnet-deb该命令的作用是,用于安装一个可以对.net项目进行打包成deb文件的工具。deb格式文件是linux系统下的一种安装包格式之一。 13、指定到在项目目录下,准备进行项目打包。 14、先输入 dotnet deb install 命令,用于下载 deb 工具。15、然后输入 dotnet restore -r linux-x64 命令,用于重置指定的程序运行目标环境,例如 linux-x64 16、最后输入 以下如图所示的命令,进行发布程序。该命令会在根目录下生成release文件夹。其中,指定操作方式是创建Deb文件,目标环境是.net6.0,以及运行时是 linux-x64环境。17、在根目录下,可以看到生成了一个deb文件,只需要把该文件拷贝到指定的linux系统上即可(前提是linux系统是带有图形界面的那种) 18、在远程ubuntu系统上,新建一个测试用的文件夹叫wpf,用于存放上面的deb文件19、使用命令,远程直接拷贝到指定的路径。远程拷贝命令说明:命令:scp -v 远程用户 1@远程地址 1:/文件路径 1/文件 1 远程用户 2@远程地址 2:/文件路径 2解释:从远程服务器 1 上面的文件 1 拷贝到远程服务器2 的文件路径 2 文件夹下 20、拷贝完成,可以开始安装了。21、使用 dpkg -i xxx.deb命令,即可开始安装。如果没有dpkg命令可以用,需要先通过命令 (需要sudo权限) apt-get install -f 进行安装一些基础的组件先。 22、此处安装完成以后,没有显示桌面图标,说明有点小问题,可能原因是xxx.desktop桌面图标文件里面配置的字符编码不是 UTF-8或者某个路径或配置文件配置不标准,大佬们可以自行去研究。此处没有桌面图标,可以进入到安装路径下,在 /usr/shard/程序名称文件夹/ 下,可以找到对应的程序文件,直接运行即可。例如此处我的程序名称是linuxwpf,则直接运行,即打开程序窗口。通过点击按钮,弹出符合预期的提示框,说明该跨平台方案是成功的。 以上就是该博客的所有内容,如果有帮助,欢迎点赞、留言或转发。转发请注明出处:https://www.cnblogs.com/weskynet/
前言:在C/S架构上,WPF无疑已经是“桌面一霸”了。在.NET生态环境中,很多小伙伴还在使用Winform开发C/S架构的桌面应用。但是WPF也有很多年的历史了,并且基于MVVM的开发模式,受到了很多开发者的喜爱。并且随着工业化的进展,以及几年前微软对.NET平台的开源,国内大多数企业的工业系统或上位机系统,也慢慢从使用MFC、QT等C++平台,转向了.NET平台。并且.NET平台上,桌面应用上,WPF由于其独特的一些特性、以及可以制作动画、无损图像等,WPF的占比也越来越高。但是大多数小伙伴可能还是按照开发Winform的传统思路来开发WPF,所以这篇文章当做是一个使用MVVM模式开发的入门教程,希望大家在开发WPF的过程中,可以享受MVVM双向绑定的快乐。 本篇文章有关环境说明:开发环境: VS 2022企业版.NET版本环境:.NET 6开发的操作系统环境:Win 10 1、先创建一个WPF应用程序,环境选择.NET 6 2、创建完成以后,如下图所示。3、再新建一个WPF类库项目,用于存放所有第三方nuget包。此处纯个人习惯,用于防止多项目引用包的时候,产生包版本不一致的问题。 4、包项目里面,添加三个核心的包。分别是:Prism.Unity 、 Prism.Unity.Extensions 和 Unity.Microsoft.DependencyInjection 其中,Prism.Unity 、 Prism.Unity.Extensions 用于提供基础的Prism框架有关的环境以及Unity容器。Unity.Microsoft.DependencyInjection 用于提供可支持属性注入的方式,如果不使用属性注入,也可以不使用。 5、WpfDemo项目里面,引用刚刚的包项目后,修改App.xaml文件里面的默认配置项。以下是默认的内容:6、替换为以下的内容。以下内容代表的是该程序引入prism框架。7、App.cs类里面,继承改为PrismApplication,并且提供几个方法的重写。如果没有重写,可能会提示错误。 8、都载入以后,运行程序,就可以启动画面了。9、项目新建Views文件夹和ViewModels文件夹。prism框架默认会自动识别存在Views文件夹的为视图端,ViewModels文件夹为VM端,用于自动双向绑定的匹配使用。并且VM类与Views视图必须名称对应,VM类的结尾必须是xxxViewModel。先建立一个登录页面,存放于Views文件夹下,然后页面引入prism框架所需的目录,如图所示。同时设置了一个页面名称,该名称后面当做参数进行传递使用。10、新建对应Login窗体的VM类 LoginViewModel,并且继承BindbleBase类,用于提供prism的双向绑定功能。11、提供用户名、密码属性,以及用于按钮触发的事件属性。并且提供了一个模拟用户登录的方法。 12、在Login.xaml文件下,新增两个输入框和一个按钮,用于模拟用户登录功能和双向绑定功能。Mode=TwoWay的意思是,前端数据变更,会自动同步到后端绑定的属性上;后端绑定的属性如果被修改值了,也会传递到前端进行同步显示。还有其他的Mode,小伙伴们可以自行去尝试。Command命令用于绑定事件属性,并且提供了一个参数,把当前页面当做参数传入进去,用于页面跳转使用;如果不需要参数的情况下,直接不需要CommandParameter属性就行。Command命令默认是鼠标单击事件,如果是其他事件需要实现,可以自定义做一些对应的事件的封装来进行实现。其他说明:任意属性都是可以通过双向绑定进行实现的,包括控件名称、以下label控件的content属性、其他属性等等一系列。大佬们可以自行玩玩,此处提供简单案例,所以只对输入框的Text属性和按钮的点击事件提供了双向绑定的功能。 13、在App.cs类里面,提供InitializeShell方法的重写,并且注册Login页面。此处可以实现启动时候打开登录页面,通过提供DialogResult属性以后,就可以打开CreateShell方法里面注册的页面了。 14、现在运行程序,打开了登录页面,进行验证一下,如下图所示,说明验证生效了。输入正确的用户名和密码就可以进入到MainWindow页面。 15、接下来试试依赖注入的使用。先创建一个WPF类库项目,提供一个LoginService类与接口当做服务;并提供UserLogin方法的实现,如下图所示。 16、项目引用以后,提供属性注入。属性注入需要使用public,并且是属性,以及添加 Dependency的标记;如果是构造函数注入,则无需这些步骤。然后在登录方法里面,提供注入方法的使用,如下图所示。 17、在App.cs类里面,先前提供的重写方法 RegisterTypes方法里面,进行服务的注册。以下提供了一个瞬时生命周期的注入,如下所示。如果要使用其他生命周期,大佬们可以自行研究,都是自带的,我就不多写了。18、最后,运行程序,查看效果,程序运行符合预期,说明使用unity ioc容器进行服务注册成功。 19、 后记——有关代码奉上:App.xaml <prism:PrismApplication x:Class="WpfDemo.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfDemo" xmlns:prism="http://prismlibrary.com/"> <Application.Resources> </Application.Resources> </prism:PrismApplication> App.cspublic partial class App : PrismApplication // Application { public App() { } protected override Window CreateShell() { return Container.Resolve<MainWindow>(); } protected override void InitializeShell(Window shell) { if (Container.Resolve<Login>().ShowDialog() == false) { Application.Current?.Shutdown(); } else { base.InitializeShell(shell); } } protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.Register<ILoginService, LoginService>(); // 默认是transient注册 } protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { } LoginViewModel.cspublic class LoginViewModel: BindableBase { [Dependency] // using Unity; public ILoginService _loginService { get; set; } private string userName=""; private string password=""; public string UserName { get { return userName; } set { SetProperty(ref userName, value); } } public string Password { get { return password; } set { SetProperty(ref password, value); } } ICommand? loginCommand; public ICommand LoginCommand { get { if (loginCommand == null) { loginCommand = new DelegateCommand<object>(UserLogin); } return loginCommand; } } void UserLogin(object obj) { if(!_loginService.UserLogin(this.UserName,this.Password)) { MessageBox.Show("用户名或密码错误"); return; } (obj as Window).DialogResult = true; } } Login.xaml<Window x:Class="WpfDemo.Views.Login" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfDemo.Views" xmlns:prism="http://prismlibrary.com/" prism:ViewModelLocator.AutoWireViewModel="True" mc:Ignorable="d" Name="loginWindow" Title="Login" Height="400" Width="600"> <Grid> <Label Content="用户名" HorizontalAlignment="Left" Margin="56,119,0,0" VerticalAlignment="Top"/> <Label Content="密码" HorizontalAlignment="Left" Margin="65,150,0,0" VerticalAlignment="Top"/> <TextBox HorizontalAlignment="Left" Margin="112,124,0,0" TextWrapping="Wrap" Text="{Binding UserName,Mode=TwoWay}" VerticalAlignment="Top" Width="143"/> <TextBox HorizontalAlignment="Left" Margin="112,154,0,0" TextWrapping="Wrap" Text="{Binding Password,Mode=TwoWay}" VerticalAlignment="Top" Width="143"/> <Button Content="登录" HorizontalAlignment="Left" Margin="166,192,0,0" Command="{Binding LoginCommand}" CommandParameter="{Binding ElementName=loginWindow}" VerticalAlignment="Top" Height="24" Width="89"/> </Grid> </Window> LoginService.cspublic class LoginService: ILoginService { public Boolean UserLogin(string userName, string password) { if(userName =="wesky" && password == "123456") { return true; } else { return false; } } } 以上就是本篇文章的全部内容,欢迎大佬们点赞、评论或转发。如需转发,记得注明出处哟~ 谢谢大家围观。
前言:基于Windows系统下的Kafka环境搭建;以及使用.NET 6环境进行开发简单的生产者与消费者的演示。 一、环境部署Kafka是使用Java语言和Scala语言开发的,所以需要有对应的Java环境,以及Scala语言环境。Java环境配置,如果不清楚的,可以查看鄙人的另一篇博客:https://www.cnblogs.com/weskynet/p/14852471.html 1、Scala环境安装,需要先下载Scala语言包,下载地址:https://www.scala-lang.org/download/scala2.html 要选择Binaries版本的环境,否则需要自己编译: 2、Kafka基于Zookeeper环境运行,zookeeper提供给kafka一系列的功能支持,所以还需要安装Zookeeper有关的环境。下载zookeeper地址:https://zookeeper.apache.org/releases.html#download 3、同样,Zookeeper也需要下载带bin 的链接,没有带bin的链接,可能是源码,需要自己编译: 4、接下来是下载主角,Kafka了。下载地址:https://kafka.apache.org/downloads.html 5、同样需要选择下载binary版本,然后根据scala的版本选择对应的版本。 6、下载的三个安装包,如图所示: 7、先安装Scala语言包环境: 8、验证Scala语言包是否安装成功:控制台窗口,输入:scala -version如果提示类似如下有关版本信息,则代表安装成功。 9、然后是安装zookeeper环境。必须先启动zookeeper,才可以使用kafka。安装zookeeper环境,先解压下载的包,然后在解压后的目录下新增data文件夹 10、然后复制data文件夹的绝对路径,备用。在conf文件夹下,编辑cfg文件 11、在cfg文件内,修改dataDir指定为上面新建的data文件夹的绝对路径。注意路径是斜杠/,如果要使用 \ 反斜杆,需要写双反斜杠 \\ 12、也要更改cfg格式的文件名称为 zoo.cfg 否则zookeeper无法识别配置文件。Zoo.cfg文件是zookeeper启动时候自动关联的默认配置文件名称。 13、然后新建环境变量 ZOOKEEPER_HOME: 14、环境变量path新增:%ZOOKEEPER_HOME%\bin 15、启动zookeeper,直接任意打开控制台,输入 zkServer 16、如果都没有报错,一般是启动成功了的。再次验证下,可以任意开个控制台,输入JPS进行查看,如下图所示,有JPS、也有QuorumPeerMain,代表zookeeper启动成功了。 17、Kafka环境安装。先解压,然后在解压后的目录下,新增logs文件夹 18、然后在Config文件夹下,修改 server.properties 文件,修改 log.dirs 的值为 新增的logs文件夹的绝对路径 19、进入到解压后的kafka目录下,在路径栏输入cmd,快速打开当前文件夹下的控制台窗口: 20、输入命令:.\bin\windows\kafka-server-start.bat .\config\server.properties进行启动Kafka服务: 21、启动Kafka报错了,可能是版本问题,kafka一般新版本对windows环境不友好,所以降级一下。此处我把kafka3.0降级为2.8: 22、此处我下载的版本为 2.13-2.8.1,各位大佬们可以按照自己意愿选择版本。可能2.x版本和3.x版本跨度比较大,所以3.0版本没法玩。 23、然后是重复以上配置kafka有关的动作,修改有关配置文件以及新增logs文件夹等。此处省略。 24、接着在低版本的kafka目录下,快速进入当前解压缩的目录下,再次输入有关命令尝试一下: 25、没有提示错误,根据提示信息,代表是启动成功了。任意打开控制台,再输入JPS查看下,可以看到Kafka,确认是启动OK了。 26、然后是要一款Kafka可视化工具,此处我选择使用offset explorer (原来是叫kafka tools,如下载地址所示),下载地址:https://www.kafkatool.com/download.html 27、安装可视化工具,默认可以一直下一步: 28、可以在安装目录下把可执行程序发送到桌面快捷方式,方便打开。 29、一些配置,包括名称、kafka版本、端口号、服务地址等 30、连接以后的效果图,如下。Topic是空的,接下来写点代码。 二、代码开发与测试31、新建类库项目,当作kafka服务类库 32、此处选择标准库2.1,用于可以给多种.net core版本使用,方便兼容。 33、引用 Confluent.Kafka 包。 34、此处新增发布服务类和订阅服务类: 35、新增的生产者发布服务方法代码如下: 代码:/// <summary> /// Description: Kafka生产者发布服务 /// CreateTime: 2022/1/21 19:35:27 /// Author: Wesky /// </summary> public class PublishService: IPublishService { public async Task PublishAsync<TMessage>(string broker, string topicName, TMessage message) where TMessage : class { var config = new ProducerConfig { BootstrapServers = broker, // kafka服务集群,例如 "192.168.0.1:9092,192.168.0.2:9092" 或者单机 "192.168.0.1:9092" Acks = Acks.All, MessageSendMaxRetries = 3, // 发送失败重试的次数 }; using (var producer = new ProducerBuilder<string, string>(config).Build()) { try { string data = Newtonsoft.Json.JsonConvert.SerializeObject(message); var sendData = new Message<string, string> { Key = Guid.NewGuid().ToString("N"), Value = data}; var report = await producer.ProduceAsync(topicName, sendData); Console.WriteLine($"消息 >>>>>: {data} \r\n发送到:{report.TopicPartitionOffset}"); } catch (ProduceException<string, string> ex) { Console.WriteLine($"消息发送失败>>>>>:\r\n Code= {ex.Error.Code} >>> \r\nError= {ex.Message}"); } } } }36、新增的消费者接收服务方法代码如下: 代码:/// <summary> /// Description: kafka 消费者订阅服务 /// CreateTime: 2022/1/21 19:36:25 /// Author: Wesky /// </summary> public class SubscribeService: ISubscribeService { /// <summary> /// 消费者服务核心代码 /// </summary> /// <typeparam name="TMessage"></typeparam> /// <param name="config">消费者配置信息</param> /// <param name="topics">主题集合</param> /// <param name="func"></param> /// <param name="cancellationToken"></param> /// <returns></returns> public async Task SubscribeAsync<TMessage>(ConsumerConfig config, IEnumerable<string> topics, Action<TMessage> func, CancellationToken cancellationToken) where TMessage : class { const int commitPeriod = 1; using (var consumer = new ConsumerBuilder<Ignore, string>(config) .SetErrorHandler((_, e) => { Console.WriteLine($"消费错误 >>>>>: {e.Reason}"); }) .SetStatisticsHandler((_, json) => { Console.WriteLine($"************************************************"); }) .SetPartitionsAssignedHandler((c, partitionList) => { string partitions = string.Join(", ", partitionList); Console.WriteLine($"分配的分区 >>>>> : {partitions}"); }) .SetPartitionsRevokedHandler((c, partitionList) => { string partitions = string.Join(", ", partitionList); Console.WriteLine($"回收的分区 >>>>> : {partitions}"); }) .Build()) { consumer.Subscribe(topics); try { while (true) { try { var consumeResult = consumer.Consume(cancellationToken); if (consumeResult.IsPartitionEOF) { continue; } if(consumeResult?.Offset % commitPeriod == 0){ try { var result = JsonConvert.DeserializeObject<TMessage>(consumeResult.Message?.Value); func(result); // 消费消息 } catch (Exception ex) { Console.WriteLine($"消费业务处理失败: {ex.Message}"); } try { consumer.Commit(consumeResult); // 手动提交 Console.WriteLine($"消费者消费完成,已提交 "); } catch (KafkaException e) { Console.WriteLine($"提交错误 >>>>> : {e.Error.Reason}"); } } } catch (ConsumeException e) { Console.WriteLine($"消费错误>>>>> : {e.Error.Reason}"); } } } catch (Exception e) { Console.WriteLine($"其他错误 >>>>> :{e.Message}"); consumer.Close(); } } await Task.CompletedTask; } }37、并且提供对应的接口服务,用于开放给外部调用,或者提供依赖注入使用: 38、新建一个控制台项目,用来当作消费者端的测试,并且新增一个方法,用来当作消费者接收到消息以后的业务处理方法体。此处控制台环境版本为.NET 6 39、消费客户端代码如下。其中,BootstrapServers也可以提供集群地址,例如 ip1:port,ip2:port…… 服务之间以半角逗号隔开。 40、再新增一个webapi项目,用来当作生产者的客户端进行发送数据。以及对kafka服务类部分进行依赖注入注册,此处使用单例。该webapi此处使用.NET 6环境,带有控制器的模式。 41、新增的控制器里面,进行生产者的注入与实现。注意:topicName参数对应上边的topic-wesky,通过主题绑定,否则消费者不认识就没办法消费到了。 控制器代码:[Route("api/[controller]/[action]")] [ApiController] public class ProducerController : ControllerBase { IPublishService _service = null; public ProducerController(IPublishService publishService) { _service = publishService; } [HttpPost] public IActionResult SendMessage(string broker,string topicName,string message) { _service.PublishAsync(broker, topicName, message); return Ok(); } }42、接下来是一些测试,如图所示: 43、最后,使用可视化管理工具Offset进行查看,可以看到对应的主题。选中主题,可以设置数据类型,这里我设置为字符串,就可以查看到对应的消息内容了。如果没有设置,默认是16进制的数据。 44、查看刚刚测试时候收发的消息队列里面的数据,如下所示: 45、一些额外补充:Kafka也是消息队列的一种,用于在高吞吐量场景下使用比较适合。如果是轻量级的,只需要用于削峰,可以使用RabbitMQ。以上只是简单的操作演示,至于要用得溜,观众朋友们可以自行补充所需的相关理论知识。可视化工具还有一款yahoo提供的开源的工具,叫kafka-manager,有兴趣的大佬们可以自行玩玩,开源地址:https://github.com/yahoo/CMAK 还有一款滴滴平台做的开源的kafka运维管理平台,有兴趣的大佬们也可以自行了解,地址:https://github.com/didi/LogiKM 以上就是该博客的全部内容,感谢各位大佬们的观看~
前言: 今天没有前言。 一、先来一点C++的资源分享,意思一下。1、c++类库源码以及其他有关资源。站点是英文的,英文不好的话可以谷歌浏览器在线翻译。http://www.cplusplus.com/ 2、C++参考手册。页面有点复古,不过有中文版本的。https://zh.cppreference.com/w/cpp 3、GUN C++库,你懂的。https://gcc.gnu.org/onlinedocs/libstdc++/index.html 4、一群大佬分享的一些中文开源资源。https://github.com/jobbole/awesome-cpp-cn 二、接下来是过渡,说点题外话,纯属发牢骚,可以直接跳过去看第三节。 C/C++大概我有五六年没玩了,基本上忘了差不多。主要是入了C#这个坑(比C++多了俩+号)。刚好最近几天在休年假,今天(2021-12-15)略感有点无趣,于是下午就想着撸一下C++,试试手感,但是很久以前的Visual C++6.0这种古老的编译器肯定不太合适了,于是最开始想到了Visual Studio Code。于是先下载了MinGW,下载地址是:https://sourceforge.net/projects/mingw/ 5、安装MinGW以后,把以下全部勾选上。如果遇到不晓得是干嘛的选项,一般默认也勾上,可以减少错误的概率。 6、MinGW安装以后,需要把安装根目录下的bin目录,加到环境变量的Path里面,这样控制台或者Power Shell里面就可以直接使用gcc或者g++命令进行编译了(使用编译器直接跑也可以,但是写命令感觉比较骚一点)。Cmd命令行输入 gcc -v或 g++ -v可以显示版本信息那些,就说明是OK的了。 7、再然后是VS CODE上面安装了C++语言环境,我安装了以下圈起来的那些。 8、然后最下面那个安装以后,可以配置运行按钮。不过我本地没配好,退而求其次,使用了VS CODE右上角的那个三角形来运行。 9、如下,点击既可编译运行。如果是多个文件路径,还需要自行配置进行链接起来。 10、同时也支持在terminal窗口使用命令进行编译。编译以后默认会生成 a.exe文件,如果需要生成指定名称的文件,可以使用 g++ xxx.cpp 指定的名称来实现。 到以上步骤以后,总感觉有点怪怪的,可能是VS CODE我还是更喜欢用来开发一些例如golang、或者前端等的东西比较上手。开发C++总感觉不太上手,也不晓得怎么跨平台发布(实力有限,搞不定)。于是,我最终换成了Visual Studio 2022这个宇宙最强IDE来进行跨平台的开发。 三、以下开始是正文,使用 VS2022 开发远程跨平台的C++程序。 11、首先,我之前申请了一个TX云服务器(CentOS 8.0系统),这下算是派上用场了。先远程过去下点东西。远程linux控制台的命令是 ssh 用户名@ip地址 12、远程过去以后,需要安装ssh server。命令:yum install -y openssl openssh-server。安装它的作用是让它可以被VS 2022编译器远程访问到。 13、进入到ssh的配置文件下做些修改。修改文件:/etc/ssh/sshd_config 14、更改一些信息,将 PermitRootLogin,RSAAuthentication,PubkeyAuthentication的注释打开并且设置为 yes。如果没有找到对应的,也可以直接新增。 15、启动sshd服务,并且设置为开机启动。命令分别是:systemctl start sshd.servicesystemctl enable sshd.service 16、然后安装gdb服务。安装命令:yum -y install gdb gdb-gdbservergdb是linux下常用的调试器,不安装可能导致编译失败或者没法编译。 17、然后是安装g++工具,先检查下g++有没有安装。有些linux系统可能会自带gcc和g++,没有自带咱们就自己安装个好了。 18、通过命令安装 gcc 和 g++。安装命令:sudo yum -y install gcc gcc-c++ kernel-devel 19、安装完毕以后,查看g++版本。显示版本号就代表安装成功了,和在Windows上安装MinGW以后的类似。不过此处的版本是4.x,和win上面的版本对比下貌似老了点,不过暂时不影响,如果不适用C++的新特性应该问题不大。如果需要使用一些新特性,就需要独立安装高版本的g++工具了,这个大佬们可以自行研究。 20、接下来做个实验。首先要对VS2022做一点配置。配置一个可以提供远程访问的东西。工具 - 选项 - 跨平台 - 连接管理器 里面,添加一个远程访问的连接。主机名=ip地址;端口默认22 21、设置完成以后。就可以开始写代码测试了。先使用VS2022新建一个c++项目。此处我使用的是控制台项目。 22、一些配置,在项目的右键属性里面,可以看到配置的远程连接地址被加进去了。并且在远程根目录下,设置了一个文件夹,叫 cpp_projects,设置以后,编译以后的代码和文件都会被丢到这个文件夹下。 23、执行程序的地方,默认也会出现上面配置的远程Linux服务器的IP地址。 24、执行程序,由于没有断电或其他中断操作,所以执行以后成功的话,会直接变回还没有启动的样子。但是此时,运行完毕以后(编译完毕以后),会发现刚才配置的文件夹确实出现在了根目录下。咱们可以在对应的debug里面进行直接运行该程序,打印出了咱们在VS2022上面开发的打印内容。25、增加头文件试一下效果,把头部信息丢到hello.h里面。 26、同时新增了控制台输入,用来测试输入,通过指针输出出来。然后运行程序。运行成功,会显示 部署成功字样,不过一闪而过,容易忽略。 27、远程linux系统上面,试一下效果。 28、瞅一下编译生成的中间文件,Obj文件夹下的xxx.o文件,貌似有点尴尬,乱码了,那就不科普了,大佬们自行玩玩。 29、同样的,在linux系统上面,也可以使用g++命令进行编译。因为代码会被自动远程拷贝过来,所以也可以直接在这上面编译和运行。 30、上面代码带有输入语句,所以控制台会被中断进行停留,此刻还可以在VS上面看到远程调用Linux控制台窗口的输出。不过只要中断没了,就立马程序也就执行完了。 以上就是本篇文章的全部内容,感谢大佬们的围观。
前言:随着.Net6的发布,Minimal API成了当下受人追捧的角儿。而这之前,程序之间通信效率的王者也许可以算得上是gRPC了。那么以下咱们先通过开发一个gRPC服务的教程,然后顺势而为,再接着比拼一下minimal api服务和gRPC服务在通信上的效率。以下,Enjoy:1、创建一个gRPC服务项目。开发模板选项如下图所示。 2、新建项目MyFirstGRPCService,用来开发gRPC服务端使用。 3、选择.Net6 LTS版本。 4、初始项目,自动引用了包 Grpc.AspNetCore,用于提供gRPC基础服务。以及Protos文件夹下有proto文件,services文件夹下有与之对应的类文件。 5、proto文件和对应的类文件的内容比对,以及它们之间的关系。 6、新增一个test.proto文件,定义服务名称和方法名称,以及参数和返回值属性。 7、项目文件,可以看到新增的proto文件被自动引入到项目里面。 8、添加完毕需要生成一下。此处更正个错误,proto文件里面,int类型需要指定为int32,否则会出问题。然后在项目目录下cmd,执行dotnet run一下。 9、执行dotnet run 以后。会在根目录的obj文件夹下,产生一个以Grpc结尾的中间类文件,该文件里面提供了服务有关的内容实现。 10、然后新增一个测试服务类,此时就可以引用自动生成的文件里面提供的Grpc类,该类和proto文件里面定义的保持一致。 11、在服务里面重写方法,以及提供参数。注意空参数也需要传一个Empty类型的参数进去。可以自行比对重写的服务与proto文件的一些关联点。 12、新增一个控制台项目TestConsole,当作客户端,用来做交互使用。并且引入所需要的包,包括Google.Protobuf、Grpc.Net.Client和Grpc.Tools。 13、把protos文件,直接复制到客户端项目目录下。Proto文件起到一个中间连接的作用,类似WCF服务的契约代理类。 14、GRPC服务端的program文件里面,对刚才新增的TestService服务进行注册(类似依赖注入的注册)。 15、打开服务端项目文件,拷贝里面的proto的引用地址。 16、客户端对应的proto内容在人品好的情况下,会在拷贝proto文件的时候自动生成。如果人品不好的情况下,就需要从服务端里面拷贝了。拷贝过来以后,需要把Server改为Client,用来指示该服务是面向客户端的。 17、按照服务端一样的方式,在项目目录下cmd,输入dotnet run,显示Hello,World的时候(控制台程序program里面默认有个输出的,没有删除,所以会显示),在obj路径下会生成中间类文件。服务端生成的是面向服务的的,客户端生成的是面向客户端的。 18、新建一个测试类,用来测试GRPC客户端调用的。客户端调用需要指定访问的GRPC地址,地址当前服务端没有指定,咱们可以在服务端的launchSettings.json文件里面获取到。 19、测试服务类需要静态引用我们在proto文件定义的服务,然后在测试方法里面进行远程过程调用。 20、启动GRPC服务端,然后客户端调用测试方法,并启动,获得到了GRPC服务端返回的结果内容,说明我们搭建的简单的gRPC服务端和客户端程序OK。 21、接下来进行一个对比测试。关于使用webapi和gRPC的访问性能测试。先新建一个minimal api项目:TestPerformanceApi。 22、新增一个POST请求方式的api接口Test,用于做测试使用。为了简单些,不带参数并且只返回两个写死的属性,一个name和一个age。(备注:如果不晓得minimal api的,可以查看我的另一篇关于minimal api的文章)。 23、新增一个控制台程序,用于测试访问minimal api服务使用。有关代码和引用的包,如下图所示。此处循环访问500次进行测试,访问方式使用HttpClientFactory。 24、访问gRPC的服务,也加个循环500次调用。 25、既然都写到这里了,那同时也新增一个传统的webapi项目好了,一起验证下。 26、新增一个测试传统webapi的项目TestTraditionalApi,然后新增一个Test2的api控制器,有关代码如图所示。为了保持和minimal api地址一致,减少其他可能性损耗,route路由也直接指定了action。 27、再新增一个控制台项目,用来测试访问传统webapi。代码同测试mini api的控制台程序,只是访问的URL地址不一样。 28、启动gRPC服务、minimal API服务、以及传统webapi服务。为了防止先启动的控制台占据资源优势,以及避免启动项的项目占据资源优势,新增一个无任何功能的项目当作启动项,然后服务全部从 调试-启动新实例 里面进行打开。 29、访问500次gRPC服务结果: 30、访问500次 minimal api服务结果: 31、访问500次传统webapi服务结果。 32、为了防止其他可能性干扰,我把控制台输出其他信息全部屏蔽掉,并且循环次数新增到2000次,并且均测试两次,降低首次访问创建实例期间的延迟。其他代码雷同,均增加至2000次*2,并取消控制台打印。 33、先测试gRPC服务的结果。第一个2000次访问,耗时4399ms,第二次耗时3140ms. 34、然后是minimal api。第一个2000次使用了53347ms,第二个2000次使用了52459ms. 35、访问传统的webapi,第一个2000次使用了92025ms,第二个2000次访问,使用了90627ms. 36、gRPC效率有点偏高,为了防止可能是网络震荡导致的问题,最后再重新测一次。第一个2000次使用了68709ms,第二个2000次使用了65987ms 37、结论:由此可见,在没有任何其他限制情况下,minimal api的访问效率最高,gRPC服务次之,传统webapi最慢。gRPC此处是传入了参数,所以也有可能增加了些许时差,有兴趣的小伙伴可以自行继续测试。本轮测试使用的开发环境是VS2022企业版,运行环境全部都是.NET 6,本地机器配置(五年多的老古董了),可以参考如下截图: 38、以上就是本文章的全部内容。欢迎大佬们留言、点赞或转发推广~~
本篇文章接前一篇,建议可以先看前篇文章,再看本文,会有更好的效果。前一篇跳转链接:https://www.cnblogs.com/weskynet/p/15046999.html 正文:Autofac通过构造函数注入如前一篇所示,获取实例都是通过构造函数进行。此处通过构造函数获取实例,还有一种通过构造函数传入IServiceProvider进行获取。该方法可以极大减少构造函数传入的实例过多所导致的构造函数参数臃肿。示例直接使用前篇项目做拓展,在控制器的测试api下面,直接使用。有关示例如下图所示: 设置断点,并运行程序查看效果。可见IWeskyTest接口已经被注入进来,并且可以访问到 Autofac通过属性注入方式在ServiceA实现类里面,添加IServiceB、IServiceC的属性。并且在ServiceA实现类里面,添加一个测试方法 Hey(),在里面对以上两个属性所对应的接口方法进行调用。代码如下: 以及Hey需要加入到抽象类接口IServiceA:对IServiceA\B\C进行服务注册。其中,提供属性的服务,注册时候必须使用PropertiesAutowired方式,如下面代码所示:接着改写控制器里面的Test方法进行测试。对应代码以及解释和对应的运行结果如下图所示:Autofac通过方法注入方式改写上面ServiceA的类为如下代码。有关代码说明如图: 对用到的IServiceA和B进行服务注册。如图所示,ServiceA里面提供了方法注入,所以需要在注册A服务的时候,使用OnActivated方法。其中,RegisterService是ServiceA服务里面提供的需要当做方法注入的方法,方法里面的IServiceB是需要被方法注入的抽象类(接口)。以下使用了瞬时,也可以使用其他的,没有限制,包括ServiceB服务注册时候,也可以使用非单例模式,不做限制。运行程序,如果先后打印ServiceA 和 ServiceB,则代表方法注入成功。运行程序结果如下:Filter过滤器里面实现支持依赖注入 先编写一个过滤器WeskyFilter,继承自 ActionFilterAttribute。并且在里面添加一个属性注入的IServiceC和一个构造函数注入的IServiceD。然后在OnActionExecuting和OnActionExecuted方法下面实现一个打印的内容,并且分别打印ServiceC实例和ServiceD实例下面的Hello方法。代码如下:对IServiceC、D进行服务注册,以及注册WeskyFilter过滤器,用以支持依赖注入:在控制器里面的Test方法上面,添加过滤器标记,并直接运行进行结果验证: 如图所示,打印出过滤器里面的内容,并且成功访问到了ServiceC和D的Hello方法,代表在过滤器里面实现依赖注入也是可以的。 以上就是本篇文章的全部内容,谢谢观看。
使用SoapCore实现在.net core平台下开发webservice;以及使用HttpClientFactory动态访问webservice。首先,需要在包项目下面引用SoapCore: 然后新建项目Wsk.Core.WebService,用于开发webservice有关功能。新项目下,需要先引用package项目,然后新建一个IWeskyWS接口,以及提供了三个Hello方法(webservice有可能不支持重载,如果后面无法进行服务引用,可以更改为Hello1,Hello2,Hello3),用于实验使用。其他介绍,如下图标注所示: 该部分代码:[ServiceContract] public interface IWeskyWS { [OperationContract] string Hello1(); [OperationContract] string Hello2(string name); [OperationContract] string Hello3(NameInfo info); } public class WeskyWS : IWeskyWS { public string Hello1() { return "Hello"; } public string Hello2(string name) { return $"Hello, {name}"; } public string Hello3(NameInfo info) { return $"Hello,{info.Name}, Age is {info.Age}"; } } [DataContract] public class NameInfo { [DataMember] public int Age { get; set; } [DataMember] public string Name { get; set; } } 现在转到启动项目下,引用该项目。然后在启动项里面,添加服务注入: 在Configure下,添加UseSoapEndpoint,以及有关注释,如图注释部分: 启动程序,并且在浏览器下指定对应的asmx地址,如果有提示下方的xml文档,则代表启动成功。 现在咱们新建一个基于.net framework的控制台项目,用来做测试使用。创建完毕以后,结构如下: 现在通过引用服务的方式进行引用一下: 在Main方法下面调用webservice,并打印,结果如下:注意有个坑:使用SoapCore开发的该Webservice,目前只能通过添加服务引用的方式被识别。使用动态访问方式,会无法访问。如果其他小伙伴解决了该问题,欢迎留言。 接下来提供一个简单的使用.net core通过HttpClientFactory来访问Webservice的方法。注意还有一个坑:该方法目前仅针对于webservice方法参数不存在实体类的情况下。如果是复杂数据,目前暂时不支持,或者是我当前未找到行之有效的方法,也欢迎各位大佬留言评论,提供更加有效的法子。 由于上面使用soapCore开发的webservice目前只能被服务引用,所以此处不对其做动态访问测试有兴趣的可以自行尝试。我先创建一个使用.net framework开发的webservice。新建一个Asp.Net Web应用程序,配置如下图: 创建以后,添加一个新建项,选择web服务,用以开发webservice测试方法:创建成功,以后,结构如下图,以及会有一个默认的HelloWorld方法。现在加点测试方法,带一个参数的Hello1,以及带两个参数的Hello2:运行以后,如果有以下页面,说明该webservice开发成功: 现在切换回Wsk.Core项目,在启动项目的控制器里面,新建一个webapi,,用来触发访问webservice的方法,进行有关验证。先添加HttpClientFactory的依赖注入: 在此之前,还需要在启动项里面,添加对HttpClient的注册: 现在在新增的webapi里面,做一些访问webservice的实现。先新建一个动态访问webservice的方法:方法代码:private String CallWebservice(string url, Dictionary<string, string> dictionary) { HttpContent content; if (dictionary != null) { content = new FormUrlEncodedContent(dictionary); } else { content = new StringContent(""); } string result = string.Empty; try { using (HttpClient client = _httpClientFactory.CreateClient()) { using (var response = client.PostAsync(url,content).Result) { if(response.StatusCode == System.Net.HttpStatusCode.OK) { result = response.Content.ReadAsStringAsync().Result; //XmlDocument xml = new XmlDocument(); //xml.LoadXml(result); //result = xml.InnerText; } } } } catch(Exception ex) { result = $"Error:{ex.Message}"; } return result; }其中,注释部分是用于获取xml内数据使用的,为了看完整的数据,所以做了注释。有兴趣的可以打开注释进行尝试。 然后在TestCallWS这个api下面对以上三个webservice方法进行访问:该webapi代码:[HttpPost] public IActionResult TestCallWS() { string url = "http://localhost:8435/WeskyService.asmx/"; string method = "HelloWorld"; string wsUrl = $"{url}{method}"; string value1 = CallWebservice(wsUrl, null); Dictionary<string, string> dic = new Dictionary<string, string>(); method = "Hello1"; wsUrl = $"{url}{method}"; dic.Add("name", "Wesky"); string value2 = CallWebservice(wsUrl, dic); dic = new Dictionary<string, string>(); method = "Hello2"; wsUrl = $"{url}{method}"; dic.Add("age", "3"); dic.Add("name", "WESKY"); string value3 = CallWebservice(wsUrl, dic); return Ok($"{value1}\n{value2}\n{value3}"); }启动程序,并且在swagger上面进行调用,看看结果: 访问成功,教程结束。 以上写法也不是最好的写法,以及使用.net core开发webservice如何能够被动态访问、以及在.net core上如何动态访问带实体参数的方法,目前还需要进一步探讨。也欢迎大佬们踊跃提供可行的技术方向。感谢大家抽时间看完该文章,希望对大家能有一点帮助。先前也有一期使用HttpClient和HttpWebRequest进行访问webapi的文章,如果有兴趣也可以莅临指导:https://www.cnblogs.com/weskynet/p/14856130.html 到此完毕,谢谢观看。
使用Logstash通过Rabbitmq接收Serilog日志到ES首先,要部署logstash为了与前面的ElasticSearch版本保持一致,此处Logstash下载的版本也是7.13.1,下载地址:https://artifacts.elastic.co/downloads/logstash/logstash-7.13.1-windows-x86_64.zip 解压以后,修改一些配置:在config目录下,修改jvm.options文件,设置内存占用最小值和最大值:如果配置比较低,建议配置成512MB即可,如果电脑或服务器配置比较好,那就请随意。如果只是普通用途,比如记录普通日志啥的,配个4G内基本足够了。在config里面,有一个logstash-sample.conf文件,可以当做参考配置,随后咱们新建一个用于接收RabbitMQ的配置文件。先来写代码~~在package包项目下,新增引用 Serilog.Sinks.RabbitMQ组件: 然后,在Program文件下面,添加serilog日志写入到RabbitMQ的一些配置:以上代码如下:logger.WriteTo.RabbitMQ((clientConfiguration, sinkConfig) => { clientConfiguration.Username = "wesky"; clientConfiguration.Password = "wesky123"; clientConfiguration.Exchange = "WeskyExchange"; clientConfiguration.ExchangeType = "direct"; clientConfiguration.DeliveryMode = RabbitMQDeliveryMode.Durable; clientConfiguration.RouteKey = "WeskyLog"; clientConfiguration.Port = 5672; clientConfiguration.Hostnames.Add("127.0.0.1"); sinkConfig.TextFormatter = new JsonFormatter(); });以上为了方便,所以写死了,大佬们可以写到配置文件里面去进行读取,这样好一点。然后,程序启动时候,进行主动创建一个Exchange为WeskyExchange的,RouteKey是WeskyLogs的消息队列,包括生产者和消费者。之前有做过简单的RabbitMQ创建的案例,所以直接在原来的基础上做一些改动:设置了两个RouteKey:WeskyLog和WeskyLog2,以及两个队列 Log1和Log2。咱们主要使用WeskyLog和 Log1。在消费者监听上面,做个过滤,对于队列是Log1的消息,直接返回不做处理,这样做到目的是消息不被消费,让logstash来消费消息:现在开始配置logstash,上面有一个logstash-sample.conf文件,拷贝一分,重命名为 rabbitmq.conf 然后往里面更改一些配置信息,如下: logstash部分配置代码:input { rabbitmq { host => "127.0.0.1" port => 5672 user => "wesky" password => "wesky123" queue => "Log1" key => "WeskyLog" exchange => "WeskyExchange" durable => true } } filter { grok { match => {"Timestamp" => "%{TIMESTAMP_ISO8601:ctime}"} add_field => ["create_time","%{@timestamp}"] } date { match => ["ctime","yyyy-MM-dd HH:mm:ss.SSS","ISO8601"] target => "@timestamp" } mutate { remove_field => ["@version","Properties","Timestamp","ctime"] rename => {"MessageTemplate" => "message"} rename => {"Level" => "level"} } ruby { code => "event.set('create_time',event.get('@timestamp').time.localtime)" } } output { elasticsearch { hosts => ["http://localhost:9200"] index => "log-%{+YYYYMMdd}" } } 注意,配置不能使用Tab,必须只能用空格,每个缩进俩空格。现在写一个测试的webapi,来看看效果。创建一个webapi,记录两条日志,一条是Infomaton,一条是Error:现在启动Wsk.Core程序,试着跑一下看看效果: 哦吼,才发现有其他的日志,所以直接打开RabbitMQ,可以看到日志被写入到了MQ里面,而且因为没有消费,所以队列一直在增加。咱们现在启动一下logstash。启动方式如下图,具体地址那些,需要根据自己具体的目录而定:可以看见,左边的消息,一下子被消费完毕,说明logstash应该是获取到MQ消息了。现在我们看一下ElasticSearch上面,是否有消息: 查询log-20210629,可以看到对应的日志信息,说明写入ES成功。在kibana上面,选择Discover,然后创建一个log的筛选,用于查询所有以log开头到索引: 刚添加会有点乱,咱们选择只查看create_time、level和message字段信息:显示内容有点乱,debug信息也都记录了,咱们把这部分过滤掉再启动。配置文件里面,修改最小日志级别为Information: 再启动程序,查看效果,瞬间清爽~~~现在通过上面的webapi,写两个日志看看效果:控制台有信息了,现在去ES上面看下日志信息: 可以看见日志也有了。现在试一下自带搜索引擎的查询的效果: 说明查询也是OK的。 另外需要注意一点:我这边索引还是log-20210629,但是实际上已经是2021年6月30日的0点24分,这个是因为ES默认是0区,咱们中国是东八区,所以会自动少8个小时进行存储。Kibana上面查询的日志时间却正常的,这个是因为Kibana默认会读取浏览器的时区,自动帮我们转换进行显示了。如果搜索日志时候,发现搜索的是单个字,没有词组那些,那可能是因为没有添加中文分词的原因。添加中文分词以及中文分词插件,可以加群索取哦~~
搭建基于Quartz组件的定时调度任务先在package包项目下,添加Quartz定时器组件: 新建类库项目Wsk.Core.QuartzNet,并且引用包类库项目。然后新建一个中间调度类,叫QuartzMiddleJob:中间Job源码:public class QuartzMiddleJob : IJob { private readonly IServiceProvider _serviceProvider; public QuartzMiddleJob(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task Execute(IJobExecutionContext context) { using (var scope = _serviceProvider.CreateScope()) { var jobType = context.JobDetail.JobType; var job = scope.ServiceProvider.GetRequiredService(jobType) as IJob; await job.Execute(context); } } } 新建一个Job工厂类,叫WeskyJobFactory,用来获取刚刚创建的中间调度类的服务: 新建一个通用执行计划类,叫WeskyJobSchedule,用于每次任务都通过该计划进行生成:计划类和枚举源码: public class WeskyJobSchedule { public WeskyJobSchedule(Type jobType, string cronExpression) { this.JobType = jobType ?? throw new ArgumentNullException(nameof(jobType)); CronExpression = cronExpression ?? throw new ArgumentNullException(nameof(cronExpression)); } /// <summary> /// Job类型 /// </summary> public Type JobType { get; private set; } /// <summary> /// Cron表达式 /// </summary> public string CronExpression { get; private set; } /// <summary> /// Job状态 /// </summary> public JobStatus JobStatu { get; set; } = JobStatus.Init; } /// <summary> /// 运行状态 /// </summary> public enum JobStatus : byte { [Description("Initialization")] Init = 0, [Description("Running")] Running = 1, [Description("Scheduling")] Scheduling = 2, [Description("Stopped")] Stopped = 3, } 现在添加一个任务,新建任务类 MyJobs,并且继承自IJob,然后在Excute方法里面,就是该任务执行调度时候会进去执行的了:似乎上面哪儿感觉不太对,咱们把原来的Job工厂里面到代码稍微调整下如下:NewJob源码:public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob; } 现在新增一个静态类QuartzJobService,用来当做调度任务的中间启动项,并且把有关的一些服务注册进来:对应源码:public static class QuartzJobService { public static void AddQuartzJobService(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } services.AddSingleton<IJobFactory, WeskyJobFactory>(); services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>(); services.AddSingleton<QuartzMiddleJob>(); services.AddSingleton<MyJobs>(); services.AddSingleton( new WeskyJobSchedule(typeof(MyJobs), "0/1 * * * * ? ") ); services.AddHostedService<WeskyJobHostService>(); } } 最后,还少了个启动项,用来程序启动的时候,进行启动定时调度任务。新建类 WeskyJobHostService ,并且新建创建调度任务方法 CreateJob和触发器方法CreateTrigger: 然后,在开始和结束方法内: 以上源码如下:public class WeskyJobHostService: IHostedService { private readonly ISchedulerFactory _schedulerFactory; private readonly IJobFactory _jobFactory; private readonly IEnumerable<WeskyJobSchedule> _jobSchedules; public WeskyJobHostService(ISchedulerFactory schedulerFactory, IJobFactory jobFactory, IEnumerable<WeskyJobSchedule> jobSchedules) { _schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory)); _jobFactory = jobFactory ?? throw new ArgumentNullException(nameof(jobFactory)); _jobSchedules = jobSchedules ?? throw new ArgumentNullException(nameof(jobSchedules)); } public IScheduler Scheduler { get; set; } public async Task StartAsync(CancellationToken cancellationToken) { Scheduler = await _schedulerFactory.GetScheduler(cancellationToken); Scheduler.JobFactory = _jobFactory; foreach (var jobSchedule in _jobSchedules) { var job = CreateJob(jobSchedule); var trigger = CreateTrigger(jobSchedule); await Scheduler.ScheduleJob(job, trigger, cancellationToken); jobSchedule.JobStatu = JobStatus.Scheduling; } await Scheduler.Start(cancellationToken); foreach (var jobSchedule in _jobSchedules) { jobSchedule.JobStatu = JobStatus.Running; } } public async Task StopAsync(CancellationToken cancellationToken) { await Scheduler?.Shutdown(cancellationToken); foreach (var jobSchedule in _jobSchedules) { jobSchedule.JobStatu = JobStatus.Stopped; } } private static IJobDetail CreateJob(WeskyJobSchedule schedule) { var jobType = schedule.JobType; return JobBuilder .Create(jobType) .WithIdentity(jobType.FullName) .WithDescription(jobType.Name) .Build(); } private static ITrigger CreateTrigger(WeskyJobSchedule schedule) { return TriggerBuilder .Create() .WithIdentity($"{schedule.JobType.FullName}.trigger") .WithCronSchedule(schedule.CronExpression) .WithDescription(schedule.CronExpression) .Build(); } }切回QuartzJobService,在 AddQuartzJobService 方法的最下方,添加上面启动服务的注册: 最后,在启动项目里面,添加对Wsk.CoreQuartz项目的引用,然后在WskService服务类下,添加对AddQuartzJobService服务的注册:启动项目,看看效果:由此可见,我们设置的每秒一次触发效果达成。为了检验是不是可以避免同一个调度任务产生并发,在调度任务方法里面,设置一个延时,看看效果:运行结果:说明在当前任务还没有完成的情况下,不会重复进入。如果要允许重复进,只需要把类上面的DisallowConcurrentExecution 标签注释掉就可以。现在还原回去,然后在Cron表达式改写成定时10秒,看看效果:运行结果: 以上就是本篇使用QuartzNet的全部内容,仅用于入门参考。对于其他定时用法、以及各种比较飘的使用,各位大佬可以自行变种。如果有什么建议或意见,也欢迎留言~~
搭建RabbitMQ简单通用的直连方法 如果还没有MQ环境,可以参考上一篇的博客:https://www.cnblogs.com/weskynet/p/14877932.html 接下来开始.net core操作Rabbitmq有关的内容。我打算使用比较简单的单机的direct直连模式,来演示一下有关操作,基本套路差不多。首先,我在我的package包项目上面,添加对RabbitMQ.Client的引用: 在Common文件夹下,新建类库项目 Wsk.Core.RabbitMQ,并且引用package项目: 在启动项目下的appsettings配置文件里面,新增一个访问RabbitMQ的配置信息: 配置部分代码:"MQ": [ { "Host": "127.0.0.1", // MQ安装的实际服务器IP地址 "Port": 5672, // 服务端口号 "User": "wesky", // 用户名 "Password": "wesky123", // 密码 "ExchangeName": "WeskyExchange", // 设定一个Exchange名称, "Durable": true // 是否启用持久化 } ]然后,在实体类项目下,新建实体类MqConfigInfo,用于把读取的配置信息赋值到该实体类下: 实体类代码:public class MqConfigInfo { public string Host { get; set; } public int Port { get; set; } public string User { get; set; } public string Password { get; set; } public string ExchangeName { get; set; } public bool Durable { get; set; } } 在刚刚新建的RabbitMQ类库项目下面,引用该实体类库项目,以及APppSettings项目。然后新建一个类,叫做ReadMqConfigHelper,以及它的interface接口,并且提供一个方法,叫ReadMqConfig,用来进行读取配置信息使用:读取配置信息类代码:public class ReadMqConfigHelper:IReadMqConfigHelper { private readonly ILogger<ReadMqConfigHelper> _logger; public ReadMqConfigHelper(ILogger<ReadMqConfigHelper> logger) { _logger = logger; } public List<MqConfigInfo> ReadMqConfig() { try { List<MqConfigInfo> config = AppHelper.ReadAppSettings<MqConfigInfo>(new string[] { "MQ" }); // 读取MQ配置信息 if (config.Any()) { return config; } _logger.LogError($"获取MQ配置信息失败:没有可用数据集"); return null; } catch (Exception ex) { _logger.LogError($"获取MQ配置信息失败:{ex.Message}"); return null; } } } 接着,新建类MqConnectionHelper以及接口IMqConnectionHelper,用于做MQ连接、创建生产者和消费者等有关操作: 然后,新增一系列创建连接所需要的静态变量: 然后,设置两个消费者队列,用来测试。以及添加生产者连接有关的配置和操作: 然后,创建消费者连接方法: 其中,StartListener下面提供了事件,用于手动确认消息接收。如果设置为自动,有可能导致消息丢失: 然后,添加消息发布方法: 在interface接口里面,添加有关的接口,用于等下依赖注入使用: 连接类部分的代码: public class MqConnectionHelper:IMqConnectionHelper { private readonly ILogger<MqConnectionHelper> _logger; public MqConnectionHelper(ILogger<MqConnectionHelper> logger) { _logger = logger; _connectionReceiveFactory = new IConnectionFactory[_costomerCount]; _connectionReceive = new IConnection[_costomerCount]; _modelReceive = new IModel[_costomerCount]; _basicConsumer = new EventingBasicConsumer[_costomerCount]; } /* 备注:使用数组的部分,是给消费端用的。目前生产者只设置了一个,消费者可能存在多个。 当然,有条件的还可以上RabbitMQ集群进行处理,会更好玩一点。 */ private static IConnectionFactory _connectionSendFactory; //RabbitMQ工厂 发送端 private static IConnectionFactory[] _connectionReceiveFactory; //RabbitMQ工厂 接收端 private static IConnection _connectionSend; //连接 发送端 private static IConnection[] _connectionReceive; //连接 消费端 public static List<MqConfigInfo> _mqConfig; // 配置信息 private static IModel _modelSend; //通道 发送端 private static IModel[] _modelReceive; //通道 消费端 private static EventingBasicConsumer[] _basicConsumer; // 事件 /* 设置两个routingKey 和 队列名称,用来做测试使用*/ public static int _costomerCount = 2; public static string[] _routingKey = new string[] {"WeskyNet001","WeskyNet002" }; public static string[] _queueName = new string[] { "Queue001", "Queue002" }; /// <summary> /// 生产者初始化连接配置 /// </summary> public void SendFactoryConnectionInit() { _connectionSendFactory = new ConnectionFactory { HostName = _mqConfig.FirstOrDefault().Host, Port = _mqConfig.FirstOrDefault().Port, UserName = _mqConfig.FirstOrDefault().User, Password = _mqConfig.FirstOrDefault().Password }; } /// <summary> /// 生产者连接 /// </summary> public void SendFactoryConnection() { if (null != _connectionSend && _connectionSend.IsOpen) { return; // 已有连接 } _connectionSend = _connectionSendFactory.CreateConnection(); // 创建生产者连接 if (null != _modelSend && _modelSend.IsOpen) { return; // 已有通道 } _modelSend = _connectionSend.CreateModel(); // 创建生产者通道 _modelSend.ExchangeDeclare(_mqConfig.FirstOrDefault().ExchangeName, ExchangeType.Direct); // 定义交换机名称和类型(direct) } /// <summary> /// 消费者初始化连接配置 /// </summary> public void ReceiveFactoryConnectionInit() { var factories = new ConnectionFactory { HostName = _mqConfig.FirstOrDefault().Host, Port = _mqConfig.FirstOrDefault().Port, UserName = _mqConfig.FirstOrDefault().User, Password = _mqConfig.FirstOrDefault().Password }; for (int i = 0; i < _costomerCount; i++) { _connectionReceiveFactory[i] = factories; // 给每个消费者绑定一个连接工厂 } } /// <summary> /// 消费者连接 /// </summary> /// <param name="consumeIndex"></param> /// <param name="exchangeName"></param> /// <param name="routeKey"></param> /// <param name="queueName"></param> public void ConnectionReceive(int consumeIndex, string exchangeName, string routeKey, string queueName) { _logger.LogInformation($"开始连接RabbitMQ消费者:{routeKey}"); if (null != _connectionReceive[consumeIndex] && _connectionReceive[consumeIndex].IsOpen) { return; } _connectionReceive[consumeIndex] = _connectionReceiveFactory[consumeIndex].CreateConnection(); // 创建消费者连接 if (null != _modelReceive[consumeIndex] && _modelReceive[consumeIndex].IsOpen) { return; } _modelReceive[consumeIndex] = _connectionReceive[consumeIndex].CreateModel(); // 创建消费者通道 _basicConsumer[consumeIndex] = new EventingBasicConsumer(_modelReceive[consumeIndex]); _modelReceive[consumeIndex].ExchangeDeclare(exchangeName, ExchangeType.Direct); // 定义交换机名称和类型 与生产者保持一致 _modelReceive[consumeIndex].QueueDeclare( queue: queueName, //消息队列名称 durable: _mqConfig.FirstOrDefault().Durable, // 是否可持久化,此处配置在文件中,默认全局持久化(true),也可以自定义更改 exclusive: false, autoDelete: false, arguments: null ); // 定义消费者队列 _modelReceive[consumeIndex].QueueBind(queueName, exchangeName, routeKey); // 队列绑定给指定的交换机 _modelReceive[consumeIndex].BasicQos(0, 1, false); // 设置消费者每次只接收一条消息 StartListener((model, ea) => { byte[] message = ea.Body.ToArray(); // 接收到的消息 string msg = Encoding.UTF8.GetString(message); _logger.LogInformation($"队列{queueName}接收到消息:{msg}"); Thread.Sleep(2000); _modelReceive[consumeIndex].BasicAck(ea.DeliveryTag, true); }, queueName, consumeIndex); } /// <summary> /// 消费者接收消息的确认机制 /// </summary> /// <param name="basicDeliverEventArgs"></param> /// <param name="queueName"></param> /// <param name="consumeIndex"></param> private static void StartListener(EventHandler<BasicDeliverEventArgs> basicDeliverEventArgs, string queueName, int consumeIndex) { _basicConsumer[consumeIndex].Received += basicDeliverEventArgs; _modelReceive[consumeIndex].BasicConsume(queue: queueName, autoAck: false, consumer: _basicConsumer[consumeIndex]); // 设置手动确认。 } /// <summary> /// 消息发布 /// </summary> /// <param name="message"></param> /// <param name="exchangeName"></param> /// <param name="routingKey"></param> public static void PublishExchange(string message, string exchangeName, string routingKey = "") { byte[] body = Encoding.UTF8.GetBytes(message); _modelSend.BasicPublish(exchangeName, routingKey, null, body); } } 现在,我把整个Wsk.Core.RabbitMQ项目进行添加到依赖注入: 然后,在启动项目里面的初始化服务里面,添加对MQ连接的初始化以及连接,并且发送两条消息进行测试: 启用程序,提示发送成功: 打开RabbitMQ页面客户端,可以看见新增了一个交换机WeskyExchange: 点进去可以看见对应的流量走势: 关闭程序,现在添加消费者的初始化和连接,然后重新发送: 可见发送消息成功,并且消费者也成功接收到了消息。打开客户端查看一下: 在WeskyExchange交换机下,多了两个队列,以及队列归属的RoutingKey分别是WeskyNet001和WeskyNet002。以及在Queue目录下,多了两个队列的监控信息: 为了看出点效果,我们批量发消息试一下: 然后启动项目,我们看一下监控效果。先是交换机页面的监控: 然后是队列1的监控: 现在换一种写法,在消费者那边加个延迟: 并且生产者的延迟解除: 再启动一下看看效果: 会发现队列消息被堵塞,必须在执行完成以后,才可以解锁。而且生产者这边并不需要等待,可以看见消息一次性全发出去了,可以继续执行后续操作: 以上就是关于使用Direct模式进行RabbitMQ收发消息的内容,发送消息可以在其他类里面或者方法里面,直接通过静态方法进行发送;接收消息,启动了监听,就可以一直存活。如果有兴趣,也可以自己尝试Fanout、Topic等不同的模式进行测试,以及可以根据不同的机器,进行配置成收发到不同服务器上面进行通信。
.net core操作ES进行读写数据操作 在Package包项目下,新增NEST包。注意,包版本需要和使用的ES的版本保持一致,可以避免因为不兼容所导致的一些问题。例如我本机使用的ES版本是7.13版本,所以我安装的NEST包也是7.13版本: 在Common文件夹下,新建类库项目 Wsk.Core.ElasticSearch,并新建类ElasticSearchConnection,用于提供一些操作方法。以及新建一个对应的接口IElasticSearchConnection。然后引用包项目,以及AppHelper项目备用: 在Entity项目下,新建一个ES配置实体类,叫ElasticConnectionInfo,以及在appsettings配置文件下,新增一组ES的连接配置信息,包括索引和url地址: 代码:public class ElasticConnectionInfo { public string Url { get; set; } public string Index { get; set; } } "ES": [ { "Index": "wesky", "Url": "http://localhost:9200" } ] 然后,在ElasticSearchConnection类下面,添加一些构造依赖注入,以及添加一个连接方法ESConnection: 接着新建一个实体类 ElasticTestDataInfo,用于做一个模拟数据测试: 代码:public class ElasticTestDataInfo { public int Code { get; set; } public string Function { get; set; } public string Message { get; set; } } 现在,在启动项目下面,新建文件夹WskHostedService,用于存放启动项有关内容。以及新建一个类,叫InitialService,并且继承自 IHostedService, IDisposable: 该方法是用于项目启动时候执行的,我们把连接ES的部分,写到这里面来。似乎接口用不到,把ElasticSearchConnection继承的IElasticSearchConnection屏蔽掉,然后把ESConnection方法设置为静态的,然后在上面创建的类中的StartAsync下面,进行ES的初始化连接: 然后在WskService类里面,通过使用AddHostedService添加对该初始化服务的注册: 现在配置完毕,在控制器里面,分别添加单个写入和批量写入的api,大概内容如下: 我配置文件里面的索引是wesky,为了确保实验效果,我先使用kibana的页面进行查询是否wesky索引是否有内容: 没有wesky索引,可以开始启动程序(备注:正常使用期间不需要删除索引,会导致写入的数据丢失,我此处只是为了方便效果验证使用)。先启动程序,为了查看连接是否成功,我把连接成功信息打印出来: 至此,连接ES部分的类代码如下: public class ElasticSearchConnection { public static ElasticClient _esClient; //private readonly ILogger<ElasticSearchConnection> _logger; //public ElasticSearchConnection(ILogger<ElasticSearchConnection> logger) //{ // _logger = logger; //} public static void ESConnection() { List<ElasticConnectionInfo> configInfo = AppHelper.ReadAppSettings<ElasticConnectionInfo>(new string[] { "ES" }); if (configInfo.Any()) { var settings = new ConnectionSettings(new Uri(configInfo.FirstOrDefault().Url)) .DefaultIndex(configInfo.FirstOrDefault().Index); _esClient = new ElasticClient(settings); Console.WriteLine("ES已连接"); } else { Console.WriteLine("ES连接未配置\n"); } } } 现在通过swagger写入一个数据看看效果: Swagger上面调用成功,我们上kibana页面上进行查询看看是否真的写入成功了: 如图所示,说明写入成功了。接下来测试批量写入的,我们发送两条记录进行测试: 使用kibana页面进行查询,看看是不是都写入成功了: 如上,说明写入成功!需要注意的一点是,写入ES里面,ES默认有1秒时间是查不出来的,需要1秒以后才可以查到记录,对于实时性不是特别高,所以不适合用于做数据库,但是用于对实时性要求不高的数据来说,问题就不大了。 现在不使用kibana进行查询,使用es自带的工具进行查询看看效果:在es根目录下,bin文件夹下有一个elasticsearch-sql-cli.bat文件,双击即可打开: 该工具可以进行使用SQL语句进行查询,咱来示范下通过它来查询刚刚写入的三个数据。由于索引就是一个表,所以我直接使用 select * from wesky; 进行查询,效果如下: 与常见数据库操作几乎一样,我们来个倒序: 接下来在程序上进行查询,先创建一个查询的api进行调用,然后查询出结果进行打印,内容如下: 运行程序,我输入name,进行匹配,返回查询结果: 出于时间关系,就不再演示显示条数的内容了,各位大佬可以自行测试。由于ES本身也支持sql查询,所以接下来演示下使用SQL语句进行查询的效果。先新建一个实体类EsSearchSql,里面只有一个query字段,用于存放查询的sql语句使用 然后新增一个api,用于可以在swagger上面输入sql语句进行查询,有关内容如下: 至此,控制器类代码整体如下:[Route("[controller]/[action]")] [ApiController] public class WSKController : ControllerBase { private readonly ITestAutofac _autofac; private readonly ILogger<WSKController> _logger; private readonly IRedisManage _redis; private readonly IHttpClientHelper _httpClient; private readonly IHttpWebRequestHelper _httpWebRequestHelper; public WSKController(ITestAutofac autofac, ILogger<WSKController> logger, IRedisManage redis, IHttpClientHelper httpClient, IHttpWebRequestHelper httpWebRequestHelper) { _autofac = autofac; _logger = logger; _redis = redis; _httpClient = httpClient; _httpWebRequestHelper = httpWebRequestHelper; } [HttpPost] public IActionResult IndexSingle([FromBody] ElasticTestDataInfo info) { ElasticSearchConnection._esClient.IndexDocument(info); return Ok("OK"); } [HttpPost] public IActionResult IndexMany([FromBody] List<ElasticTestDataInfo> info) { ElasticSearchConnection._esClient.IndexMany<ElasticTestDataInfo>(info); return Ok("OK"); } [HttpPost] public IActionResult SearchByLinq(string name) { var searchResponse = ElasticSearchConnection._esClient.Search<ElasticTestDataInfo>(s => s .From(0) .Size(2) .Query(q => q .Match(m => m .Field(f => f.Message) .Query(name) ) ) ); var info = searchResponse.Documents; return Ok(info); } [HttpPost] public IActionResult SearchBySql(string sql) { string res = string.Empty; try { string url = "http://127.0.0.1:9200/_xpack/sql?format=csv"; // format=csv,可以过滤掉其他不需要的内容,仅返回报表格式 EsSearchSql esData = new() { query = sql}; // 赋值对应查询的sql语句 string jsonData = JsonConvert.SerializeObject(esData); res = _httpClient.Post(url, jsonData); // 使用post进行发送查询请求 } catch(Exception ex) { res = ex.Message; } return Ok(res); } } 启动程序,我输入 select * from wesky 当作参数进行查询,看下效果: 如上,说明使用SQL查询也成功了。使用两种方法都可以查询,看个人喜好了~~ 同时,如果需要做一些可视化报表什么的,也都是可以使用的。一些更详细的内容,就不再做过多操作了,大佬们可以自己玩,祝大家好运~~
开发通用的访问webapi方法。 在common工具文件夹下,新建一个类库项目:Wsk.Core.WebHelper,并引用Package包项目,然后新建一个类HttpClientHelper,用于使用HttpClient方法进行访问webapi: 新建一个接口IHttpClientHelper,用于HttpClientHelper继承该接口。然后接口内新增一个返回泛型类型的通用的POST访问webapi的方法: 接着,在HttpClientHelper类里面,进行对该方法的实现:说明:虽然使用了using,可以自动释放资源;但是难免还是需要一点时间。在finally下面通过手动释放资源,比自动释放资源,资源释放率会更快,在并发场景下,性能会更好一点点。当然,此处可以不适用using,因为手动释放了,以上纯属个人喜好的风格写法。再来一个使用Basic加密进行访问的通用方法,写法如上,具体请看代码示例。先新建带用户名和密码参数的接口: 然后,在HttpClientHelper里面进行对应的实现: 以上为使用POST的方式进行,如果需要使用例如GET、PUT等,可以自行尝试,写法类似。接口代码:public interface IHttpClientHelper { /// <summary> /// 通用访问webapi方法 /// </summary> /// <param name="url"></param> /// <param name="data"></param> /// <returns></returns> T Post<T>(string url, string data); /// <summary> /// 带用户名和密码的通用访问webapi方法 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="url"></param> /// <param name="data"></param> /// <param name="account">用户名</param> /// <param name="pwd">密码</param> /// <returns></returns> T Post<T>(string url, string data, string account, string pwd); }实现类代码:public class HttpClientHelper:IHttpClientHelper { readonly ILogger<HttpClientHelper> _logger; public HttpClientHelper(ILogger<HttpClientHelper> logger) { _logger = logger; } public T Post<T>(string url, string data) { var result = default(T); using (HttpClient client = new HttpClient()) { try { client.Timeout = new TimeSpan(0, 0, 10); // 10是秒数,用于设置超时时长 HttpContent content = new StringContent(data); content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); client.DefaultRequestHeaders.Connection.Add("keep-alive"); Task<HttpResponseMessage> res = client.PostAsync(url, content); if (res.Result.StatusCode == System.Net.HttpStatusCode.OK) { result = JsonConvert.DeserializeObject<T>(res.Result.Content.ReadAsStringAsync().Result); } else { _logger.LogError($"访问webapi方法状态码错误:\r url:{url} \r data:{data} \r 状态码:{res.Result.StatusCode}"); } } catch (Exception ex) { _logger.LogError($"访问webapi方法异常:\r url:{url} \r data:{data} \r 异常信息:{ex.Message}"); } finally { client.Dispose(); } } return result; } public T Post<T>(string url, string data, string account, string pwd) { var result = default(T); using (HttpClient client = new HttpClient()) { try { string authorization = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(account + ":" + pwd)); client.DefaultRequestHeaders.Add("authorization", authorization); client.Timeout = new TimeSpan(0, 0, 10); HttpContent content = new StringContent(data); content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); Task<HttpResponseMessage> res = client.PostAsync(url, content); if (res.Result.StatusCode == System.Net.HttpStatusCode.OK) { result = JsonConvert.DeserializeObject<T>(res.Result.Content.ReadAsStringAsync().Result); } else { _logger.LogError($"访问带用户名参数的webapi方法状态码错误:\r url:{url} \r data:{data} \r 状态码:{res.Result.StatusCode}"); } } catch (Exception ex) { _logger.LogError($"访问带用户名参数的webapi方法异常:\r url:{url} \r data:{data} \r 异常信息:{ex.Message}"); } finally { client.Dispose(); } } return result; } }现在再新建一个使用HttpWebRequest的通用访问webapi的方式。在WebHelper项目下面,新建 HttpWebRequestHelper类,以及IHttpWebRequestHelper接口:在接口里面,新建一个通用的泛型类型的访问webapi的请求接口:然后,在HttpWebRequestHelper类里面,进行有关的实现:HttpWebRequest没有Dispose方法,所以此处没有使用using写法,最后需要手动使用Abort方法进行释放资源。接口代码:public interface IHttpWebRequestHelper { /// <summary> /// 通用方法 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="url"></param> /// <param name="data"></param> /// <param name="method">默认POST</param> /// <returns></returns> T Request<T>(string url, string data, string method = "POST"); }接口实现代码:public class HttpWebRequestHelper:IHttpWebRequestHelper { readonly ILogger<HttpWebRequestHelper> _logger; public HttpWebRequestHelper(ILogger<HttpWebRequestHelper> logger) { _logger = logger; } public T Request<T>(string url, string data,string method="POST") { var result = default(T); HttpWebRequest request = null; try { request = (HttpWebRequest)WebRequest.Create(url); var byteData = Encoding.UTF8.GetBytes(data); request.Method = method; request.ContentType = "application/json"; request.ContentLength = data.Length; request.Timeout = 10000; // 超时时间,毫秒单位 using (var stream = request.GetRequestStream()) { stream.Write(byteData, 0, data.Length); } var response = (HttpWebResponse)request.GetResponse(); // 发送请求 using (var resStream = response.GetResponseStream()) // 读取数据 { using (var reader = new StreamReader(resStream, Encoding.UTF8)) { result = JsonConvert.DeserializeObject<T>(reader.ReadToEnd()); } } } catch (Exception ex) { _logger.LogError($"使用HttpWebRequest访问webapi方法异常:\r url:{url} \r data:{data} \r 异常信息:{ex.Message}"); } finally { if (request != null) { request.Abort(); // 释放资源 } } return result; } }现在开发两个webapi进行测试。首先把该类库项目,添加到启动项目的项目引用里面。然后,在启动项目里面的AutofacRegister里面,添加对Wsk.Core.WebHelper类库项目的所有接口进行依赖注入注册:注册部分代码:var assemblysWebhelper = Assembly.Load("Wsk.Core.WebHelper"); // container.RegisterAssemblyTypes(assemblysWebhelper) .SingleInstance() .AsImplementedInterfaces() .EnableInterfaceInterceptors();新建一个实体类,用来当作参数和返回值的测试: 接着,在控制器里面写几个测试方法进行测试,测试内容如下:控制器部分代码:[Route("[controller]/[action]")] [ApiController] public class WSKController : ControllerBase { private readonly ITestAutofac _autofac; private readonly ILogger<WSKController> _logger; private readonly IRedisManage _redis; private readonly IHttpClientHelper _httpClient; private readonly IHttpWebRequestHelper _httpWebRequestHelper; public WSKController(ITestAutofac autofac, ILogger<WSKController> logger, IRedisManage redis, IHttpClientHelper httpClient, IHttpWebRequestHelper httpWebRequestHelper) { _autofac = autofac; _logger = logger; _redis = redis; _httpClient = httpClient; _httpWebRequestHelper = httpWebRequestHelper; } [HttpPost] public IActionResult HelloWorld(string url1,string url2) { TestWebHelperInfo info = new TestWebHelperInfo(); info.name = "Hello"; var value1 = _httpClient.Post<TestWebHelperInfo>(url1,JsonConvert.SerializeObject(info)); info.name = "World"; var value2 = _httpWebRequestHelper.Request<TestWebHelperInfo>(url2, JsonConvert.SerializeObject(info)); return Ok($"value1:{value1.name} value2:{value2.name}"); } [HttpPost] public IActionResult Test1([FromBody] TestWebHelperInfo info) { return Ok(info); } [HttpPost] public IActionResult Test2([FromBody] TestWebHelperInfo info) { return Ok(info); } }运行,然后测试一下1和2接口是否可以使用,如果可以使用,拷贝对应的url地址,当作参数传给主测试api里面。获得到请求的url地址前缀是:http://localhost:35678/WSK/,带入参数进行验证: 由此可见,两个通用方法都可用。备注:如果不适用泛型,也可以直接使用返回String即可,不需要进行类型转换。
.net core 编写通用的Redis功能在 Package项目里面,添加包:StackExchange.Redis:在Common工具文件夹下,新建 Wsk.Core.Redis类库项目,并新建 RedisManage 类和对应接口 IRedisManage,如下图。然后,在该项目里面,引用共用包项目Wsk.Core.Package,用以使用Redis有关功能。在RedisManage类里面,新增几个用于连接Redis配置的全局变量,以及一个构造函数:在配置文件里面,新建Redis的有关配置信息:配置的Json文本:"Redis": [ { "Name": "Wesky", "Ip": "127.0.0.1", "Port": 6379, "Password": "wesky123", "Timeout": 30, "Db": 3 } ] 其中,Name是别名,可以任意起。Ip是Redis的服务端地址,例如安装本地,就是127.0.0.1,端口号Port默认是6379,密码可以通过Redis安装的根目录下的配置文件进行设置,Timeout是连接的超时时间,Db是使用Redis的DB区,一般Redis的DB区默认是0到15。注意:此处的配置使用的是数组,用于将来进行Redis分布式操作的可拓展。看下redis安装目录下的配置文件局部一览: 如果打算修改该配置文件来实现修改有关的配置信息,但是没有效果,注意在windows服务里面把redis服务进行重新启动。如果还不行,就把另外一个conf配置文件也做同样的操作,然后再重启redis服务。 接着,在解决方案下,新建一个文件夹,叫DataEntities,该文件夹当做将来存放各种实体类或有关项目的目录。现在咱们在该新建的文件夹下,新建一个类库项目:Wsk.Core.Entity,然后新建一个实体类,叫RedisConfig,用于读取到配置文件的Redis信息进行赋值使用: 回到Redis类库项目,在RedisManage类里面,添加一个方法ReadRedisSetting,用于读取该配置信息,并赋值给ConfigurationOptions类: ReadRedisSetting代码: private ConfigurationOptions ReadRedisSetting() { try { List<RedisConfig> config = AppHelper.ReadAppSettings<RedisConfig>(new string[] { "Redis" }); // 读取Redis配置信息 if (config.Any()) { ConfigurationOptions options = new ConfigurationOptions { EndPoints = { { config.FirstOrDefault().Ip, config.FirstOrDefault().Port } }, ClientName = config.FirstOrDefault().Name, Password = config.FirstOrDefault().Password, ConnectTimeout = config.FirstOrDefault().Timeout, DefaultDatabase = config.FirstOrDefault().Db, }; return options; } return null; } catch(Exception ex) { _logger.LogError($"获取Redis配置信息失败:{ex.Message}"); return null; } }然后,新建一个方法ConnectionRedis,用于连接Redis:方法代码:private ConnectionMultiplexer ConnectionRedis() { if (this._redisConnection != null && this._redisConnection.IsConnected) { return this._redisConnection; // 已有连接,直接使用 } lock (_redisConnectionLock) { if (this._redisConnection != null) { this._redisConnection.Dispose(); // 释放,重连 } try { this._redisConnection = ConnectionMultiplexer.Connect(_configOptions); } catch (Exception ex) { _logger.LogError($"Redis服务启动失败:{ex.Message}"); } } return this._redisConnection; }在构造函数里面,进行Redis连接的实现: 现在,做个简单的测试,新建一个Set方法和一个GetValue方法,用于测试效果:在IRedisMagane接口里面,添加以上有关的接口方法:现在转到启动项目上,启动项目新增对Wsk.Core.Redis项目的引用,并且在WskService类下面,新增对刚刚新增的Redis管理类的依赖注入的注册:现在,在控制器里面,添加有关构造函数的依赖注入,然后做个实践,验证下是否成功。先改写控制器内容,先设置一对key/value,然后进行读取并返回: 启动项目,输入Hello Redis! ,然后查看返回对应的内容: 我们打开Redis管理客户端看看,是不是可以看见有这个值:因为我们配置的是Db = 3,所以在Db3这个地方,可以看见我们的Key,点击Tesk,即可在右边窗口看见对应的内容。说明Redis操作成功。最后,咱们对Redis管理类进行一些其他功能的添加,以及接口方法的添加,用于完善它的功能链,包括移除、读取实体数据、清空、异步操作等。以下截图为对应的接口方法展示:接口部分源码:public interface IRedisManage { /// <summary> /// 设置一个 键值对 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="ts"></param> void Set(string key, object value, TimeSpan ts); /// <summary> /// //获取 Reids 缓存值 /// </summary> /// <param name="key"></param> /// <returns></returns> string GetValue(string key); /// <summary> /// 获取序列化值 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="key"></param> /// <returns></returns> TEntity Get<TEntity>(string key); /// <summary> /// 判断Key是否存在 /// </summary> /// <param name="key"></param> /// <returns></returns> bool Get(string key); /// <summary> /// 移除某个Key和值 /// </summary> /// <param name="key"></param> void Remove(string key); /// <summary> /// 清空Redis /// </summary> void Clear(); /// <summary> /// 异步获取 Reids 缓存值 /// </summary> /// <param name="key"></param> /// <returns></returns> Task<string> GetValueAsync(string key); /// <summary> /// 异步获取序列化值 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="key"></param> /// <returns></returns> Task<TEntity> GetAsync<TEntity>(string key); Task SetAsync(string key, object value, TimeSpan cacheTime); Task<bool> GetAsync(string key); /// <summary> /// 异步移除指定的key /// </summary> /// <param name="key"></param> /// <returns></returns> Task RemoveAsync(string key); /// <summary> /// 异步移除模糊查询到的key /// </summary> /// <param name="key"></param> /// <returns></returns> Task RemoveByKey(string key); /// <summary> /// 异步全部清空 /// </summary> /// <returns></returns> Task ClearAsync(); }接口实现部分的整体源码: public class RedisManage : IRedisManage { public volatile ConnectionMultiplexer _redisConnection; private readonly object _redisConnectionLock = new object(); private readonly ConfigurationOptions _configOptions; private readonly ILogger<RedisManage> _logger; public RedisManage(ILogger<RedisManage> logger) { _logger = logger; ConfigurationOptions options = ReadRedisSetting(); if (options == null) { _logger.LogError("Redis数据库配置有误"); } this._configOptions = options; this._redisConnection = ConnectionRedis(); } private ConfigurationOptions ReadRedisSetting() { try { List<RedisConfig> config = AppHelper.ReadAppSettings<RedisConfig>(new string[] { "Redis" }); // 读取Redis配置信息 if (config.Any()) { ConfigurationOptions options = new ConfigurationOptions { EndPoints = { { config.FirstOrDefault().Ip, config.FirstOrDefault().Port } }, ClientName = config.FirstOrDefault().Name, Password = config.FirstOrDefault().Password, ConnectTimeout = config.FirstOrDefault().Timeout, DefaultDatabase = config.FirstOrDefault().Db, }; return options; } return null; } catch(Exception ex) { _logger.LogError($"获取Redis配置信息失败:{ex.Message}"); return null; } } private ConnectionMultiplexer ConnectionRedis() { if (this._redisConnection != null && this._redisConnection.IsConnected) { return this._redisConnection; // 已有连接,直接使用 } lock (_redisConnectionLock) { if (this._redisConnection != null) { this._redisConnection.Dispose(); // 释放,重连 } try { this._redisConnection = ConnectionMultiplexer.Connect(_configOptions); } catch (Exception ex) { _logger.LogError($"Redis服务启动失败:{ex.Message}"); } } return this._redisConnection; } public string GetValue(string key) { return _redisConnection.GetDatabase().StringGet(key); } public void Set(string key, object value, TimeSpan ts) { if (value != null) { _redisConnection.GetDatabase().StringSet(key, JsonConvert.SerializeObject(value), ts); } } public void Clear() { foreach (var endPoint in this.ConnectionRedis().GetEndPoints()) { var server = this.ConnectionRedis().GetServer(endPoint); foreach (var key in server.Keys()) { _redisConnection.GetDatabase().KeyDelete(key); } } } public bool Get(string key) { return _redisConnection.GetDatabase().KeyExists(key); } public TEntity Get<TEntity>(string key) { var value = _redisConnection.GetDatabase().StringGet(key); if (value.HasValue) { //需要用的反序列化,将Redis存储的Byte[],进行反序列化 return JsonConvert.DeserializeObject<TEntity>(value); } else { return default(TEntity); } } public void Remove(string key) { _redisConnection.GetDatabase().KeyDelete(key); } public bool SetValue(string key, byte[] value) { return _redisConnection.GetDatabase().StringSet(key, value, TimeSpan.FromSeconds(120)); } public async Task ClearAsync() { foreach (var endPoint in this.ConnectionRedis().GetEndPoints()) { var server = this.ConnectionRedis().GetServer(endPoint); foreach (var key in server.Keys()) { await _redisConnection.GetDatabase().KeyDeleteAsync(key); } } } public async Task<bool> GetAsync(string key) { return await _redisConnection.GetDatabase().KeyExistsAsync(key); } public async Task<string> GetValueAsync(string key) { return await _redisConnection.GetDatabase().StringGetAsync(key); } public async Task<TEntity> GetAsync<TEntity>(string key) { var value = await _redisConnection.GetDatabase().StringGetAsync(key); if (value.HasValue) { return JsonConvert.DeserializeObject<TEntity>(value); } else { return default; } } public async Task RemoveAsync(string key) { await _redisConnection.GetDatabase().KeyDeleteAsync(key); } public async Task RemoveByKey(string key) { var redisResult = await _redisConnection.GetDatabase().ScriptEvaluateAsync(LuaScript.Prepare( //模糊查询: " local res = redis.call('KEYS', @keypattern) " + " return res "), new { @keypattern = key }); if (!redisResult.IsNull) { var keys = (string[])redisResult; foreach (var k in keys) _redisConnection.GetDatabase().KeyDelete(k); } } public async Task SetAsync(string key, object value, TimeSpan cacheTime) { if (value != null) { await _redisConnection.GetDatabase().StringSetAsync(key, JsonConvert.SerializeObject(value), cacheTime); } } public async Task<bool> SetValueAsync(string key, byte[] value) { return await _redisConnection.GetDatabase().StringSetAsync(key, value, TimeSpan.FromSeconds(120)); } }由于时间关系,就不一一列举各自的功能了,各位大佬们可以自行尝试。最后,说个小窍门:Key值如果使用冒号,在客户端上面查看就会自动变成分层结构。举个例子,改写控制器,原本设置的Test这个Key,改写为Test:Wesky: 然后,启动程序,让他执行一下,用于生成一条记录,例如此处我写入:Hello Key’s Rules:由此可见,Key值分层显示了。在一些场合下,可以使用冒号来分隔开不同层级的Key,以便于在客户端查看。
添加通用读取配置文件功能在Wsk.Core.Package项目下,新增Microsoft.Extensions.Configuration包:在启动项目下,设置appsettings.json属性为始终复制: 新建一个文件夹Common,用于存放工具类项目。并且新建项目:Wsk.Core.AppSettings,引用package包项目,然后新建一个读取配置文件的通用类,叫AppHelper。目录结构如图: 在AppHelper类里面,新建静态操作方法有关代码,用于读取根目录下的配置文件信息:代码: public class AppHelper { private static IConfiguration _config; public AppHelper(IConfiguration configuration) { _config = configuration; } /// <summary> /// 读取指定节点的字符串 /// </summary> /// <param name="sessions"></param> /// <returns></returns> public static string ReadAppSettings(params string[] sessions) { try { if (sessions.Any()) { return _config[string.Join(":", sessions)]; } } catch { return ""; } return ""; } /// <summary> /// 读取实体信息 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="session"></param> /// <returns></returns> public static List<T> ReadAppSettings<T>(params string[] session) { List<T> list = new List<T>(); _config.Bind(string.Join(":", session), list); return list; } }在启动项目下,新建文件夹ConfigServices,用于存放各种服务的添加项目。现在,看下目前的启动项下的ConfigureServices方法:我们把该方法做个简化。在ConfigServices下新建一个静态类,叫 WskService,用于写入各种自带的方法进行集成;再新建一个静态类SwaggerService,用于存放Swagger功能的集成:然后,把swagger的功能进行移植,在SwaggerService类下面进行注册:再把上面有一个添加控制器的功能进行注册到WskService下面。然后,把对swagger的注册也加入到该服务下: 最后,在ConfigureServices下面把所有内容删掉,然后添加WskServices的注册:接下来,添加对刚刚我们写的读取配置文件类的注册。在ConfigureServices里面进行添加注册单例模式,放在注册服务的最上面,这样其他服务就可以在注册时候也可以引用该功能进行读取配置文件了: 现在做个测试,在配置文件里面新建一段配置信息:json内容:"Test": { "A": "Hello", "B": { "C": "World" } } 在控制器方法里面,做个打印测试: 示例代码:[HttpPost] public IActionResult HelloWorld() { string a= AppHelper.ReadAppSettings("Test", "A"); string b = AppHelper.ReadAppSettings("Test", "B", "C"); return Ok($"{a} ***** {b}"); }启动程序,并执行api,看看返回的结果:
使用Serilog来实现日志记录 先安装Serilog六件套神装包:也可以对个别相应的包进行删除等,都是可以的。例如,标注的1是读取配置文件的,如果不需要通过配置文件进行操作,就可以使用这个包。2是打印到控制台的,如果不需要打印到控制台,也可以不引用。3是写入到文件的,如果不需要写入到文件,也是可以不提供的。我在此处全部引入,方便可以使用多种日志记录方法。Async是异步写入日志,一般需要引入。我们先在启动项目的Program类里面,新增一些对Serilog的支持操作: 然后,在控制器里面添加对Logger的依赖注入,并写一些不同日志等级的日志记录功能: 然后启动项目,并执行一下该api方法,查看到日志打印的结果:由于默认消息记录级别是Warn,所以debug消息类型就看不见了。以及写入文本的日志,由于设定输出到本目录,所以可以在解决方案里面直接看见: 接下来使用配置文件的方式进行日志操作:本部分serilog配置文件代码:"Serilog": { "MinimumLevel": { "Default": "Debug", //最小日志记录级别 "Override": { //系统日志最小记录级别 "Default": "Warning", "System": "Warning", "Microsoft": "Warning" } }, "WriteTo": [ { "Name": "Console" }, //输出到控制台 { "Name": "Async", //异步写入日志 "Args": { "configure": [ { "Name": "File", //输出文件 "Args": { "path": "log/log.txt", "outputTemplate": "{NewLine}Date:{Timestamp:yyyy-MM-dd HH:mm:ss.fff}{NewLine}LogLevel:{Level}{NewLine}Class:{SourceContext}{NewLine}Message:{Message}{NewLine}{Exception}", "rollingInterval": "3" //日志文件生成精度:1:年 2:月 3:日 4:小时 } } ] } } ] }在Program里面,注释掉原先的代码,然后新增一条通过配置文件进行读取的语句: 因为配置文件里面设置的最小默认等级是Debug,所以现在可以全部打印出来:配置文件里面配置的rollingInterval值为3,代表每天生成;path代表根目录,设置log/log.txt代表根目录下的log文件夹,按照每天(日期)生成的log开头的txt日志文件。所以我们可以看见在根目录下产生了一个日志文件log20210530.txt:以上,代表Serilog通过配置文件成功。其中,还可以通过配置文件+启动项配置两个打配合进行配置出更适合自己的日志记录风格,此处不再赘述,欢迎自己尝试。另外,Serilog还可以实现seq可视化功能,不过seq是收费的,所以这边不做演示。也可以通过ElasticSearch+Kibana进行开发,实现日志可视化和日志搜索引擎功能,该部分功能敬请期待,将来会有这部分教程放出,现在还没到时候。 本篇有关源码:Program: public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } /// <summary> /// Serilog日志模板 /// </summary> static string serilogDebug = System.Environment.CurrentDirectory + "\\Log\\Debug\\.log"; static string serilogInfo = System.Environment.CurrentDirectory + "\\Log\\Info\\.log"; static string serilogWarn = System.Environment.CurrentDirectory + "\\Log\\Warning\\.log"; static string serilogError = System.Environment.CurrentDirectory + "\\Log\\Error\\.log"; static string serilogFatal = System.Environment.CurrentDirectory + "\\Log\\Fatal\\.log"; static string SerilogOutputTemplate = "{NewLine}时间:{Timestamp:yyyy-MM-dd HH:mm:ss.fff}{NewLine}日志等级:{Level}{NewLine}所在类:{SourceContext}{NewLine}日志信息:{Message}{NewLine}{Exception}"; public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseServiceProviderFactory(new AutofacServiceProviderFactory()) // 添加Autofac .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>() .UseUrls("http://*:35678") .UseSerilog((context, logger) =>//注册Serilog { logger.ReadFrom.Configuration(context.Configuration); logger.Enrich.FromLogContext(); //logger.WriteTo.Console(); // 输出到Console控制台 //// 输出到配置的文件日志目录 //logger.WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Debug).WriteTo.Async(a => a.File(serilogDebug, rollingInterval: RollingInterval.Hour, outputTemplate: SerilogOutputTemplate))) // .WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Information).WriteTo.Async(a => a.File(serilogInfo, rollingInterval: RollingInterval.Hour, outputTemplate: SerilogOutputTemplate))) // .WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Warning).WriteTo.Async(a => a.File(serilogWarn, rollingInterval: RollingInterval.Hour, outputTemplate: SerilogOutputTemplate))) // .WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Error).WriteTo.Async(a => a.File(serilogError, rollingInterval: RollingInterval.Hour, outputTemplate: SerilogOutputTemplate))) // .WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Fatal).WriteTo.Async(a => a.File(serilogFatal, rollingInterval: RollingInterval.Hour, outputTemplate: SerilogOutputTemplate))); }); }); }WSKController:[Route("api/[controller]")] [ApiController] public class WSKController : ControllerBase { private readonly ITestAutofac _autofac; private readonly ILogger<WSKController> _logger; public WSKController(ITestAutofac autofac, ILogger<WSKController> logger) { _autofac = autofac; _logger = logger; } [HttpPost] public IActionResult HelloWorld() { // _autofac.Test(); _logger.LogInformation("Info Message……"); _logger.LogWarning("Warning Message……"); _logger.LogDebug("Debug Message……"); _logger.LogError("Error Message……"); return Ok(); } }
使用Docker部署应用程序首先确保已经安装Docker 桌面软件,如下图: 然后,把需要部署到Docker上面的项目,咱们先添加Docker的支持,启动项目右键 -> 添加 -> Docker支持,选择 Linux 然后会自动生成Dockerfile文件,在里面可以看见Docker有关的配置信息。 我们需要对Dockerfile的配置文件做一些更改: 然后修改dockerfile文件的属性: 在启动项目的Properties下,修改launchSetting.Json启动项的配置文件,把默认启动的端口号也改成和docker默认的端口号一样的: 接着,在Program下面,CreateHostBuilder里面绑定指定的端口号: 然后,在Startup里面,Configure下面需要做点修改:swagger默认是写在 if (env.IsDevelopment())判断语句里面的,代表使用编译器启动,才会进来。我们把swagger有关的写到外面去,不然发布以后,可能会看不到swagger的页面: 一切准备就绪,我们准备发布一下:项目右键,选择发布,选择发布到本地文件夹: 随便先自定义一个文件夹,例如我存放在D盘的Wsk_Publish下面: 对发布的一些选项进行更改,把删除现有文件的选项设为 True: 然后,右上角点击发布,发布成功即可: 下面是把发布的程序部署到docker上的关键操作了。按住windows按键+X,使用管理员权限启动 power shell,然后定位到发布的程序的根目录下: 然后,使用 docker build -t 镜像别名(例如我起个别名,叫wesky) .注意最后面需要加空格,然后一个点然后就会自动下载有关的一些资源。为了看清文字,我调了下背景色,以及执行效果如下: 由于使用的是默认的,所以可能拉取资源会比较慢,也可以在Dockerfile配置文件把默认使用microsoft的地址改为其他的地址,这里不提供该方面的镜像地址,因为我都是用原生的。等待一段时间,都拉取好了以后,我们打开docker客户端,可以看见镜像已经安装好了:我们现在用命令来启动它: docker run -d -p 35678:35678 --name wesky wesky命令说明,见图中说明。 运行成功,会显示出一串16进制字符串,代表OK了。这个时候,切换回docker客户端,可以看见程序正在运行: 点击正在运行的镜像,就可以打开一些监控页面,包括日志、资源占用等信息: 功能按钮信息,待自己去发现。现在,我们试试在本机上打开swagger,并执行之前的api进行打印一串符号。地址是本机ip+设置的端口号: 可以打开页面,并且测试成功,到此部署程序到docker圆满结束,撒花~~ 备注:如果发现docker无法使用,请确认是否开启系统虚拟化,怎么开启在该系列的第一篇文章有教程。如果虚拟化已启动,就看看docker当前是linux还是windows容器。我这边使用的是linux容器,如果你是windows容器需要切换Linux,需要在电脑桌面docker图标右键,选择 switch to linux container……,如果显示的是 switch to windows container…… 就代表你已经使用的是linux容器了。
Autofac的简单使用: 由于将来可能引用很多包,为了保持统一队形,我们再新建一个类库项目Wsk.Core.Package,当做包的引用集合:删掉Class1,把Wsk.Core、Wsk.Core.Filter里面到包删掉,引用到Package里面,然后需要用到包的项目,都引用package这个类库项目。这样可以防止将来项目多了,版本环境如果不一致导致的版本冲突。更改以后的目录架构:添加依赖注入的两个关键包:Autofac.Extensions.DependencyInjection 和 Autofac.Extras.DynamicProxy 然后在Program添加Autofac的支持:接着,新建一个类库项目,例如叫做 Wsk.Core.Service,然后新建一个类,叫做TestAutofac,以及对应的接口 ITestAutofac,以及一个方法叫做Test,用于做依赖注入实现的测试使用: 接下来,在启动项目里面添加该依赖项目,然后在Startup里面的ConfigureServices方法里面,添加对ITestAutofac的单例注册实现: 改写先前的WSK控制器,添加构造函数的依赖注入,以及修改或新增一个用于测试的方法,此处改写了HelloWorld方法,为了看出效果,只打印以上方法的输出内容:测试一下效果:执行程序,并在swagger上面手动调用:显示预期内容,代表依赖注入成功。接下来,使用注入整个类库来实现依赖注入,用于接口和类很多的情况下,为了方便,就可以把类库下面的所有接口全部暴露出来进行依赖注入。在启动项目里面,新建一个文件夹,叫Common,在里面新建类AutofacRegister,并继承Autofac.Module ,在类下面重写 Load 方法:在startup里面,注释掉上面的实现方法,新建ConfigureContainer容器: 最后进行启动测试测试成功,完结撒花~~ 最后总结一点东西:AddSingleton(),只在首次请求会创建服务,后续所有请求都会使用该实例。AddScoped(),不同的请求,实例不同。AddTransient(),临时服务,每次请求都会创建一个新的服务实例 本篇章有关源码如下: AutofacRegisterpublic class AutofacRegister: Autofac.Module { protected override void Load(ContainerBuilder container) { var assemblysServices = Assembly.Load("Wsk.Core.Service"); // 需要暴露接口所在的程序集 container.RegisterAssemblyTypes(assemblysServices) .SingleInstance() // 设置单例注入 .AsImplementedInterfaces() // 把所有接口全暴露出来 .EnableInterfaceInterceptors(); } }ConfigureContainerpublic void ConfigureContainer(ContainerBuilder container) { container.RegisterModule(new AutofacRegister()); }TestAutofac 、ITestAutofac public class TestAutofac:ITestAutofac { public void Test() { Console.WriteLine("This is Autofac Test ……"); } }public interface ITestAutofac { void Test(); }
Filter的基本用法 代码在最下方使用filter过滤器,来实现拦截接口信息。咱们先新建一个项目,在原有的webapi上面,选择添加项目,添加一个类库项目: 我起个名字,就叫 Wsk.Core.Filter: 然后,我们把自带的控制器删除掉,咱们手撸一个。以下是要删掉的部分:然后新建一个api控制器,例如叫 WSKController 然后,新建一个webapi方法,例如HelloWorld: 然后,我们来做个打印输出到日志,切换回我们的Wsk.Core.Filter上面,删除默认的Class1,然后新增一个类,就叫HelloFilter,它需要继承于ActionFilterAttribute 不存在需要先手动引用有关的包:Microsoft.AspNetCore.Mvc 接下来,好戏开始了,直接上代码: 重写4个方法,具体作用,如图上注释。我们现在只做简单验证,验证进接口之前、以及执行接口完毕,都会发生什么。我们给他们打印点东西。先在webapi方法里面写个打印的内容: 然后,在OnActionExecuting方法里面写点接收前的打印内容:在 OnResultExecuted方法写点webapi方法执行完毕返回时候的打印内容: 然后,在接口项目里面,引用该类库项目: 在属性上方添加 [HelloFilter]实现切面拦截: 接下来准备duang一下见证奇迹,运行程序,走一个:如图,奇迹见证完毕。拦截器内部代码如下:public class HelloFilter: ActionFilterAttribute { /// <summary> /// Action方法调用之前执行 /// </summary> /// <param name="context"></param> public override void OnActionExecuting(ActionExecutingContext context) { var descriptor = context.ActionDescriptor as ControllerActionDescriptor; string param = string.Empty; string globalParam = string.Empty; foreach (var arg in context.ActionArguments) { string value = Newtonsoft.Json.JsonConvert.SerializeObject(arg.Value); param += $"{arg.Key} : {value} \r\n"; globalParam += value; } Console.WriteLine($"webapi方法名称:【{descriptor.ActionName}】接收到参数为:{param}"); } /// <summary> /// Action 方法调用后,Result 方法调用前执行 /// </summary> /// <param name="context"></param> public override void OnActionExecuted(ActionExecutedContext context) { } /// <summary> /// Result 方法调用前执行 /// </summary> /// <param name="context"></param> public override void OnResultExecuting(ResultExecutingContext context) { } /// <summary> /// Result 方法调用后执行 /// </summary> /// <param name="context"></param> public override void OnResultExecuted(ResultExecutedContext context) { var descriptor = context.ActionDescriptor as ControllerActionDescriptor; string result = string.Empty; if (context.Result is ObjectResult) { result = Newtonsoft.Json.JsonConvert.SerializeObject(((ObjectResult)context.Result).Value); } Console.WriteLine($"webapi方法名称【{descriptor.ActionName}】执行的返回值 : {result}"); } }
2023年05月
2023年02月