使用asp.net mvc引擎开发插件系统 - 产品经理是条狗 - 博客园

mikel阅读(1446)

来源: 使用asp.net mvc引擎开发插件系统 – 产品经理是条狗 – 博客园

一、前言

我心中的插件系统应该是像Nop那样(更牛逼的如Orchard,OSGI.NET),每个插件模块不只是一堆实现了某个业务接口的dll,然后采用反射或IOC技术来调用,而是一个完整的mvc小应用,我可以在后台控制插件的安装和禁用,目录结构如下:

生成后放在站点根目录下的Plugins文件夹中,每个插件有一个子文件夹

Plugins/Sms.AliYun/

Plugins/Sms.ManDao/

我是一个有强迫症的的懒人,我不想将生成的dll文件拷贝到bin目录。

二、要解决的问题

1.ASP.NET引擎默认只会加载“bin”文件夹中的dll,而我们想要的插件文件则是分散在Plugins目录下的各个子目录中。

2.视图中使用了模型时如何处理?默认情况下RazorViewEngine使用BuildManager将视图编译成动态程序集,然后使用Activator.CreateInstance实例化新编译的对象,而使用插件dll时,当前的AppDomain不知道如何解析这种引用了模型的视图,因为它不存在于“bin”或GAC中。更糟糕的是,不会收到任何错误消息,告诉您为什么它不工作,或者问题在哪。相反,他会告诉你,从View目录中找不到文件。

3.某个插件正挂在站点下运行着,直接覆盖插件的dll,会告诉你当前dll正在使用,不能被覆盖。

4.视图文件不放站点的View目录中,该如何加载。

三.Net 4.0让这一切变成可能

Net4.0有一个新特性是在应用程序初始化之前执行代码的能力(PreApplicationStartMethodAttribute),这个特性使得应用程序在Application_Star前可以做一些工作,例如我们可以在应用启动之前告知我们的mvc插件系统的dll放在哪,做预加载处理等。关于.net的几个新特性,有歪果仁写得有博客来介绍,点我。,关于PreApplicationStartMethodAttribute,有博友已经写过了,点我。 Abp的启动模块应该也是使用PreApplicationStartMethodAttribute这个特性原理来实现的,具体是不是这样还没看。

四、解决方案

1.修改主站点web.config目录,让运行时除了加载bin目录中的文件,还可以从其它目录加载

 <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="Plugins/temp/" />
    </assemblyBinding>
  </runtime>

2.开发一个简易的插件管理类,这个类的作用就是在Application_Start之前就把Plugins各子目录中的dll拷贝到第1步指定的文件夹中,为了让demo尽可能简单,没有对重复的dll进行检测(比如插件中引用了ef程序集,主站点也引用了,在站点bin目录中已经存在ef的dll了,就没必要再把插件中的dll拷贝到上面设置的动态程序集目录中)

复制代码
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Compilation;
using System.Web.Hosting;
[assembly: PreApplicationStartMethod(typeof(Plugins.Core.PreApplicationInit), "Initialize")]
namespace Plugins.Core
{
    public class PreApplicationInit
    {

        static PreApplicationInit()
        {
            PluginFolder = new DirectoryInfo(HostingEnvironment.MapPath("~/plugins"));
            ShadowCopyFolder = new DirectoryInfo(HostingEnvironment.MapPath("~/plugins/temp"));
        }

        /// <summary>
        /// 插件所在目录信息
        /// </summary>
        private static readonly DirectoryInfo PluginFolder;

        /// <summary>
        /// 程序应行时指定的dll目录
        /// </summary>
        private static readonly DirectoryInfo ShadowCopyFolder;

        public static void Initialize()
        {
            Directory.CreateDirectory(ShadowCopyFolder.FullName);
            //清空插件dll运行目录中的文件
            foreach (var f in ShadowCopyFolder.GetFiles("*.dll", SearchOption.AllDirectories))
            {
                f.Delete();
            }
            foreach (var plug in PluginFolder.GetFiles("*.dll", SearchOption.AllDirectories).Where(i=>i.Directory.Parent.Name== "plugins"))
            {
                File.Copy(plug.FullName, Path.Combine(ShadowCopyFolder.FullName, plug.Name), true);
            }
            foreach (var a in
                ShadowCopyFolder
                .GetFiles("*.dll", SearchOption.AllDirectories)
                .Select(x => AssemblyName.GetAssemblyName(x.FullName))
                .Select(x => Assembly.Load(x.FullName)))
            {
                BuildManager.AddReferencedAssembly(a);
            }

        }
    }
}
复制代码

 

3.如何让View引擎找到我们的视图呢?答案是重写RazorViewEngine的方法,我采用了约定大于配置的方式(假设我们的插件项目命名空间为Plugins.Apps.Sms,那么默认的控制器命名空间为Plugins.Apps.Sms.Controllers,插件生成后的文件夹必须为/Plugins/Plugins.Apps.Sms/),通过分析当前控制器就可以知道当前插件的View目录位置

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.WebPages.Razor;

namespace Plugins.Web
{
    public class CustomerViewEngine : RazorViewEngine
    {

        /// <summary>
        /// 定义视图页所在地址。
        /// </summary>
        private string[] _viewLocationFormats = new[]
        {
            "~/Views/Parts/{0}.cshtml",
            "~/Plugins/{pluginFolder}/Views/{1}/{0}.cshtml",
            "~/Plugins/{pluginFolder}/Views/Shared/{0}.cshtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml",
        };
        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            string ns = controllerContext.Controller.GetType().Namespace;
            string controller = controllerContext.Controller.GetType().Name.Replace("Controller", "");
            //说明是插件中的控制器,View目录需要单独处理
            if (ns.ToLower().Contains("plugins"))
            {
                var pluginsFolder = ns.ToLower().Replace(".controllers", "");
                ViewLocationFormats = ReplacePlaceholder(pluginsFolder);
            }
            return base.FindView(controllerContext, viewName, masterName, useCache);
        }
        /// <summary>
        /// 替换pluginFolder占位符
        /// </summary>
        /// <param name="folderName"></param>
        private string[] ReplacePlaceholder(string folderName)
        {
            string[] tempArray = new string[_viewLocationFormats.Length];
            if (_viewLocationFormats != null)
            {
                for (int i = 0; i < _viewLocationFormats.Length; i++)
                {
                    tempArray[i] = _viewLocationFormats[i].Replace("{pluginFolder}", folderName);
                }
            }
            return tempArray;
        }
    }
}
复制代码

然后在主站点的Global.asax中将Razor引擎指定为我们重写过的

 

4.开始制作一个插件目录,跟我们平时建立的MVC项目并没有太大区别,只是发布时需要做一些设置。

.生成路径要按照第3条的约定来写,不然会找不到视图文件

.View目录下的web.config和.cshtml文件要复制到生成目录(在文件中点右键)

3.设置引用项目中的生成属性,主程序下面已经有了的就把“复制到输出目录”设置为无,要不然拷贝到动态bin目录时会出错,可以对第2步中的那个类改造一下,加入文件比较功能,bin目录中没有的,才拷贝到动态bin目录中。

