1、C#编写简单的聊天程序 引言 这是一篇基于 Socket 进行网络编程的入门文章,我对于网络编程的学习并不够深入,这篇文章是对于自己知识的一个巩固,同时希望能为初学的朋友提供一点参考。文章大体分为四个部分:程序的分析与设计、 C#网络编程基础 (篇外篇 )、聊天程序的实现模式、程序实现。 程序的分析与设计 1.明确程序功能 如果大家现在已经参加了工作,你的经理或者老板告诉你,“小王,我需要你开发一个聊天程序”。那么接下来该怎么做呢?你是不是在脑子里有个雏形,然后就直接打开VS2005 开始设计窗体,编写代码了呢?在开始 之前,我们首先需要进行软件的分析与设计。就拿本例来说,如果只有这么一句话“
2、一个聊天程序”,恐怕现在大家对这个“聊天程序”的概念就很模糊,它可以是像 QQ 那样的非常复杂的一个程序,也可以是很简单的聊天程序;它可能只有在对方在线的时候才可以进行聊天,也可能进行留言;它可能每次将消息只能发往一个人,也可能允许发往多个人。它还可能有一些高级功能,比如向对方传送文件等。所以我们首先需要进行分析,而不是一上手就开始做,而分析的第一步,就是搞清楚程序的功能是什么,它能够做些什么。在这一步, 我们的任务是了解程序需要做什么,而 不是如何去做。 了解程序需要做什么,我们可以从两方面入手,接下来我们分别讨论。 1.1 请求客户提供更详细信息 我们可以做的第一件事就是请求客户提供更加详
3、细的信息。尽管你的经理或老板是你的上司,但在这个例子中,他就是你的客户(当然通常情况下,客户是公司外部委托公司开发软件的人或单位)。当遇到上面这种情况,我们只有少得可怜的一条信息“一个聊天程序”,首先可以做的,就是请求客户提供更加确切的信息。比如,你问经理“对这个程序的功能能不能提供一些更具体的信息?”。他可能会像这样回答:“哦,很简单,可以登录聊天程序,登 录的时候能够通知其他在线用户,然后与在线的用户进行对话,如果不想对话了,就注销或者直接关闭,就这些吧。” 有了上面这段话,我们就又可以得出下面几个需求: 1. 程序可以进行登录。 2. 登录后可以通知其他在线用户。 3. 可以与其他用户进
4、行对话。 4. 可以注销或者关闭。 1.2 对于用户需求进行提问,并进行总结 经常会有这样的情况:可能客户给出的需求仍然不够细致,或者客户自己本身对于需求就很模糊,此时我们需要做的就是针对用户上面给出的信息进行提问。接下来我就看看如何对上面的需求进行提问,我们至少可以向经理提出以下问题: NOTE: 这里我穿插一个我在见到的一个印象比较深刻的例子:客户往往向你表达了强烈的意愿他多么多么想拥有一个属于自己的网站,但是,他却没有告诉你网站都有哪些内容、栏目,可以做什么。而作为开发者,我们显然关心的是后者。 1. 登录时需要提供哪些内容?需不需要提供密码? 2. 允许多少人同时在线聊天? 3. 与在
5、线用户聊天时,可以将一条消息发给一个用户,还是可以一次将消息发给多个用户? 4. 聊天时发送的消息包括哪些内容? 5. 注销和关闭有什么区别? 6. 注销和关闭对对方需不需要给对方提示? 由于这是一个范例程序,而我在为大家讲述,所以我只能再充当一下客 户的角色,来回答上面的问题: 1. 登录时只需要提供用户名称就可以了,不需要输入密码。 2. 允许两个人在线聊天。(这里我们只讲述这种简单情况,允许多人聊天需要使用多线程) 3. 因为只有两个人,那么自然是只能发给一个用户了。 4. 聊天发送的消息包括:用户名称、发送时间还有正文。 5. 注销并不关闭程序,只是离开了对话,可以再次进行连接。关闭则
6、是退出整个应用程序。 6. 注销和关闭均需要给对方提示。 好了,有了上面这些信息我们基本上就掌握了程序需要完成的功能,那么接下来做什么?开始编码了么?上面的这些属于 业务流程 ,除非你对它已经非常熟悉,或 者程序非常的小,那么可以对它进行编码,但是实际中,我们最好再编写一些用例,这样会使程序的流程更加的清楚。 1.3 编写用例 通常 一个用例对应一个功能或者叫需求,它是程序的一个执行路径或者执行流程。 编写用例的思路是:假设你已经有了这样一个聊天程序,那么你应该如何使用它?我们的使用步骤,就是一个用例。用例的特点就每次只针对程序的一个功能编写,最后根据用例编写代码,最终完成程序的开发。我们这里
7、的需求只有简单的几个:登录,发送消息,接收消息,注销或关闭,上面的分析是对这几点功能的一个明确。接下来我们首先编写第一个用例 :登录。 在开始之前,我们先明确一个概念:客户端,服务端。因为这个程序只是在两个人(机器)之间聊天,那么我们大致可以绘出这样一个图来: 我们期望用户 A 和用户 B 进行对话,那么我们就需要在它们之间建立起连接。 尽管“用户 A”和“用户 B”的地位是对等的,但按照约定俗称的说法:我们将发起连接请求的一方称为客户端(或叫本地),另一端称为服务端(或叫远程) 。所以 我们的登录过程,就是“用户 A”连接到“用户 B”的过程,或者说客户端(本地)连接到服务端(远程)的过程。
8、在分析这个程序的过程中,我们总是将其分为两部分,一部分为 发起连接、发送消息的一方(本地),一方为接受连接、接收消息的一方(远程)。 登录和连接(本地) 主路径 可选路径 1.打开应用程序,显示登录窗口 2.输入用户名 3.点击“登录”按钮,登录成功 3.“登录”失败 如果用户名为空,重新进入第 2步。 4.显示主窗口,显示登录的用户名称 5.点击“连接”,连接至远程 6.连接成功 6.1 提示用户,连接已经成功。 6.连接失败 6.1 提示用户,连接不成功 5.在用户界面变更控件状态 5.2 连接为灰色,表示已经连接 5.3 注销为亮色, 表示可以注销 5.4 发送为亮色,表示可以发消息 这
9、里我们的用例名称为登录和连接,但是后面我们又打了一个括号,写着“本地”,它的意思是说,登录和连接是客户端,也就是发起连接的一方采取的动作。同样,我们需要写下当客户端连接至服务端时,服务端采取的动作。 登录和连接(远程) 主路径 可选路径 1-4 同客户端 5.等待连接 6.如果有连接, 自动 在用户界面显示“远程主机连接成功” 接下来我们来看发送消息。在发送消息时,已经是登录了的,也就是“用户 A”、“用户 B”已经做好了连接,所以我们现 在就可以只关注发送这一过程: 发送消息(本地) 主路径 可选路径 1.输入消息 2.点击发送按钮 2.没有输入消息,重新回到第 1 步 3.在用户界面上显示
10、发出的消息 3.服务端已经断开连接或者关闭 3.1 在客户端用户界面上显示错误消息 然后我们看一下接收消息,此时我们只关心接收消息这一部分。 接收消息(远程) 主路径 可选路径 1.侦听到客户端发来的消息, 自动 显示在用户界面上。 注意到这样一点: 当远程主机向本地返回消息时,它的用例又变为了上面的用例“发送消息(本地)” 。因为它们的角色已 经互换了。 最后看一下注销,我们这里研究的是当我们在本地机器点击“注销”后,双方采取的动作: 注销(本地主动) 主路径 可选路径 1.点击注销按钮,断开与远程的连接 2.在用户界面显示已经注销 3.更改控件状态 3.1 注销为灰色,表示已经注销 3.2
11、 连接为亮色,表示可以连接 3.3 发送为灰色,表示无法发送 与此对应,服务端应该作出反应: 注销(远程被动) 主路径 可选路径 1.自动 显示远程用户已经断开连接。 注意到一点: 当远程主动注销时,它采取的动作为上面的“本地主动”,本地 采取的动作则为这里的“远程被动”。 至此,应用程序的功能分析和用例编写就告一段落了,通过上面这些表格,之后再继续编写程序变得容易了许多。另外还需要记得,用例只能为你提供一个操作步骤的指导,在实现的过程中,因为技术等方面的原因,可能还会有少量的修改。如果修改量很大,可以重新修改用例;如果修改量不大,那么就可以直接编码。这是一个迭代的过程,也没有一定的标准,总之
12、是以高效和合适为标准。 2.分析与设计 我们已经很清楚地知道了程序需要做些什么,尽管现在还不知道该如何去做。我们甚至可以编写出这个程序所需要的接口,以后 编写代码的时候,我们只要去实现这些接口就可以了。这也符合面向接口编程的原则。另外我们注意到,尽管这是一个聊天程序,但是却可以明确地划分为两部分,一部分发送消息,一部分接收消息。另外注意上面标识为 自动 的语句,它们暗示这个操作需要通过事件的通知机制来完成。关于委托和事件,可以参考这两篇文章: C#中的 委托和事件 - 委托和事件的入门文章,同时捎带讲述了 Observer 设计模式和 .NET 的事件模型 C#中的委托和事件 (续 ) - 委
13、托和事件更深入的一些问题,包括异常、超时的处理,以及使用委托来异步调用方法。 2.1 消息 Message 首先我们可以定义消息,前面我们已经明确了消息包含三个部分:用户 名、时间、内容,所以我们可以定义一个结构来表示这个消息: public struct Message private readonly string userName; private readonly string content; private readonly DateTime postDate; public Message(string userName, string content) this.userNam
14、e = userName; this.content = content; this.postDate = DateTime.Now; public Message(string content) : this(“System“, content) public string UserName get return userName; public string Content get return content; public DateTime PostDate get return postDate; public override string ToString() return St
15、ring.Format(“01: rn2rn“, userName, postDate, content); 2.2 消息发送方 IMessageSender 从上面我们可以看出,消息发送方主要包含这样几个功能: 登录 、 连接 、 发送消息 、注销 。另外在连接成功或失败时还要通知用户界面,发送消息成功或失败时也需要通知用户界面,因此,我们可以让连接和发送消息返回一个布尔类型的值,当它为真时表示连接或发送成功,反之则为失败。因为登录没有任何的业务逻辑,仅仅是记录控件的值并进行显示,所以我不打算将它写到接口中。 因此我们可以得出它的接口大致如下: public interface IMessa
16、geSender bool Connect(IPAddress ip, int port); / 连接到服务端 bool SendMessage(Message msg); / 发送用户 void SignOut(); / 注销系统 2.3 消息接收方 IMessageReceiver 而对于 消息接收方,从上面我们可以看出,它的操作全是 被动 的:客户端连接时 自动提示,客户端连接丢失时显示 自动 提示,侦听到消息时 自动 提示。注意到上面三个词都用了“自动”来修饰,在 C#中,可以定义委托和事件,用于当程序中某种情况发生时,通知另外一个对象。在这里,程序即是我们的 IMessageRece
17、iver,某种情况就是上面的三种情况,而另外一个对象则为我们的用户界面。因此,我们现在首先需要定义三个委托: public delegate void MessageReceivedEventHandler(string msg); public delegate void ClientConnectedEventHandler(IPEndPoint endPoint); public delegate void ConnectionLostEventHandler(string info); 接下来,我们注意到接收方需要 侦听 消息,因此我们需要在接口中定义的方法是StartListen()
18、和 StopListen()方法, 这两个方法是典型的技术相关,而不是业务相关,所以从用例中是看不出来的 ,可能大家现在对这两个方法是做什么的 还不清楚,没有关系,我们现在并不写实现,而定义接口并不需要什么成本,我们写下 IMessageReceiver 的接口定义: public interface IMessageReceiver event MessageReceivedEventHandler MessageReceived; / 接收到发来的消息 event ConnectionLostEventHandler ClientLost; / 远程主动断开连接 event ClientC
19、onnectedEventHandler ClientConnected; / 远程连接到了本地 void StartListen(); / 开始侦听端口 void StopListen(); / 停止侦听端口 我记得曾经看过有篇文章说过,最好不要在接口中定义事件,但是我忘了他的理由了,所以本文还是将事件定义在了接口中。 2.4 主程序 Talker 而我们的主程序是既可以发送,又可以接收,一般来说,如果一个类像获得其他类的能力,以采用两种方法:继承和复合 。因为 C#中没有多重继承,所以我们无法同时继承实现了 IMessageReceiver 和 IMessageSender 的类。那么我们
20、可以采用复合,将它们作为类成员包含在 Talker 内部: public class Talker private IMessageReceiver receiver; private IMessageSender sender; public Talker(IMessageReceiver receiver, IMessageSender sender) this.receiver = receiver; this.sender = sender; 现在,我们的程序大体框架已经完成,接下来要关注的就是如何实现它,现在让我们由设计走入实现,看看实现一个网络聊天程序,我们需要掌握的技术吧。 C#
21、网络编程基础 (篇外篇 ) 这部分的内容请参考 C#网络编程 系列文章,共 5 个部分较为详细的讲述了基于Socket 的网络编程的初步内容。 编写程序代码 如果你已经看完了上面一节 C#网络编程,那么本章完全没有讲解的必要了,所以我只列出代码,对个别值得注意的地方稍微地讲述一下。首先需要了解的就是,我们采用的是三个模式中开发起来难度较大的一种,无服务器参与的模式。还有就是我们没有使用广播消息,所以需要提前知道连接到的远程主机的地址和端口号。 1.实现 IMessageSender 接口 public class MessageSender : IMessageSender TcpClient
22、 client; Stream streamToServer; / 连接至远程 public bool Connect(IPAddress ip, int port) try client = new TcpClient(); client.Connect(ip, port); streamToServer = client.GetStream(); / 获取连接至远程的流 return true; catch return false; / 发送消息 public bool SendMessage(Message msg) try lock (streamToServer) byte buf
23、fer = Encoding.Unicode.GetBytes(msg.ToString(); streamToServer.Write(buffer, 0, buffer.Length); return true; catch return false; / 注销 public void SignOut() if (streamToServer != null) streamToServer.Dispose(); if (client != null) client.Close(); 这段代码可以用朴实无华来形容,所以我们直接看下一段。 2.实现 IMessageReceiver 接口 pu
24、blic delegate void PortNumberReadyEventHandler(int portNumber); public class MessageReceiver : IMessageReceiver public event MessageReceivedEventHandler MessageReceived; public event ConnectionLostEventHandler ClientLost; public event ClientConnectedEventHandler ClientConnected; / 当端口号 Ok 的时候调用 - 需要
25、告诉用户界 面使用了哪个端口号在侦听 / 这里是业务上体现不出来,在实现中才能体现出来的 public event PortNumberReadyEventHandler PortNumberReady; private Thread workerThread; private TcpListener listener; public MessageReceiver() (IMessageReceiver)this).StartListen(); / 开始侦听:显示实现接口 void IMessageReceiver.StartListen() ThreadStart start = new
26、ThreadStart(ListenThreadMethod); workerThread = new Thread(start); workerThread.IsBackground = true; workerThread.Start(); / 线程入口方法 private void ListenThreadMethod() IPAddress localIp = IPAddress.Parse(“127.0.0.1“); listener = new TcpListener(localIp, 0); listener.Start(); / 获取端口号 IPEndPoint endPoin
27、t = listener.LocalEndpoint as IPEndPoint; int portNumber = endPoint.Port; if (PortNumberReady != null) PortNumberReady(portNumber); / 端口号已经 OK,通知用户界面 while (true) TcpClient remoteClient; try remoteClient = listener.AcceptTcpClient(); catch break; if (ClientConnected != null) / 连接至本机的远程端口 endPoint = remoteClient.Client.RemoteEndPoint as IPEndPoint; ClientConnected(endPoint); / 通知用户界面远程客户连接 Stream streamToClient = remoteClient.GetStream(); byte buffer = new byte8192; while (true) try int bytesRead = streamToClient.Read(buffer, 0, 8192); if (bytesRead = 0) throw new Exception(“客户端已断开连接 “);
Copyright © 2018-2021 Wenke99.com All rights reserved
工信部备案号:浙ICP备20026746号-2
公安局备案号:浙公网安备33038302330469号
本站为C2C交文档易平台,即用户上传的文档直接卖给下载用户,本站只是网络服务中间平台,所有原创文档下载所得归上传人所有,若您发现上传作品侵犯了您的权利,请立刻联系网站客服并提供证据,平台将在3个工作日内予以改正。