概述
在现在的工作中,最重要的一个环节就是沟通,所谓“沟通”,是指工作中大家的互相交流,交流的方式有多种多样,可以通过电子邮件,电话或网上沟通工具,每种方式都有自己的特点,比如电子邮件具有归档功能,并可以方便的追查之前的交流记录,但是电子邮件的沟通速度稍慢;电话的沟通最为快捷,可以用最快的方式表达自己的思想,使大家的思路达成共识,但是电话沟通的结果很难被跟踪,如果没有一个正式的会议纪要,则会在短时间内再次被人淡忘;第三种常用的方式就是种用网上的一些沟通工具,如MSN,QQ等,针对这些沟通工具,我也大致做了个比较,仅代表我个人观点,无意替IM工具做广告,也无意诋毁任何厂商:
1. YAHOO
我们在实际项目中,最重要的沟通就是所谓的“群聊”的方式,即一个项目组在物理位置上并不能坐在一起,因此需要经常一起通过“聊天”的方式进行沟通。YAHOO在这方面提供了很好的群聊功能,并且支持语音群聊。但是它有一个不太方便的地方,就是接收者必须同意后才可以开始接收消息,否则是无法接收到群消息的,这样造成群组工作中消息传达容易出现丢失,影响工作。
2. MSN
MSN目前的市场占用量相当大,但是在中国来说,它的登录一直是一个让人头痛的问题,经常会出现无法登录的状况,特别是在我们公司内部,70%的人无法正常使用MSN,除了登录问题外,还有一个问题,就是MSN会偶尔丢失信息,在极端的时候,可以看到聊天的一方在拼命的回答,另一方只是不停的问“你收到了吗?”这个问题的不良后果非常可怕。
3. QQ
QQ是在国产IM工具中一个相当出色,相当具有代表性的老大哥,它的群聊功能相当不错,但是建立群组有一个限制,就是必须具有一个“太阳”才能建一个群,或者申请QQ会员才可以建立更多的群,这是与其它工具最大的区别,可能是它商业化运作的比较成功的缘故吧。如果公司内部有多个项目组,就要建立多个群组,应该感觉起来不太理想。
4. Skype
这是一款以语音效果见长的软件,我在进行语音聊天的时候,这是首选的工具,它的语音无论是从清晰度或延时方面,都比其它工具有很大的优势。Skype新版的群组功能也相当强健,但是Skype有一个最大的特点就是不支持离线发送消息,只有发送端和接收端同时在线的时候,消息才会被发出,这就很容易造成消息传达的失误,比如我们在和海外分公司沟通的时候,时差非常大,我发出消息后,对方不在线,而对方在线的时候,我又不在线,只有两端同时在线的时候,才会接收到消息,所以经常会收到很久之前的离线消息,对工作的负面影响较大。
5. OfficeIM
我在尝试了上述几个主要的工具后,才试图去寻找更合适的工具。
上述四个工具均属于通用软件,需要internet支持,仅在局域网内,是无法使用的,对于有些公司来说,外网的访问权限需要开通,有些公司甚至不允许访问外网,而对于我们公司来说,访问外网需要经过严格的审察,而对QQ来说,应该是禁止使用的。于是我想找一个小型的沟通平台架设在自己的公司内部。
这个工具属于收费软件,暂且不说费用如何,我发现了它的一个问题,就是一个员工只能隶属于组织结构的一个节点,这对于我们项目管理的方式来说,基本已经被否定,因为我们一个人需要属于多个项目组。
6. 飞鸽传书UM
这是一款源自于日本的软件,客户端免费使用,服务器端会产生费用,这款软件不但支持组织结构,而且一个人员可以同时隶属于多个项目组,我进了试用,效果不错,但是后来发现了一个比较严重的问题(说实话现在记不起具体的问题了),我直接电话与他们客服沟通,确认此问题无法解决。
以上仅是我自己找的一些IM工具,我相信世界上有数不清的类似的工具,应该也有完全适合我的,只是我没有找到而已,不管这么多了,既然找不到,何不尝试自己写一个呢,哈,说起来有点天方夜谭,但是我有自知之明,我知道我需要什么样的功能,我不会去做很多比较花哨漂亮的效果出来。说干就开始准备吧,我首先要做的是找一个圈内的高人来指点一下IM的一般工作方式,得知最常用的方式是需要在聊天的客户端之间建立TCP连接,实现客户端之间的互相通信。天呢,我因为长时间只是在写C#程序,对这些底层通讯的方法基本没太多的概念,这样放弃?不行,在继续求教后得知,也有人直接利用WebService,建立客户端与服务器的通讯,客户端之间互相不通讯,全部通过服务器来中转,哈,其实这正是我想象的方式,没想到真有人这么做的。我也知道这种方式有很多的缺点,但是我想了一下我的实际场景,应该没有太大的问题。说了这么多,让我们出发吧!
架构
在概述部分我们曾提起过一点关于架构的问题,为了简化应用,我们必须在最短的时间内,用最快速的方式来实现一个最简单的聊天的功能,因为我们做这个工具是为了项目本身服务,而它本身并不是作为一个专门的项目来存在的。
其中数据库采用Oracle10g,可以尽可能保证大的并发量及历史信息的存储,因为此系统已经明确为企业内部使用,并且全部是与工作相关的,为了防止客户端聊天记录的丢失,本架构要求在服务器上保留所有的聊天记录,以待后期审计。
第二部分为WebService服务器,这个服务器是客户端与数据库之间交互的通道,在客户端与数据库服务器之间传递信息。在这种架构模式下,客户端之间没有互相的通信,全部需要通过服务器来中转。
第三部分就是客户端,也就是企业内部用户。在本系统中,不提供公开注册的功能,所有的用户必须由后台进行注册,并分配一个初始密码,用户登录后可以自行修改自己的密码,这种方式保证系统的安全性及私密性。
本系统在使用过程中,已经由项目组多个成员不断的完善,现在已经相当好用,并且新的功能也在不断的添加进来。下面分成三部分依次对数据库、WebService及客户端程序做一个介绍。
数据库说明
数据库主要存储以下内容:
Ø 用户信息
Ø 组织结构信息
Ø 消息历史记录
Ø 其它辅助信息
下面对每个表进行一个详细的说明:
1. 用户表:ZR_USER
字段名 |
字段类型 |
说明 |
USER_ID |
INTEGER |
用户的内部ID,在和其它表关联的时候使用此字段 |
USER_ALIAS |
VARCHAR2(255) |
用户的登录名,在数据库中按大写字母来存储 |
NAME |
VARCHAR2(100) |
用户的真实姓名,在聊天工具中显示真实姓名 |
PASSWORD |
VARCHAR2(255) |
用户登录的密码,由系统进行初始化,用户登录后可以自己修改 |
STATUS |
INTEGER |
状态 0表示不可以登录 1表示可以登录 |
CREATION_USER |
INTEGER |
此记录的创建用户 |
CREATION_TIME |
DATE |
创建时间 |
LAST_MOD_TIME |
DATE |
此记录的最后修改时间 |
DELETED |
INTEGER |
删除标志 0:正常 1:已经删除 |
LAST_LOGIN_TIME |
DATE |
用户最后登录系统的时间,在本系统中,用这个字段的值来判断用户是否在线 |
2. 组织表:ZR_ORGANIZATION
字段名 |
字段类型 |
说明 |
ORG_ID |
NUMBER |
组织的内部ID,供关联使用 |
PARENT_ID |
NUMBER |
上级组织的ID |
ORG_TYPE |
NUMBER |
组织类型 |
ORG_CODE |
VARCHAR2(100) |
组织代码 |
ORG_NAME |
VARCHAR2(200) |
组织名称 |
STATUS |
INTEGER |
状态 0表示临时群组 1表示永久生效的群组 |
CREATION_USER |
INTEGER |
创建者 |
CREATION_TIME |
DATE |
创建时间 |
LAST_MOD_TIME |
DATE |
最后修改时间 |
DELETED |
INTEGER |
删除标志 |
ORDERING |
INTEGER |
排序值 |
3. 用户-组织对应表:ZR_USER_ORGANIZATION
字段名 |
字段类型 |
说明 |
ORG_ID |
NUMBER |
组织的内部ID,供关联使用 |
USER_ID |
NUMBER |
用户ID,在这个表中,可以描述出用户与组织的多对多的关系 |
4. 消息表:ZR_MESSAGE,目前考虑所有的消息均在服务器上保存,直到需要删除为止。
字段名 |
字段类型 |
说明 |
OID |
NUMBER |
本表的主键 |
TO_ID |
NUMBER |
接收人的ID,可以是个人,也可是一个群组 |
TO_TYPE |
NUMBER |
接收者的类型 0表示个人 1表示群组 |
CREATION_TIME |
DATE |
发送的时间 |
FROM_ID |
NUMBER |
发送者的ID |
CONTENT |
VARCHAR2(2000) |
发送的内容,根据字段宽度,不能超过1000个汉字 |
5. 消息查阅历史表:ZR_MESSAGE_HIS,在系统中需要记录哪些消息被哪些人看过,以免下次重复发送,因为发给某个群组的消息,需要群组中所有成员都有权阅读,所以不能简单地在消息中加一个“是否已经阅”的字段。
字段名 |
字段类型 |
说明 |
HIS_ID |
NUMBER |
本表的主键 |
USER_ID |
NUMBER |
接收人的ID |
MESSAGE_ID |
NUMBER |
消息的ID |
CREATION_TIME |
DATE |
发送的时间 |
BATCH_ID |
NUMBER |
批ID,某个会员可能同时接收一批的消息,而不是一个个的发送,在此记录批的信息,以减少与服务器的交互量 |
利用Oracle作为存储数据库,除了它的大容量支持是一个很好的特性之外,能编写非常优秀的存储过程,也是它的一个非常大的优势。所谓存储过程,就是在数据库中写一些程序,以保证可以快速方便的操作数据库,而不用编写非常复杂的C#代码。
本系统中有一个命名为ZR_TIM_PKG的包,里面包括以下几个主要的存储过程,在下面给出一个详细的说明:
1. 检查用户的登录状态
function GetUserStatus(lastLoginTime in date) return number is
value number := 0;
begin
if (lastLoginTime is not null) then
if (sysdate - lastLoginTime < 2 / 24 / 60) then
value := 1;
end if;
end if;
return value;
end;
本函数主要用于取出用户的登录状态,在本系统中,认为最后登录日期超过两分钟视为离线。
2. 发送消息,此函数用于接收客户端发来的消息,并记录在数据库中,函数体如下:
function SendMessage(toID number,
toType number,
fromID number,
content varchar2) return number is
pragma autonomous_transaction;
value number := 0;
begin
select seq_message.nextval into value from dual;
insert into zr_message
(oid, to_id, to_type, creation_time, from_id, content)
values
(value, toID, toType, sysdate, fromID, content);
commit;
return value;
end;
3. 取得消息,本函数主要用于判断哪些消息需要发送给指定的用户,即不能发送重复,也不能丢下任何有用的信息,如果信息是发送给某个群组,则群组里的所有成员都需要得到这条消息,程序如下:
function GetMessage(userID in number) return number is
pragma autonomous_transaction;
value number;
mBatchID number;
begin
select seq_message_batch.nextval into mBatchID from dual;
insert into zr_message_his
(his_id, user_id, message_id, creation_time, batch_id)
select 1, userID, oid, sysdate, mBatchID
from zr_message m
where (to_id in
(select org_id
from zr_organization
start with org_id in (select org_id
from zr_user_organization
where user_id = userID)
connect by prior parent_id = org_id) or to_id = userID)
and from_id != userID
and not exists (select 'x'
from zr_message_his h
where user_id = pUserID
and h.message_id = m.oid);
if (sql%rowcount = 0) then
value := 0;
else
value := mBatchID;
end if;
update zr_user set last_login_time = sysdate where user_id = userID;
commit;
return value;
end;
段代码是整个存储过程中最长的一段,在判断是否已经读过的地方,有一个小技巧,代码片断如下:
not exists (select 'x' from zr_message_his h where user_id = pUserID and h.message_id = m.oid)
在写程序的时候,一般都喜欢用not in的语法,这种语法写法简单,易懂,但是有一个很大的问题,就是效率不好保证,在要求表设计必须满足一系列的条件之后,有时还不能达到效果。而not exists则对效率的满足会更容易一些。
上面还用到了一个“批”的概念,即一次可以向用户发送多条消息,依靠batch_id这个字段来保证批的完整性。
4. 创建临时的群聊
本系统的操作方式是在后台协助建立好了相关的项目组或组织架构,并创建相关的用户,用户自己无法注册,也无法选择自己所在的组,在聊天的时候,可以选择一对一的聊天,也可以对项目组整个发起群聊。这两种方式基本满足了聊天的要求,但是对于一些特别的情况,比如一个项目组里有十多个人,而我只想和其中的两个或三个群聊,这时如果以整个项目组为接收对象,则很多人会收到无用的信息,甚至工作会受到打扰,为此,接受我们项目组团队成员的建议,加入了“临时群聊”的概念,就是可以临时组建一个群组,在这个小的群组里进行聊天,这个函数的作用就是建立这个群组,并标志这个群组为临时性。好了, 现在看一下代码部分:
function InviteUser(pUserID in number, pGuestIDs in varchar2)
return varchar2 is
pragma autonomous_transaction;
value varchar2(1000);
mGroupID number;
mGroupName varchar2(1000) := ' ';
mName varchar2(100); -- 每个人的名字,临时变量
cursor cursor_ is select name from zr_user where user_id
in (select user_id from zr_user_organization where org_id = mGroupID);
begin
select seq_group_temp.nextval into mGroupID from dual;
insert into zr_organization (org_id, parent_id, status,deleted,
creation_time, creation_user, ordering)
values (mGroupID, 0, 0, 0, sysdate, pUserID, 0);
insert into zr_user_organization (user_id, org_id)
select to_number(a.column_value), mGroupID
from table(split(pGuestIDs, ',')) a
union all
select pUserID, mGroupID from dual;
open cursor_;
fetch cursor_ into mName;
while (not cursor_%notfound) loop
mGroupName := mGroupName || mName || ',';
fetch cursor_ into mName;
end loop;
close cursor_;
mGroupName := rtrim(mGroupName, ',');
update zr_organization set org_name = mGroupName where org_id = mGroupID;
commit;
value := mGroupID || '~' || mGroupName;
return value;
end;
到此为止,数据库部分已经大致介绍完成,虽然代码比较冗长,但是为了不让读者产生任何误解,所以还是贴了大部分代码出来。下面的一个环节就是WebService的编写,敬请待等下期。
本文转自Aicken(李鸣)博客园博客,原文链接:http://www.cnblogs.com/isline/archive/2010/04/12/1710343.html,如需转载请自行联系原作者