發(fā)個(gè)牢騷,,博客園發(fā)博文竟然不能寫副標(biāo)題。這篇既為我的服裝DRP系列第二篇,,也給為WCF增加UDP綁定系列收個(gè)尾,。原本我打算記錄開發(fā)過程中遇到的一些問題和個(gè)人見解,不過寫到一半發(fā)現(xiàn)要寫的東西實(shí)在太多,,有些問題甚至不好描述,,又擔(dān)心誤導(dǎo)讀者,就作罷了,。 說到即時(shí)通訊大伙都會(huì)第一時(shí)間想到QQ等聊天軟件,,似乎跟服裝DRP八竿子打不著。即時(shí)通信翻譯自Instant Messaging,,如果我把它解釋為即時(shí)消息推送,,再將其放之于企業(yè)應(yīng)用中就好理解了。舉例:上級(jí)給下級(jí)發(fā)貨,,下級(jí)能第一時(shí)間知道貨已發(fā)出,,就用不著打電話詢問或滿心期待地頻繁刷新列表;下級(jí)店鋪賣出一單,,正在為銷售淡季發(fā)愁的老板看到蹦出的提示消息,,瞬間有了信心…… 這個(gè)功能對不明真相的客戶并沒有多少吸引力,因?yàn)榇蟛糠諧S軟件似乎都能做到這一點(diǎn),,只不過——或多或少延遲個(gè)幾秒或幾分鐘,,當(dāng)然客戶對延遲一無所知。但是做技術(shù)的知道這個(gè)延遲代表什么:頻繁地訪問數(shù)據(jù)源,,頻繁地將“最新”數(shù)據(jù)與本地?cái)?shù)據(jù)作比較[or直接使用獲取到的數(shù)據(jù)]刷新UI。假設(shè)對數(shù)據(jù)實(shí)時(shí)精度控制為1分鐘,,有1000個(gè)客戶端運(yùn)行,,平均每個(gè)客戶端對10種數(shù)據(jù)類型感興趣(比如數(shù)據(jù)類型(即時(shí)通信中可稱為消息類型)包括入庫、發(fā)貨、零售和調(diào)撥,,或者基礎(chǔ)資料的修改等等),,那么每分鐘就會(huì)產(chǎn)生額外的10000次的數(shù)據(jù)庫的訪問量,注意大部分訪問都沒有任何作用(除了副作用),,而且假如沒有合理地設(shè)置篩選條件[及其它改善手段],,那么訪問產(chǎn)生的數(shù)據(jù)量,大部分也可能是無用的,。另外,,合理的數(shù)據(jù)結(jié)構(gòu)和邏輯設(shè)計(jì)以滿足對各類數(shù)據(jù)類型的提示也是個(gè)不小的難點(diǎn),畢竟數(shù)據(jù)類型多種多樣,,就單個(gè)數(shù)據(jù)類來說,,也有多個(gè)屬性,假如用戶對其中的某些屬性感興趣,,如何設(shè)計(jì)一種方式使得數(shù)據(jù)庫中某條記錄的某些字段變動(dòng)時(shí)能檢索到,,嘖嘖,水很深喲,。 注意:即時(shí)通訊和BS幾乎沒關(guān)系,,BS應(yīng)用先天不足,只能采用定時(shí)讀取數(shù)據(jù)庫的方式來模擬即時(shí)通信,,同上述的大部分CS軟件,。也許用插件能行,但是插件本質(zhì)上也是CS中的C,。上回說到BS的缺點(diǎn),,這里又能加上一條,呵呵,,開個(gè)玩笑,。 請時(shí)刻注意本文所說的IM并非單純的聊天軟件,而是為企業(yè)應(yīng)用系統(tǒng)服務(wù)的輔助類工具,。它應(yīng)該具有相對獨(dú)立性,、良好的擴(kuò)展性和簡便的應(yīng)用性(應(yīng)用是對用戶和開發(fā)人員兩者來說的,用戶能方便的使用它,,開發(fā)人員能方便地將它接入系統(tǒng)),。按照本系列慣例,列客戶關(guān)注的幾個(gè)功能需求:
需求看似挺多,,其實(shí)技術(shù)實(shí)現(xiàn)起來難點(diǎn)就一個(gè):UDP打洞。單純打洞而言,,直接用Socket編碼相當(dāng)簡單,。不過為了提升自己對WCF的理解,我決定使用WCF來完成,,后來發(fā)現(xiàn)這真是自討苦吃(一些知識(shí)要點(diǎn)記錄在為WCF增加UDP綁定(儲(chǔ)備篇))中,。依托WCF框架進(jìn)行UDP通信與直接使用Socket相比,也有很多好處,,比如消息的傳遞被封裝為方法的調(diào)用,,更符合咱“高層開發(fā)者”的口味。WCF原生支持的綁定類型并沒有給實(shí)現(xiàn)打洞提供太多可用信息(TCP等若干綁定能獲取發(fā)送端IPEndPoint信息),,因此我使用微軟后來提供的UDP綁定封裝示例,,并增加了設(shè)置通信端口和獲取發(fā)送端IPEndPoint的功能,這兩者是實(shí)現(xiàn)打洞的前提,,此處不予贅述,。下面關(guān)注業(yè)務(wù)代碼。 1 /// <summary>
2 /// 用戶終端
3 /// </summary>
4 [DataContract]
5 public class UserPoint
6 {
7 /// <summary>
8 /// 用戶標(biāo)識(shí)
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 /// 用戶主機(jī)用于偵聽和發(fā)送消息的網(wǎng)絡(luò)地址(和端口)
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,子類并非定義在當(dāng)前程序集,此處用顯式轉(zhuǎn)換
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 }
接著定義服務(wù)契約,,由于客戶端會(huì)相互通信,在打洞時(shí)服務(wù)端也會(huì)調(diào)用客戶端方法,,因此所有客戶端在運(yùn)行時(shí)也要寄宿服務(wù),。 服務(wù)端: 1 [ServiceContract(Namespace = "http://www./erp/")]
2 public interface IServerService
3 {
4 /// <summary>
5 /// 用戶登入[到服務(wù)器端用戶列表]
6 /// </summary>
7 [OperationContract(IsOneWay = true)]
8 void UserLogin(UserPoint user);
9
10 /// <summary>
11 /// 用戶登出[移出服務(wù)器端用戶列表]
12 /// </summary>
13 [OperationContract(IsOneWay = true)]
14 void UserLogout(UserPoint user);
15
16 /// <summary>
17 /// 叫用戶A給用戶B方向發(fā)一條消息(打洞)
18 /// </summary>
19 /// <param name="callingUser">打洞方</param>
20 /// <param name="waitingUserID">等待方標(biāo)識(shí)</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 }
注意已映射端口在一段時(shí)間不使用后會(huì)自動(dòng)失效。我在本地測試時(shí),,100秒端口還能用,,能相互通信,120秒后失效,,服務(wù)器再通過原先端口給客戶端發(fā)送訊息,,客戶端不再接收到。為了維持有效性,,需要客戶端定時(shí)給服務(wù)器發(fā)送消息(反之應(yīng)該也可以,?),。HoldMyPort就是這個(gè)作用,,一般實(shí)現(xiàn)為空方法,。 客戶端[服務(wù)]: 1 /// <summary>
2 /// 客戶端服務(wù),主要用來接收各種消息
3 /// </summary>
4 [ServiceContract(Namespace = "http://www./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 }
當(dāng)用戶登錄系統(tǒng)時(shí),,發(fā)送訊息給服務(wù)器,,服務(wù)端將執(zhí)行下述方法: 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 //獲取傳進(jìn)的消息屬性
17 MessageProperties properties = context.IncomingMessageProperties;
18 //獲取消息發(fā)送的遠(yuǎn)程終結(jié)點(diǎn)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)//避免在循環(huán)過程中集合被修改
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 }
測試該方法需要三臺(tái)最好處于不同局域網(wǎng)內(nèi)的機(jī)子,其中一臺(tái)通過NAT映射為公網(wǎng)服務(wù)器,。
接下來到了重點(diǎn):打洞。少年們兩眼綻放出異樣的光芒,,卻不知道當(dāng)事者的辛苦,。其實(shí)關(guān)鍵代碼相當(dāng)簡單。 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)//信任用戶(已經(jīng)建立信任連接)不需要打洞
8 {
9 invokeAction();
10 }
11 else
12 {
13 Action action = () =>
14 {
15 int maxTryCount = 3;//最大嘗試次數(shù)
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));//服務(wù)器叫對方給我打招呼
20 Thread.Sleep(500);
21 }
22 if (user.IsTrustMe)
23 {
24 invokeAction();
25 }
26 };
27 action.BeginInvoke(null, null);
28 }
29 }
這里有個(gè)問題,,當(dāng)通信雙方處于相同局域網(wǎng),,應(yīng)該期望它們直接通信,省略打洞步驟,。方法是在用戶登錄時(shí)將本機(jī)IP和端口號(hào)(未映射)同時(shí)發(fā)送到服務(wù)端,,當(dāng)客戶端A和客戶端B的映射IP相同則說明他們處于同一內(nèi)網(wǎng),然后根據(jù)本機(jī)地址直連通信,。不過這應(yīng)該有兩個(gè)問題需要解決:當(dāng)局域網(wǎng)內(nèi)存在多級(jí)子網(wǎng)NAT,,A、B分屬不同層,,那么它們還要進(jìn)行內(nèi)部局域網(wǎng)打洞,;本機(jī)IP有時(shí)候并不能準(zhǔn)確獲取,特別有些軟件能生成虛擬IP,。 在打洞成功后我們將對方的IsTrustMe設(shè)置成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 }
現(xiàn)在就可以直連通信咯。
經(jīng)測試,,打洞過程一般嘗試1次就能連接成功,,此處每次等待500毫秒。 關(guān)于組播,。原本打算采用組播的方式群發(fā)消息(包括所有終端用戶其它用戶上下線的提示消息),,不成想,路由器默認(rèn)情況下是不會(huì)轉(zhuǎn)發(fā)組播包的,,必須在路由器上進(jìn)行配置才行,,解決該問題需要網(wǎng)管進(jìn)行配合,不是編程就能解決的,。而且一般的路由器都不支持組播,,也就是說,目前很多路由器不支持組播協(xié)議,,所以,,局域網(wǎng)的路由器不會(huì)將這個(gè)組播信息傳輸出去,so,,外面的電腦以及路由根本就不知道你這個(gè)組播的信息,。有專門支持組播的路由,不過貌似價(jià)格不菲,。如果路由器不支持組播的話,,那么你的交換機(jī)就把你的組播數(shù)據(jù)當(dāng)成廣播數(shù)據(jù)了,,廣播只能在局域網(wǎng)里面。(該段話來自網(wǎng)絡(luò)),。按照這個(gè)說法,,外部組播數(shù)據(jù)想要進(jìn)入內(nèi)網(wǎng)也困難重重(對or錯(cuò)?),。因此我改用循環(huán)發(fā)送方式,。 最后截個(gè)消息查詢和消息接收權(quán)限的圖,,消息接收權(quán)限設(shè)置我目前將之放入角色管理中,。
至此,IM核心功能基本實(shí)現(xiàn)完畢,,能滿足目前系統(tǒng)的需求(還有大數(shù)據(jù)傳輸?shù)葐栴}暫時(shí)未涉及到就不考慮了),。所謂企業(yè)通訊工具不過是在此基礎(chǔ)上功能的累加,以后再加入吧,。:) 后記:竊以為消息提示只是IM基本輔助功能,,IM還能幫助系統(tǒng)即時(shí)刷新。舉例:當(dāng)權(quán)限管理員為我新增了幾個(gè)模塊權(quán)限,,按照平常的做法,,需要我注銷后重新登錄才能看到,現(xiàn)在只要將新增的模塊信息發(fā)送給我,,我這邊系統(tǒng)自動(dòng)將它們構(gòu)造進(jìn)左側(cè)菜單樹中即可,;我正在下拉框中選擇下級(jí)機(jī)構(gòu)準(zhǔn)備為他發(fā)貨,下拉框中的數(shù)據(jù)項(xiàng)突然增加了一個(gè),,原因是機(jī)構(gòu)管理員錄入了一個(gè)新機(jī)構(gòu),;…… 轉(zhuǎn)載本文請注明出處:http://www.cnblogs.com/newton/archive/2013/01/26/2877500.html 標(biāo)簽: WCF, UDP, P2P, 服裝DRP
|
|