本文将介绍如何通过编写一个小型 HTTP服务器来解决使用WinFormsWebBrowser控件时遇到的一些问题。通常,开发者会直接设置 WebBrowser.DocumentText 属性来显示 HTML,但这种方法存在一些局限性。通过调用 WebBrowser.Navigate 方法并让服务器提供 HTML 和支持文件,可以更接近真实网站的体验,从而让 WebBrowser 控件更加高效。
在开发一个 HTML 编辑器应用程序时,使用了WinForms的 WebBrowser 控件作为显示组件。HTML 内容会随着每次按键而动态变化,因此需要动态地将其发送到控件。最初,使用了 WebBrowser.DocumentText 属性,但这引发了各种小问题。
首先,HTML 中的 和
C#
+ WebBrowserEx
//
父目录
+ Article
//
基础目录
+ WebBrowserEx.html
//
HTML 文件
+ WebBrowserEx
//
子目录
+ demo.png
//
图片文件
+ src.zip
//
压缩文件
+ WebBrowserEx
//
代码目录
+ WebBrowserEx.sln
//
解决方案文件
+ ...
HTML 可能看起来像这样:
<img src='WebBrowserEx/demo.png'>
<a href='WebBrowserEx/src.zip'>
为了解决这个问题,通过向 HTML 的 <head> 元素添加 <base> 标签来修改发送到控件的 HTML:
<base href='file:///C:/...path.../WebBrowserEx/Article/'>
这解决了图片的问题,但由于某些原因(可能是安全性),链接并没有解决。这是当时能找到的最好的折中方案。
另一个问题是锚点链接。HTML 通常包含一个“目录”列表,基本上是指向锚点的链接列表:
...
<h2>
目录
</h2>
<ul>
<li>
<a href='#Introduction'>
介绍
</a>
</li>
...
<h2>
<a name='Introduction'>
介绍
</a>
</h2>
...
由于需要 <base> 标签来显示图片,锚点链接被组合以包含 file:// 协议,给出的 URL 如下:
file:///C:/...path.../WebBrowserEx/Article/#Introduction
这些链接没有指向正确的位置,因此内部链接无法工作。
总而言之,使用 WebBrowser.DocumentText 属性会导致关于哪些标签有效哪些无效的不可接受的妥协。既然接受了 WebBrowser 控件没有适当的HTTP服务器就不会满意,想知道编写一个服务器有多困难。结果证明这非常容易。
.NET Framework 自 2.0 版本以来就包含了困难的部分:实现HTTP/1.1 协议。相关类位于 System.Net 命名空间的 System.dll 中:
C#
class HttpListener // 处理套接字级连接
class HttpListenerContext // 保存请求和响应对象
class HttpListenerRequest // 将请求解析为属性
class HttpListenerResponse // 将响应编译为有效的 HTTP
Stream response.OutputStream // 以二进制形式保存响应实体
所要做的就是使用 URL 来返回 HTML 字符串,或者在磁盘上找到文件并将它们作为二进制写入 OutputStream。好吧,确实还需要设置一些头部属性,例如 ContentType,但真的:框架中编码的知识量惊人!
如果要求与相符,只需要将 WebBrowser 实例替换为 WebBrowserEx 并调用 LoadHtml(string html, string baseDirectory) 而不是设置 DocumentText。就是这样。
基本服务器代码非常简单。如果要求略有不同,只需要更改获取二进制数据的位置。例如,如果图片存储在程序集中的资源中,可以从那里提供它们。不再有奇怪的 URI 和 Base64 编码!
通常不会在文章本身中提供完整的源代码,但由于总共只有 200 行:
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Mime;
using System.Text;
using System.Threading;
namespace Common
{
public class WebBrowserEx : System.Windows.Forms.WebBrowser
{
string _Html = String.Empty;
string _BaseDirectory = String.Empty;
public void LoadHtml(string html, string baseDirectory)
{
if (SynchronizationContext.Current != _UI)
throw new ApplicationException("WebBrowserEx.LoadHtml must be called on the UI thread.");
InitServer();
_Html = html;
_BaseDirectory = baseDirectory;
if (_Server == null)
return;
_Server.SetHtml(html, baseDirectory);
Navigate(_Server.Url);
}
SynchronizationContext _UI = null;
bool _TriedToStartServer = false;
Server _Server = null;
public WebBrowserEx()
{
_UI = SynchronizationContext.Current;
}
void InitServer()
{
if (_TriedToStartServer)
return;
_TriedToStartServer = true;
_Server = new Server();
}
public class Server
{
string _Html = String.Empty;
string _BaseDirectory = String.Empty;
public void SetHtml(string html, string baseDirectory)
{
_Html = html;
_BaseDirectory = baseDirectory;
}
public Uri Url {
get {
return new Uri("http://" + "localhost" + ":" + _Port + "/");
} }
HttpListener _Listener = null;
int _Port = -1;
public Server()
{
var rnd = new Random();
for (int i = 0; i < 100; i++)
{
int port = rnd.Next(49152, 65536);
try
{
_Listener = new HttpListener();
_Listener.Prefixes.Add("http://localhost:" + port + "/");
_Listener.Start();
_Port = port;
_Listener.BeginGetContext(ListenerCallback, null);
return;
}
catch (Exception x)
{
_Listener.Close();
Debug.WriteLine("HttpListener.Start:\n" + x);
}
}
throw new ApplicationException("Failed to start HttpListener");
}
public void ListenerCallback(IAsyncResult ar)
{
_Listener.BeginGetContext(ListenerCallback, null);
var context = _Listener.EndGetContext(ar);
var request = context.Request;
var response = context.Response;
Debug.WriteLine("SERVER: " + _BaseDirectory + "" + request.Url);
response.AddHeader("Cache-Control", "no-cache");
try
{
if (request.Url.AbsolutePath == "/")
{
response.ContentType = MediaTypeNames.Text.Html;
response.ContentEncoding = Encoding.UTF8;
var buffer = Encoding.UTF8.GetBytes(_Html);
response.ContentLength64 = buffer.Length;
using (var s = response.OutputStream) s.Write(buffer, 0, buffer.Length);
return;
}
var filepath = Path.Combine(_BaseDirectory, request.Url.AbsolutePath.Substring(1));
Debug.WriteLine("--FILE: " + filepath);
if (!File.Exists(filepath))
{
response.StatusCode = (int)HttpStatusCode.NotFound;
// 404
response.StatusDescription = response.StatusCode + " Not Found";
response.ContentType = MediaTypeNames.Text.Html;
response.ContentEncoding = Encoding.UTF8;
var buffer = Encoding.UTF8.GetBytes("404 Not Found");
response.ContentLength64 = buffer.Length;
using (var s = response.OutputStream) s.Write(buffer, 0, buffer.Length);
return;
}
byte[] entity = null;
try
{
entity = File.ReadAllBytes(filepath);
}
catch (Exception x)
{
Debug.WriteLine("Exception reading file: " + filepath + "\n" + x);
response.StatusCode = (int)HttpStatusCode.InternalServerError;
// 500
response.StatusDescription = response.StatusCode + " Internal Server Error";
response.ContentType = MediaTypeNames.Text.Html;
response.ContentEncoding = Encoding.UTF8;
var buffer = Encoding.UTF8.GetBytes("500 Internal Server Error");
response.ContentLength64 = buffer.Length;
using (var s = response.OutputStream) s.Write(buffer, 0, buffer.Length);
return;
}
response.ContentLength64 = entity.Length;
switch (Path.GetExtension(request.Url.AbsolutePath).ToLowerInvariant())
{
// images
case ".gif": response.ContentType = MediaTypeNames.Image.Gif; break;
case ".jpg":
case ".jpeg": response.ContentType = MediaTypeNames.Image.Jpeg; break;
case ".tiff": response.ContentType = MediaTypeNames.Image.Tiff; break;
case ".png": response.ContentType = "image/png"; break;
// application
case ".pdf": response.ContentType = MediaTypeNames.Application.Pdf; break;
case ".zip": response.ContentType = MediaTypeNames.Application.Zip; break;
// text
case ".htm":
case ".html": response.ContentType = MediaTypeNames.Text.Html; break;
case ".txt": response.ContentType = MediaTypeNames.Text.Plain; break;
case ".xml": response.ContentType = MediaTypeNames.Text.Xml; break;
// let the user agent work it out
default: response.ContentType = MediaTypeNames.Application.Octet; break;
}
using (var s = response.OutputStream) s.Write(entity, 0, entity.Length);
}
catch (Exception x)
{
Debug.WriteLine("Unexpected exception. Aborting...\n" + x);
response.Abort();
}
}
}
}