1.简介
dotnettyrpc是一个基于dotnetty的跨平台rpc框架,支持.net45以及.net standard2.0
2.产生背景
传统.net开发中遇到远程调用服务时,多以wcf为主。而wcf虽然功能强大,但是其配置复杂,不易于上手。而且未来必定是.net core的天下,wcf暂不支持.net core(只有客户端,无法建立服务端)。市面上的其他.net的 rpc框架诸如grpc、surging甚至微服务框架orleans等,这些框架功能强大,性能也很好,并且比较成熟,但是使用起来不够简单。基于上述比较(无任何吹捧贬低的意思),鄙人不才撸了一个轮子dotnettyrpc,它的定位是一个跨平台(.net45和.net standard)、简单却实用的rpc框架
3.使用方法
3.1引入dotnettyrpc
打开nuget包管理器,搜索dotnettyrpc即可找到并使用
或输入nuget命令:install-package dotnettyrpc
3.2定义服务接口
public interface ihello
{
string sayhello(string msg);
}
public class hello : ihello
{
public string sayhello(string msg)
{
return msg;
}
}
3.3服务端
using coldairarrow.dotnettyrpc;
using common;
using system;
namespace server
{
class program
{
static void main(string[] args)
{
rpcserver rpcserver = new rpcserver(9999);
rpcserver.registerservice();
rpcserver.start();
console.readline();
}
}
}
3.4客户端
using coldairarrow.dotnettyrpc;
using common;
using system;
namespace client
{
class program
{
static void main(string[] args)
{
ihello client = rpcclientfactory.getclient("127.0.0.1", 9999);
var msg = client.sayhello("hello");
console.writeline(msg);
console.readline();
}
}
}
3.5运行
先运行服务端,再运行客户端,即可在客户端输出hello
4.结语
本机测试一次rpc请求平均0.4ms左右,性能不高,但是足以应对绝大多数业务场景,重在简单实用。可以优化的地方很多,还望大家多多支持。
github地址:https://github.com/coldairarrow/dotnettyrpc
qq群:373144077
web后台快速开发框架(.net core)
coldairarrow
目录
第1章 目录 1
第2章 简介 3
第3章 基础准备 4
3.1 开发环境要求 4
3.2 基础数据库构建 4
3.3 运行 5
第4章 详细教程 7
4.1 代码架构 7
4.1.1总体架构 7
4.1.2基础设施层 8
4.1.3数据仓储层 10
4.1.4数据实体层 14
4.1.5业务逻辑层 15
4.1.6应用展示层 17
4.2 功能架构 18
4.2.1全局配置 18
4.2.2快速开发 18
4.2.3管理员登录 25
4.2.4系统用户管理 26
4.2.5系统角色管理 27
4.2.6权限管理 28
4.2.7接口秘钥管理 29
4.2.8系统日志 30
第5章 结语 30
本框架旨在为.net开发人员提供一个web后台快速开发框架,采用本框架,能够极大的提高项目开发效率。
整个框架包括三个版本:
.net新版,采用.net452,github地址为:https://github.com/coldairarrow/coldairarrow.fx.net.easyui.github
.net40版,采用.net40,github地址为:https://github.com/coldairarrow/coldairarrow.fx.net40.easyui.github
.net core版,采用.net core2.1, github地址为:https://github.com/coldairarrow/coldairarrow.fx.core.easyui.github
以上三个版本中,.net新版,主要支持最新的技术方案,作为主要生产版本;.net40版是为了兼容windows 2003服务器而降级的版本,功能正常。.net core 版本是未来的发展方向,能够跨平台,并且涉及linux、docker、nginx、微服务等概念,已完成移植。
开发环境要求
操作系统:windows 10
开发工具:visual studio 2017
sdk:安装.net core2.1及以上
数据库:sqlserver2008 r2及以上(若在部署在linux上访问sqlserver2008 r2请给数据库打上sp2 补丁,不然会出问题)
基础数据库构建
使用本框架需要构建基础数据库,具体步骤如下:
创建基础数据库的sql脚本文件在:/docs/初始化文件/db.sql,在数据库中运行db.sql脚本即可创建数据库:coldairarrow.fx.net.easyui.git
若sql运行出错,请直接使用同目录下的coldairarrow.fx.net.easyui.github.bak还原数据库
打开src目录下coldairarrow.fx.net.easyui.git.sln的凯发娱发k8的解决方案,如下图
如下图所示依次展开05.coldairarrow.web=> appsettings.json,配置数据库连接字符串,name不用修改,connectionstring改为上述创建的数据库(若不清楚数据库连接字符串请自行百度搜索教程)
自此基础数据库配置完成。
运行
请先还原nuget包
然后将05.coldairarrow.web设为启动项目,成功运行即可进入以下页面
代码架构
总体架构
框架组成结构一共分为5层,如上图所示,分别如下:
基础设施层:此层为最底层,可以为其余所有层服务。主要提供了项目开发所需的各种帮助类:数据库访问帮助类、文件操作帮助类、二维码生成帮助类、分拣压缩帮助类等等其余帮助类;拓展类:字符串拓展类、集合操作拓展类、表达式树拓展类等等其余拓展类;还集成了个人编写的socket通讯框架,wcf拓展使用框架,windows服务容器。这些丰富的类库都是为开发人员提供了开发中常用的功能,为快速开发提供强有力的保障。
数据仓储层:这层主要为对数据库操作crud的简单封装,以entityframework为核心,采用简单工厂、抽象工厂、工厂方法、三个工厂设计模式,使开发人员进行crud只需要极为简单的代码即可完成。本层还提供了数据库事务的支持,更是提供了分布式事务支持,为数据库操作提供必备的保障。使用本层提供的接口,关心具体的数据库类型,比如是采用sqlserver数据库或者mysql数据库,开发人员只需要关心具体的业务逻辑实现,哪怕更换数据库,也无需更改业务逻辑代码,只需要更改简单的数据库配置即可。总之,本层为开发人员对数据库的操作提供了简单高效的操作接口,可以极大的提高开发效率。
实体层:这层主要为orm框架数据库表对应的实体类,为业务逻辑层和应用层服务。
业务逻辑层:本层是开发人员主要编写层,通过调用数据仓储层操作数据库,并为应用层提供所需的接口,处理具体操作的业务逻辑,可以说是最为复杂的一层。
应用层:本层在本框架中表现形式为asp.net mvc网站,其实也可以泛指其余的表现形式:控制台程序、windows服务程序、winform程序、wpf程序等等。本层为具体的应用,负责系统功能的实现。
基础设施层
此层为最底层,可以为其余所有层服务。主要提供了项目开发所需的各种类库,主要为以下几种类库:
- 拓展帮助类
利用c#的语法糖(是由英国计算机科学家彼得·约翰·兰达(peter j. landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用),可以在类上拓展自定义方法,这样开发人员在使用的时候就可以十分的方便,既能减少重复的代码又能加快开发效率。具体包含但不限于以下类:
object拓展:object是所有类的基类。现在前后端数据交互中,采用json是选择,因此对象json序列化与反序列化的使用就十分的频繁,通过给object拓展tojson方法,就可以将数据对象直接转为对应的json字符串,能够极大的简化json序列化所需要的代码。
byte拓展:byte,即字节,一个字节是8个比特位,十进制数值范围在0-255。由于byte与二进制是直接对应的,而计算机中一切数据都是二进制,所以关于byte与其他数据类型之间的转换就显得尤为重要,编写byte相关的拓展方法能够方便byte的使用。例如,拓展将byte数组转为16进制字符串的方法,能够将字节数组直接转为对应的16进制字符串,现在物联网的发展也十分的迅速,在物联网开发中,关键是建立与硬件之间的通信,但是由于硬件的配置一般都不高,因此与硬件之间的通信大多以字节为单位,这种情况下,使用该拓展方法就可以提高开发效率。
expression拓展:expression,即表达式树,在linq查询中经常使用。在日常使用中,where筛选估计是用得最多的,但是大多数开发人员都是使用最原始的where筛选,当筛选条件增多,筛选条件变复杂的时候,原始的where虽然也可以胜任,但是就会导致代码重复,不够简洁雅观,此时可以为expression
iqueryable
- 工具帮助类
主要提供了常用的一些帮助类,包含但不限于:
office办公文档导入导出帮助类:当今,随着人们环保意识的增强,以及各行业对办公模式需求的不断升级,现代化、信息化建设步伐的加快,无纸化办公已经由概念逐渐应用到多个行业领域中,办公中,各种办公文件,尤其是excel文件与word文件经常需要导入与导出,但是c#操作excel与word文件并不简单,经常困扰着开发者,由此,本框架提供了office文档操作帮助类,封装简化了对办公文档的常用操作,能够降低开发难度,提高开发效率。
http请求模拟帮助类:在传统网站开发中,一般都是前端浏览器向后台发起请求,但是,现在的系统与系统之间的合作越来越紧密,经常需要后端向后端发起请求,即需要后端模拟http请求,但编写一个完整的http请求并不是那么容易,因此本框架提供http请求帮助类,开发人员只需要传入需要请求的url地址与参数即可完成http请求操作,使用起来简单高效,能够极大的提高开发效率。
- 缓存操作帮助类
在现代化系统开发中,随着业务量的增大,系统性能就难以满足要求,要达到性能要求,一方面可以采用更好的硬件,但是成本较高,而另一方面就是使用缓存,有效使用缓存能够提高吞吐量与并发量,所需成本较低,是绝大多数用户的首选。
.net framework框架提供了系统缓存,虽然使用简单方便,但是不支持分布式,因此大多选择诸如redis和memcached缓存,但是不同的缓存为开发者提供的接口不一样,当使用不同的缓存时开发人员又需要去学习别的缓存操作接口,十分的麻烦,为解决这个问题,本框架提供了缓存操作帮助类。缓存操作帮助类将缓存核心操作抽象定义成操作接口:添加缓存、删除缓存、设置缓存过期时间,然后再用redis与系统缓存实现抽象接口,当使用缓存时可以使用同样的操作接口操作不同的缓存,能够降低开发人员学习成本,提高开发效率。
- 数据库操作帮助类
无论什么网站,只要需要对数据进行操作,那么大多离不开数据库。数据库目前使用最多的就是三大关系型数据库:sqlserver、mysql与oracle,访问数据库需要一系列的程序:首先需要创建数据库连接对象,紧接着打开数据库,其次传入数据库操作命令,然后执行命令,最后得到数据结果,若每次访问数据库都要写一遍这些流程,那么将会极大的阻碍开发效率,因此,本框架提供了数据库操作帮助类,将数据库操作所需要的流程封装,开发人员只需要关心具体的sql语句的编写即可,并且支持三大关系型数据库,适合不同的开发人员使用。
数据仓储层
在后端开发中,数据库操作是最频繁的,每一个后端开发人员或多或少都会接触,甚至不少开发人员每天的工作就是与数据库打交道,所以可见数据库操作是多么的重要。在现在开发的过程中,绝大多数开发人员只是以编写sql语句的方式操作数据库,这种方式是操作数据库最基本最原始的方式,简单高效,但是在编写sql语句的过程中,极容易因马虎大意而编写错误,就会出现一个现象:开发人员面对着一堆sql语句在debug,而且每次都需要开发人员自己去手写sql语句,其开发效率极低。哪怕开发人员足够出色,能够保证编写的sql语句较低的出错率,但是,不同的数据库所需要的sql语句还是有差异的,这就需要开发人员学习不同的数据库sql语法,添加学习成本。而且在项目开发中难免会遇到更换数据库的情况,这时还需要花费大量的精力去进行修改sql语句。
在本框架的数据仓储层中,上述问题即可迎刃而解。数据仓储层,不同于传统三层架构中的数据层,其核心继承关系图如下图所示(图6-1):
图6-1 数据仓储类图
如上图所示,首先定义了数据操作接口irepository,该接口包含了增、删、改、查、事物控制等数据库常用核心操作,能够满足对数据库的常用操作,dbrepository类实现了irepository接口,主要以orm框架entity framework为基础,封装实现了大部分irepository所需的操作,sqlserverrepository、mysqlrepository和postgresqlrepository分别实现具体数据库对应的数据仓储,而且继承自同一个数据操作接口irepository,因此在具体的使用上,可以实现以同一个操作方法访问不同的数据库,当遇到需要更换数据库的情况时,采用本框架开发的系统能够不改代码而正常运行,这一点能够极大的降低软件开发成本。并且以entity framework为核心,不需要编写sql语句就能够完成绝大部分的数据库操作,再加上简洁的linq配合,彻底将开发人员从sql语句中解放出来,让开发人员能够更加专注于业务逻辑的实现,能够极大的提高软件开发效率。
irepository代码如下:
using system;
using system.collections.generic;
using system.data;
using system.data.common;
using system.data.entity;
using system.linq;
using system.linq.expressions;
namespace coldairarrow.datarepository
{
public interface irepository
{
#region 数据库连接相关方法
dbcontext getdbcontext();
#endregion
#region 事物提交
///
/// 开始单库事物
/// 注意:若要使用跨库事务,请使用distributedtransaction
///
void begintransaction();
bool endtransaction();
#endregion
#region 增加数据
void insert
void insert
void bulkinsert
#endregion
#region 删除数据
void deleteall
void delete
void delete
void delete
void delete
void delete
#endregion
#region 更新数据
void update
void update
void updateany
void updateany
#endregion
#region 查询数据
t getentity
list
iqueryable
datatable getdatatablewithsql(string sql);
datatable getdatatablewithsql(string sql, list
list
list
#endregion
#region 执行sql语句
void executesql(string sql);
void executesql(string sql, list
#endregion
}
}
数据实体层
由于框架主要采用了entity framework作为orm框架,这其中数据库实体映射必不可少,需要将数据库中每张表映射到类中,并且一张表一个类。这些实体类即能够作为数据库操作中的实体,还能够作为dto(data transfer object),将这些实体类划分为独立的一层,能够方便对实体的管理,易于开发与维护。
业务逻辑层
在整个后端开发中,业务逻辑的处理是最复杂的,因为从技术角度来讲,很多技术都能够实现代码复用,即无需重复造轮子(重造轮子是重复创造一个已经存在的基本方法或者被其他人优化),而且只要会使用了就能够快速投入生产中,虽然技术可以从设计上实现代码重用,降低学习成本,但是不同的系统其业务逻辑通常是不可复制的,因此开发人员可以不关心具体数据仓储的实现技术,但是不得不关心具体业务逻辑的实现,既然业务逻辑无法避免又那么复杂,那么设计出合理的业务逻辑架构来加快开发效率就显得尤为重要。
本框架将业务逻辑独立一层,其核心继承关系如下图(图6-2)所示:
图6-2 业务逻层次图
如上图所示:首先定义了一个业务逻辑基类basebusiness
应用展示层
这层也可以称之为应用层,其余的层表现形式都是类库,而这一层负责具体项目应用的实施,比如可以使用控制台程序、windows服务程序、winform程序、wcf程序等等,在本项目中使用了asp.net mvc网站项目负责具体功能的实现。
aop(aspect oriented programming)使用:aop,即面向切片编程,利用aop能够将系统各个部分进行隔离,从而降低模块之间的耦合度,提高程序可用性,同时提高开发效率。本框架中主要使用了以下aop:
管理员登录校验checkloginattribute:在后台管理系统中,管理员只有登录后才能够进行相关操作,一般通过session来记录管理员登录信息,最简单直接就是在每一个请求中都需要判断一遍管理员是否登录,这无疑将会导致很多的重复代码,此时,通过将登陆校验作为一个特性,只需要在需要登录的控制器或方法上添加该特性即可完成管理员登录校验,这样就能够减少大量的重复代码,加快开发效率。
签名校验checksignattribute:当后端接口需要给外部系统调用时,若不对接口访问进行限制,那么就会面临恶意请求攻击的风险,轻则影响系统性能,重则导致系统瘫痪,数据被恶意串改,此时,保证接口的安全性就十分关键。保证接口的安全性,主要就是按照一定的签名算法,对请求者传入的参数进行签名校验,只有通过才能够正常访问,原始做法就是在每个请求中去进行签名校验,这同样会导致大量的重复代码,这时通过引入签名校验特性,只需要在需要签名校验的控制器或方法中加入特性即可,使用简单方便,开发效率也高。
应用层还在视图中使用了通用布局模板,并统一了代码规范,再集成了jquery以及一些其它常用的js类库,以layui为主要前端ui框架,界面风格统一,开发效率高。
功能架构
全局配置
在01.coldairarrow.util中的globalswitch类中,设置了各个参数,其中runmodel需要重点关注一下,若runmodel==runmodel.localtest,则系统会直接跳过登录,默认使用admin超级管理员登录,其它参数请看注释。
快速开发
使用此功能请确保globalswitch.runmodel= runmodel.localtest
此功能为本框架的核心功能,能够自动生产完整的可运行代码,具体使用如下:
首选需要有数据库源,因为代码生成是根据数据库表来生成的。
菜单:开发=>快速开发=>数据库连接管理
若列表中没有目标数据源,则添加数据库连接
数据连接名、连接字符串、数据库类型(目前不支持oracle,有空余时间再研究)即可。添加完成后即可看到连接字符串信息。
有了数据库连接之后,即可进行代码生成。
菜单:开发=>快速开发=>代码生成
选择数据库,然后勾选需要生成代码的数据库表,点击生成代码会弹出生成选项(这里暂时只能勾选dev_project,其余表全是系统基础表,不要勾选,否则会被覆盖,导致异常,请勾选自己的业务表进行生成):
生成选项中可以选择需要生成的类型,可以默认生成实体层、业务逻辑层、控制器和视图。
生成区域对应mvc中的areas,请按需填入(必填)
这里示例填写projectmanage,点击生成按钮,即可完成代码生成。生成后的代码在项目凯发娱发k8的解决方案中,将代码文件包括进入项目
默认生成后的文件是隐藏的,需要点击显示所有文件按钮,即可看到生成后的新文件
右键新的文件夹,包括在项目中
由于是新生成的代码,所以才配置新的菜单
如上图,在web项目中的wwwroot/config/systemmenu.config中配置菜单,模仿原有菜单即可,其中url是指页面的路径,permission是指若需要访问此菜单需要的权限(对应权限配置),若没有此权限,则菜单也中不会显示此菜单,修改完成后重新编译生成(权限相关模块进行了缓存,重新生成会清缓存),重新运行即可看到新的项目菜单如下:
整个代码生成过程,无需编写代码即可完成一张表的crud,当然需要根据具体业务中进行相应的修改,本次示例中字段比较少,但是当一张表的字段很多时,那么此功能能够将开发效率提高几个档次。
管理员登录
若要使用登录功能,请将globalswitch中的runmodel改为runmodel.publish
默认超级管理员账号为:admin
密码为:123456
系统用户管理
管理系统登录的用户
菜单:系统=>系统管理=>用户管理,如下页面
点击右侧设置权限,可以设置用户权限,详情见<权限管理>模块
具体权限相关配置见权限管理模块
系统角色管理
管理系统角色,角色是权限的载体,合理分配角色有利于权限管理
菜单:系统=>系统管理=>角色管理
操作中可以设置角色的权限,详情见<权限管理>模块
权限管理
一般情况下,后台管理系统多少会涉及权限管理,因此本框架提供了一个灵活、高效、简洁的权限管理系统。
首先,权限分为两种,即操作权限和数据权限,其中操作权限报货系统用户权限和appid权限,系统用户权限就是指操作用户具备哪些权限,而当对外提供api接口时,为了保证接口的安全性(若不在意可忽略),通常会提供接口签名算法,其中appid和appsecret是必备的,通过对appid设置权限,即可控制接口的权限。数据权限比较复杂,若采用纯sql方式,那么会更加复杂,本框架全程采用ef作为orm框架,通过对iqueryable
用户权限:若对每个用户都设置对应的权限,那么工作量无疑是十分巨大的,因此引入了角色的概念,角色是权限的集合载体,那么属于此角色的用户就继承了角色的权限,当然某些特殊用户需要拥有自己的不属于角色的特殊权限,因此最终用户拥有的权限就是自己的权限和所属角色权限的并集。
权限使用:
权限定义:
如上图,在permission.config中定义了各个权限
权限配置:
在系统用户管理和系统角色管理中可以设置用户和角色的权限,把需要的权限勾选即可。
权限使用:
如上图所示,在需要控制权限的页面中,调用方法:permissionmanage.operatorhaspermissionvalue("sysuser.manage")
这个方法是判断操作者用户是否含有sysuser.manage权限值,其中sysuser是指permission.config中定义的module的value属性,manage是指permission中的value属性,用.连接即是最终权限值。
更详细的使用方式,请参考源代码。
接口秘钥管理
菜单:系统=>系统管理=>接口秘钥管理
系统日志
菜单:系统=>系统管理=>系统日志
欢迎使用本框架,若觉得不错,请比心
github:https://github.com/coldairarrow,请statrt
博客园:https://www.cnblogs.com/coldairarrow/
qq群:373144077
本人将会对这个快速开发框架不断完善与维护,希望能够帮助到各位
若遇到任何问题或需要凯发娱发k8的技术支持,请联系我。
---------------------学习永无止境,技术永无上限,代码就是艺术-----------------------
设计原则:万物皆对象
前言:在上一篇的0配置使用wcf中,虽然使用已经很方便了,但是对于最求极致简洁得人来说(比如我),客户端需要通过手动引用服务才能够调用服务接口,那么有没有办法能够绕过手动引用这一步,并且直接通过调用地址使用呢?答案肯定是有的,不然我这篇文章就毫无意义了,而我是从来不做无意义之事,人狠话不多,下面介绍如何简单、优雅、高效的使用wcf
正文:
首先需要引入框架,框架代码以及demo源码在最后的git地址中!
服务端:
定义接口:
using system.servicemodel; namespace wcfserver { [servicecontract] public interface imyservice { [operationcontract] string hello(); } }
实现接口:
namespace wcfserver { public class myservice : imyservice { public string hello() { return "hello world!"; } } }
这里只是简单输出hello world,别的操作只需要仿造即可!
服务端启动:
using coldairarrow.util.wcf; using system; namespace wcfserver { class program { static void main(string[] args) { wcfhostwcfhost = new wcfhost ("http://localhost:14725", "http://localhost:14725/mex"); wcfhost.handlehostopened = new action
服务端与上次的使用没多大区别
注意:服务端启动必须要以管理员身份运行!
客户端:
using coldairarrow.util.wcf; using system; using wcfserver; namespace wcfclient { class program { static void main(string[] args) { var client = wcfclientfactory.createclientbyurl("http://localhost:14725/myservice"); var data = client.hello(); console.writeline(data); console.readkey(); } } }
客户端的使用不需要再从地址引用服务了,直接通过调用wcfclientfactory.createclientbyurl方法就可以返回操作接口,其中需要传入泛型接口类,也就是服务端中的imyservice。
服务端运行后,客户端直接运行即可!
运行截图如下:
服务端截图:
客户端截图:
可以看到,使用起来十分地简单方便,可以极大的提高开发效率!
老规矩,全部源码及demo在github:https://github.com/coldairarrow/easywcf
大家用得爽了别忘了点星星哦~~~
分割线------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
经过了差不多半年,毕业设计也终于完成了,我的毕设是后台快速开发框架,不出意外这个框架会永远伴随着我,我也会一直完善它,希望它能够在我的职业生涯中发放光彩!
毕设虽然完成了,但是探索技术的步伐是永远不会停止的!大家一起加油~~~
end
设计原则:万物皆对象
背景:微软提供了一套强大的通信框架wcf,了解请看百度百科:ttps://baike.baidu.com/item/wcf/7374854?fr=aladdin
虽然这套通信框架很强大,但是配置起来也不简单,因此导致很多人望而却步(包括我),我这人向来不喜欢麻烦,喜欢简单,最好就是给我一个对象,告诉我怎么传参就使用是最爽的,我相信应该有很多人跟我一样的想法,因此,这篇文章应运而生,没错,就是零配置使用wcf,下面我会详细道来。
正文:
1、核心类库,注意,需要引用程序集
system.servicemodel
wcfhost.cs
using system; using system.servicemodel; using system.servicemodel.description; using system.threading.tasks; namespace coldairarrow.util.wcf { ////// wcf服务代码控制类(必须开启管理员权限) /// ///服务处理 ///服务接口 public class wcfhost{ #region 构造函数 /// /// 构造函数 /// /// http基地址(服务器真实地址),默认为:http://127.0.0.1:14725/ /// http获取服务引用的地址(服务器真实地址),默认为:http://127.0.0.1:14725/mex public wcfhost(string baseurl= "http://127.0.0.1:14725/", string httpgeturl= "http://127.0.0.1:14725/mex") { _servicehost = new servicehost(typeof(service), new uri(baseurl)); //serviceendpoint 终结点 包含address地址 binding绑定 contracts契约 简称abc _servicehost.addserviceendpoint(typeof(iservice), new wshttpbinding(), typeof(service).name); //添加服务终结点 if (_servicehost.description.behaviors.find() == null) { //判断是否在配置文件中定义了元数据终结点 servicemetadatabehavior metadata = new servicemetadatabehavior(); metadata.httpgetenabled = true; metadata.httpgeturl = new uri(httpgeturl); _servicehost.description.behaviors.add(metadata);//添加元数据终结点 } } #endregion #region 私有成员 private servicehost _servicehost; #endregion #region 外部接口 /// /// 开始wcf服务 /// public void starthost() { task task = new task(() => { try { if (handlehostopened != null) _servicehost.opened = new eventhandler(handlehostopened); if (_servicehost.state != communicationstate.opened) { _servicehost.open(); } } catch (exception ex) { handleexception?.invoke(ex); } }); task.start(); } #endregion #region 事件处理 ////// 当wcf服务开启后执行 /// public action
2、服务端使用:
服务接口定义:
iservice.cs
using system.servicemodel; namespace _01.wcfserver { ////// 对外提供的接口规范,必须要servicecontract特性 /// [servicecontract] public interface iservice { ////// 对外提供的接口方法,必须operationcontract特性,方法不能重载 /// ///[operationcontract] string hello(); } }
服务接口实现:
service.cs
namespace _01.wcfserver { ////// 接口具体实现类 /// public class service : iservice { ////// 方法具体实现 /// ///public string hello() { return "hello world"; } } }
服务端运行:注意,必须以管理员权限运行
program.cs
using coldairarrow.util.wcf; using system; namespace _01.wcfserver { class program { static void main(string[] args) { //创建wcf服务对象,泛型参数service为实现类,iservice为服务接口 //第一个参数baseurl为服务基地址(必须为真实地址) //第二个参数httpgeturl为服务引用地址(必须为真实地址),也就是客户端添加服务引用时用的地址 wcfhostwcfhost = new wcfhost ("http://localhost:14725", "http://localhost:14725/mex"); //当wcf服务开启后执行的事件 wcfhost.handlehostopened = new action
客户端使用:
program.cs
using system; namespace _02.wcfclient { class program { static void main(string[] args) { //servicereference1为引用时自定义的命名空间 //serviceclient为具体实现类,service为类名,client为后缀 //可以在很多地方使用,比如控制台,winform,asp.net网站等,把它当做一个类库就很好理解了66666 servicereference1.serviceclient client = new servicereference1.serviceclient(); //调用service提供的hello方法,wcf服务端必须运行 var data = client.hello(); console.writeline(data); console.readkey(); } } }
详细使用步骤:
1、运行wcf服务端,必须以管理员权限
2、打开浏览器,测试wcf是否成功开启
3、客户端引用服务
右键引用,引用服务,输入服务地址(即wcf初始化时第二个参数)
3、客户端代码调用
2、客户端成功运行
总结:
全程实现真正的0配置搭建了wcf服务,满不满意,意不意外,惊不惊喜,爽不爽~~
最后,惯例,全部代码代码在github,欢迎大家点赞~
https://github.com/coldairarrow/wcf
背景:
首先向各位前辈,大哥哥小姐姐问一声好~
这是我第一次写博客,目前为一个即将步入大四的学生,上学期在一家公司实习了半年,后期发现没有动力,而且由于薪水问题(废话嘛),于是跳槽到这家新的公司。
说到socket,想必大家都或多或少有所涉及,从最初的计算机网络课程,讲述了tcp协议,而socket就是对协议的进一步封装,使我们开发人员能够更加容易轻松的进行软件之间的通信。
这个星期刚好接受一个共享车位锁的项目,需要使用socket与硬件进行通信控制,说白了也就是给锁发送指令,控制其打开或者关闭,再就是对app开放操作接口,使其方便测试以及用户的使用。这其中核心就是socket的使用,再开发出这个功能之后,我发现使用起来很不方便,于是耗时2天抽象其核心功能并封装成框架,最后使用这个框架将原来的项目重构并上线,极大的提高了软件的可拓展性,健壮性,容错率。
个人坚信的原则:万物皆对象
好了,不废话了,下面进入正文
正文:
1、首先简单讲下c#中socket的简单使用。
第一步:服务端监听某个端口
第二步:客户端向服务端地址和端口发起socket连接请求
第三步:服务端收到连接请求后创建socket连接,并维护这个连接队列。
第四步:客户端和服务端已经建立双工通信(即双向通信),客户端和服务端可以轻松方便的给彼此发送信息。
至于简单使用的具体实现代码全部被我封装到项目中了,如果需要学习简单的实现,可以看我的源码,也可以自行百度,有很多的教程
2、核心,框架的使用
其实,说其为框架,可能有点牵强,因为每个人对框架都有自己的理解,但是类库和框架又有什么本质区别呢?全部都是代码~哈哈,扯远了
首先,空说无凭,先放上所有的代码:
服务端源文件:
socketserver.cs
using system; using system.collections.generic; using system.net; using system.net.sockets; namespace coldairarrow.util.sockets { ////// socket服务端 /// public class socketserver { #region 构造函数 ////// 构造函数 /// /// 监听的ip地址 /// 监听的端口 public socketserver(string ip, int port) { _ip = ip; _port = port; } ////// 构造函数,监听ip地址默认为本机0.0.0.0 /// /// 监听的端口 public socketserver(int port) { _ip = "0.0.0.0"; _port = port; } #endregion #region 内部成员 private socket _socket = null; private string _ip = ""; private int _port = 0; private bool _islisten = true; private void startlisten() { try { _socket.beginaccept(asyncresult => { try { socket newsocket = _socket.endaccept(asyncresult); //马上进行下一轮监听,增加吞吐量 if (_islisten) startlisten(); socketconnection newclient = new socketconnection(newsocket, this) { handlerecmsg = handlerecmsg == null ? null : new action(handlerecmsg), handleclientclose = handleclientclose == null ? null : new action (handleclientclose), handlesendmsg = handlesendmsg == null ? null : new action (handlesendmsg), handleexception = handleexception == null ? null : new action (handleexception) }; newclient.startrecmsg(); clientlist.addlast(newclient); handlenewclientconnected?.invoke(this, newclient); } catch (exception ex) { handleexception?.invoke(ex); } }, null); } catch (exception ex) { handleexception?.invoke(ex); } } #endregion #region 外部接口 /// /// 开始服务,监听客户端 /// public void startserver() { try { //实例化套接字(ip4寻址协议,流式传输,tcp协议) _socket = new socket(addressfamily.internetwork, sockettype.stream, protocoltype.tcp); //创建ip对象 ipaddress address = ipaddress.parse(_ip); //创建网络节点对象包含ip和port ipendpoint endpoint = new ipendpoint(address, _port); //将 监听套接字绑定到 对应的ip和端口 _socket.bind(endpoint); //设置监听队列长度为int32最大值(同时能够处理连接请求数量) _socket.listen(int.maxvalue); //开始监听客户端 startlisten(); handleserverstarted?.invoke(this); } catch (exception ex) { handleexception?.invoke(ex); } } ////// 所有连接的客户端列表 /// public linkedlistclientlist { get; set; } = new linkedlist (); /// /// 关闭指定客户端连接 /// /// 指定的客户端连接 public void closeclient(socketconnection theclient) { theclient.close(); } #endregion #region 公共事件 ////// 异常处理程序 /// public actionhandleexception { get; set; } #endregion #region 服务端事件 /// /// 服务启动后执行 /// public actionhandleserverstarted { get; set; } /// /// 当新客户端连接后执行 /// public actionhandlenewclientconnected { get; set; } /// /// 服务端关闭客户端后执行 /// public actionhandlecloseclient { get; set; } #endregion #region 客户端连接事件 /// /// 客户端连接接受新的消息后调用 /// public actionhandlerecmsg { get; set; } /// /// 客户端连接发送消息后回调 /// public actionhandlesendmsg { get; set; } /// /// 客户端连接关闭后回调 /// public actionhandleclientclose { get; set; } #endregion } }
using system; using system.net.sockets; using system.text; namespace coldairarrow.util.sockets { ////// socket连接,双向通信 /// public class socketconnection { #region 构造函数 public socketconnection(socket socket,socketserver server) { _socket = socket; _server = server; } #endregion #region 私有成员 private readonly socket _socket; private bool _isrec=true; private socketserver _server = null; private bool issocketconnected() { bool part1 = _socket.poll(1000, selectmode.selectread); bool part2 = (_socket.available == 0); if (part1 && part2) return false; else return true; } #endregion #region 外部接口 ////// 开始接受客户端消息 /// public void startrecmsg() { try { byte[] container = new byte[1024 * 1024 * 2]; _socket.beginreceive(container, 0, container.length, socketflags.none, asyncresult => { try { int length = _socket.endreceive(asyncresult); //马上进行下一轮接受,增加吞吐量 if (length > 0 && _isrec && issocketconnected()) startrecmsg(); if (length > 0) { byte[] recbytes = new byte[length]; array.copy(container, 0, recbytes, 0, length); //处理消息 handlerecmsg?.invoke(recbytes, this, _server); } else close(); } catch (exception ex) { handleexception?.invoke(ex); close(); } }, null); } catch (exception ex) { handleexception?.invoke(ex); close(); } } ////// 发送数据 /// /// 数据字节 public void send(byte[] bytes) { try { _socket.beginsend(bytes, 0, bytes.length, socketflags.none, asyncresult => { try { int length = _socket.endsend(asyncresult); handlesendmsg?.invoke(bytes, this, _server); } catch (exception ex) { handleexception?.invoke(ex); } }, null); } catch (exception ex) { handleexception?.invoke(ex); } } ////// 发送字符串(默认使用utf-8编码) /// /// 字符串 public void send(string msgstr) { send(encoding.utf8.getbytes(msgstr)); } ////// 发送字符串(使用自定义编码) /// /// 字符串消息 /// 使用的编码 public void send(string msgstr,encoding encoding) { send(encoding.getbytes(msgstr)); } ////// 传入自定义属性 /// public object property { get; set; } ////// 关闭当前连接 /// public void close() { try { _isrec = false; _socket.disconnect(false); _server.clientlist.remove(this); handleclientclose?.invoke(this, _server); _socket.close(); _socket.dispose(); gc.collect(); } catch (exception ex) { handleexception?.invoke(ex); } } #endregion #region 事件处理 ////// 客户端连接接受新的消息后调用 /// public actionhandlerecmsg { get; set; } /// /// 客户端连接发送消息后回调 /// public actionhandlesendmsg { get; set; } /// /// 客户端连接关闭后回调 /// public actionhandleclientclose { get; set; } /// /// 异常处理程序 /// public actionhandleexception { get; set; } #endregion } }
using system; using system.net; using system.net.sockets; using system.text; namespace coldairarrow.util.sockets { ////// socket客户端 /// public class socketclient { #region 构造函数 ////// 构造函数,连接服务器ip地址默认为本机127.0.0.1 /// /// 监听的端口 public socketclient(int port) { _ip = "127.0.0.1"; _port = port; } ////// 构造函数 /// /// 监听的ip地址 /// 监听的端口 public socketclient(string ip, int port) { _ip = ip; _port = port; } #endregion #region 内部成员 private socket _socket = null; private string _ip = ""; private int _port = 0; private bool _isrec=true; private bool issocketconnected() { bool part1 = _socket.poll(1000, selectmode.selectread); bool part2 = (_socket.available == 0); if (part1 && part2) return false; else return true; } ////// 开始接受客户端消息 /// public void startrecmsg() { try { byte[] container = new byte[1024 * 1024 * 2]; _socket.beginreceive(container, 0, container.length, socketflags.none, asyncresult => { try { int length = _socket.endreceive(asyncresult); //马上进行下一轮接受,增加吞吐量 if (length > 0 && _isrec && issocketconnected()) startrecmsg(); if (length > 0) { byte[] recbytes = new byte[length]; array.copy(container, 0, recbytes, 0, length); //处理消息 handlerecmsg?.invoke(recbytes, this); } else close(); } catch (exception ex) { handleexception?.invoke(ex); close(); } }, null); } catch (exception ex) { handleexception?.invoke(ex); close(); } } #endregion #region 外部接口 ////// 开始服务,连接服务端 /// public void startclient() { try { //实例化 套接字 (ip4寻址协议,流式传输,tcp协议) _socket = new socket(addressfamily.internetwork, sockettype.stream, protocoltype.tcp); //创建 ip对象 ipaddress address = ipaddress.parse(_ip); //创建网络节点对象 包含 ip和port ipendpoint endpoint = new ipendpoint(address, _port); //将 监听套接字 绑定到 对应的ip和端口 _socket.beginconnect(endpoint, asyncresult => { try { _socket.endconnect(asyncresult); //开始接受服务器消息 startrecmsg(); handleclientstarted?.invoke(this); } catch (exception ex) { handleexception?.invoke(ex); } }, null); } catch (exception ex) { handleexception?.invoke(ex); } } ////// 发送数据 /// /// 数据字节 public void send(byte[] bytes) { try { _socket.beginsend(bytes, 0, bytes.length, socketflags.none, asyncresult => { try { int length = _socket.endsend(asyncresult); handlesendmsg?.invoke(bytes, this); } catch (exception ex) { handleexception?.invoke(ex); } }, null); } catch (exception ex) { handleexception?.invoke(ex); } } ////// 发送字符串(默认使用utf-8编码) /// /// 字符串 public void send(string msgstr) { send(encoding.utf8.getbytes(msgstr)); } ////// 发送字符串(使用自定义编码) /// /// 字符串消息 /// 使用的编码 public void send(string msgstr, encoding encoding) { send(encoding.getbytes(msgstr)); } ////// 传入自定义属性 /// public object property { get; set; } ////// 关闭与服务器的连接 /// public void close() { try { _isrec = false; _socket.disconnect(false); handleclientclose?.invoke(this); } catch (exception ex) { handleexception?.invoke(ex); } } #endregion #region 事件处理 ////// 客户端连接建立后回调 /// public actionhandleclientstarted { get; set; } /// /// 处理接受消息的委托 /// public actionhandlerecmsg { get; set; } /// /// 客户端连接发送消息后回调 /// public actionhandlesendmsg { get; set; } /// /// 客户端连接关闭后回调 /// public actionhandleclientclose { get; set; } /// /// 异常处理程序 /// public actionhandleexception { get; set; } #endregion } }
上面放上的是框架代码,接下来介绍下如何使用
首先,服务端使用方式:
using coldairarrow.util.sockets; using system; using system.text; namespace console_server { class program { static void main(string[] args) { //创建服务器对象,默认监听本机0.0.0.0,端口12345 socketserver server = new socketserver(12345); //处理从客户端收到的消息 server.handlerecmsg = new action((bytes, client, theserver) => { string msg = encoding.utf8.getstring(bytes); console.writeline($"收到消息:{msg}"); }); //处理服务器启动后事件 server.handleserverstarted = new action (theserver => { console.writeline("服务已启动************"); }); //处理新的客户端连接后的事件 server.handlenewclientconnected = new action ((theserver, thecon) => { console.writeline($@"一个新的客户端接入,当前连接数:{theserver.clientlist.count}"); }); //处理客户端连接关闭后的事件 server.handleclientclose = new action ((thecon, theserver) => { console.writeline($@"一个客户端关闭,当前连接数为:{theserver.clientlist.count}"); }); //处理异常 server.handleexception = new action (ex => { console.writeline(ex.message); }); //服务器启动 server.startserver(); while (true) { console.writeline("输入:quit,关闭服务器"); string op = console.readline(); if (op == "quit") break; } } } }
客户端使用方式:
using coldairarrow.util.sockets; using system; using system.text; namespace console_client { class program { static void main(string[] args) { //创建客户端对象,默认连接本机127.0.0.1,端口为12345 socketclient client = new socketclient(12345); //绑定当收到服务器发送的消息后的处理事件 client.handlerecmsg = new action((bytes, theclient) => { string msg = encoding.utf8.getstring(bytes); console.writeline($"收到消息:{msg}"); }); //绑定向服务器发送消息后的处理事件 client.handlesendmsg = new action ((bytes, theclient) => { string msg = encoding.utf8.getstring(bytes); console.writeline($"向服务器发送消息:{msg}"); }); //开始运行客户端 client.startclient(); while (true) { console.writeline("输入:quit关闭客户端,输入其它消息发送到服务器"); string str = console.readline(); if (str == "quit") { client.close(); break; } else { client.send(str); } } } } }
最后运行测试截图:
总结:
其最方便之处在于,将如何创建连接封装掉,使用人员只需关注连接后发送什么数据,接收到数据后应该如何处理,等等其它的很多事件的处理,这其中主要依托于匿名委托的使用,lambda表达式的使用。
框架里面主要使用了异步通讯,以及如何控制连接,详细我就不多说了,大家应该一看就懂,我只希望能给大家带来便利,最后大家有任何问题、意见、想法,都可以给我留言。
最后,附上所有源码项目地址,若觉得有一定价值,还请点赞~
github地址:https://github.com/coldairarrow/sockets
设计原则:万物皆对象
背景:在我的项目中,即需要与硬件通过socket连接通讯,又需要给app提供wcf服务操作接口,虽然都完成了,但是却是一个控制台(虽然我很喜欢控制台,因为它简单易用),把它放到服务器运行,总有一个黑乎乎的窗口,总感觉不雅(原谅我的强迫症)。于是各种百度谷歌如何创建运行windows服务程序,就像sqlserver数据那样在后台默默运行奉献就可以了。
但是,很多都是那么的麻烦,需要批处理什么的,而我这个人向来喜欢简洁,于是便设计了这么个windows服务辅助类,没错,就是0命令。
正文:
1、类库源码我就不放了,最后都放到github上
2、使用方法:
using coldairarrow.util.windowsservice; using system; namespace windowsservicetest { class program { public static void main(string[] argc) { //创建服务容器,第一个参数为指定服务名,第二个参数为主函数入口的参数argc windowsservicecontainer servicecontainer = new windowsservicecontainer("a_test_service", argc); //服务启动时执行的事件,即可以看做控制台的主函数main即可 servicecontainer.handleonstart = new action(args => { //可以在这里添加你需要服务干的事情,比如创建socket通讯,wcf服务,balabala......... //让它在后台默默地工作把~~~~~~~~~~ }); //处理日志的事件 servicecontainer.handlelog = new action (log => { console.writeline(log); }); //处理异常的事件 servicecontainer.handleexception = new action (ex => { console.writeline(ex.message); }); //开始运行服务 servicecontainer.start(); } } }
直接运行控制台即可:
选择1进行安装服务:
没错,你可以看见服务已经成功运行了!!!,是不是很假单?
选择2进行服务卸载:
总结:
通过对服务操作一系列的封装,使将控制台程序变成服务程序非常的简单,不需要任何的批处理命令,只需要简单的几行代码即可,感觉很爽的请点赞!
老规矩,github地址:
https://github.com/coldairarrow/windowsservicedemo