4.生成后的目录结构如下:

 

5.跑一下,一切正常,插件中的控制器工作正常,视图中引用了Model也没问题

到此,一个插件系统的核心部分就算完成了,你可继续进行扩展,增加插件的发现、安装、卸载功能,这些相对于核心功能来说,都是小儿科。后续我会基于Abp框架出一个插件系统的文章,有兴趣的把小板凳准备好,瓜子花生买上:)

五、源代码

点我下载

ASP.NET MVC 4 插件化架构简单实现-思路篇 - cjnmy36723 - 博客园

mikel阅读(1104)

来源: ASP.NET MVC 4 插件化架构简单实现-思路篇 – cjnmy36723 – 博客园

用过和做过插件的都会了解插件的好处,园子里也有很多和讨论,但大都只些简单的加载程序集什么的,这里主要讨论的就是使用 ASP.NET MVC 4 来实现每个插件都可以完全从主站点剥离出来,即使只是一个插件,也是一个完整的站点,同时也可以和其它插件一起组装成一个庞大的系统。

参考资料:

ASP.NET MVC 4 源码。

Orchard 源码。

MVC3PlugInDemo 源码。

ASP.NET MVC的Razor引擎:View编译原理

基于ASP.NET MVC3 Razor的模块化/插件式架构实现

基于OSGi.NET开发ASP.NET MVC 3.0插件化应用程序

http://stackoverflow.com/questions/6923572/asp-net-mvc-3-portable-area-view-doesnt-find-my-model

首先,非常感谢以上几位大牛分享的文章,由于文笔不好,对.NET 了解也不够,希望大家多多指点。

理想情况下是希望能够像Orchard那样,可以运行时修改代码,又或者可以直接把插件(包含页面、样式、图片等资源文件)编译成一个DLL来使用(但是这样的做法会对前端与美工修改不便,而且就算改一个字也要重新编译一个DLL),只是依然还没找到方法ORZ。

最终结构图如下。

 

当把插件的站点发布出来后,目录名为插件名,并将该目录及目录下的所有文件复制到Plugins目录下即可自动安装并运行,不需要重启程序池。

 

要实现这么一个架构,最初认为,只要使用 Assembly.LoadFile(name);方法来加载外部的程序集,并且使用反射创建控制器,在定义一下MVC的模板引擎的搜索路径不就可以了吗?

 

当具体实现之后,发现,在不使用强类型的模型绑定时,可以正常使用,但是,使用了强类型的模型绑定时,则会出现以下错误。

问题产生原因:

.NET 会把.cshtml 与相关的程序集进行编译,之后访问的是编译后的临时程序集,但是,由于没有引用进入系统中,这里编译的时候没有该程序集,就会出现错误。

那么,要解决这个问题,就需要在编译时,把需要的程序集,都一起编译了,但是,怎么样才可以实现?

直接引用并使用类库的时候,系统会自行编译到一起了,所以解决办法有两种:

1、在系统启动前的预编译时,手动把相关的程序集增加进系统中,这样就是一个实际存在于系统的程序集,在页面编译时自然会编译进去。

在google查找相关的解决方法时,发现了该方法:

BuildManager.AddReferencedAssembly(assembly);

在查MSDN有这么一段话:此方法必须在 Global.asax 文件中的 Application_Start 事件发生前调用。

也就意味着加载程序集的方法就必须要在预启动阶段就是加载了。

并且使用上面的方法,来把程序集加到系统里。

虽然这样可以正常使用了,但是,偶尔还是会有出现编译错误的异常。

在调试阶段下,只有重新生成的代码时可以正常运行,重新生成之后的代码,在点启动调试时,就会出现编译错误问题,调试发现,在这个时候,系统并没有将需要的程序集加载到系统中,有大牛了解的话希望指点下原因。

但是,在使用了Web.config 配置文件中的节点“probing”以后,把相关的程序集复制到“probing”指定的目录下,就能正常运行了。

但是, 由于上面的那段代码只可以在预启动阶段使用,所有注定了该方法有个缺点,就是每个更新插件时,都要重启或者回收一次程序池,没能正常的做到插件化的灵活性。

 

2、在模板(cshtml)进行编译前,把外部引用的相关的程序集增加到编译信息中,这样在对模板进行编译的同时,会把该程序集也编译进去。

在谷歌娘的帮助下,找到了该事件:

RazorBuildProvider.CodeGenerationStarted

从名字可以看出,这是在编译启动时触发的事件,具体功能不明ORZ。

可以通过该事件,把外部的程序集增加到 RazorBuildProvider 类中。

provider.AssemblyBuilder.AddAssemblyReference(plugin.Assembly);

provider 是 RazorBuildProvider 类的一个实例。
plugin.Assembly 是一个页面所使用的程序集。
只要把这段代码放到模板引擎的搜索视图的位置,即可根据需要,将增加外部的程序集,由于重复增加会出现已添加组件异常,所以,这里加了个 isLoadAssembly 变量来确认是否已增加过。

 

复制代码
        /// <summary>
        /// 给运行时编译的页面加了引用程序集。
        /// </summary>
        /// <param name="pluginName"></param>
        private void CodeGeneration(string pluginName)
        {
            RazorBuildProvider.CodeGenerationStarted += (object sender, EventArgs e) =>
            {
                RazorBuildProvider provider = (RazorBuildProvider)sender;

                var plugin = PluginManager.GetPlugin(pluginName);

                if (plugin != null)
                {
                    provider.AssemblyBuilder.AddAssemblyReference(plugin.Assembly);
                }
            };
        }
复制代码

 

 

 

我不喜欢每装一个插件,都要重启一次,所以我选择使用了第二种方法。

下一篇,将使用第二种方法来进行具体的实践并发布源码。

ASP.NET MVC 4 插件化架构简单实现-思路篇

ASP.NET MVC 4 插件化架构简单实现-实例篇

我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(五)框架及Web项目的组件化 - 萧秦 - 博客园

mikel阅读(979)

来源: 我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(五)框架及Web项目的组件化 – 萧秦 – 博客园

一、组件化印象

1、先给大家看一张截图

image

如果我告诉大家,这就是一个web管理系统发布后的所有内容,你们会不会觉得太简洁了,只有一个web.config、一个Global.asax文件,其它的都是dll文件,没有aspx、cshtml、html页面,没有js css images文件,但它的确能跑起来,跑起来之后的截图如下

image

2、再看我新建一个项目的流程

新建一个ASP.NET MVC 4的空的项目

image

image

点确定之后,创建项目,默认的项目目录结构如下

image

然后打开nuget包管理界面,选择我们需要安装的组件 Zephyr.Web.Sys,这是我们权限管理系统

image

点安装进行安装,实现上就是引入dll文件

image

安装完成后,直接F5运行,就出现了以下页面

image

点创建后

image

我们选择MySQL输入相应的数据库信息测试连接通过后,点创建

