用于桌面应用程序开发的 Web 技术






4.94/5 (23投票s)
WebBrowser 自定义和 localhost 上的 http/https 服务器的示例,作为基于 Web 技术实现跨平台应用程序的引擎。
引言
开发跨平台应用程序一直是一个挑战,目的是降低成本并提高开发性能。为此,人们采用了许多方法,并存在大量库/框架。在这篇文章中,我不会重新发明轮子。我只是想分享我的经验,以及在构建应用程序时遇到的一些技巧和问题,希望对大家有所帮助。
问题在于开发一个可以在Windows OS和MAC OS上运行的应用程序。UI应看起来现代,并与这两种操作系统风格一致。还有一个额外的要求是实现跨浏览器扩展的解决方案。
在我看来,遵循这些要求最简单的方法是使用 Web 技术。
为了开发跨平台应用程序,我选择使用 Html 和 JavaScript 来实现用户界面和业务逻辑。为了实现这一目标,我决定在Windows OS上使用WebBrowser ActiveX控件,在MAC OS上使用QT Webkit来运行 Web UI。
在我看来,开发跨浏览器扩展最简单的方法是本地 http/https 服务器,它作为Windows OS上的服务或MAC OS上的守护进程。
在本文中,我将重点介绍Windows OS及其解决方案,如果有人觉得本文有趣且有帮助,我也可以提供MAC OS的代码和介绍。
目录
学习要点
WebBrowser 控件自定义
使用 Web 技术开发 UI 的方式之所以被选中,是因为它可以轻松地适配到 Web 浏览器中运行,并用于构建 Windows 应用商店应用程序,从而可以实现单一源代码来开发门户应用程序和桌面应用程序,所有这些应用程序都将呈现统一的风格,甚至共享相同的业务逻辑和数据模型。现代 Web 浏览器允许您渲染强大的用户界面。此外,该源代码还可以用于开发 Windows 应用商店应用程序,只需稍作修改。在附带的源代码中,您将找到桌面应用程序和 Windows 应用商店应用程序的示例。Web 用户界面的示例使用了React.js JavaScript框架和Bootstrap v3.3.5的样式。应用程序本身是用 C#.NET 和 WebBrowser
控件构建的。
Internet Explorer 的反对者可能会说它不是一个好的选择,而且 Internet Explorer 早就名声扫地了。相信我,事实并非如此。它是一个功能强大且高度可定制的控件。也许,MSDN上有大量关于 WebBrowser
activeX 控件的内容,有时会让人难以找到合适的信息。我希望这些技巧能帮助您找到正确的方向。
我们开始吧。首先需要做的是自定义 WebBrowser
控件(我在这里不讨论如何创建 Winforms 应用程序以及如何将控件嵌入 WinForms,所有这些都可以在提供的源代码中找到)。
在真实的桌面应用程序中,需要禁用或隐藏 WebBrowser
的一些功能。例如,您不需要标准的上下文菜单,或者您不希望显示 JavaScript 代码中发生异常时出现的标准错误处理程序,或者您不想允许将内容拖放到控件中,或者不希望使用标准按键快捷方式。前三项可以通过 WebBrowser
接口轻松实现,例如
...ScriptErrorsSuppressed = true;
...IsWebBrowserContextMenuEnabled = false;
...AllowWebBrowserDrop = false;
然后,要自定义或禁用标准按键快捷方式,您需要创建一个自定义控件并继承自 WebBrowser
控件,然后重写 PreProcessMessage
方法。例如
public override bool PreProcessMessage(ref Message msg)
{
int num = ((int)msg.WParam) | ((int)Control.ModifierKeys);
if ((msg.Msg != 0x102) && Enum.IsDefined(typeof(LimitedShortcut), (LimitedShortcut)num))
{
return false;
}
return base.PreProcessMessage(ref msg);
}
其中 LimitedShortcut
提供了允许的按键集合。
此外,您可能还希望控制 WebBrowser
控件可能发生的事件,以及标准事件未传播到主机的内容。例如,对于 window.close()
方法,拥有一个关闭事件会很有用
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case (int)Msg.WM_PARENTNOTIFY:
if (!DesignMode)
{
if (m.WParam.ToInt32() == (int)Msg.WM_DESTROY)
{
Closing(this, EventArgs.Empty);
}
}
DefWndProc(ref m);
break;
default:
base.WndProc(ref m);
break;
}
}
另一个技巧是如何显示由 WebBrowser
控件中运行的 JavaScript 启动的模态 DialogBox
(例如 FolderBrowsDialog
),并防止出现“long-running script”(长时间运行的脚本)错误。这可以通过提供 InewWindowManager
接口的实现来完成,该接口管理从 WebBrowser
控件启动的弹出窗口。这有点像一种 hack。在接口的实现中,您可以抑制所有弹出窗口,包括 JavaScript 引擎错误消息,如“long-running script”,或者当然,您可以过滤可以出现的弹出窗口类型。
INewWindowManager
接口的互操作
[ComImport(), ComVisible(true),
Guid("D2BC4C84-3F72-4a52-A604-7BCBF3982CBB"),
InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
public interface INewWindowManager
{
[return: MarshalAs(UnmanagedType.I4)]
[PreserveSig]
int EvaluateNewWindow(
[In, MarshalAs(UnmanagedType.LPWStr)] string pszUrl,
[In, MarshalAs(UnmanagedType.LPWStr)] string pszName,
[In, MarshalAs(UnmanagedType.LPWStr)] string pszUrlContext,
[In, MarshalAs(UnmanagedType.LPWStr)] string pszFeatures,
[In, MarshalAs(UnmanagedType.Bool)] bool fReplace,
[In, MarshalAs(UnmanagedType.U4)] uint dwFlags,
[In, MarshalAs(UnmanagedType.U4)] uint dwUserActionTime);
}
INewWindowManager
接口的实现
[ComVisible(true)]
[Guid("901C042C-ECB3-4f0f-9BE7-D096AEFD1BDE")]
public class NewWindowManager : INewWindowManager
{
public int EvaluateNewWindow(string pszUrl,
string pszName,
string pszUrlContext,
string pszFeatures,
bool fReplace,
uint dwFlags,
uint dwUserActionTime)
{
return 0; //0 To suppress all pop-up windows,
//or it is possible to filter pop-ups which can be shown
}
}
为了重写通用窗口管理器,您需要实现自定义的 WebBrowserSite
,并实现 IServiceProvider
接口,以提供一种通用的访问机制来查找由 GUID 标识的服务。现在,在 public
int QueryService(ref Guid guidService, ref Guid riid, out IntPtr ppvObject)
方法的实现中,调用者通过该方法指定服务 ID(SID
,一个 GUID
)、要返回的接口的 IID
(在本例中是 INewWindowManager
接口)。您可以通过调用者接口指针变量(ppvObject
)的地址来提供 INewWindowManager
接口的自定义实现。
继承自 IServiceProvider
的自定义 WebBrowserSite
实现。
protected class WebBrowserSiteExt : WebBrowserSite, IServiceProvider, IDocHostShowUI
{
#region Fields
private Guid _managerId = new Guid("D2BC4C84-3F72-4a52-A604-7BCBF3982CBB");
private readonly NewWindowManager _manager;
#endregion
public WebBrowserSiteExt(WebBrowser host)
: base(host)
{
_manager = new NewWindowManager();
}
#region Implementation of IServiceProvider
public int QueryService(ref Guid guidService, ref Guid riid, out IntPtr ppvObject)
{
if ((guidService == _managerId && riid == _managerId))
{
ppvObject = Marshal.GetComInterfaceForObject(_manager, typeof(INewWindowManager));
if (ppvObject != IntPtr.Zero)
{
return 1;
}
}
ppvObject = IntPtr.Zero;
return -1;
}
#endregion
...
}
此外,有时继承您的 WebBrowserSite
实现并继承自 IDocHostShowUI
会很有用,以便提供自己的显示消息框和帮助的机制。
#region IDocHostShowUI Members
public int ShowMessage(IntPtr hwnd, string lpstrText,
string lpstrCaption, uint dwType, string lpstrHelpFile, uint dwHelpContext, ref int lpResult)
{
lpResult = 0;
return 0; //To suppress display native message box.
}
public int ShowHelp(IntPtr hwnd, string pszHelpFile, uint uCommand,
uint dwData, tagPOINT ptMouse, object pDispatchObjectHit)
{
return 0; //To suppress display native message box.
}
#endregion
上述技巧展示了如何自定义标准 WebBrowser
的各种方法。您可能需要更多的自定义,我相信您可以利用这个示例来创建自己的。
JavaScript 可访问的对象模型声明
您需要做的下一件重要事情是外部对象,这些对象可以被脚本代码访问。这可以通过创建自己的对象并通过 WebBrowser
控件的 ObjectForScripting
属性来传播,从而轻松实现。您可以在 ExternalObject
、webConsole
、LocalStorage
和 XMLHttpRequest
中找到这些对象的示例。ExternalObject
封装了所有这些对象并提供对它们的访问。请确保您将传递给 JavaScript 的所有类都具有 Serializable
和 ComVisible(true)
属性。
在这里,我想分享一些可能很有趣的技巧。
第一个是方法的声明,例如 XMLHttpRequest
的 open 方法有可选参数,因此要将其暴露给 JavaScript,应该这样声明:
public void open(string method, string url, [Optional]object async,
[Optional]object username, [Optional]object password)
声明为 [Optional]object
的参数将被封送为包装在 RCW 中的 COM 对象。因此,要获取类型的确切值,我们需要做类似下面的事情:
if (Marshal.IsComObject(prms))
{
IReflect reflect = prms as IReflect;
PropertyInfo[] infos = null;
if (reflect != null)
{
// obtain the ITypeInfo interface from the object
infos = reflect.GetProperties(BindingFlags.GetProperty |
BindingFlags.Instance | BindingFlags.Public);
foreach (PropertyInfo info in infos)
{
return Convert.ToBoolean(info.GetValue(reflect, null));
}
}
}
else if(!(prms is Missing))
{
return Convert.ToBoolean(prms);
}
RCW 每次将 COM 接口指针映射到它时,其引用计数都会增加。因此,要递减引用计数,我们必须调用 ReleaseComObject
来允许系统释放传递的对象。因此,我建议为传递给您方法的每个 COM 对象调用 ReleaseComObject
方法。下面是一个示例。
protected void ReleaseComObject(object obj)
{
if (Marshal.IsComObject(obj))
{
Marshal.ReleaseComObject(obj);
}
}
您可能会问我为什么提供了自己的 XmlHttpRequest
类型实现。这是为了解决原生 XmlHttpRequest
对象的跨域问题。
自定义 XmlHttpRequest
是使用 .NET Framework 的异步模型来发送请求和接受响应的。选择这种方法是因为,在实践中,每次新请求都会创建一个新的 XmlHttpRequest
实例,而且通常是小型且快速执行的请求,因此为了提高性能,最好避免使用线程、传递上下文和在某些情况下使用同步对象。
在 JavaScript 端,需要按如下方式重写标准的 XmlHttpRequest
:
if (window.external && window.external.console)
{
window.console = window.external.console.constructor();
}
if (window.external && window.external.XMLHttpRequest)
{
XMLHttpRequest = function()
{
var xmlRequest = window.external.XMLHttpRequest;
return xmlRequest.constructor().Target;
};
}
我遇到的下一个问题是 jQuery 如何处理响应。我使用 jQuery 从 Web UI 发送请求,当我向本地主机上的服务器发送请求时(jQuery 使用了重写的 XmlHttpRequest
),客户端从未收到事件。问题在于 jQuery 先发送请求,然后订阅 onreadystatechange
事件;如果响应来得比通常快,客户端就永远收不到事件。因此,为了解决这个问题,我需要重写 jQuery 中的 send
方法。我使用的是 jQuery 版本 1.10.2。也许在其他版本中已修复。
像这样
(function ()
{
var transport = function (s)
{
if (!s.crossDomain || jQuery.support.cors)
{
var callback;
return {
send: function (headers, complete)
{
var handle, i, xhr = s.xhr();
if (s.username)
{
xhr.open(s.type, s.url, s.async, s.username, s.password);
}
else
{
xhr.open(s.type, s.url, s.async);
}
if (s.xhrFields)
{
for (i in s.xhrFields)
{
xhr[i] = s.xhrFields[i];
}
}
if (s.mimeType && xhr.overrideMimeType)
{
xhr.overrideMimeType(s.mimeType);
}
if (!s.crossDomain && !headers["X-Requested-With"])
{
headers["X-Requested-With"] = "XMLHttpRequest";
}
try
{
for (i in headers)
{
xhr.setRequestHeader(i, headers[i]);
}
}
catch (err) { }
callback = function (_, isAbort)
{
var status, responseHeaders, statusText, responses;
try
{
if (callback && (isAbort || xhr.readyState === 4))
{
callback = undefined;
if (handle)
{
xhr.onreadystatechange = jQuery.noop;
if (xhrOnUnloadAbort)
{
delete xhrCallbacks[handle];
}
}
if (isAbort)
{
if (xhr.readyState !== 4)
{
xhr.abort();
}
}
else
{
responses = {};
status = xhr.status;
responseHeaders = xhr.getAllResponseHeaders();
if (typeof xhr.responseText === "string")
{
responses.text = xhr.responseText;
}
try
{
statusText = xhr.statusText;
}
catch (e)
{
statusText = "";
}
if (!status && s.isLocal && !s.crossDomain)
{
status = responses.text ? 200 : 404;
}
else if (status === 1223)
{
status = 204;
}
}
}
} catch (firefoxAccessException)
{
if (!isAbort)
{
complete(-1, firefoxAccessException);
}
}
if (responses)
{
complete(status, statusText, responses, responseHeaders);
}
};
xhr.onreadystatechange = callback;
xhr.send((s.hasContent && s.data) || null);
},
abort: function ()
{
if (callback)
{
callback(undefined, true);
}
}
};
}
};
jQuery.ajaxTransport('script', transport);
jQuery.ajaxTransport('text', transport);
})();
下一个技巧是如何将事件传播到 JavaScript。我们可以通过反射发出事件到脚本,JavaScript 中的每个方法都是对象,因此要为 JavaScript 提供回调,我们需要将函数(对象)存储在我们的事件发射器中,并在时机到来时,只需执行以下示例:
System.Type t = callback.GetType();
try
{
List<object> args = new List<object>();
if (state != null)
{
foreach (object arg in state)
{
args.Add(arg);
}
}
t.InvokeMember("", System.Reflection.BindingFlags.InvokeMethod,
null, callback, args.ToArray());
}
catch (Exception e){…}</object></object>
您可以在 ScriptEventDelegate
类中找到源代码,并在外部对象中使用示例。
在这个主题中,我或多或少地涵盖了在创建带有 Web UI 的应用程序时遇到的所有问题。接下来,我想分享我在“浏览器扩展”方面的经验。
浏览器扩展
在这个主题中,我将分享如何以简单的方式构建跨 Web 浏览器扩展的经验。老实说,它不完全是扩展,它只是一个本地http/https 服务器,负责处理在 WebBrowser
中运行的 Web 应用程序发出的请求。但这个解决方案适用于您有一个在浏览器中运行的 Web 应用程序,并且想在浏览器外部执行操作,例如通过用户事件启动桌面应用程序或调用系统方法等。
所以,首先,这是关于如何创建一个带有自签名证书的 https 服务器,以便能够从 http 或 https 运行的 Web 应用程序发出请求。要为 localhost 创建自签名证书,需要 3 个步骤:
- 创建根证书
makecert.exe -r -pe -n "CN=<cert name>" -ss CA -sr LocalMachine -a sha1 -sky signature -cy authority -sv CA.pvk CA.cer
- 使用第一步生成的根证书为 localhost 创建服务器身份验证证书
makecert.exe -pe -n "CN=localhost" -a sha1 -sky exchange -eku 1.3.6.1.5.5.7.3.1 -ic CA.cer -iv CA.pvk -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12 -sv server.pvk server.cer
- 将 pvk 容器导出为 pfx
pvk2pfx.exe -pvk server.pvk -spc server.cer -pfx server.pfx
您可以关注下面的链接获取更详细的信息:
现在您需要将它们安装到系统证书存储中,当然,您希望在应用程序安装程序中执行此操作,并希望该过程用户体验顺畅。为了让它在Internet Explorer、Chrome和Safari上工作,您必须将其安装在:
- 受信任的根证书颁发机构 (CA) 的 X.509 证书存储
- 中间证书颁发机构 (CA) 的 X.509 证书存储
- 个人证书的 X.509 证书存储
在分配给本地计算机的证书存储下。您的安装程序必须具有管理员权限。但之后,该证书将可供本地计算机上的任何帐户使用。
您可以在MSDN上找到如何安装证书以及如何将它们与 localhost 关联到特定端口:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa364503(v=vs.85).aspx
https://msdn.microsoft.com/en-us/library/windows/desktop/aa364649(v=vs.85).aspx.
在我的源代码中,您会找到 win32 方法的包装器和使用它们的示例。
SSLCertInstaller
封装了 win32 方法,用于安装证书并将其绑定到 localhost 的特定端口。
下面是如何使用包装器的示例:
using (SSLCertInstaller installer = new SSLCertInstaller(StoreName.Root, StoreLocation.LocalMachine))
{
installer.InstallCertificate(new X509Certificate2(
Path.Combine((new FileInfo(Assembly.GetExecutingAssembly().Location)).DirectoryName,
_cacert), "",
X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.Exportable));
installer.InstallCertificate(new X509Certificate2
(Path.Combine((new FileInfo(Assembly.GetExecutingAssembly().Location)).DirectoryName,
_percert), "",
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable));
}
using (SSLCertInstaller installer =
new SSLCertInstaller(StoreName.CertificateAuthority, StoreLocation.LocalMachine))
{
installer.InstallCertificate(new X509Certificate2(
Path.Combine((new FileInfo
(Assembly.GetExecutingAssembly().Location)).DirectoryName, _cacert), "",
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable));
}
using (SSLCertInstaller installer =
new SSLCertInstaller(StoreName.My, StoreLocation.LocalMachine))
{
installer.InstallCertificate(new X509Certificate2(
Path.Combine((new FileInfo
(Assembly.GetExecutingAssembly().Location)).DirectoryName, _percert), "",
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable));
installer.GrantAccess(_domainname);
int port = Win32API.GetNextVacantPort
(Utils.Properties.Settings.Default.DEF_HTTPS_PORT, Utils.Properties.Settings.Default.ATTEMPTS_COUNT);
Helpers.WritePortToFile(port);
installer. AssociateCertificate(_domainname, _ipaddress, port, true);
}
一切都按照MSDN工作。我可能想强调一点,有必要更改私钥文件的访问权限,以便本地计算机上的任何授权用户帐户都可以访问它。以下是执行此操作的方法:
public void GrantAccess(string CN)
{
X509Certificate2Collection certificate =
store.Certificates.Find(X509FindType.FindBySubjectName, CN, false);
if (certificate.Count == 0)
{
throw new NotFoundException(string.Format
("Certificate {0} is not found at certificate store.", CN));
}
RSACryptoServiceProvider rsa = certificate[0].PrivateKey as RSACryptoServiceProvider;
if (rsa != null)
{
string keyfilepath =
FindKeyLocation(rsa.CspKeyContainerInfo.UniqueKeyContainerName);
FileInfo file = new FileInfo(keyfilepath + "\\" +
rsa.CspKeyContainerInfo.UniqueKeyContainerName);
FileSecurity fs = file.GetAccessControl();
fs.AddAccessRule(new FileSystemAccessRule
(new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null),
FileSystemRights.FullControl, AccessControlType.Allow));
file.SetAccessControl(fs);
}
_isAccessed = true;
}
我们为Internet Explorer、Chrome和Safari安装了证书,所以它们都使用同一个系统证书存储,但遗憾的是FF和Opera使用自己的存储。我找到了将证书安装到FF存储的方法,但我没有找到Opera的方法,如果有人有经验,我将不胜感激。
对于FF,我使用了 NSS Tools certutil 来更新证书存储。
自签名证书已创建并安装到系统中,现在我们可以创建在localhost上运行的http/https 服务器。.NET Framework 拥有丰富的类层次结构来实现这一点。您可以使用任何一个,我选择了像 HttpListener
这样高端的类。使用该类,服务器可以在几行代码内创建。
这是一个例子。
_worker = new Thread(() =>
{
ServiceSettings.InitializeLog(GlobalUtils.Properties.Settings.Default.SERVICE_NAME);
WriteInfoToLog("Initializing...");
_config = new ServiceSettings();
_handlersManager.Load(Path.Combine(_config.HTTPDir, "handlers"));
_rmService = new RemoteServer(_config.HTTPPort, _config.HTTPSPort);
WriteInfoToLog("Starting a work");
_locker.IsLocked = false;
_stopServer.Reset();
using (HttpListener listener = new HttpListener())
{
try
{
WriteInfoToLog("Listening On secure port: " + _rmService.HttpsPort.ToString());
WriteInfoToLog("Listening On port: " + _rmService.HttpPort.ToString());
listener.Prefixes.Add("https://+:" + _rmService.HttpsPort.ToString() + "/");
listener.Prefixes.Add("http://+:" + _rmService.HttpPort.ToString() + "/");
listener.Start();
}
catch (Exception e)
{
WriteErrorToLog("Exception at HTTPServer.Listen:
" + e.Message + "\r\n" + e.StackTrace);
}
WriteInfoToLog("Waiting for connection...");
List<waithandle> handles = new List<waithandle>();
handles.Add(_stopServer);
try
{
_rmService.Start();
}
catch (Exception ex)
{
WriteErrorToLog("Exception at HTTPServer.Listen
when Remote Http Server starting: " + ex.Message + "\r\n" + ex.StackTrace);
}
WriteInfoToLog("Started a work");
while (listener.IsListening)
{
try
{
// Create a new user connection using TcpClient returned by
IAsyncResult result = listener.BeginGetContext(DoAcceptTcpClientCallback, listener);
handles.Add(result.AsyncWaitHandle);
WaitHandle.WaitAny(handles.ToArray());
handles.Remove(result.AsyncWaitHandle);
result.AsyncWaitHandle.Close();
if (_stopServer.WaitOne(0, true))
{
listener.Stop();
return;
}
_locker.Check();
}
catch (Exception ex)
{
if (listener != null)
{
listener.Stop();
}
WriteErrorToLog(ex.StackTrace);
}
}
WriteInfoToLog("Stopped a work");
}
});
_worker.Start();
其中同步对象 ManualResetEvent _stopServer
用于在服务器停止时中止监听。
服务器使用 .NET Framework 的异步模型,就像上面描述的 XmlHttpRequest
一样。所有写入文件(在文件上传时)和从文件读取的操作都是异步的。我认为对于短操作避免使用线程可以使服务器更节省内存。我认为如果将服务器用作Windows 服务,这一点至关重要。
此外,如果您将服务器用作 Windows 服务,我建议在一个工作线程中启动实际工作,以便尽可能快速地初始化和启动服务。另一个技巧是,也许将服务设置为依赖于例如“RpcSs
”服务是一个好主意。不确定这是否是正确的选择,但我试图通过这种方法解决的问题是延迟服务启动,直到其他系统服务加载完毕,而我的服务可以依赖于它们。
服务器非常简单,它只处理静态内容,但我添加了扩展它的可能性,通过实现类似 fast CGI 的东西。只需要做的是在您的库中实现 IHandler
接口,并将其放在特定位置。系统将在服务器初始化时自动加载它。
public interface IHandler : IDisposable
{
string Name { get; }
void SetEnvironment(IHandlerEnvironment env);
bool IsSupported(IHttpContext context);
void Process(IHttpContext context);
}
您可以在 HandlerExample1
项目中找到处理程序的示例。
我忘记说的一件事是服务器的端口。如上所述,您需要将证书分配给 localhost 的特定端口,您将用于监听。如何共享端口?在我的示例中,我有一个简单的文件,但我认为更有效的方法是注册表。现在该端口应由客户端,桌面客户端和 Web 应用程序使用。对于桌面应用程序,这非常直接,共享端口我们可以使用例如Remoting(您可以在 http 服务器和客户端的代码中找到此示例),但对于在浏览器中运行的 Web 应用程序,这不起作用。因此,对于这类应用程序,我们可以选择一个可用的端口范围(选择范围是因为有些端口可能不可用),并在 Web 应用程序中找到本地服务器,只需简单地迭代它们。
加快请求处理速度
我已添加更改以改进请求处理的性能。
以前,在处理每个请求(例如获取静态资源(如图像))时,读操作和写到输出流的操作没有并行执行。在网络带宽较小的情况下,这一点可能会很明显。
这些更改允许并行执行源读取和输出流写入,该解决方案通过输入和输出数据队列得到解决。读取时会将新的数据缓冲区添加到队列中,当输出流准备好写入新的数据块时,缓冲区将从队列中移除。这些更改允许并行启动几个读取操作。
如何使用
在附带的源代码中,您会找到三个解决方案;.\_build\ AppJSRunnerAndServer.sln, .\_build\ AppJSRunnerAndService.sln 用于 Visual Studio 2008,.\_build\WinStoreApp.sln 用于 Visual Studio 2012。
前两个解决方案封装了项目,用于创建客户端和http/https **服务**。* AppJSRunnerAndServer.sln* 解决方案仅创建客户端的桌面应用程序和http/https **服务器**的桌面应用程序。
AppJSRunnerAndService.sln 解决方案执行类似的工作,只是它创建http/https **服务**而不是桌面http/https **服务器**应用程序。
它们都使用相同的源代码。当解决方案成功构建后,您会在文件夹.\ _debug* 或.\_release* 中找到,具体取决于您选择的配置。在该文件夹中,只需运行其中一个文件start_http_server.bat*(如果您构建了 *AppJSRunnerAndServer.sln* 解决方案)或register_http_service.bat*(如果您构建了 *AppJSRunnerAndService.sln* 解决方案),然后运行 *webclient.exe*。如果一切都已正确构建,您将看到带有 Web UI 的应用程序。该应用程序会在系统托盘中创建一个图标。它只是一个示例,几乎什么都不做。您可以拖动窗口的标题栏,将其固定在桌面上,导航通过选项卡并启动 FolderBrowserDialog
。但您可以替换 UI 和逻辑。*只是可能看看拖动是如何处理的,特别是用什么来确定什么是标题*。
Web 项目本身可以在目录.\sources\JsSource\* 中找到。只需查看它如何在 Web 浏览器(例如 Internet Explorer、Chrome 或 FF)中运行,只需启动 http/https 服务,然后在浏览器中导航到“https://:8081/index.html”或“https://:8190/index.html”。请注意,要通过 https 运行,您需要将应用程序构建为服务,然后运行register_http_service.bat*。对于本地服务器,证书必须为本地用户安装,因此要使 https 服务器正常工作,您需要对证书安装程序进行一些更改。
最后,我将非常感谢任何评论和改进。