面向对象设计vs.实用主义
这种方法的缺点之一是你必须使用一个大的switch语句结束,但是前辈一直教导我们大的switch语句是较差的设计的表现。通常的面向对象(Object Oriented,OO)的途径是使用多态性(polymorphism)的。为了达到这个目的,我们先建立一个抽象的基类(base class),接着从该类衍生出所有的消息对象。每个类需要执行串行化、并行化和处理消息等多个方法,主要的代码是:
· 读取消息类型
· 建立实例(使用反射)
· 调用虚HandleMessage()函数
这样做是可以实现的,但是效果很差,我并不喜欢。首先,编写建立实例的代码很难,并且由于它使用了反射,它的速度更慢。更重要的是,消息的处理过程并不在HandleMessage()函数之内,这意味着它必须是共享库的一部分。这是不宜使用的,因为消息的处理过程与消息如何传递没有什么关系。由于这些问题的存在,我依然决定使用较少面向对象但是更加容易编写的途径。
前面的示例只处理了单个消息。在现实世界中,我们需要同时处理多个消息。
服务器端的多线程
我的最终的目标是把该服务器程序的功能添加到一个已有的应用程序中。因为不希望修改已有的应用程序的代码,我就必须在某个线程上运行服务器程序。同样,我希望可以同时接受多个连接。
上面的例子在端口9999上监听,但是由于一个客户端只能与一个端口对话,我需要为每个连接使用不同端口的途径。SocketListener类将在9999端口上监视,当新的连接请求到达的时候,它将查找一个未使用的端口并把它发送回给客户端。下面是这个类的大致情形:
public class SocketListener { int port; Thread thread;
public SocketListener(int port) { this.port = port; ThreadStart ts = new ThreadStart(WaitForConnection); thread = new Thread(ts); thread.IsBackground = true; thread.Start(); }
public void WaitForConnection() { // 主要的代码 } }
WaitForConnection()是执行所有这些操作的方法。这个类的构造函数执行建立新线程的任务,这个线程将运行WaitForConnection()。打开套接字并接受连接与前面的例子相似。下面是该线程的主循环:
while (true) { Console.WriteLine("Waiting for initial connection"); listener.Start(); Socket socket = listener.AcceptSocket(); NetworkStream stream = new NetworkStream(socket); BinaryReader reader = new BinaryReader(stream); BinaryWriter writer = new BinaryWriter(stream);
Console.WriteLine("Connection Requested");
int userPort = port + 1; TcpListener specificListener; while (true) { try { specificListener = new TcpListener(localAddr, userPort); specificListener.Start(); break; } catch (SocketException) { userPort++; } } //远程用户应该使用specificListener。 //把该端口发送回给远程用户,并为我们在该端口上建立服务器应用程序。 SocketServer socketServer = new SocketServer(specificListener);
writer.Write(userPort); writer.Close(); reader.Close(); stream.Close(); socket.Close(); }
我希望能够支持多个连接,因此使用一个端口以便于客户端表明它们希望建立一个连接,接着服务器程序找到一个空的端口并把该端口发送回给客户端,该端口用于特定客户端的连接。
我没有找到查找未使用端口的方法,因此该While循环用于找出未使用的端口。接着它把该端口号发送回客户端并清除对象。
此处还有需要指出的一点点微妙之处。SocketServer的原始版本把端口号作为一个参数。不幸的是,这意味着在该端口上建立监听器之前客户端不能作出请求,这是很不好的。为了防止出现这种情况,我在给客户端发送端口号之前建立了TcpListener,它确保不会出现这种紧急情况。
SocketServer类建立了额外的线程,并使用了下面的主循环:
try { while (true) { MessageType messageType = (MessageType) reader.ReadInt32();
switch (messageType) { case MessageType.RequestEmployee: Employee employee = new Employee("Eric Gunnerson", "One Microsoft Way"); employee.Send(writer); break; } } } catch (IOException) {
} finally { socket.Close(); }
这个主循环是一个简单的获取请求/处理请求的循环。try-catch-finally在此处用于当客户端断开连接的时候从异常中恢复过来。
客户端的事件
在客户端,我编写了一个Windows传统客户端程序,可以供PC使用也可以供Pocket PC使用。该Windows窗体环境是基于事件的,而且使用事件处理套接字消息也是理想的。这是通过SocketClient类实现的。第一步是为每个消息定义一个委托和事件:
public delegate void EmployeeHandler(Employee employee); public event EmployeeHandler EmployeeReceived;
第二步是编写发送事件的代码:
case MessageType.Employee: Employee employee = new Employee(reader); if (EmployeeReceived != null) form.Invoke(EmployeeReceived, new object[] {employee}); break;
当事件发生的时候就应该更新窗体了。为了更可靠,这个操作需要在主UI线程上发生。这是通过调用窗体的Invoke()实现的,它将安排在主UI线程上调用的委托。
因为这种基于消息的体系结构,服务器程序要有对于异步事件的内建的支持。示例有一个CurrentCount消息,它是由服务器程序每秒钟发送的。
总结
我对这个基于套接字的体系结构很满意,它是轻量级的、易于使用的,并且它可以同时在PC和Pocket P
|