image

创建成功

image

现在登录跳转到登陆页面

image

登录成功后

image

权限系统中页面测试都没有问题

image

然后我们回过头看程序,什么都没改变,

image

只是增加了一些引用及web.config中添加了一些信息。

image

那回过头来,这样发布后,不就是我们之前所看到了什么都没有的Web应用程序了

image

大家是不是也是很感兴趣,我当时也只是一个想法,但直正驱动我去完成这些功能的还是以下几点:

a 代码的重用性提高,可以很方便的用NuGet管理类库或系统。即使不用NuGet管理,直接把dll文件拷贝到新建项目的bin下面,然后修改下web.config也是可以的。比以前我们引用项目的方式方便很多。

b 创建一个新项目时只需要安装组件即可

c 发布后的程序干净,项目更新只需覆盖对应的dll即可。

d 更方便做成产品发布

 

二、NuGet操作

那么我们要做到前面所看到的,我们要做哪些工作?

1、准备好要组件化的的类库

image

2、创建Nuget包

创建NuGet包有很多种方法,我采用的是编译时自动生成NuGet包,

首先要启用NuGet包还原功能(Eable NuGet Package Restore),然后VS会自动下载添加一个.nuget的文件夹

image

然后编辑项目工程文件,添加最后一行<BuildPackage>true</BuildPackage>

image

然后编译后就会在bin目录下发现一个后缀为nupkg的文件,这就是我们要的nuget包

image

当然你也可以在程序包管理器控制台输入命令行进行打包
也可以下载一个图形化的打包器nuget package explorer

3、推送到NuGet服务上

你可以在https://www.nuget.org/注册上帐号,放在nuget官网上
也可以自己搭建一个NuGet服务器:新建一个空项目,安装NuGet.Server然后直接发布就可以了。具体教程网上也很多了。

上传NuGet包只需要执行以下命令
nuget push Zephyr.Core.2.0.0.0.nupkg -s http://192.168.1.100:8888

到这里就已经ok了,你可以在Nuget包管理界面中添加一个程序包源服务地址,然后就可以安装你刚刚上传的包了

 

三、打包Web应用会碰到的问题

1、资源的嵌入

资源的嵌入分两种,一种是静态资源,直接内嵌到程序集中
第二种是动态的页面,可以预编译成类放到程序集中

image

 

2、资源的访问

获取嵌入资源的访问地址

public static string GetResourceUrl(Type type string embedFileName)
{
var page = new Page();
    return page.ClientScript.GetWebResourceUrl(type, embedFileName);
}

当然你也可以通过controller去处理

复制代码
[AllowAnonymous]
[MvcMenuFilter(false)]
[WebFrameworkFilter]
public class ResourceController : Controller
{
    public ActionResult Index()
    {
        var assemblyName = ResourceVirtualPathProvider.ResourceVirtualPath.GetAssemblyName(Request.Path);
        var filename = ResourceVirtualPathProvider.ResourceVirtualPath.GetResourceName(Request.Path);
        var assembly = ReflectionHelper.GetAssembly(assemblyName);

        var stream = ResourceHelper.GetFileStream(assembly,filename);
        var contentType = ResourceHelper.GetContentType(filename);

        if (stream == null)
            return HttpNotFound();

        return File(stream, contentType);
    }
}
复制代码

四、框架组件化操作

我的框架Zephyr.Net主要分为:

类库 描述 依赖
Zephyr.Utils 工具类库
Zephyr.Library 常用其它类库
Zephyr.Data 数据库访问组件
Zephyr.Core 框架核心类 Zephyr.Utils Zephyr.Library Zephyr.Data
Zephyr.Web.Resource 静态资源
Zephyr.Web.Mvc 纵云Mvc框架 Zephyr.Core
Zephyr.Web.Sys 权限管理系统 Zephyr.Web.Mvc

以上是七个独立的项目,依赖关系也不直接引用项目,而是从nuget上引用发布程序包,

其中前面四个是类库
Zephyr.Web.Resource是嵌入静态资源的程序集,如果不引入这个文件,直接把静态资源文件夹拷贝进项目中也是可以的。
Zephyr.Web.Mvc是我们的Mvc框架基础,已经包括了mvc及web api很多的路由注册过滤器设定等设置,并且有Login页面和Index页面,安装了这个组件,即使不安装纵云权限系统组件,程序也一样可以跑起来,只是没有权限系统,是个空的架子。
Zephyr.Web.Sys就是权限系统了,把权限系统的mvc及webapi控制器、View页面、js以及数据库初始化处理都放在这个程序集中。
以后我们开发新的项目也可以做成跟这个权限系统一样,都放在一个dll中(当然也可以分多个,把controller model等都分成不同的层),以后只要引入或者bin下面放入这个dll就会动态引入这个模块,非常方便。

打包成nuget packages发布到nuget服务上,大功就告成了,如果有更新,只需要更新版本号重新发布就行了

image

这样一来,基本就把我的框架以及权限系统都做成组件了,如果新项目需要引入,或者多个项目升级都很方便了。

.NET框架交流群(三)  21549700

我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(一) - 萧秦 - 博客园

mikel阅读(931)

来源: 我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(一) – 萧秦 – 博客园

一、前言

之前的博客一直都还没写到框架的实现及权限系统,今天开始写我的权限系统,我以前做过的项目基本上都有权限管理这个模块,但各个系统都会有一些不太一样,有些简单点,有些稍微复杂一点,一句话,我们做的系统都离不开这个权限系统。所以网上很多人尝试做一个通用的权限系统,不评论他们做的怎么样,只是说在网上能找到的直接能用的应该不多,适用的并且能集成到项目中的就更少了,所以还是考虑自己做一个,不一定很通用,但足够自己用的权限系统。

二、需求分析

关于权限系统的文章网上多如牛毛,很多都是基于角色的访问控制(RBAC)设计。但是发现完全实现RBAC的理论其实不一定好用,我想做一款适合自己的。所以首先我们必要明确我们要去实现哪些东西。

1、权限资源
a.菜单权限  经理和业务员登陆系统拥有的功能菜单是不一样的
b.按钮权限  经理能够审批,而业务员不可以
c.数据权限  A业务员看不到B业务员的单据
d.字段权限  某些人查询客户信息时看不到客户的手机号或其它字段

2、用户,应用系统的具体操作者,我这里设计用户是不能直接分配权限的,必须要分配一个角色,角色中再分配权限,如果某个用户权限比较特殊,可以为他专门建一个角色来应用解决,因为如果用户也可以分配权限系统就会复杂很多。

3、角色,为了对许多拥有相似权限的用户进行分类管理,定义了角色的概念,以上所有的权限资源都可以分配给角色,角色和用户N:N的关系。

4、机构,树形的公司部门结构,国内公司用的比较多,它实际上就是一个用户组,机构和角色N:N的关系,机构和用户也设计成N:N的关系,也就是说有时候一个用户可以从属于两个部门,这种情况在我们客户需求中的确都出现过。

