发个牢骚,博客园发博文竟然不能写副标题。这篇既为我的服装DRP系列第二篇,也给为WCF增加UDP绑定系列收个尾。原本我打算记录开发过程中遇到的一些问题和个人见解,不过写到一半发现要写的东西实在太多,有些问题甚至不好描述,又担心误导读者,就作罢了。

说到即时通讯大伙都会第一时间想到QQ等聊天软件,似乎跟服装DRP八竿子打不着。即时通信翻译自Instant Messaging,如果我把它解释为即时消息推送,再将其放之于企业应用中就好理解了。举例:上级给下级发货,下级能第一时间知道货已发出,就用不着打电话询问或满心期待地频繁刷新列表;下级店铺卖出一单,正在为销售淡季发愁的老板看到蹦出的提示消息,瞬间有了信心……

这个功能对不明真相的客户并没有多少吸引力,因为大部分CS软件似乎都能做到这一点,只不过——或多或少延迟个几秒或几分钟,当然客户对延迟一无所知。但是做技术的知道这个延迟代表什么:频繁地访问数据源,频繁地将“最新”数据与本地数据作比较[or直接使用获取到的数据]刷新UI。假设对数据实时精度控制为1分钟,有1000个客户端运行,平均每个客户端对10种数据类型感兴趣(比如数据类型(即时通信中可称为消息类型)包括入库、发货、零售和调拨,或者基础资料的修改等等),那么每分钟就会产生额外的10000次的数据库的访问量,注意大部分访问都没有任何作用(除了副作用),而且假如没有合理地设置筛选条件[及其它改善手段],那么访问产生的数据量,大部分也可能是无用的。另外,合理的数据结构和逻辑设计以满足对各类数据类型的提示也是个不小的难点,毕竟数据类型多种多样,就单个数据类来说,也有多个属性,假如用户对其中的某些属性感兴趣,如何设计一种方式使得数据库中某条记录的某些字段变动时能检索到,啧啧,水很深哟。

注意:即时通讯和BS几乎没关系,BS应用先天不足,只能采用定时读取数据库的方式来模拟即时通信,同上述的大部分CS软件。也许用插件能行,但是插件本质上也是CS中的C。上回说到BS的缺点,这里又能加上一条,呵呵,开个玩笑。

请时刻注意本文所说的IM并非单纯的聊天软件,而是为企业应用系统服务的辅助类工具。它应该具有相对独立性、良好的扩展性和简便的应用性(应用是对用户和开发人员两者来说的,用户能方便的使用它,开发人员能方便地将它接入系统)。按照本系列惯例,列客户关注的几个功能需求:

  1. 在线用户管理(这在大中型服装企业比较有用,能有效跟进各个分支机构的分销系统使用情况);
  2. 系统消息广播;
  3. 单点登录(当已有相同账号在线时,两种处理方式,一是登录失败,一是仿QQ,将原在线用户踢下线;用数据库方式能实现前一种。); 
  4. 业务事件成功后可自动[对N个目标客户端]发送消息;
  5. 用户接收消息权限管理(是否能接收某个类型的消息);
  6. 消息提示;
  7. 消息查询(目前并未提供往期历史消息查询)
  8. 企业通讯工具(重点是美工,推后)
  9. ……

需求看似挺多,其实技术实现起来难点就一个:UDP打洞。单纯打洞而言,直接用Socket编码相当简单。不过为了提升自己对WCF的理解,我决定使用WCF来完成,后来发现这真是自讨苦吃(一些知识要点记录在为WCF增加UDP绑定(储备篇))中。依托WCF框架进行UDP通信与直接使用Socket相比,也有很多好处,比如消息的传递被封装为方法的调用,更符合咱“高层开发者”的口味。WCF原生支持的绑定类型并没有给实现打洞提供太多可用信息(TCP等若干绑定能获取发送端IPEndPoint信息),因此我使用微软后来提供的UDP绑定封装示例,并增加了设置通信端口和获取发送端IPEndPoint的功能,这两者是实现打洞的前提,此处不予赘述。下面关注业务代码。

 1 /// <summary>
