今天打算把Community Server 2.0在vista IIS 7.0下跑起来,设置完虚拟目录后浏览,发现HttpContext.Current.Request抛出了System.Web.HttpException,检查了一下IIS下设置成集成管道模式,换成经典管道模式,则不抛出异常,百思不得其解,后来发现msdn上有如下一段话
不包含自定义模块或处理程序的 Web 应用程序通常无需更改即可在 IIS 7.0 集成模式下正常工作。对于依赖于自定义模块或处理程序的 Web 应用程序,需要执行以下步骤来使其能够在集成模式下运行:
使用本主题稍后的将 Web Config 文件迁移到集成模式部分中描述的方法之一,在 Web.config 文件的 system.webServer 节中注册自定义模块和处理程序。
仅在自定义模块的 Init 方法中定义 HttpApplication 请求管道事件(如 BeginRequest 和 EndRequest)的事件处理程序。
请确保您已解决 Upgrading ASP.NET Applications to IIS 7.0: Differences between IIS 7.0 Integrated Mode and Classic mode(将 ASP.NET 应用程序升级到 IIS 7.0:IIS 7.0 集成模式和经典模式之间的区别)的“Known Differences Between Integrated Mode and Classic Mode”(集成模式和经典模式之间的已知区别)部分中讨论的问题。
而CommunityServer 的写法如下
Global.ascx.cs
protected void Application_Start(Object sender, EventArgs e)
{
Jobs.Instance().Start();
EventLogs.Info("CS.Web Started", "Application", 200);
}
Jobs会调用HttpContext.Current.Request,在集成管道模式下抛出异常。
还有一个问题,在集成管道模式下涉及到CS的url rewriting全部不起作用,找不到路径,我开始试图修改处理程序映射中的“检查文件是否存在”取消打勾,不过却找不到该选项。
后来发现换成经典管道模式也正常了。这里记录一下,给有相同问题的朋友提个醒。
上一节给大家演示了建立连接的关键代码,连接建立好后,就可以进行数据传输了。数据传输包含从服务器端到客户端和从客户端到服务器端,两者差别不大。
数据的传输,TcpClient的GetNetworkStream是关键,通过它我们可以得到NetworkStream网络流,客户端和服务器主要的工作就是对其读出和写入。关于如何构造稳定且性能好的网络应用,如何进行复杂的封包和解包,这里我们不考虑,我们使用StreamReader和StreamWriter来封装NetStream,读取和写入的代码如下
TcpClient client; StreamReader sr; StreamWriter sw; this.userName = ""; NetworkStream netStream = client.GetStream(); sr = new StreamReader(netStream, System.Text.Encoding.UTF8); sw = new StreamWriter(netStream, System.Text.Encoding.UTF8); //读取数据 string output=sr.ReadLine(); //写入数据 string input=""; sw.WriteLine(); sw.Flush(); |
客户端和服务器的通信一般是由客户端向服务器端发请求,服务器端接受请求并处理,再将结果重新发回客户端
发送请求和应答的协议格式可以自己定义,游戏中使用的协议格式为 命令,参数1,参数2……
先看一下客户端请求登录
service.SendToServer("Login," + textBoxName.Text.Trim()); class Service { ListBox listbox; StreamWriter sw; public Service(ListBox listbox, StreamWriter sw) { this.listbox = listbox; this.sw = sw; } public void SendToServer(string str) { sw.WriteLine(str); sw.Flush(); } } |
服务器接受请求并处理,一般来说是根据不同的请求创建不同的线程进行处理,见本节后半部分
private void ReceiveData(object obj) { //User类封装了TcpClient,StreamReader和StreamWriter User user = (User)obj; TcpClient client = user.client; string receiveString = null; receiveString = sr.ReadLine(); service.SetListBox(string.Format("来自{0}:{1}", user.userName, receiveString)); string[] splitString = receiveString.Split(','); switch (splitString[0]) { case "Login": user.userName = string.Format("[{0}--{1}]", splitString[1], client.Client.RemoteEndPoint); service.SendToOne(user, "Welcome"); break; default: break; } } |
class Service { private ListBox listbox; private delegate void SetListBoxCallback(string str); private SetListBoxCallback setListBoxCallback; public Service(ListBox listbox) { this.listbox = listbox; setListBoxCallback = new SetListBoxCallback(SetListBox); } public void SetListBox(string str) { if (listbox.InvokeRequired) { listbox.Invoke(setListBoxCallback, str); } else { listbox.Items.Add(str); listbox.SelectedIndex = listbox.Items.Count - 1; listbox.ClearSelected(); } } public void SendToOne(User user, string str) { try { user.sw.WriteLine(str); user.sw.Flush(); SetListBox(string.Format("向{0}发送{1}", user.userName, str)); } catch { SetListBox(string.Format("向{0}发送信息失败", user.userName)); } } } |
服务器负责监听并根据不同的用户启动不同的线程
IPAddress localAddress; int port = 51888; TcpListener myListener; Service service = new Service(listbox); IPAddress[] addrIP = Dns.GetHostAddresses(Dns.GetHostName()); localAddress = addrIP[0]; myListener = new TcpListener(localAddress, port); myListener.Start(); service.SetListBox(string.Format("开始在{0}:{1}监听客户连接", localAddress, port)); ThreadStart ts = new ThreadStart(ListenClientConnect); Thread myThread = new Thread(ts); myThread.Start(); |
ListenClientConnect方法
while (true) { TcpClient newClient = null; try { newClient = myListener.AcceptTcpClient(); } catch { break; } User user = new User(newClient); userList.Add(user); service.SetListBox(string.Format("{0}进入", newClient.Client.RemoteEndPoint)); service.SetListBox(string.Format("当前连接用户数:{0}",userList.Count)); //basilwang 2008-09-06 to receive data from client ParameterizedThreadStart pts = new ParameterizedThreadStart(ReceiveData); Thread threadReceive = new Thread(pts); threadReceive.Start(user); } |
此处的启用了ParameterizedThreadStart 线程处理带参数的ReceiveData方法,主要是防止同客户端传输数据的过程出现的异常不能正确处理导致线程崩溃,每一个客户端分配一个线程可以保证客户端各自的独立性;同时ParameterizedThreadStart允许传递参数。
至此我们完成了一个简单的服务器客户端应答,希望能对初学者有所帮助。
利用TCP开发网络应用程序,可以采用同步或者异步的方式,这个游戏采用的是同步的工作方式,比较简单,系列教程也主要介绍同步的工作方式。
网络通信的前提就是客户端和服务器端的通信,在服务器端,程序需要不断的监听客户端是否有连接请求,已保证多个客户端的连接,服务器通过套接字识别客户端;而客户端只需要指定哪个服务器即可。一旦双方建立连接并创建了对应的套接字,就可以互相传输数据了。客户端和服务器端发送和接受数据的方法都是一样的,区别仅是方向不同。
在同步TCP网络应用程序中,发送、接受和监听语句均采用阻塞方式工作,一般有如下步骤:
(1)创建一个包含所采用的网络类型、数据传输类型和协议类型的本地套接字对象,并将其余服务器的IP地址和端口号绑定。可通过Socket类或者TcpListener类完成。
(2)在指定的端口进行监听,以便接受客户端的连接请求。
(3)一旦接受了客户端的连接请求,就根据客户端发送的连接信息创建与该客户端对应的Socket对象或者TcpClient对象。
(4)根据创建的Socket对象或者TcpClient对象,分别与每个连接的客户进行数据传输。
(5)根据传送信息的情况确定是否关闭与对方的连接。
本文的目的是完成前三步,即创建服务器和客户端的连接,服务器将根据对应客户端建立的TcpClient对象得到客户端的信息
服务器端部分代码
IPAddress localAddress; int port = 51888; TcpListener myListener; Service service = new Service(listbox); IPAddress[] addrIP = Dns.GetHostAddresses(Dns.GetHostName()); localAddress = addrIP[0]; myListener = new TcpListener(localAddress, port); myListener.Start(); service.SetListBox(string.Format("开始在{0}:{1}监听客户连接", localAddress, port)); ThreadStart ts = new ThreadStart(ListenClientConnect); Thread myThread = new Thread(ts); myThread.Start(); |
我们可以看到建立了一个TcpListener,并且调用了TcpListener.Start();接着启动了一个线程,循环的接受客户端的请求并建立对应的TcpClient对象,看一下ListenClientConnect方法
while (true) { TcpClient newClient = null; try { newClient = myListener.AcceptTcpClient(); } catch { break; } User user = new User(newClient); userList.Add(user); service.SetListBox(string.Format("{0}进入", newClient.Client.RemoteEndPoint)); service.SetListBox(string.Format("当前连接用户数:{0}",userList.Count)); } |
其中的while(true)用法看起来比较奇怪,但没什么问题,保证应答多个客户端的请求。
客户端的代码
TcpClient client = null; try { client = new TcpClient(Dns.GetHostName(), 51888); } catch { MessageBox.Show("与服务器连接失败", "", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } |
客户端代码很简单,因为不需要传输数据,这样当服务器端监听到请求后,利用建立的TcpClient对象的到该客户端的信息,通过调用service.SetListBox打印到ListBox中(在游戏中每个窗口都有一个ListBox,使大家看到服务器和客户端交互的一些信息,方便调试)
Service代码
class Service { private ListBox listbox; private delegate void SetListBoxCallback(string str); private SetListBoxCallback setListBoxCallback; public Service(ListBox listbox) { this.listbox = listbox; setListBoxCallback = new SetListBoxCallback(SetListBox); } public void SetListBox(string str) { if (listbox.InvokeRequired) { listbox.Invoke(setListBoxCallback, str); } else { listbox.Items.Add(str); listbox.SelectedIndex = listbox.Items.Count - 1; listbox.ClearSelected(); } } } |
大家一定会对SetListBox的写法比较奇怪,这里实际上是多线程中调用winform 的方法,来看网上的一段话
每一个从Control类中派生出来的WinForm类(包括Control类)都是依靠底层Windows消息和一个消息泵循环(message pump loop)来执行的。消息循环都必须有一个相对应的线程。由于最初消息循环的缘故,只有创建该form的线程才能调用其事件处理方法。
换句话说,如果你在你自己的线程中调用这些方法,则它们会在该线程中处理事件,而不是在创建该form的主线程中进行处理,这时就需要通过Control.Invoke方法返回窗体主线程执行相关操作。
同时,由于程序中大量使用了SetListBox方法,因此将其修改为自动判断是否需要Invoke,而使用该方法时不需要关心此细节。Invoke的第一个参数是一个SetListBoxCallback委托,此处也可以用匿名函数实现,代码更简洁。
以上是本文的几个需要注意的地方,本例的内容没有涉及数据传输,比较简单,但却非常基础,希望能够对大家有所帮助。