当然除了上面提到的主要对象还包括以下对象

5、菜单

6、按钮

7、授权代码 以前我做的项目中控制数据权限有以下两种:
a.用部门编码来控制,只能查询到自己所属部门的数据
b.用项目编码来控制,只能查询到自己所属项目的数据。
这样就有一个问题,我到底要用什么字段来控制数据权限呢,所以我们就设计了一个授权代码,如果你是用机构来控制数据权限,那么你可以把授权代码建成跟机构一样的树,如果用项目编码控制,可以把它建成项目树,用其它字段控制都一样,所以会比较灵活,也把机构或项目解放出来了。

8、数据字典

9、系统参数

10、日志查询 登陆日志、操作日志(xx人xx时间对xx对象做了xx操作 自定义的)、系统日志(系统运行错误记录等 log4net输出的)

 

三、设计实现

1、表结构设计

image

 

2 程序设计

我还是采用mvc4.0 + web api + easyui + knockout去实现。实际上之前我就实现的差不多,最近也一直在花时间更新完善。
这个权限系统太大了,这一篇肯定是说不完的,我这里就先总体上说了一下设计思路,接下每一篇写具体实现,一篇一个功能,大家期待我的下一篇吧。

3、UI设计

不多说了,先上图吧,我在第一篇博客中贴过以前的版本,最近也有一些更新,再贴一次吧,这次截图比较详细点,也配了一些文字的说明介绍。

功能1:菜单导航
菜单导航】1:包括新增修改删除功能,直接在grid中编辑
image

【菜单导航】2:选择图标
image

【菜单导航】3:选择页面按钮 定义这个功能有只些按钮image

【菜单导航】4:管理按钮库 增删改
image

 

功能2:组织结构
【组织结构】1:图形化的树结构,可以新增编辑删除节点
image

【组织结构】2:编辑节点
image

【组织结构】3:设置角色
image

功能3:授权代码
【授权代码】树形grid 可以增添编辑删除
image

 

功能4:角色管理
【角色管理】1:直接在grid中 增添编辑删除
image

【角色管理】2:点编辑权限弹出角色极限窗口,点击选择菜单权限image

【角色管理】3:编辑按钮权限,grid中只会出现菜单权限中选中的行,每个功能有哪些按钮在菜单中已定义好,这里只要勾选按钮即可
image

【角色管理】4:编辑数据权限 可以多选,但只有一个默认值,该角色新增的业务单据的授权代码为默认的授权代码。
image

【角色管理】5:编辑字段权限 grid中只会出现菜单权限中选中的行,直接在允许或拒绝中输入字段名,多个字段用逗号隔开
image

【角色管理】6:点击管理成员,弹出此窗口
image

【角色管理】7:成员管理 点击添加按钮
image

 

功能5:用户管理
【用户管理】1:左边机构可以过滤,直接在grid中增删改查用户
image

【用户管理】2:设置机构 可多选,这里机构录入的太多了,出现了滚动条,少些会好看点。
image

【用户管理】3:设置角色 可多选
image

【用户管理】4:用户设置 用户登陆后的一些配置信息,比如指定用户的桌面,默认的项目,不同的系统可能会有不同的需求,所以这里设计可添加删除的,右上角下拉出来的[个人设置]页面也指向这里的数据image

 

功能6:数据字典
【数据字典】1:左边类别树可过滤,左边是树形grid,可以有层级关系,直接在grid中编辑
image

【数据字典】2:点击管理类别,类别可在弹出的grid中增删改查
image

 

功能7:系统参数
【系统参数】直接grid中编辑,设计了一个是否可编辑字段,只能编辑 是否可编辑为true的参数
image

 

功能8:操作日志
【操作日志】1:登陆日志
image

【操作日志】2:业务操作记录
image

【操作日志】3:系统日志
image

【操作日志】4:系统日志 查看系统日志
image

四、后述

如果大家感兴趣,就在右下角帮我【推荐】一下吧,谢谢大家了。我接下来就按照功能列表一篇一篇的来写,下一篇就先开始写菜单管理。
这里整个系统都是采用我的zephyr.net框架开发,应用mvc4.0 webapi easyui knockoutjs开发的实现的。
很多朋友私信我一些问题,所以干脆我创建了一个技术交流QQ群:群一:328510073(已满),群二:167813846,欢迎大家来交流。

基于办公的 IM 的基础设计

mikel阅读(839)

http://blog.codingnow.com/2017/08/im_for_office.html

现在的 IM 在设计上是基于会话的,多个人可以组成一个会话,相当于一个聊天室,当一个人加入到一个会话后,就可以看到从加入开始之后这个聊天室里所有参与人的发言。有的 IM 会把两人对话也抽象成同一个东西,也可能出于优化的考虑把双人对话特殊处理。

所以,这些 IM 在操作界面上会有一个会话列表:表现出来会是联系人名单、聊天群列表等等。选中会话列表中的项目,进入会话查看聊天记录、发言,就是这类 IM 的使用逻辑。

我认为,这种对即时通讯的抽象方式,其实是不适合办公环境的。和日常个人社交环境不同,办公群体其实是一个相对关系密切的团体,我们通常不会拉黑一个同事不让他给你发消息,也不会拒收公司发的通告,也不会因为一个同事平常不和你打交道就拒绝建立联系。项目组里的讨论,也未见得是多么保密的事情,需要防止隔壁组的同事旁听。你也很少会在办公 IM 上和妹子私聊谈人生理想。

我们这几年使用腾讯的 RTX 作为公司办公使用,我就感受到了太多这类设计缺陷。比如,有同事找我有事,我忽略了他的私聊信息;找人一般在对方活跃的项目群组里吼;程序群沦为了日常扯淡的位置,常常同时讨论着不同的问题,线索及其混乱。“群”这个设计,我在很多年前就思考过 ,我一直觉得需要在根本上换个角度看待社交聊天的需求。

我现在的想法是这样的:

作为办公 IM ,我们不应该基于固定会话(群)来设计,而应该是“通知”和“话题”。

所谓通知,就是有人发起了一条消息,他需要把这条消息传达给某些对象,对象可以是人,也可以是某个组织:比如程序、游戏项目组、等等。

组织并非是群那样的聊天室,而仅仅是一个标签,由人来关注标签,而不是去组织这个聊天室里有多少听众。

而话题,则是由消息或旧话题衍生而来。任何通知消息、话题内部消息,都可以变成一个新话题。话题也可以包含在一则消息里转发给某个对象。

用户的客户端应该把所有的通知按时间线排列在一起,呈现在同一个地方。也就是说,无论是谁给我发消息(默认就是通知)都应该投递在一起,而不是像现在 RTX 那样只是在系统托盘里闪烁提醒、也不是微信 qq 那样,联系人名单上多出一个小红点。

而一旦我回复一个通知消息,其实就把这则通知转化成了一个话题,在时间线上,话题内的消息是归属在一起的。同一时刻,无论你的思维切换多么快,其实在短时间内你只能聚焦在一个话题上,所以客户端界面是很容易表达的,把当前话题展开在主界面(通知的时间线)即可,切换话题后自然可以折叠起来。