2 /// 用户终端
3 /// </summary>
4 [DataContract]
5 public class UserPoint
6 {
7 /// <summary>
8 /// 用户标识
9 /// </summary>
10 [DataMember]
11 public string UserGuid { get; set; }
12
13 [DataMember]
14 public int UserID { get; set; }
15 [DataMember]
16 public string UserName { get; set; }
17 [DataMember]
18 public int OrganizationID { get; set; }
19 [DataMember]
20 public string OrganizationName { get; set; }
21
22 /// <summary>
23 /// 用户主机用于侦听和发送消息的网络地址(和端口)
24 /// </summary>
25 [DataMember]
26 public string NetPointAddress { get; set; }
27
28 public string UDPIMIPPort
29 {
30 get
31 {
32 if (string.IsNullOrEmpty(NetPointAddress))
33 return "";
34 else
35 return "soap.udp://" + NetPointAddress;
36 }
37 }
38
39 //给子类使用
40 //WCF不支持继承,可以使用KnowType,子类并非定义在当前程序集,此处用显式转换
41 public UserPoint ConvertToBase()
42 {
43 return new UserPoint
44 {
45 OrganizationID = this.OrganizationID,
46 OrganizationName = this.OrganizationName,
47 UserID = this.UserID,
48 UserName = this.UserName,
49 NetPointAddress = this.NetPointAddress,
50 UserGuid = this.UserGuid
51 };
52 }
53 }

接着定义服务契约,由于客户端会相互通信,在打洞时服务端也会调用客户端方法,因此所有客户端在运行时也要寄宿服务。

服务端:

 1 [ServiceContract(Namespace = "http://www.tuoxie.com/erp/")]
2 public interface IServerService
3 {
4 /// <summary>
5 /// 用户登入[到服务器端用户列表]
6 /// </summary>
7 [OperationContract(IsOneWay = true)]
8 void UserLogin(UserPoint user);
9
10 /// <summary>
11 /// 用户登出[移出服务器端用户列表]
12 /// </summary>
13 [OperationContract(IsOneWay = true)]
14 void UserLogout(UserPoint user);
15
16 /// <summary>
17 /// 叫用户A给用户B方向发一条消息(打洞)
18 /// </summary>
19 /// <param name="callingUser">打洞方</param>
20 /// <param name="waitingUserID">等待方标识</param>
21 [OperationContract(IsOneWay = true)]
22 void CallUserToPunchHole(UserPoint callingUser, string waitingUserGuid);
23
24 /// <summary>
25 /// 维持映射端口
26 /// </summary>
27 [OperationContract(IsOneWay = true)]
28 void HoldMyPort();
29 }

注意已映射端口在一段时间不使用后会自动失效。我在本地测试时,100秒端口还能用,能相互通信,120秒后失效,服务器再通过原先端口给客户端发送讯息,客户端不再接收到。为了维持有效性,需要客户端定时给服务器发送消息(反之应该也可以?)。HoldMyPort就是这个作用,一般实现为空方法。

客户端[服务]:

 1 /// <summary>
2 /// 客户端服务,主要用来接收各种消息
3 /// </summary>
4 [ServiceContract(Namespace = "http://www.tuoxie.com/erp/")]
5 public interface IClientService
6 {
7 /// <summary>
8 /// 用户上线通知
9 /// </summary>
10 [OperationContract(IsOneWay = true)]
11 void NotifyWhenUserLogin(UserPoint user);
12
13 /// <summary>
14 /// 用户下线通知
15 /// </summary>
16 [OperationContract(IsOneWay = true)]
17 void NotifyWhenUserLogout(UserPoint user);
18
19 /// <summary>
20 /// 消息通知
21 /// </summary>
22 [OperationContract(IsOneWay = true)]
23 void NotifyMessage(IMessage message);
24
25 /// <summary>
26 /// 打洞
27 /// </summary>
28 [OperationContract(IsOneWay = true)]
29 void NotifyPunchHole(UserPoint waitingUser);
30
31 /// <summary>
32 /// sbody say "hi" to me
33 /// <remarks>属于打洞过程</remarks>
34 /// </summary>
35 [OperationContract(IsOneWay = true)]
36 void SayHi(UserPoint callingUser);
37
38 /// <summary>
39 /// 踢我下线
40 /// </summary>
41 [OperationContract(IsOneWay = true)]
42 void KickOff(UserPoint user);
43 }

