原文转载:https://www.west-wind.com/presentations/dotnetWebRequest/dotnetWebRequest.htm
HTTP内容检索是应用程序的重要组成部分。虽然.NET减少了通过Web服务框架,ADO.NET和XML类中的内置机制从Web显式检索内容的需要,但仍然需要直接检索Web内容并将其作为文本或数据下载操作进入文件。在本文中,Rick描述了HttpWebRequest和 HttpWebResponse类的功能,并提供了一个易于使用的包装类。该类简化了HTTP访问,并在易于使用的单一界面中提供了大多数常见功能,同时仍然可以完全访问HttpWebRequest 类的基本功能。
上个星期我决定我需要一个很好的有用的项目来扔在.NET上,以继续我的学习曲线,而实际构建一些我有用的东西。几年前,我写了一个Web监控软件包,允许我监视一组网站,并在站点关闭并且没有响应时发出警报。该应用程序已经显示其年龄,并且由于它是使用C ++开发的,它具有不可维护的笨重的用户界面。我认为这将是一个很好的“培训”应用程序重新构建.NET。此应用程序执行.NET Framework内置的HTTP功能,需要设置和运行多个线程,挂起事件并管理一小组数据,而无需数据库后端,并最终提供Windows Form UI。在将来将此应用程序转换为Windows服务的同时也是一个很好的功能。这个应用程序可以让我探索一个编程环境的各种功能。在本月的文章中,我将描述我需要专门构建的HTTP检索机制的一些功能。
.NET中的新HTTP工具
.NET Framework提供了用于检索在单个包中强大且可扩展的HTTP内容的新工具。如果您曾在.NET应用程序中工作过并尝试检索HTTP内容,您可能会知道有许多不同的工具可用:WinInet (Win32 API),XMLHTTP(MSXML的一部分)和最近的新的WinHTTP COM库。这些工具总是在某些情况下工作,但是并不适合所有实例的账单。例如,WinInet 无法在没有多线程支持的情况下在服务器上扩展。XMLHTTP太简单,不支持HTTP模型的所有方面。WinHTTP 是COM的最新工具,解决了许多这些问题,但Win9x上根本不起作用,
.NET框架通过一对HttpWebRequest类和HttpWebResponse类大大简化了HTTP访问。这些类以简单的方式提供了通过HTTP协议提供的所有功能。从Web返回内容的基本知识需要很少的代码(见清单1)。
清单1:通过HTTP简单检索Web数据。
string lcUrl =“ http://www.west-wind.com/TestPage.wwd ”;
// ***建立请求
HttpWebRequest loHttp =
(HttpWebRequest)WebRequest.Create(lcUrl);
// ***设置属性
loHttp.Timeout = 10000; // 10秒
loHttp.UserAgent =“代码示例Web客户端”;
// ***检索请求信息头
HttpWebResponse loWebResponse =(HttpWebResponse)loHttp.GetResponse();
Encoding enc = Encoding.GetEncoding(1252); // Windows默认代码页
StreamReader loResponseStream =
新的StreamReader(loWebResponse.GetResponseStream(),enc);
string lcHtml = loResponseStream.ReadToEnd();
loWebResponse.Close();
loResponseStream.Close();
很简单,对吧?但是,在这个简单之下,也是很大的力量。我们先看看这是如何工作的。
首先创建HttpWebRequest 对象,该对象是用于启动Web请求的基础对象。调用静态WebRequest.Create()方法用于解析URL并将解析的URL传递到请求对象中。如果传递的URL具有无效的URL语法,则此调用将抛出异常。
请求部分控制出站HTTP请求的结构。因此,它处理HTTP头的配置,其中最常见的表示为HttpWebRequest 对象的属性。一些示例是UserAgent,ContentType,Expires甚至一个Cookie集合,它直接映射到发送响应时设置的头值。标题也可以使用Headers字符串集显式设置,您可以向其添加整个标题字符串或键值对。一般来说,这些属性可以解决所有常见的头文件,所以很少需要使用明确的设置头来最有可能仅支持特殊协议(例如,SOAP请求的SoapAction)。
在该示例中,除了设置几个可选属性(UserAgent (客户端的浏览器,否则为空),以及请求的超时)之外,我对此请求不做任何事情。如果你需要把数据POST到服务器上,你需要再多做一些工作 - 稍后再说一遍。
流畅的交易
一旦将HTTP请求配置为发送数据,对GetResponse()的调用实际上就会消失,并将HTTP请求发送到Web服务器。此时,请求发送头并从Web服务器检索第一个HTTP结果缓冲区。
当上面的代码执行GetResponse() 调用时,只能从Web服务器返回一小部分数据。第一个组件包含HTTP头部和数据的第一部分,这是内部简单缓冲的,直到从流本身读取。来自此初始请求的数据用于设置HttpWebResponse 对象的属性,因此您可以查看ContentType, ContentLength,StatusCode, Cookies等内容。
接下来,使用GetResponseStream() 方法返回一个流。该流指向来自Web服务器的实际二进制HTTP响应。Streams为处理如何从Web服务器中提取数据提供了很大的灵活性(请参阅 Streams和StreamReader侧栏)。如上所述,对GetResponse()的调用只返回一个初始的内部缓冲区 - 以检索实际的数据,并从Web服务器读取其余的结果文档,您必须读取流。
在上面的例子中,我使用一个StreamReader 对象从单个操作中的数据返回一个字符串。但是要意识到,由于返回了流,我可以直接访问流,并读取较小的块来说明提供有关HTTP下载进度的状态信息。
还要注意,当 创建StreamReader时,我不得不明确提供编码类型 - 在这种情况下,CodePage 1252是Windows默认代码页。这是重要的,因为数据作为字节流传输,没有编码会导致任何扩展字符的无效字符转换。CodePage 1252适用于英文或欧洲语言内容以及二进制内容。理想情况下,您将需要在运行时决定使用哪种编码 - 例如二进制文件可能应将流输出到文件或其他位置,而不是转换为字符串,而日本的页面应使用适当的Unicode编码语言。
我使用StreamReader对象,它提供了一个简单的机制来将流的内容检索为字符串或字符数组。它还提供了方便的ReadToEnd() 方法,它可以在单个批中检索整个流。读取流的操作是实际从Web服务器检索数据(除了读取的初始块以检索标题)。在这种情况下,调用单个读取操作,并使用请求阻塞检索数据,直到数据已被返回为止。如果您想提供反馈,您还可以使用StreamReader的 Read()方法读取块中的数据,该方法允许您指定要读取的数据的大小。您' d运行这个循环,并提供每个读取所需的任何状态信息。使用此机制,您可以检索数据并提供进度信息。
StreamReader还使用BaseStream属性来公开底层的原始流,因此StreamReader是一个很好的对象,用于传递流数据。
发布数据
以上示例仅检索本质上是HTTP GET请求的数据。如果要将数据发送到服务器,可以使用HTTP POST操作。POST数据是指将数据作为请求负载的一部分发送到Web服务器的过程。POST操作都将数据发送到服务器并检索响应。
发布使用流将数据发送到服务器,因此发布数据的过程与检索数据相反(参见清单2)。
清单2:将数据发布到Web服务器
string lcUrl =“http://www.west-wind.com/testpage.wwd”;
HttpWebRequest loHttp =
(HttpWebRequest)WebRequest.Create(lcUrl);
// ***发送任何POST数据
string lcPostData =
“Name =”+ HttpUtility.UrlEncode(“Rick Strahl“)+
“&Company =”+ HttpUtility.UrlEncode(“西风”);
loHttp.Method = “POST”;
byte [] lbPostBuffer = System.Text。
Encoding.GetEncoding(1252).GetBytes(lcPostData);
loHttp.ContentLength = lbPostBuffer.Length;
Stream loPostData = loHttp.GetRequestStream();
loPostData.Write(lbPostBuffer,0,lbPostBuffer.Length);
loPostData.Close();
HttpWebResponse loWebResponse =(HttpWebResponse)loHttp.GetResponse();
Encoding enc = System.Text.Encoding.GetEncoding(1252);
StreamReader loResponseStream =
新的StreamReader(loWebResponse.GetResponseStream(),enc);
string lcHtml = loResponseStream.ReadToEnd();
loWebResponse.Close();
loResponseStream.Close();
请确保您在HttpWebRequest.GetResponse()调用之前立即使用此POST代码。所有其他操作的Request对象没有任何效果,因为标头使用POST缓冲区发送。其余的代码与之前所示的相同 - 您检索到响应,然后读取流以获取结果数据。
POST数据在发送到服务器时需要进行适当的编码。如果您将信息发布到网页,则必须确保将POST缓冲区正确编码为键值对,并使用URLEncoding作为值。您可以使用静态方法S ystem.Web.HttpUtility.UrlEncode()对数据进行编码。在这种情况下,请确保在项目中包含System.Web命名空间。请注意,只有当您发布到典型的HTML页面时,这是必要的 - 如果您发布XML或其他应用程序内容,则可以按原样发布原始数据。使用本文中包含的自定义类更容易做到这一点。
要将实际数据发送到POST缓冲区中,必须首先将数据转换为字节数组。我们再次需要对字符串进行正确的编码。使用GetBytes() 方法使用Encoding.GetEncoding( 1252)编码,该方法使用Windows标准ANSI代码页返回字节数组。然后,您应该设置ContentLength属性,以便服务器可以知道进入的数据流的大小。最后,您可以使用从HttpWebRequest.GetRequestStream()返回的输出流将POST数据写入服务器。在一个Write()方法调用中,使用适当的字节数组,将整个字节数组写入流中。这将写入数据并等待完成。与检索操作一样,流操作是实际导致数据发送到服务器的实际操作,因此如果要提供进度信息,您可以发送较小的块,并在需要时向用户提供反馈。
超越基础
使用HttpWebRequest / HttpWebResponse的基本操作 是直接的。但是,如果您构建使用HTTP访问的典型应用程序,您会发现您必须设置一些其他属性; 对象属性特别。.NET框架的一个很好的功能是在框架的许多领域重用的常见对象的一致性。
如果您在ASP.NET应用程序中使用身份验证,则服务器上使用的对象与客户端具有相同的界面。在本节中,我将介绍身份验证,代理配置和使用Cookies的主题,这些是您作为客户端解决方案安装HttpWebRequest可能需要做的一些事情。HttpWebRequest / Response 使用的所有这三个对象都是标准对象,您可以在框架的其他位置找到它们。
认证
登录Web内容对于分布式应用程序来说是非常常见的安全措施。Web认证通常包括基本身份验证(通常是通过应用程序驱动的提示操作系统帐户)或NTLM(集成文件安全性)。
要验证用户,请使用Credentials属性:
WebRequest.Credentials = new NetworkCredential(“username”,“password”);
如果您使用基本认证,只有用户名和密码才有意义,而使用NTLM,您也可以传递域名。如果您从Windows客户端应用程序对NTLM资源(服务器文件系统上设置的权限)进行身份验证,还可以使用当前登录的用户凭据,如下所示:
WebRequest.Credentials = CredentialCache.DefaultCredentials;
HttpWebRequest处理身份验证HTTP协议请求的导航,因此如果验证请求的验证,则验证的请求像其他任何操作一样。如果由于认证而导致请求失败,则抛出异常。
代理服务器配置
如果你想建立一个坚实的Web前端到一个客户端应用程序,你将不得不处理那些坐在防火墙/代理服务器上的客户端,你的应用程序将不得不处理这些设置。幸运的是,使用WebProxy类成员获取信息使HttpWebRequest变得非常无痛。要配置代理,可以使用如下代码:
// 传递代理字符串并绕过本地机器
WebProxy loProxy = new WebProxy(“http://proxy-server.mine.com:8080”,true);
// ** ByPassList
string [] cByPass = new string [2];
cByPass [1] =“http://someserver.net”;
cByPass [2] = http://192.0.0.1
loProxy.BypassList = cByPass;
// **代理验证
loProxy.Credentials = new NetworkCredential(“proxyusername”,“pass”);
Request.Proxy = loProxy;
向Proxy对象提供多少细节取决于特定的代理服务器。例如,不需要旁路列表,大多数代理不需要用户名和密码,在这种情况下,您不需要提供凭据。
WebProxy也可以将所有参数填充到构造函数中,如下所示:
Request.Proxy = new WebProxy(
“http://proxy-server.mine.com:8080",true,cBypass,new NetworkCredential(...));
HTTP Cookie
HTTP Cookies是HTTP协议的状态管理实现,许多Web页面都需要它们。如果您使用远程HTTP功能来驱动网站(以下URL等),您将在许多情况下必须能够支持Cookie。
Cookie通过在客户端存储令牌来工作,因此客户端真正负责管理创建的任何cookie。通常,浏览器会为您管理所有这些,但是在这里,没有任何浏览器可以在应用程序前端进行协助,我们有责任自行跟踪此状态。这意味着当服务器为一个请求分配一个cookie时,客户端必须挂起来,并在应用的下一个请求(基于网站和虚拟目录)的情况下将其发送回服务器。HttpWebRequest和HttpWebResponse 提供容器用于保存发送和接收端的cookie,但不会自动保留它们,从而成为您的责任。
因为Cookie集合在这些对象中被很好地抽象,所以保存和恢复它们是相当容易的。使此工作的关键是对Cookie集合持久化对象引用,然后每次重用相同的cookie存储。
为了做到这一点,我们假设你正在一个表单上运行请求(或者一些其他的类 - 在下面的例子中)。您将创建一个名为Cookies的属性:
CookieCollection Cookies;
在请求发送到服务器之前,在连接的请求结束处,您可以检查以前保存的一组Cookie,如果使用它们:
Request.CookieContainer = new CookieContainer();
if(this.Cookies!= null &&
this.Cookies.Count> 0)
Request.CookieContainer.Add(this.Cookies);
因此,如果您以前已经检索到Cookie,则将它们存储在Cookie属性中,然后添加到Request的CookieContainer属性中。CookieContainer 是一组Cookie集合,它可以存储多个站点的Cookie。在这里,我只处理跟踪一组单个Cookie的单个请求。
在接收端,一旦在调用GetWebResponse()之后已经检索到请求头,则可以使用如下代码:
//将Cookie保存在持久对象上
if(Response.Cookies.Count> 0)
this.Cookies = Response.Cookies;
这将保存cookie集合,直到下一个请求被重新分配给发送到服务器的请求为止。请注意,这是一个非常简单的Cookie管理方法,只有在给定网站上设置了单个或单个Cookie集合时才会起作用。如果多个Cookie设置在网站的多个不同位置,您实际上必须检索个别的Cookie并将其单独存储到Cookie集合中。以下是一些代码:
if(loWebResponse.Cookies.Count> 0)
if(this.Cookies == null)
{
this.Cookies = loWebResponse.Cookies;
}
其他
{
//如果我们已经有cookie更新列表
foreach(Cookie oRespCookie in
loWebResponse.Cookies)
{
bool bMatch = false;
foreach(Cookie oReqCookie in
this.oCookies){
if(oReqCookie.Name ==
oRespCookie.Name) {
oReqCookie.Value =
oRespCookie.Name;
bMatch = true;
打破;
}
}
如果(!bMatch)
this.Cookies.Add(oRespCookie);
}
}
}
这应该给你一个很好的起点。此代码仍然不处理像域和虚拟路径之类的东西,也不处理保存的cookie,但是对于大多数应用程序,上述应该是足够的。
把它包起来
到目前为止,您可能会了解HttpWebRequest对象提供的功能。使用这些对象是一个直接的过程,它确实需要相当数量的代码,并且使用这些对象需要相当数量的代码和许多类的知识。
由于我在几乎每个应用程序中使用HTTP访问,所以我决定创建一个名为wwHttp的包装类 ,它简化了整个过程(该类包含在本文的代码下载中)。而不是创建两个单独的请求和响应对象,单个类处理单个对象中具有简单的字符串属性。该类处理为您设置POST变量,从字符串而不是对象创建任何身份验证和代理设置,管理cookie,并提供可选的简化错误处理程序,设置属性而不是抛出异常。它允许访问基础对象,您可以传入WebRequest对象并检索对Request和Response对象的引用,所以你可以简单地获得两个世界的最好的,而不放弃框架类的任何功能。该类还提供了几种用于返回字符串,流和输出到文件的重载方法。当下载数据以在GUI应用程序中反馈时,可以将该类设置为在缓冲区检索点处触发事件。
首先添加命名空间:
使用Westwind.Tools.Http;
使用该类可以简单地从网站检索内容:
wwHttp loHttp = new wwHttp();
loHttp.Username =“ricks”;
loHttp.Password =“password”;
loHttp.ProxyAddress =“http://proxy-server.hawaii.rr.com:8080”;
loHttp.AddPostKey( “姓名”,”Rick Strahl“);
loHttp.AddPostKey(“公司”,“西风科技”);
loHttp.HandleCookies = true; //启用自动跟踪5个Cookie
string lcHtml = loHttp.GetUrl(“http://www.west-wind.com/TestPage.wwd”);
大多数这些属性设置是可选的,但是类中的所有内容都可以使用简单的字符串访问。AddPostKey() 自动创建UrlEncoded 字符串。支持多种不同的POST模式,包括UrlEncoded(0),Multi-Part(2)和raw XML(4)POST。
相应的GetUrl()方法有几个不同的签名。您可以传入一个预配置的可选WebRequest对象,因此如果您需要设置其中一个来 覆盖某些未被暴露的属性,那么可以覆盖它们。例如:
HttpWebRequest oReq =(HttpWebRequest)WebRequest.Create(lcUrl);
oReq.Expires = -1
oReq.Headers.Add( “SOAPACTION”, “http://west-wind.com/soap#Method”);
wwHttp Request = new wwHttp();
string lcHTML = Request.GetUrl(oReq);
wwHttp 还暴露了可以用于快速检查错误条件的Error和 ErrorMsg属性:
lcHtml = Request.GetUrl(lcUrl);
if(Request.Error)
MessageBox.Show(Request.ErrorMsg);
其他
this.txtResult.Text = lcHtml;
Explicit错误检索是默认的,但是您可以使用ThrowExceptions 属性让类将异常传递给代码。
发布数据
正如我之前提到的,POST数据在Web请求应用程序中很重要,并且将数据转换为适当的发布格式可能会需要可能的相当数量的代码。wwHttp通过处理不同POST模式 的AddPostKey()方法的多次重载来抽象进程:1 - URLEncoded窗体变量,2 - 多部分窗体变量和文件,4 - XML或原始POST 缓冲区。基本方法如图所示在清单3中。
清单3:wwHttp :: AddPostKey处理POST数据
public void AddPostKey(string Key,byte [] Value)
{
if(this.oPostData == null)
{
this.oPostStream = new MemoryStream();
this.oPostData = new BinaryWriter(this.oPostStream);
}
if(Key ==“RESET”)
{
this.oPostStream = new MemoryStream();
this.oPostData = new BinaryWriter(this.oPostStream);
}
开关(this.nPostMode)
{
情况1:
this.oPostData.Write(Encoding.GetEncoding(1252).GetBytes(
键+“=”+
System.Web.HttpUtility.UrlEncode(Value)+“&”));
打破;
情况2:
this.oPostData.Write(Encoding.GetEncoding(1252).GetBytes(
“ - ”+ this.cMultiPartBoundary +“ r n”+
“Content-Disposition:form-data; name = ”“+ Key +
“” r n r n“));
this.oPostData.Write(Value);
this.oPostData.Write(Encoding.GetEncoding(1252).GetBytes(“ r n”));
打破;
默认:
this.oPostData.Write(Value);
打破;
}
}
此方法依赖于流oPostStream来保存用户可能向服务器发送的累积POST数据。一个BinaryWriter对象用于实际写入流,而不必像原始流那样计算字节数。
接下来的实际POST数据是使用实际写入的数据流写入()中的的BinaryWriter。请注意,此版本的AddPostKey()接受一个byte []输入参数而不是一个字符串,因此可以以原始格式写入数据。
本的BinaryWriter 写入()方法重载允许字符串参数,但是,这并没有正确地为我的编码是在输出搞砸了工作。相反,上面的代码使用如前所述的正确编码,明确地执行任何字符串(包括静态字符串,例如多部分形式的vars的字符串)到字节数组的翻译。再次,这是非常棘手的,因为您可以在BinaryWriter上设置编码,但似乎没有任何效果。上面显示的代码是唯一正确运行的解决方案。
这个方法有几个重载。最重要的是一个字符串版本:
public void AddPostKey(string Key,string Value)
{
this.AddPostKey(密钥,Encoding.GetEncoding(1252).GetBytes(值));
}
这只是将字符串转换为具有适当编码的字节数组。另一个版本接受单个POST缓冲区,通常用于XML或二进制内容。
public void AddPostKey(string FullPostBuffer)
{
this.oPostData.Write(Encoding.GetEncoding(1252).GetBytes(FullPostBuffer));
}
这一个直接写入二进制写入器。
最后还有一个AddPostFile() 方法,它允许您在使用多部分表单(PostMode = 2)运行时将文件POST到服务器,以提供HTML文件上传功能。
清单3.1:多部分表单的HTTP文件上传方法
public bool AddPostFile(string Key,string FileName)
{
byte [] lcFile;
if(this.nPostMode!= 2){
this.cErrorMsg =“仅允许使用多部分表单的文件上传”;
this.bError = true;
返回假
}
尝试
{
FileStream loFile = new FileStream(FileName,System.IO.FileMode.Open,
System.IO.FileAccess.Read);
lcFile = new byte [loFile.Length];
loFile.Read(lcFile,0,(int)loFile.Length);
loFile.Close();
}
catch(异常e)
{
this.cErrorMsg = e.Message;
this.bError = true;
返回假
}
this.oPostData.Write(Encoding.GetEncoding(1252).GetBytes(
“ - ”+ this.cMultiPartBoundary +“ r n” +
“Content-Disposition:form-data; name = ”“+ Key +”“filename = ”“+
新的FileInfo(FileName).Name +“” r n r n“));
this.oPostData.Write(lcFile);
this.oPostData.Write(Encoding.GetEncoding(1252).GetBytes(“ r n”));
返回真
}
在发送请求之前,AddPost样式方法处理收集POST数据。实际发送发生在wwHttp类的主GetUrlStream()方法中。
清单3.2:将POST数据发送到服务器
// ***处理POST缓冲区(如果有)
if(this.oPostData!= null)
{
Request.Method =“POST”;
switch(this.nPostMode)
{
情况1:
Request.ContentType =“application / x-www-form-urlencoded”;
打破;
情况2:
Request.ContentType =“multipart / form-data; boundary =”+
this.cMultiPartBoundary;
this.oPostData.Write(Encoding.GetEncoding(1252).GetBytes(“ - ”+
this.cMultiPartBoundary +“ r n”));
打破;
情况4:
Request.ContentType =“text / xml”;
打破;
默认:
goto案例1;
}
Stream loPostData = Request.GetRequestStream();
// ***将内存流复制到请求流
this.oPostStream.WriteTo(loPostData);
// ***关闭内存流
this.oPostStream.Close();
this.oPostStream = null;
// ***关闭二进制作者
this.oPostData.Close();
this.oPostData = null;
// ***关闭请求流
loPostData.Close();
}
此代码通过检查我们是否已经在POST缓冲区中写入了一些内容,并通过指定内容类型来配置POST请求来完成对POST数据的请求。在多部分形式POST的情况下,将结尾字符串添加到内容的末尾。
写出数据需要从保存我们累积的POST数据的内存流中获取数据,并将其写入实际将POST数据发送到服务器的请求流(loPostData)。
正如你可以看到很多事情正在发生,以正确地POST数据到服务器,wwHttp照顾你的细节没有额外的代码。
发射事件
GetUrl()方法的其他版本返回一个StreamReader对象,另一个版本GetUrlEvents(),每当数据到达缓冲区时触发OnReceiveData事件。该事件提供当前字节和总字节计数(如果可用)以及两个标志完成和取消。完成后,您可以知道请求何时完成,而取消标志可让您的代码停止下载数据。
要启动事件运行,您只需将事件处理程序连接到事件:
HttpWebRequest loHttp = new wwHttp();
loHttp.OnReceiveData + = new
wwHttp.OnReceiveDataHandler(this.loHttp_OnReceiveData);
string lcHtml = loHttp.GetUrlEvents(this.txtUrl.Text.TrimEnd(),4096);
this.txtHTML.Text = lcHtml;
loHttp.OnReceiveData - =
new wwHttp.OnReceiveDataHandler(
this.loHttp_OnReceiveData);
确保在请求结束时断开处理程序,或将其设置在仅运行一次的静态位置。事件处理程序方法可以在返回给您的OnReceiveDataArgs obhect中的数据中进行一些工作(清单3):
清单4:实现wwHttp :: OnReceiveData事件
private void loHttp_OnReceiveData(object sender,
wwHttp.OnReceiveDataEventArgs e) {
如果(e.Done)
MessageBox.Show(“下载完成!”);
else if(e.NumberOfReads == 20){
MessageBox.Show(“取消...太大”);
e.Cancel = true;
}
其他
this.oStatus.Panels [0] .Text =
e.CurrentByteCount.ToString()+“of ”+
e.TotalBytes.ToString()+“bytes read”;
}
使用事件很容易。在wwHttp类上创建事件有更多的参与,需要三个步骤:
首先需要在课堂上定义实际的事件:
公共 事件 OnReceiveDataHandler OnReceiveData;
接下来,事件的参数需要被包装成一个包含参数作为属性的类:
公共类OnReceiveDataEventArgs
{
public long CurrentByteCount = 0;
public long TotalBytes = 0;
public int NumberOfReads = 0;
public char [] CurrentChunk;
public bool Done = false;
public bool Cancel = false;
}
创建一个公共代理,作为要调用的事件的方法签名:
public delegate void
OnReceiveDataHandler(object sender,
OnReceiveDataEventArgs e);
如果要传递自定义参数,则只需定义此委托。如果不需要任何参数,您可以使用标准的System.EventHandler代理定义事件。这三个组成事件界面。
要实际触发事件,您可以简单地运行代码并调用调用该用户分配给该事件的函数指针。以下是wwHttp的相关代码,演示了如何读取响应循环以及如何在每个更新周期触发事件。
清单5:读取响应流和触发事件
StreamReader oHttpResponse = this.GetUrlStream(Url);
if(oHttpResponse == null)
返回“”;
long lnSize = BufferSize;
// ***使用StringBuilder创建结果字符串
StringBuilder loWriter = new StringBuilder((int)lnSize);
// ***创建用作事件参数的参数结构
OnReceiveDataEventArgs oArgs = new OnReceiveDataEventArgs();
oArgs.TotalBytes = lnSize;
while(lnSize> 0) {
lnSize = oHttpResponse.Read(lcTemp,0,(int)BufferSize);
if(lnSize> 0)
{
loWriter.Append(lcTemp,0,(int)lnSize);
lnCount ++;
lnTotalBytes + = lnSize;
// ***如果挂起,请提起事件
if(this.OnReceiveData!= null)
{
/// ***更新事件处理程序
oArgs.CurrentByteCount = lnTotalBytes;
oArgs.NumberOfReads = lnCount;
oArgs.CurrentChunk = lcTemp;
// ***调用事件方法
this.OnReceiveData(此,oArgs);
// ***检查取消标志
如果(oArgs.Cancel)
goto CloseDown;
}
}
} // while
此代码的关键是委托OnReceiveData (有关代理的更多信息,请参阅代理侧栏)。它作为一个函数指针,它将上述示例中的指定方法指向表单。在流读取循环中,每次检索新的缓冲区时,都会调用此方法。
我们可以同时走路口吃嚼口香糖!
事件很酷,但如果您以截屏模式运行,那么它们并不是很有用,因为我已经显示出来了。HttpWebRequest / HttpWebResponse也可以使用BeginGetResponse / EndGetResponse 方法在异步模式下运行。大多数流类提供了这种机制,允许您指定一个回调方法来收集从这些请求中检索出的输出(您也可以以这种方式异步发送数据)。
然而,在玩了一段时间后,然后在.NET框架中查看本机线程支持,结果是更容易创建一个我自己的新线程,并将线程操作封装在一个类中。以下示例同时在几个线程上运行多个wwHttp对象,同时也使用OnReceiveData 事件的信息更新表单。图1显示了什么样的形式。当HTTP请求被检索时,主表单线程仍然可用于执行其他任务,因此您可以围绕该表单移动,因此表单的UI始终保持活动状态。
图1 - 此示例表同时运行两个HTTP请求,同时触发更新表单状态字段的事件。当这些请求运行时,表单保持“活动”。
这个过程在.NET中非常简单,部分原因是.NET使线程方法变得容易。这使得它很容易将线程的处理创建为一个很好的封装格式,并提供了一个简单的封装机制,用于将数据传递到一个线程中,并保持与其他应用程序隔离的数据。
确保将System.Threading命名空间添加到使用线程的表单中。以下代码定义了使用FireUrls()方法触发HTTP请求的线程处理程序类(清单5)。
清单6:实现Thread类
公共类GetUrls
{
public string Url =“”;
public wwHttpMultiThread ParentForm = null;
public int Instance = 1;
public void FireUrls()
{
wwHttp oHttp = new wwHttp();
oHttp.OnReceiveData + =
new wwHttp.OnReceiveDataHandler(this.OnReceiveData);
oHttp.Timeout = 5;
string lcHTML = oHttp.GetUrlEvents(this.Url,4096);
if(oHttp.Error)
this.ParentForm.lblResult.Text = oHttp.ErrorMsg;
}
public void OnReceiveData(object sender,
wwHttp.OnReceiveDataEventArgs e)
{
if(this.Instance == 1)
this.ParentForm.lblResult.Text =
e.CurrentByteCount.ToString()+“bytes”;
其他
this.ParentForm.lblResult2.Text =
e.CurrentByteCount.ToString()+“bytes”;
}
}
这个类没有什么特别的 - 事实上,任何类都可以做为线程处理程序(只要你编写线程安全的代码)。这个简化的实现包括一个可以访问表单上的状态标签的ParentForm的引用。Instance属性用于识别哪个请求正在更新哪个窗体控件。这里的实际代码非常像以前使用wwHttp对象显示的代码。请注意,此代码为事件处理程序分配线程操作类的方法。该方法然后调用ParentForm 并更新标签。
表单上的调用代码创建了两个调用FireUrls方法的线程,如下所示(清单6):
清单7:创建和运行线程
private void cmdGo_Click(object sender,System.EventArgs e)
{
GetUrls oGetUrls = new GetUrls();
oGetUrls.Url = this.txtUrl.Text;
oGetUrls.ParentForm = this;
ThreadStart oDelegate =
新的ThreadStart(oGetUrls.FireUrls);
Thread myThread = new Thread(oDelegate);
myThread.Start();
GetUrls oGetUrls2 = new GetUrls();
oGetUrls2.ParentForm = this;
oGetUrls2.Url = this.txtUrl2.Text;
oGetUrls2.Instance = 2;
ThreadStart oDelegate2 =
新的ThreadStart(oGetUrls2.FireUrls);
Thread myThread2 = new Thread(oDelegate2);
myThread2.Start();
}
要启动一个线程,ThreadStart()函数被调用,它接受一个函数指针(基本上是一个类中一个特定方法的引用)作为参数。这将返回一个可用于创建新线程的代理,并指示它使用此指针开始运行。您可以传递static类方法的静态地址的实例变量。在大多数情况下,您将需要使用动态实例对象变量的方法,因为它可以通过设置处理中需要的属性来完全设置实例。将实际线程实现类作为包装器,用作高级调用机制和参数packager到您的实际处理代码。如果需要将数据传回给其他对象,则可以使该实例成为另一个对象的成员。例如,我可以使此对象成为窗体的一部分,然后允许窗体访问“线程”类的成员,并且两者都可以共享数据。
创建线程并运行它们非常简单,但确保您可以干净地管理数据,以防止同时从不同线程访问共享数据。共享数据需要通过某种同步来保护。事实上,您可以看到为什么这是一个问题,如果您在下载仍在运行时点击示例表单上的Go链接几次。随着多线程更新表单上的相同字段,您会看到数字来回跳转。由于多个实例同时写入标签,代码最终会爆炸。解决方法是使用synchronized方法来处理更新,或使用单独的表单来显示更新信息(每个请求的新表单)。
本文的代码:
http://www.west-wind.com/presentations/dotnetWebRequest/dotnetWebRequest.zip