话题并不是聊天室、它更像是论坛的帖子。一个话题可以有很多人参与(至少发言一次),更应该支持更多的人浏览。我们不应该按聊天室的思路:用户只有在加入聊天室的那一刻开始,才能收到后续的消息,而应该像论坛那样,他只是打开了这个话题帖,可以随时聊天过去到现在发生的事情。话题内的任何一个消息,都可以由用户展开为新话题,老话题对新话题只是一个引用链接而已,并不需要有层级关系,我们也可以把任意一个话题或尚未转化为话题的消息转发出去,如果有人对他评论,就生成了新话题。

话题是一个有时效性的东西,对于办公来说,如果一个话题超过 8 小时没有新的消息,就可以认为这个话题已经结束了。但是事后我们依然可以对老话题浏览,或是继续讨论,而继续讨论就是生成的一个新话题了。

只要生成话题足够方便,每个用户的主时间线上就只会有不多的通知消息,信息传达更为有效。而管理每天的消息、检索旧消息也有很强的时间线。不像现有的 IM 群聊天,每天的聊天内容会被自然的组织成话题,这些话题上标识了参与人数、归属的组织的 tag 、继承于哪个父话题或通知、经历的时间段、衍生出哪些后续话题,等等。

即使是两个人之间的对话,也同样应该是话题的形式,而不应该把消息直接组织成一长串的聊天历史。话题未必有明确的主题,只是一种更自然的信息聚合形式而已。

对于办公场合来说,有意个最重要的优势:用户群有足够的自律。基于这种自律,我认为上面的思路若实现出来很容易推广使用。

在自律之外,或许还需要一些权限管理。这些权限管理应该是相对松散简单的,主要是限制用户订阅特定组织的 tag (比如一般员工不能订阅管理层的 tag ),限制围观特定话题(比如两人之间的私聊话题默认就是不对第三人开放权限的),话题可以锁定不准转发。权限设置的细节还需要进一步推敲。

android锁屏(三) - 老牛比 - CSDN博客

mikel阅读(1014)

来源: android锁屏(三) – 老牛比 – CSDN博客

锁屏服务类。1,启动后首先注册接受屏幕的关闭和开启的广播:广播中处理开屏锁屏任务;2,获取电话管理服务,注册通话状态监听:监听手机响铃状态、挂起状态、空闲状态。以Handler方式执行不同的任务。同样这里也是处理闹钟啊一些可以你认为可以在锁屏时候运行的东西。这里我没写,Dome而已,还是凑合看看吧

  1. import Android.app.KeyguardManager;
  2. import Android.app.Service;
  3. import android.app.KeyguardManager.KeyguardLock;
  4. import android.content.BroadcastReceiver;
  5. import android.content.Context;
  6. import android.content.Intent;
  7. import android.content.IntentFilter;
  8. import android.os.Handler;
  9. import android.os.IBinder;
  10. import android.os.Message;
  11. import android.os.PowerManager;
  12. import android.telephony.PhoneStateListener;
  13. import android.telephony.TelephonyManager;
  14. import android.util.Log;
  15. public class LockService extends Service {
  16.     protected static final String TAG = “SAFLockService”;
  17.     KeyguardManager mKeyguardManager;
  18.     private KeyguardLock mKeyguardLock = null;
  19.     private TelephonyManager tm;
  20.     private Context mContext;
  21.     @Override
  22.     public void onCreate() {
  23.         super.onCreate();
  24.         mContext = this.getApplicationContext();
  25.         Log.v(TAG, “=============onCreate:================”);
  26.         IntentFilter filter = new IntentFilter();
  27.         filter.addAction(Intent.ACTION_SCREEN_OFF);
  28.         filter.addAction(Intent.ACTION_SCREEN_ON);
  29.         registerReceiver(mScreenReceiver, filter);
  30.         mKeyguardManager = (KeyguardManager) this
  31.                 .getSystemService(Context.KEYGUARD_SERVICE);
  32.         mKeyguardLock = mKeyguardManager.newKeyguardLock(“”);
  33.         mKeyguardLock.disableKeyguard();
  34.         tm = (TelephonyManager) this
  35.                 .getSystemService(Context.TELEPHONY_SERVICE);
  36.         SAFLockPhoneCallListener myPhoneCallListener = new SAFLockPhoneCallListener();
  37.         tm.listen(myPhoneCallListener, PhoneStateListener.LISTEN_CALL_STATE);
  38.         SAFLockConstants.loadInitParam(this);
  39.         this.setForeground(true);
  40.     }
  41.     @Override
  42.     public IBinder onBind(Intent intent) {
  43.         return null;
  44.     }
  45.     @Override
  46.     public void onDestroy() {
  47.         super.onDestroy();
  48.         mKeyguardLock.reenableKeyguard();
  49.         mHandler.removeMessages(SAFLockConstants.EVENT_LOCK_SCREEN);
  50.         unregisterReceiver(mScreenReceiver);
  51.     }
  52.     /**
  53.      * set a listener to listen the phonestate,that we can do somthing by one
  54.      * of phonestates.
  55.      * 
  56.      * @author liao 
  57.      */
  58.     public class SAFLockPhoneCallListener extends PhoneStateListener {
  59.         public void onCallStateChanged(int state, String incomingNumber) {
  60.             switch (state) {
  61.             // Phone in idle
  62.             case TelephonyManager.CALL_STATE_IDLE:
  63.                 Log.v(TAG, “=============CALL_STATE_IDLE:================”);
  64.                 SAFLockConstants.PHONE_STATE = TelephonyManager.CALL_STATE_IDLE;
  65.                 if (SAFLockConstants.isLockecd) {
  66.                     mHandler.removeMessages(SAFLockConstants.EVENT_LOCK_SCREEN);
  67.                     mHandler.sendEmptyMessage(SAFLockConstants.EVENT_LOCK_SCREEN);
  68.                 }
  69.                 break;
  70.             // Phone is hung up
  71.             case TelephonyManager.CALL_STATE_OFFHOOK:
  72.                 Log.v(TAG, “=============CALL_STATE_OFFHOOK:================”);
  73.                 SAFLockConstants.PHONE_STATE = TelephonyManager.CALL_STATE_OFFHOOK;
  74.                 break;
  75.             // Cell phone ring
  76.             case TelephonyManager.CALL_STATE_RINGING:
  77.                 Log.v(TAG, “=============CALL_STATE_RINGING:================”);
  78.                 SAFLockConstants.PHONE_STATE = TelephonyManager.CALL_STATE_RINGING;
  79.                 break;
  80.             default:
  81.                 Log.v(TAG, “=============default:================”);
  82.                 break;
  83.             }
  84.         }
  85.     }
  86.     private Handler mHandler = new Handler() {
  87.         @Override
  88.         public void handleMessage(Message msg) {
  89.             switch (msg.what) {
  90.             case SAFLockConstants.EVENT_LOCK_SCREEN:
  91.                 // ====read user customization
  92.                 SAFLockConstants.updateLockIntent(
  93.                         “saf.cmcc.lock.THEME_DEFAULT”, mContext);
  94.                 // keep the task running 2s in sleeping mode.
  95.                 PowerManager pm = (PowerManager) mContext
  96.                         .getSystemService(Context.POWER_SERVICE);
  97.                 // Keep the CPU running, and the screen and keyboard light may
  98.                 // be closed.
  99.                 PowerManager.WakeLock mWakeLock = pm.newWakeLock(
  100.                         PowerManager.PARTIAL_WAKE_LOCK, “SAFLock”);
  101.                 mWakeLock.acquire(2000L);
  102.                 Intent mLockScreenIntent = new Intent(mContext,
  103.                         SAFFullScreenActivity.class);
  104.                 mLockScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  105.                 mLockScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
  106.                 mContext.startActivity(mLockScreenIntent);
  107.                 break;
  108.             default:
  109.                 break;
  110.             }
  111.         }
  112.     };
  113.     private BroadcastReceiver mScreenReceiver = new BroadcastReceiver() {
  114.         @Override
  115.         public void onReceive(Context context, Intent intent) {
  116.             String action = intent.getAction();
  117.             if (Intent.ACTION_SCREEN_OFF.equalsIgnoreCase(action)) {
  118.                 // load intent
  119.                 if (SAFLockConstants.LOCK_ENABLE
  120.                         && SAFLockConstants.PHONE_STATE == TelephonyManager.CALL_STATE_IDLE) {
  121.                     // =====send message after a short delay
  122.                     if (!SAFLockConstants.isLockecd) {
  123.                         mHandler.removeMessages(SAFLockConstants.EVENT_LOCK_SCREEN);
  124.                         mHandler.sendEmptyMessageDelayed(
  125.                                 SAFLockConstants.EVENT_LOCK_SCREEN, 100);
  126.                     }
  127.                 }
  128.             } else if (Intent.ACTION_SCREEN_ON.equalsIgnoreCase(action)) {
  129.                 // ====remove unprocessed event.
  130.                 mHandler.removeMessages(SAFLockConstants.EVENT_LOCK_SCREEN);
  131.             }
  132.         }
  133.     };
  134. }