当用户登录系统时,发送讯息给服务器,服务端将执行下述方法:

 1 public void UserLogin(UserPoint user)
2 {
3 var users = MainWindowVM.OnlineUsers.Where(o => o.UserID == user.UserID).ToArray();
4 lock (((ICollection)MainWindowVM.OnlineUsers).SyncRoot)
5 {
6 if (users.Count() > 0)
7 {
8 Parallel.ForEach(users, u =>
9 {
10 MainWindowVM.OnlineUsers.Remove(u);
11 ServerService.InvokeClientService(u, service => service.KickOff(u.ConvertToBase()));
12 });
13 }
14 }
15 OperationContext context = OperationContext.Current;
16 //获取传进的消息属性
17 MessageProperties properties = context.IncomingMessageProperties;
18 //获取消息发送的远程终结点IP和端口
19 IPEndPoint endpoint = properties[RemoteEndpointMessageProperty.Name] as IPEndPoint;
20 user.NetPointAddress = endpoint.ToString();
21 lock (((ICollection)MainWindowVM.OnlineUsers).SyncRoot)
22 {
23 MainWindowVM.OnlineUsers.Add(new ServerUserPoint(user) { LoginTime = DateTime.Now });
24 }
25 NotifyWhenUserLogin(user);
26 }
27
28 /// <summary>
29 /// 通知所有在线用户有新用户上线了
30 /// </summary>
31 /// <param name="user">上线用户</param>
32 private void NotifyWhenUserLogin(UserPoint user)
33 {
34 lock (((ICollection)MainWindowVM.OnlineUsers).SyncRoot)//避免在循环过程中集合被修改
35 {
36 for (int i = 0; i < MainWindowVM.OnlineUsers.Count; i++)
37 {
38 var u = MainWindowVM.OnlineUsers.ElementAtOrDefault(i);
39 if (u != null && u.UserID != user.UserID)
40 InvokeClientService(u, service => service.NotifyWhenUserLogin(user));
41 }
42 }
43 }

测试该方法需要三台最好处于不同局域网内的机子,其中一台通过NAT映射为公网服务器。
单点登录:当有相同账号用户在线或系统管理员在服务端使用了踢TA下线的功能后,该账号已在线用户将被强制退出系统。原本面对这样的需求,我们常常在用户数据表中增加一个标识用户是否在线的字段,当用户登录成功置为1,退出则置为0。但这只能实现后续用户登录失败,而不会给已在线用户带来任何影响,另外会带来一个发生率较高的问题:系统异常退出,极端的情况诸如断电,那么用户以后就再也登录不了了,除非增加一个重置状态的功能,假如用户数多的话,那系统管理员就有的忙了。无论如何,这不是一个好的方法。假如有一天,客户希望取消同时在线数的限制,或者,取消部分用户的同时在线数限制,那么开发人员就有的忙了。有了IM,一切都变得相当轻松。我们只要在用户登入IM时进行相应的处理即可,我们甚至可以决定哪些用户不能重复登入,哪些可以重复登入。由于IM相对独立,改动起来比较方便,而且IM服务端只运行在服务器上,也不存在部署问题。强制用户退出只需要请求相应客户端的KickOff操作,此时客户端扮演服务端的角色。

接下来到了重点:打洞。少年们两眼绽放出异样的光芒,却不知道当事者的辛苦。其实关键代码相当简单。

 1 public static void SendMessageTo(ClientUserPoint user, IMessage message)
2 {
3 Action invokeAction = () =>
4 {
5 InvokeClientService(user, service => service.NotifyMessage(message));
6 };
7 if (user.IsTrustMe)//信任用户(已经建立信任连接)不需要打洞
8 {
9 invokeAction();
10 }
11 else
12 {
13 Action action = () =>
14 {
15 int maxTryCount = 3;//最大尝试次数
16 for (int i = 0; i < maxTryCount && !user.IsTrustMe; i++)
17 {
18 InvokeClientService(user, service => service.SayHi(CurrentUser));//我先打招呼
19 InvokeServerService(service => service.CallUserToPunchHole(user.ConvertToBase(), CurrentUser.UserGuid));//服务器叫对方给我打招呼
20 Thread.Sleep(500);
21 }
22 if (user.IsTrustMe)
23 {
24 invokeAction();
25 }
26 };
27 action.BeginInvoke(null, null);
28 }
29 }

