InChatter系统之服务器开发(二)
现在我们继续进行InChatter系统的服务器端的开发,今天我们将实现服务契约同时完成宿主程序的开发,今天结束之后服务器端将可以正常运行起来。
系统的开发是随着博客一起的,颇有点现场直播的感觉,所有在写博的过程中,可能会回头重新讲解和修复以前的设计不合理的地方,同时也可能会融合新的想法以及功能模块,提前跟各位看客交代下,请大家见谅。不过我想这个过程对大家也是有利的,在这个过程中,一是带大家重新回顾一下以前的设计想法并与现在进行比较,二是可以增长大家的项目设计的感觉,增长经验,这也是项目开发中不可避免的。所以,这也是我坚持直播的原因,如果文章中有什么不对的地方或者修改意见,欢迎大家指正,好的修改建议我会在开发过程中融入进来。
本人文笔较差,表达可能不够详尽,如果什么不节的地方,欢迎大家指正。
一、服务器端的开发
我们修改了服务契约,增加了一个GetOnlineClient的方法
[OperationContract(IsOneWay = false, IsInitiating = false, IsTerminating = false)] string GetOnlineClient();
通过这个方法,我们可以获取除了自己以外其他的所有在线客户端,同时修改了Login方法的返回值,从而我们可以得到登录的反馈状态
[OperationContract(IsOneWay=false,IsInitiating=true,IsTerminating=false)] bool Login(string clientId);
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; using InChatter.Service.Data; namespace InChatter.Service { public class Chat : IChat { public static Dictionary<string, IChatCallback> Callbacks = new Dictionary<string, IChatCallback>(); public bool Login(string clientId) { if (Callbacks.Keys.Contains(clientId)) { return false; } IChatCallback callback; //告知其他用户,新用户上线 BroadcastUserState(clientId, true); callback = OperationContext.Current.GetCallbackChannel<IChatCallback>(); Callbacks.Add(clientId, callback); //返回在线客户端 return true; } public string GetOnlineClient() { IChatCallback callback = OperationContext.Current.GetCallbackChannel<IChatCallback>(); //获取除本客户端以外的其他在线客户端 var result = Callbacks.Where(p => p.Value != callback).Select(p => p.Key).ToList(); return string.Join(",", result); } public void SendMsg(Data.InChatterMessage message) { if (message == null) { return; } if (message.Type == "notice") { SendNotice(message); } else if (message.Type == "msg") { try { Callbacks[message.ReceiverID].ReceiveMsg(message); } catch { Logout(message.ReceiverID); } } } public void Logout(string clientId) { try { lock (Callbacks) { //移除退出用户的callBacks Callbacks.Remove(clientId); } //广播下线消息 BroadcastUserState(clientId, false); } catch { } } /// <summary> /// 通知其他用户上线或者下线消息 /// </summary> /// <param name="employeeId"></param> /// <param name="isLogin"></param> private void BroadcastUserState(string clientId, bool isLogin) { List<string> list = new List<string>(); foreach (var item in Callbacks) { InChatterMessage txt = new InChatterMessage(); txt.Content = ""; txt.SenderID = clientId; txt.ReceiverID = item.Key; if (isLogin) { txt.Type = "logon"; } else { txt.Type = "logoff"; } txt.SendTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); try { item.Value.ReceiveMsg(txt); } catch { list.Add(item.Key); } } RemoveDisconnectCallBacks(list); } /// <summary> /// 发送通知消息,接受者为所有在线客户 /// </summary> /// <param name="msg"></param> private void SendNotice(InChatterMessage msg) { List<string> list = new List<string>(); foreach (var item in Callbacks) { try { item.Value.ReceiveMsg(msg); } catch { list.Add(item.Key); } } RemoveDisconnectCallBacks(list); } /// <summary> /// 移除断开连接客户端 /// </summary> /// <param name="list"></param> private void RemoveDisconnectCallBacks(List<string> list) { if (list.Count > 0) { //移除出错的callBack foreach (var item in list) { Callbacks.Remove(item); } //将连接通道出错的项认定为下线,发送下线消息给在线客户端 foreach (var item in list) { BroadcastUserState(item, false); } } } } }
如果我们定义一个关于Chat类的一个构造函数,那么我们可以看到客户端与服务器建立回话时,都会调用构造函数初始化一个Chat的实例,也就是说每一个客户端与服务器端对应的一个Chat类交互。
在Chat类中我们定义一个静态的全局变量Callbacks,我们在该变量中存储客户端标识Id和对应的回调实体,这样便可以在所有的Chat类中操作我们需要的回调,来完成对指定客户端的操作。
二、宿主程序的实现
using System; using System.Collections.Generic; using System.Linq; using System.ServiceModel; using System.ServiceModel.Description; using System.Text; using System.Threading.Tasks; namespace InChatter.Service.Host { class Program { static void Main(string[] args) { Uri baseUri = new Uri("http://localhost:1378/InChatter"); using (ServiceHost host = new ServiceHost(typeof(Chat), baseUri)) { NetTcpBinding binding = new NetTcpBinding(); binding.Security.Mode = SecurityMode.None; //会话保持时间 binding.ReceiveTimeout = TimeSpan.FromHours(2); host.AddServiceEndpoint(typeof(IChat), binding, "net.tcp://localhost:1121/InChatter"); host.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true }); host.Opened += host_Opened; try { host.Open(); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.WriteLine("Press 'exit' to exit!"); string enterStr = Console.ReadLine(); while (enterStr.ToLower() != "exit") { enterStr = Console.ReadLine(); } } } public static void host_Opened(object sender, EventArgs e) { Console.WriteLine("Service Opened!"); } } }
启动运行,在程序没有错误的情况下,如果防火墙开启,则会有如下提示:
这个代表我们的程序没有错误,但是运行以后呢?
系统出错了,提示不具有命名空间访问权限,其实就是我们的程序需要以管理员身份运行。
OK,成功了!
这里我们通过代码实现了整个服务的寄宿,同时我们也可以使用配置文件来实现:
下面我们是WCF配置工具来实现客户端的配置:
1.打开WCF服务配置编辑器
2.找到服务契约生成的dll,点击打开
3.选中我们的服务实现类型
4.点击下一步,在类型中选择TCP
5.进入下一步,输入终结点地址,如下所示
5.点击下一步,并完成,保存配置后,将配置文件复制到宿主程序的目录下,覆盖默认的App.config
生成的配置文件如下:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.serviceModel> <services> <service name="InChatter.Service.Chat"> <endpoint address="net.tcp://localhost:1121/InChatter" binding="netTcpBinding" bindingConfiguration="" contract="InChatter.Service.IChat" /> </service> </services> </system.serviceModel> </configuration>
代码调用也比较简单:
using System; using System.Collections.Generic; using System.Linq; using System.ServiceModel; using System.ServiceModel.Description; using System.Text; using System.Threading.Tasks; namespace InChatter.Service.Host { class Program { static void Main(string[] args) { using (ServiceHost host = new ServiceHost(typeof(Chat))) { host.Opened += host_Opened; try { host.Open(); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadLine(); } } public static void host_Opened(object sender, EventArgs e) { Console.WriteLine("Service Opened!"); } } }
同样需要以管理员方式启动程序!
但是目前的配置文件存在一定的问题,稍后在客户端处理的时候,我们再详细讲解~
服务端已经可以正常运行了,源码提供给大家:下载源码(到CodePlex下载最新版本)