注释是英文的,上头要求,没办法

android 锁屏(二) - 老牛比 - CSDN博客

mikel阅读(920)

来源: android 锁屏(二) – 老牛比 – CSDN博客

这篇呢,我们讲程序的入口。

SAFScreenReceiver.Java :作为锁屏模块的入口。通过获取开机 和 锁屏键的广播,根据SAFScreenReceiver中存储是否允许锁屏,允许则启动锁屏服务

  1. package saf.cmcc.home.lock;
  2. import Android.content.BroadcastReceiver;
  3. import Android.content.Context;
  4. import Android.content.Intent;
  5. import android.util.Log;
  6. /**
  7.  * this class role is to receiver the system broadcast:open phone
  8.  * @author issuser
  9.  *
  10.  */
  11. public class SAFScreenReceiver extends BroadcastReceiver {
  12.     private static final String TAG = “SAFScreenReceiver”;
  13.     @Override
  14.     public void onReceive(Context context,Intent intent) {
  15.         String action = intent.getAction();
  16.         if(Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(action)
  17.                 || Intent.ACTION_USER_PRESENT.equalsIgnoreCase(action)){
  18.             Log.v(TAG, “action:”+action);
  19.             SAFLockConstants.loadInitParam(context);
  20.             if(SAFLockConstants.LOCK_ENABLE){
  21.                 Intent i = new Intent(context, SAFLockService.class);
  22.                 context.startService(i);
  23.             }
  24.         }
  25.     }
  26. }

抱歉,没注释,不知道要写什么的。凑合看着吧

android锁屏原理(一) - 老牛比 - CSDN博客

mikel阅读(1028)

来源: android锁屏原理(一) – 老牛比 – CSDN博客

最近忙着跳槽,跑来移动后的第一个项目就是做Android定制的锁屏模块。目前已经差不多了,就缺UI工程师的图片了。在这里给大家讲讲我做的这个。先画张图吧

 

不知道大家看了这图恶心不,反正我恶心了,呵呵。还是回正题吧

首先从左边看吧,由上而下。1,第一个ScreenReceiver是作为锁屏的入口。接收系统开机和屏幕关闭开启广播,根据设置中保存是否可以锁屏来启动锁屏服务。

2,ScreenService作为锁屏程序中的核心类,启动后注册接收屏幕开/关广播,分别处理对应的任务。其次是对手机状态的监听,就是那个电话的三状态,空闲、响铃、挂起(接听),分别处理。处理闹钟什么的,都一样。呵呵,不在阐述。最后是处理一些锁屏的初始化数据。

3,ScreenActiviy,这个类命名有点2,估计会误导他人。作用很简单,被锁屏服务调用后启动,然后在这个Activity中处理要显示哪个主题的锁屏,就是调用哪个LockScreenActivity。相当于一个控制器(Controller)吧。

4,LockScreenActivity,就是我们手机在锁屏后看到的那个界面。这里的话就要看你怎么处理啦。就是要处理一些解锁动作啊,时间啊,日期啊,电量什么的。这个类代码比较多。后边我会贴上。上个图体现啊,别笑话,目前没图片,随便上网了拔下一些。

很烂吧,哈哈

5,LockTamplateActivity,这个类是是LockScreenActivity的基类(模板类)。没啥,就一些唧唧哇哇的东西,不过还提供了一些给子类的公共方法。还有就是负责屏蔽Home键,回退键,搜索键,菜单,还有状态栏。

6,LockConstants,看名字就知道干嘛了,一个常量类嘛,一些数据的存放,初始化什么的。保存方式是用SharedPreferences。

7,LockSettingActivity嘛,就是设置一些锁屏的功能的,保存还是SharedPreferences。

基本思路:毫无疑问,每次需要显示LockScreen—锁屏界面时,我们并不真正的去锁屏,而只是提供了一个空的方法去给系统调用,让系统觉得我们“锁屏”了,同样也不去真正的隐藏“锁屏”界面,提供一个空壳给系统调用。由于可能涉及 到其它问题,例如:能否下拉状态栏,按下POWER键后,屏幕很快休眠等。Come on ,我们需要统一做处理。——引用一前辈的话

好了,下一篇我会讲讲每个木块的实现。

基于XMPP协议的Android即时通信系 - 老牛比 - CSDN博客

mikel阅读(1059)