这里有个问题,当通信双方处于相同局域网,应该期望它们直接通信,省略打洞步骤。方法是在用户登录时将本机IP和端口号(未映射)同时发送到服务端,当客户端A和客户端B的映射IP相同则说明他们处于同一内网,然后根据本机地址直连通信。不过这应该有两个问题需要解决:当局域网内存在多级子网NAT,A、B分属不同层,那么它们还要进行内部局域网打洞;本机IP有时候并不能准确获取,特别有些软件能生成虚拟IP。

在打洞成功后我们将对方的IsTrustMe设置成true。

1 public void SayHi(UserPoint callingUser)
2 {
3 if(VMGlobal.CurrentUser != null)
4 {
5 var user = IMHelper.OnlineUsers.Find(o => o.UserGuid == callingUser.UserGuid);
6 if (user != null)
7 user.IsTrustMe = true;
8 }
9 }

现在就可以直连通信咯。

经测试,打洞过程一般尝试1次就能连接成功,此处每次等待500毫秒。

关于组播。原本打算采用组播的方式群发消息(包括所有终端用户其它用户上下线的提示消息),不成想,路由器默认情况下是不会转发组播包的,必须在路由器上进行配置才行,解决该问题需要网管进行配合,不是编程就能解决的。而且一般的路由器都不支持组播,也就是说,目前很多路由器不支持组播协议,所以,局域网的路由器不会将这个组播信息传输出去,so,外面的电脑以及路由根本就不知道你这个组播的信息。有专门支持组播的路由,不过貌似价格不菲。如果路由器不支持组播的话,那么你的交换机就把你的组播数据当成广播数据了,广播只能在局域网里面。(该段话来自网络)。按照这个说法,外部组播数据想要进入内网也困难重重(对or错?)。因此我改用循环发送方式。

最后截个消息查询和消息接收权限的图,消息接收权限设置我目前将之放入角色管理中。

至此,IM核心功能基本实现完毕,能满足目前系统的需求(还有大数据传输等问题暂时未涉及到就不考虑了)。所谓企业通讯工具不过是在此基础上功能的累加,以后再加入吧。:)

后记:窃以为消息提示只是IM基本辅助功能,IM还能帮助系统即时刷新。举例:当权限管理员为我新增了几个模块权限,按照平常的做法,需要我注销后重新登录才能看到,现在只要将新增的模块信息发送给我,我这边系统自动将它们构造进左侧菜单树中即可;我正在下拉框中选择下级机构准备为他发货,下拉框中的数据项突然增加了一个,原因是机构管理员录入了一个新机构;……

转载本文请注明出处:http://www.cnblogs.com/newton/archive/2013/01/26/2877500.html

我的服装DRP之即时通讯——为WCF增加UDP绑定(应用篇)的更多相关文章

  1. 利用WCF双工模式实现即时通讯

    概述 WCF陆陆续续也用过多次,但每次都是浅尝辄止,以将够解决问题为王道,这几天稍闲,特寻了些资料看,昨晚尝试使用WCF的双工模式实现了一个简单的即时通讯程序,通过服务端转发实现客户端之间的通讯.这只 ...

  2. android环境下的即时通讯

    首先了解一下即时通信的概念.通过消息通道 传输消息对象,一个账号发往另外一账号,只要账号在线,可以即时获取到消息,这就是最简单的即使通讯.消息通道可由TCP/IP UDP实现.通俗讲就是把一个人要发送 ...

  3. 【原创】轻量级即时通讯技术MobileIMSDK:Android客户端开发指南

    申明:MobileIMSDK 目前为个人维护的原创开源工程,现陆续整理了一些资料,希望对需要的人有用.如需与作者交流,见文章底签名处,互相学习. MobileIMSDK开源工程的代码托管地址请进入 G ...

  4. 【原创】轻量级移动设备即时通讯技术MobileIMSDK的常见问题解答

    申明:MobileIMSDK 目前为个人原创开源工程且已发布,现整理了一些有关MobileIMSDK的常见的问题,希望对需要的人有用,谢谢.如需与作者交流,见文章底部个人签名处,互相学习. Mobil ...

  5. 【原创】轻量级移动端即时通讯技术 MobileIMSDK 发布了

    申明:MobileIMSDK目前为个人原创开源工程,投入了大量的时间和精力,希望对需要的人有所帮助.如需与作者交流,见文章底部个人签名处,互相学习.Q群:215891622,欢迎共同志趣者学习和交流. ...

  6. ActiveMQ 即时通讯服务 浅析

      一. 概述与介绍 ActiveMQ 是Apache出品,最流行的.功能强大的即时通讯和集成模式的开源服务器.ActiveMQ 是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provi ...

  7. APP邂逅即时通讯云,让你的手机APP聊起来

     #推荐活动# #线下沙龙# 明天下午在IC咖啡 —— <APP邂逅即时通讯云,让你的手机APP聊起来>, http://url.cn/Y8sYo5 

  8. 岁末年初3Q大战惊现高潮,360震撼推出Android "3Q" IM即时通讯

    岁末年初3Q大战惊现高潮,360震撼推出Android "3Q" IM即时通讯 看过了QQ和360斗争的开端高潮,当然现在还不能说这场斗争已经结束,在我看来这次的事件未尝不是一个适 ...

  9. Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE

    1. 前言 Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询.Comet技术.WebSocket技术.SSE(Serve ...

随机推荐

  1. 往github上上传项目

    点击start a project 新建 下面部分传送http://blog.csdn.net/s740556472/article/details/55000019 如图: 这里我们有一个步骤需要做 ...

  2. pyhton2与python3的使用区别

    刚刚开始学习python这门编程语言,考虑到python不同版本的一些用法不同,收集整理了一份python2与python3之间的区别,目前可能不全 编码(核心类) Python2默认编码ascii, ...

  3. Spring核心接口之InitializingBean

    一.InitializingBean接口说明 InitializingBean接口为bean提供了属性初始化后的处理方法,它只包括afterPropertiesSet方法,凡是继承该接口的类,在bea ...

  4. shell 命令 文件(解)压缩 tar,zip, gzip,bzip2

    1.gzip / gunzip [ gzip data.c]  对文件进行压缩,生成 data.c.gz    同时删除了原文件    同时压缩两个文件     [gunzip  data.c.gz  ...

  5. MySQL入门基础知识

    1.MySQL环境变量的配置 操作数据库时,要进入bin目录,如下: 但是如果进行配置环境变量,就不必切换路径,如下图所示,即使没有在G:\mysql-8.0.16-winx64\bin下,数据库依然 ...

  6. es 3.0 、es 5.0 、es 6.0

    es 5.0 的严格模式 “use strict” /在页面最顶端写启动全局 es 5.0 严格模式 为什么使用字符串可以 向下兼容 ,,不会报错 可以写在局部方法中,推荐使用 (例如 不再兼容es ...

  7. springboot2.0整合springsecurity前后端分离进行自定义权限控制

    在阅读本文之前可以先看看springsecurity的基本执行流程,下面我展示一些核心配置文件,后面给出完整的整合代码到git上面,有兴趣的小伙伴可以下载进行研究 使用maven工程构建项目,首先需要 ...

  8. Python自学:第五章 使用函数range( )

    # -*- coding: GBK -*- for value in range(1,5): print(value) 输出为: 1 2 3 4

  9. 贪心数列构造——cf1157D

    一开始将数列设置为0 1 2 3 4 5 6... 然后从左到右遍历,每位不够就增加即可 #include<bits/stdc++.h> using namespace std; #def ...

  10. dp转图论——cf1070A好题

    dp的状态转移很像一张有向图:每个状态为一个点,每中转移方案是一条有向边 本题要求是求出最小的数,那我们用状态[i,j]表示模i,数位和为j,那么从每个点出发的十条有向边代表[0,9]十个数 从每个状 ...