这段时间公司要做基于XMPP协议的聊天社交软件,提前学了些。搜了些资料,总结了一下。发出来。      设计基于开源的XMPP即时通信协议,采用C/S体系结构,通过GPRS无线网络用TCP协议连接到服务器,以架设开源的Openfn’e服务器作为即时通讯平台。         系统主要由以下部分组成:一是服务器,负责管理发出的连接或者与其他实体的会话,接收或转发XML(ExtensibleM

来源: 基于XMPP协议的Android即时通信系 – 老牛比 – CSDN博客

以前做过一个基于XMPP协议的聊天社交软件,总结了一下。发出来。

设计基于开源的XMPP即时通信协议,采用C/S体系结构,通过GPRS无线网络用TCP协议连接到服务器,以架设开源的Openfn’e服务器作为即时通讯平台。

系统主要由以下部分组成:一是服务器,负责管理发出的连接或者与其他实体的会话,接收或转发XML(ExtensibleMarkup Language)流元素给授权的客户端、服务器等;二是客户终端。它与服务器相连,通过XMPP获得由服务器或任何其它相关的服务所提供的全部功能。三是协议网关。完成XMPP协议传输的信息与外部消息系统可识别信息间的翻译。再就是XMPP网络。实现各个服务器、客户端间的连接。系统采用客户端(Client)/服务端(Server)架构体系结构。

 

客户端:

客户端基于Android平台进行开发。负责初始化通信过程,进行即时通信时,由客户端负责向服务器发起创建连接请求。系统通过GPRS无线网络与Internet网络建立连接,通过服务器实现与android客户端的即时通信脚。

服务器端:

服务器端则采用Openfire作为服务器。允许多个客户端同时登录并且并发的连接到一个服务器上。服务器对每个客户端的连接进行认证,对认证通过的客户端创建会话,客户端与服务器端之间的通信就在该会话的上下文中进行。

 

1.1服务器端设计(这块几乎可以说是有成品了。不用多纠结)

        Androidpn服务器端是Java语言实现的,基于openfire开源工程,Web部分采用的是spring框架,这一点与openfire是不同的。Androidpn服务器包含两个部分,一个是监听特定端口上的XMPP服务,负责与客户端的XMPPConnection类进行通信,作用是用户注册和身份认证,并发送推送通知消息。另外一部分是Web服务器,采用一个轻量级的HTTP服务器,负责接收用户的Web请求。

       主要的四个组成部分,分别是SessionManager,Auth Manager,PresenceManager以及Notification Manager。SessionManager负责管理客户端与服务器之间的会话,Auth Manager负责客户端用户认证管理,Presence Manager负责管理客户端用户的登录状态,NotificationManager负责实现服务器向客户端推送消息功能。

       系统客户端基于Android手机平台。采用XMPP作为即时通讯协议。XMPP是基于XML,实现任意两个网络终端准实时的交换结构化信息的通信协议。采用Android平台提供的XML解析包对XML进行解析。由于应用活动都运行于主线程。故用多线程技术来解决系统通讯问题。针对通信安全问题.系统的用户信息和聊天信息在客户端存储在Android平台自身所带的SQLite数据库中,多媒体文件和图片文件存储在Android平台虚拟文件存储设备SD Card中。

 

通讯模块负责与服务器建立通讯旧。通过创建3个线程来进行处理。分别负责消息的发送、接收和心跳信息的发送;解析模块主要用来解析XML数据流。根据解析元素不同类型封装成不同的数据对象:数据模块定义整个客户端中大部分的数据类型和对象;应用模块包括即时通信、图片浏览和音乐播放。是客户端和用户交流的接口;加密模块对发送和接收的消息进行加解密。以确保通讯数据的安全。

 

系统的客户端分为5大模块进行设计开发,如图2所示。

       通讯模块负责与服务器建立通讯旧。通过创建3个线程来进行处理。分别负责消息的发送、接收和心跳信息的发送;解析模块主要用来解析XML数据流。根据解析元素不同类型封装成不同的数据对象:数据模块定义整个客户端中大部分的数据类型和对象;应用模块包括即时通信、图片浏览和音乐播放。是客户端和用户交流的接口;加密模块对发送和接收的消息进行加解密。以确保通讯数据的安全。

加密(首先将二进制码转换成BASE64码,在转换成BASE64码之后,再进行MD5加密,)

XMPP服务器之间、客户与服务器之间采用的是TCP连接罔。TCP提供一种瓦向连接、可靠的字节流服务。保持一个实时双向的传输通道。TCP将用户数据打包构成报文段。它发送数据后启动一个定时器,等待对端数据确认,另一端对收到的数据进行确认,对失序的数据重新排序,并丢弃重复数据;TCP提供端到端的流量控制。计算和验证一个强制性的端到端检验。但是GPRS网络对TCP链路存在一个限制。当TCP链路在长时间无有数据流量时。会自动降低此链路的优先级直至强制断开此链路。所以在应用中.采用发送心跳的方式来维持此链路。

 数据格式

XML是XMPP系统架构的核心。它能表述几乎任何一种结构化数据。特别是XMPP利用XML数据流进行客户端一服务器端、服务器端一服务器端的通信。XML数据流一般是由客户端发起至服务端,XML数据流的有效时间直接与用户的在线会话有效时间相关联。

 协议消息格式

XMPP协议包括3个顶层XML元素:Message、Presence和IQm。Message用来表示传输的消息,当用户发送一条消息时。就会在流的上下文中插入一个Message元素,中间有用户发送的相关信息;Presence用来表示用户的状态。当用户改变自己的状态时。就会在数据流的上下文中插入一个Presence元素,用来表示用户现在的状态;IQ用来表示一种请求,响应机制,从一个实体发送请求,另外一个实体接受请求并响应。

后台Servic:

从类的层次看这个结构比较简单,让其变得复杂的是,其里面有三个线程:主线程,进行Xmpp通信线程,连接出错重试线程。

对图说明:

  1. 在NotificationService里创建一个单线程,让其对服务器进行连接,由于使用Xmpp连接服务器要分为三步:连接,注册,登陆。所以用一个栈来保存要执行的Task任务(ConnectTask,RegisterTask,LoginTask),还后再按这个顺序进行执行。

  2. 连接Xmpp服务器的线程用的是Executors.newSingleThreadExecutor(),这个本身可以不停的submit任务。为什么还要自己用一个栈来保存Task了

  3. 连接线程在连接,注册,登陆的过程中,都有可能出错,都可能会失败,这时我就要有一个重连的机制,在Androidpn里开了另外一个线程来进行重试,其重试不是每次都按多少秒来进行重试,而是有其自己的规则。

  4. 在LoginTask里,如果登陆了服务器端,其就会注册一个监听器,用于监听服务器push的数据包(Packet),再通过发送广播的方式来通知要进行显示的程序。

  5. 在登陆服务器后,也有可能出错,所以在登陆后,会设置一个ConnectionListener,用于监听连接出错的时候,再合适重连线程,进行重连

  6. 在登陆过程中,有一种错误要单独处理,就是账号和密码无效的时候,这个时候其返回的状态码是401,这种情况应该把本地保存的帐号和密码都清掉,再重新进行连接,不然会永远都登陆不上服务器端。

由于该系统所有的功能实现都是基于网络间的XML流的通信,所以,需要有一个模块专门负责网络问通信和XML流的处理,主要功能包括服务器和客户端之问通信时TCP套接字的处理,XML流的解析、存储等功能。

数据模块负责XML流的解析和封装的XML模块,主要功能是:将XML流解析成java对象,将iava对象封装成XML流;

其流程是XMPP服务器接收到XML流之后,会有渎取器将其读取出来并将其作为入口参数传入XML解析器,XML解析器通过对其命名空间的解析,从而确定将剩余的XML元素解析出来并传入相应的;ava对象中,从而最终将XML转换成iava对象,然后将iava对象传入应用程序模块中,实现其请求完成的功能并返回iava对象,但是该iava对象不能在网络中直接传输,必须先转换成XML节,于是,该iava对象会被传入XML封装器中,被封装成XML节,通过XMPP服务器的发送端口发往目的节点。

java对象处理模块处理流程如下:当该模块接收到iava对象时,会先将该对象通过解密算法和解密密钥解密成base64码,然后f耳将base64码转换成二进制码,从而实现对java对象的解析。当完成业务逻辑处理后,该模块会将返回的java对象先由二进制码转换成base64码,然后用加密算法将其加密,这里的加密算法是由双方在建立会话时通过三次握手协议协商的。

当XML节被封装成java对象后,必须被转发至订:确的模块中加以处理,这就要求有一个路由转发模块,如图3—3所示。该模块的实现原理是:在系统启动时加载该路由模块,从而在内存中创建了一块路由模块,记录了命名空和功能模块之间的对应关系,当iava对象被封装好之后,系统会读出其命名空间,再在路由表中查找其所对应的模块,从而动态地加载该模块,并将该java对象转发至该模块,从而实现路山转发的功能。

       需要客户端源码的朋友可以去我的资源里找,或者本博文系列的最后一篇有地址。服务端源码不能给,那是公司的东西,抱歉!不要再问了哈

openfire+spark+smack实现即时通讯-Android资料下载-eoe 移动开发者论坛 - Powered by Discuz!

mikel阅读(953)

Android资料下载,eoe 移动开发者论坛

来源: openfire+spark+smack实现即时通讯-Android资料下载-eoe 移动开发者论坛 – Powered by Discuz!

最近公司项目需要用到即时通讯功能,经过调研发现openfire+spark+smack可以实现。在网上找了很久,资料都十分有限,即使有些朋友实现了也说的不清不楚。于是决定自己研究,耗时一周的时间实现了文字通讯和文件传输的功能。现发帖分享给有需要的朋友,也希望大家能够把自己的资料都分享出来,不要敝扫自珍。

废话说够了,现在开始上干货:

1.openfire是什么?
openfire是一个即时通讯服务器,也称之为即时通讯平台。它是基于XMPP协议的,大家所熟悉的通讯软件QQ、MSN和Gtalk等等,其中Gtalk就是基于XMPP协议的实现。
在即时通讯中往往因为需要保存一些状态或者数据所以不能采用点对点通讯,而是需要搭建服务器来转发。
下载地址:http://www.igniterealtime.org/downloads/index.jsp
下载完毕以后根据提示安装、配置,然后访问所配置的服务器地址上的9090端口即可访问官方为我们实现好了的openfire后台管理系统。

2.spark是什么?
spark从本质上来说就是一个运行在PC上的java程序,你可以看成是官方为我们实现好的运行在PC上的客户端,我们只需要下载使用即可。
(当然如果你项目的需求现有的spark无法满足,你可以选择在官方下载spark的源码,对它进行修改)
下载地址:http://www.igniterealtime.org/downloads/index.jsp

3.smack是什么?
smack你可以看成是一套封装好了的用于实现XMPP协议传输的API,它是一个非常简单并且功能强大的类库,给用户发送消息只需要三行代码。
下载地址:http://www.igniterealtime.org/downloads/index.jsp
这里需要强调一点,我们在Android上开发用的其实是移植版的类库asmack,而asmack现在在网上流传的版本是有BUG的,在传输文件的时候会报空指针异常。我在这个问题上也是耗时最多的,在网上找了许多网友修改过的版本,都不顶用,最后也是在eoe论坛中找到了靠谱的asmack修订版:
详见:http://www.eoeandroid.com/thread-186418-1-1.html

好了,读到这里,相信你对这套实现思路的一些基本概念已经清楚了,那么请准备好上面的三板斧,下面就开始写代码吧。

文字通讯的重要代码片段:(请注意更换你自己的服务器地址、用户名和密码以及想要访问的用户名)

// 连接参数
ConnectionConfiguration connConfig = new ConnectionConfiguration(“192.168.0.1”, 5222);
connConfig.setReconnectionAllowed(true);
connConfig.setSecurityMode(SecurityMode.disabled); // SecurityMode.required/disabled
connConfig.setSASLAuthenticationEnabled(false); // true/false
connConfig.setCompressionEnabled(false);
// 配置服务器
XMPPConnection connection = new XMPPConnection(connConfig);

try {
// 连接服务器
connection.connect();
// 用户登录
connection.login(“joe”, “123456”);
// 向另一个用户发出聊天
Chat chat = connection.getChatManager().createChat(“admin@192.168.0.1/Spark 2.6.3”, new MessageListener() {
// 消息回复函数
@Override
public void processMessage(Chat arg0, Message arg1) {

System.out.println(“Received message: ” + arg1.getBody());

try
{
arg0.sendMessage(“我已收到”);
} catch (XMPPException e)
{
e.printStackTrace();
}

}
});
// 发送聊天信息
chat.sendMessage(“Hello!”);

文件传输的重要代码片段:(请注意更换你自己的服务器地址、用户名和密码以及想要访问的用户名)

// 连接参数
ConnectionConfiguration connConfig = new ConnectionConfiguration(“192.168.0.1”, 5222);
connConfig.setReconnectionAllowed(true);
connConfig.setSecurityMode(SecurityMode.disabled); // SecurityMode.required/disabled
connConfig.setSASLAuthenticationEnabled(false); // true/false
connConfig.setCompressionEnabled(false);
// 配置服务器
XMPPConnection connection = new XMPPConnection(connConfig);

try {
// 连接服务器
connection.connect();
// 用户登录
connection.login(“joe”, “123456”);
// 准备发送的文件
File file = new File(PATH);

FileTransferManager transferManager = new FileTransferManager(
connection);
OutgoingFileTransfer outgoingFileTransfer = transferManager
.createOutgoingFileTransfer(“admin@192.168.0.1/Spark 2.6.3”);
// 发送文件
outgoingFileTransfer.sendFile(file, file.getName());

// 接收文件监听
transferManager
.addFileTransferListener(new FileTransferListener() {

public void fileTransferRequest(
FileTransferRequest request) {
try {
// 接收文件
IncomingFileTransfer transfer = request
.accept();
// 接收文件存放的位置
transfer.recieveFile(new File(PATH));

} catch (Exception e) {
Log.e(“RecFile Ex In!”,
e.getMessage());
}
}
});

} catch (XMPPException e) {
e.printStackTrace();
}

}

OK,大功告成。最后再附上两个demo的工程文件: