CASSINI源代码分析
CASSINI 源代码分析
2004-11-10 http://blog.csdn.net/shanhe/
为什么要分析 CASSINI?
Cassini (卡西尼)是 asp.net 上的一个开源项目。主要给出一个脱离 IIS 实现 asp.net 执行环境。项目演示了如何自己创建一个 web server ,并且运行一个 asp.net 应用程序。
研究 Cassini 可以了解:
1 、 .net 环境下的 web server 如何实现,注意那些问题
2 、 asp.net 的执行本质
3 、了解 appDomain 的执行概念
安装
没有什么问题,默认即可
运行
出现问题,主要是我的 .net framework 是 1.0.3705 而 系统下载的经过编译的 cassini.dll 是在 v1.1.4322 下编译,更麻烦的是经过签名的。所以启动时候回提示找不到 v1.1.4322 的 framework
而我的目标是想分析这个程序,该如何进行?
既然获得了整个源代码,我就应该从理论上熟悉并且可以掌握整个系统,我建立一个工程,将全部 .cs 文件和资源文件加入到一个项目,同时删除原有项目默认的 form1 ,因为 CassiniWebServer.cs 已经有一个 main 入口。
调试,解决问题
1、 编译 OK ,但是,无法启动,原来缺少 .ico 文件。不是已经加入到工程了么?原来,我们的编译程序在 bin\debug 目录下,所以我将 CassiniWebServer..ico 文件拷贝到 bin\debug\ 下即可启动
2、 输入 asp.net 应用程序根目录、端口、执行点, OK ,点击 Start ,出现错误,好像告知我端口被占用。不可能!我的 80 端口 8080 端口都没有被占用,难道是有木马?我用 fport.exe 查询端口,发现根本没有程序占用。那究竟是什么原因?好在可以启动进行调试,我跟踪到 CassiniWebServer.cs 的 CassiniForm 类的 Start() 函数,发现在以下代码处发生错误
try {
_server = new Cassini.Server(portNumber, _virtRoot, _appPath);
_server.Start();
}
catch {
ShowError(
"Cassini Managed Web Server failed to start listening on port " + portNumber + ".\r\n" +
"Possible conflict with another Web Server on the same port.");
portTextBox.SelectAll();
portTextBox.Focus();
return;
} 也就是在创建 server 的时候出错。
但是不利的是,源代码并未交待错误原因,而是武断的告诉我“ Cassini Managed Web Server failed to start listening on port XXX ”,显然,是不慎重的(估计作者对 Exception 不感冒)。
为了深入了解原因,我加入了
catch (Exception e){
MessageBox.Show(e.ToString());
……
这样,就显示了一个错误提示。原来是,在 asp.net 的创建应用域需要用到的程序集在私有目录或者 GAC 中。之前安装后,看到 .snk 证书文件就是为了签名加密并拷贝到全局 dll 中的。但是我没有这样做,所以 CLR 会在启动进程的当前目录的子目录 bin\ 下寻找需要的程序集。而我将原本是两个程序集的源码集合到一个程序集,导致执行搜索程序集失败。直到原因后,建立 bin 目录,并拷贝编译后的 exe 文件到 asp.net 的执行起点的 bin 目录后,系统启动 OK.
3、 测试,通过 ie 测试访问,发现一切正常,就好像在 IIS 下运行一样。
--------------------
我们从启动程序的部分开始分析吧。
启动的入口是 Main 函数,这个函数仅仅存在于 CassiniWebServer ,而 CassiniWebServer 继承自 Form 类,但是我们看到,该类并没有实现代码(仅仅是提供一个入口)。在 Main 函数中,仅仅有两行代码:
[STAThread]
public static int Main(String[] args) {
Application.Run(new CassiniForm(args));
return 0;
}
静态方法 Application.Run 在当前线程环境( STAThread )下启动一个 CassiniForm 的实例,启动消息循环,并且使窗体可见。
从 MSDN 上我们了解到:
Application 类具有用于启动和停止应用程序和线程以及处理 Windows 消息的方法。调用 Run 以启动当前线程上的应用程序消息循环,并可以选择使某窗体可见。调用 Exit 或 ExitThread 来停止消息循环。当您的程序在某个循环中时,调用 DoEvents 来处理消息。调用 AddMessageFilter 以向应用程序消息泵添加消息筛选器来监视 Windows 消息。 IMessageFilter 使您可以阻止引发某事件或在调用某事件处理程序前执行特殊操作。
既然分析到了 CassiniForm ,我们来看看这个继承自 Form 的类。
阅读代码我们看到:
除了一些 GUI 元素支持成员变量外,还有几个成员私有变量:
private static String _appPath; // 用于存储 asp.net 启动的应用程序物理路径
private static String _portString; //web server 监听端口号
private static String _virtRoot; // 应用程序的虚拟目录
private Cassini.Server _server; // the web server 源代码中的注释
CassiniForm 的构造函数也没有什么特别,我们看到有一个关键的 Start() ,从名字我们大致可以猜到是 web server 启动过程。仔细来看看:
private void Start()
{
_appPath = appDirTextBox.Text;
if (_appPath.Length == 0 || !Directory.Exists(_appPath)) {
ShowError("Invalid Application Directory");
appDirTextBox.SelectAll();
appDirTextBox.Focus();
return;
}
_portString = portTextBox.Text;
int portNumber = -1;
try {
portNumber = Int32.Parse(_portString);
}
catch {
}
if (portNumber <= 0) {
ShowError("Invalid Port");
portTextBox.SelectAll();
portTextBox.Focus();
return;
}
_virtRoot = vrootTextBox.Text;
if (_virtRoot.Length == 0 || !_virtRoot.StartsWith("/")) {
ShowError("Invalid Virtual Root");
vrootTextBox.SelectAll();
vrootTextBox.Focus();
return;
}
// 以上一大段都是检验参数,看看用户输入的参数是否符合要求。从这里我们也已看看软件的容错性多么重要,国外的程序员大都很重视,值得我借鉴。关键的实现在于下面的两行代码
try {
_server = new Cassini.Server(portNumber, _virtRoot, _appPath);
_server.Start();
}
// 我们可以知道在此,我们先生 new 一个 Cassini.Server 然后调用 Start 过程。出错了就报告错误。源代码中并没有 catch(Exception e) 的语句,是我为了察看错误提示信息加上去的。通过这样做,我知道编译后的程序如何执行。
catch (Exception e){
MessageBox.Show(e.ToString());
ShowError(
"Cassini Managed Web Server failed to start listening on port " + portNumber + ".\r\n" +
"Possible conflict with another Web Server on the same port.");
portTextBox.SelectAll();
portTextBox.Focus();
return;
}
startButton.Enabled = false;
appDirTextBox.Enabled = false;
portTextBox.Enabled = false;
vrootTextBox.Enabled = false;
browseLabel.Visible = true;
browseLink.Text = GetLinkText();
browseLink.Visible = true;
browseLink.Focus();
}
CassiniForm 精华差不多了。顺藤摸瓜,我们来看看 Cassini.Server 。这个类实际上就是我们整个 Cassini 的主要类。
Cassini.Server 的主要数据成员有:
private int _port; // 端口,大概是 web server 的端口
private String _virtualPath; // 虚拟目录,也就是应用程序执行的虚拟路径
private String _physicalPath; // 物理路径
private String _installPath; //
private WaitCallback _restartCallback; // 用于重新启动 host 的回调函数
private Host _host; //asp.net 应用程序的真正宿主,从名字看 J
构造函数最后调用中有:
_restartCallback = new WaitCallback(RestartCallback); // 指定一个回调函数供 Restart 时候使用。 RestartCallback 主要动作就是 CreateHost() 和 Start() ,但是需要注意是采用线程调度方式
_installPath = GetInstallPathAndConfigureAspNetIfNeeded();// 这个函数搜索注册表获取 aspnet_isapi.dll 的路径。该 dll 执行 asp.net 的基本框架功能(当然是在建立 asp.net 应用程序之后)。
CreateHost();
Start() 很简单,就是判断 hots 是否存在,并且启动 host ,看来关于 cassini.server 类重点是看看 CreateHost 是怎么回事。
private void CreateHost() {
_host = (Host)ApplicationHost.CreateApplicationHost(typeof(Host), _virtualPath, _physicalPath);
_host.Configure(this, _port, _virtualPath, _physicalPath, _installPath);
}
原来是调用 ApplicationHost 的唯一方法 CreateApplicationHost 建立一个执行 asp.net 的 appDomain 环境。
其中的 Host 是一个要在新应用程序域中创建的由用户提供的类的名称。此处是一个自定义的 cassini.Host ,是用来承载 asp.net 应用程序的。
--------------------
internal class Host : MarshalByRefObject {……}
首先,我们看到 Host 仅能够在 cassini 项目中使用,因为是 internal 的类定义。另外,继承自 MarshalByRefObject ,允许在支持远程处理的应用程序中跨应用程序域边界访问对象。我们联想到 asp.net 对于应用程序的执行方式是应用程序域为划分边界的,作为 Host 必须能够支持跨应用程序域,以便多个应用程序在其边界内执行。
先来看看其成员变量:
private bool _started; // 是否已经启动
private bool _stopped; // 是否停止了
private Server _server; // 对外交互父对象
private int _port; // 以下大致应该是一个 web server 应当具有的内部变量
private String _virtualPath;
private String _lowerCasedVirtualPath;
private String _lowerCasedVirtualPathWithTrailingSlash;
private String _physicalPath;
private String _installPath;
private String _physicalClientScriptPath;
private String _lowerCasedClientScriptPathWithTrailingSlashV10;
private String _lowerCasedClientScriptPathWithTrailingSlashV11;
private Socket _socket; // 通信用的套接字
private WaitCallback _onStart; // 回调函数
private WaitCallback _onSocketAccept;
private EventHandler _onAppDomainUnload; // 事件监控,当某个应用程序域将要退出时候发生,由 Host 类处理。
public override Object InitializeLifetimeService() {
return null; // never expire lease
} 是重载 MarshalByRefObject 的成员,正如源代码中的注释说言,通过返回 null 告诉 .net framework 永远不要将此类的实例失效过期。分布式垃圾回收负责控制服务器应用程序的生存期,并负责在它们的生存期到期时删除它们。传统上,分布式垃圾回收使用引用计数和 Ping 进行控制。这在每个对象有少数几个客户端时可以很好地工作,但在每个对象有数千个客户端时效率很低。生存期服务可采用传统分布式垃圾回收器的功能,并在客户端数目增加时能很好地扩展。
Configure 函数很值得分析,因为在 Server.CreateHost 中出现过,看看:
public void Configure(Server server, int port, String virtualPath, String physicalPath, String installPath) {
_server = server;
_port = port;
_virtualPath = virtualPath;
_lowerCasedVirtualPath = CultureInfo.InvariantCulture.TextInfo.ToLower(_virtualPath);
_lowerCasedVirtualPathWithTrailingSlash = virtualPath.EndsWith("/") ? virtualPath : virtualPath + "/";
_lowerCasedVirtualPathWithTrailingSlash = CultureInfo.InvariantCulture.TextInfo.ToLower(_lowerCasedVirtualPathWithTrailingSlash);
_physicalPath = physicalPath;
_installPath = installPath;
// 以上都是赋值和参数检验代码
_physicalClientScriptPath = installPath + "\\asp.netclientfiles\\";
// 以下开始确定 asp.net 提供的客户端脚本路径
String version4 = FileVersionInfo.GetVersionInfo(typeof(HttpRuntime).Module.FullyQualifiedName).FileVersion; // 查找当前 asp.net 运行时模块的版本信息
String version3 = version4.Substring(0, version4.LastIndexOf('.'));
_lowerCasedClientScriptPathWithTrailingSlashV10 = "/aspnet_client/system_web/" + version4.Replace('.', '_') + "/";
_lowerCasedClientScriptPathWithTrailingSlashV11 = "/aspnet_client/system_web/" + version3.Replace('.', '_') + "/";
// 下面开始为套接字设置准备回调函数
_onSocketAccept = new WaitCallback(OnSocketAccept);
_onStart = new WaitCallback(OnStart);
// start watching for app domain unloading
_onAppDomainUnload = new EventHandler(OnAppDomainUnload);
Thread.GetDomain().DomainUnload += _onAppDomainUnload;
}
显然, config 函数做了以下事情:
1、 将一些配置参数传入到 host 类的内部变量
2、 确定 asp.net 提供的一些环境,譬如脚本环境、脚本存在的路径,以便 asp.net 的一些组件可以顺利执行(就象在 IIS 中一样)
3、 准备一些回调函数、事件监听函数处理发生的事件
外部使用 host 还通过 Start 过程,我们看看:
public void Start() {
if (_started) // 已经启动了,不可启动两个实例
throw new InvalidOperationException();
_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_socket.Bind(new IPEndPoint(IPAddress.Any, _port));
_socket.Listen((int)SocketOptionName.MaxConnections);
_started = true;
ThreadPool.QueueUserWorkItem(_onStart);
}
该函数完成:
1、 排除启动两个实例
2、 在给定的端口上启动监听
3、 启动 _onStart 回调函数线程,运行监听套接字,但是是异步调用, start 函数完成后马上返回。
那 _onStart 函数是怎么处理的呢?
private void OnStart(Object unused) {
while (_started) {
try {
Socket socket = _socket.Accept();
ThreadPool.QueueUserWorkItem(_onSocketAccept, socket);
}
catch {
Thread.Sleep(100);
}
}
_stopped = true;
}
此线程一直进行循环,调用套接字的 Accept 函数,当一个连接接入到,马上调用一个新的线程( _onSocketAccept )和一个新的 socket ,然后产生一个新的线程来处理客户请求。遇到例外就暂停后继续 Accept 引入的套接字。一旦 _started 标志被设置 false ,则停止监听退出。
private void OnSocketAccept(Object acceptedSocket) {
Connection conn = new Connection(this, (Socket)acceptedSocket);
conn.ProcessOneRequest();
}
此处线程马上产生一个 Connection 对象,并调用 Connection 的 ProcessOneRequest 来处理。看来真正处理一个 web request 的过程在 Connection 对象内。
-------------------------
因为 connection 对象仅仅跟 host 对象相关,且处理一个套接字,所以其数据成员仅有:
private Host _host; // 指向宿主对象
private Socket _socket; // 当前套接字
我们知道 host 调用且仅了 conn.ProcessOneRequest(); 方法,所以我们首先要找到此方法( coneetion 很多方法,我们先看看主要的):
public void ProcessOneRequest() { // wait for at least some input
if (WaitForRequestBytes() == 0) {
WriteErrorAndClose(400);
return;
}
Request request = new Request(_host, this);
request.Process();
}
从代码来看,此过程首先要确保 socket 有数据读入,如果发生无法读取,就向 socket 写入一个 400 错误;如果有数据读入,那么构造一个 requese 对象,调用 request 的 Process 方法(呵呵,麻烦事大家总是一层层甩给别人 J )。
还是继续分析 Connection 对象, WaitForRequestBytes() 实际上是单独线程来读取 socket ,如果 socket 连接但是没有数据流,就至多等待 10 秒。
WriteErrorAndClose(int); 函数调用 WriteErrorAndClose(int , string)
{
String body = Messages.FormatErrorMessageBody(statusCode, _host.VirtualPath);
if (message != null && message.Length > 0)
body += "\r\n<!--\r\n" + message + "\r\n-->";
WriteEntireResponseFromString(statusCode, null, body, false);
}
WriteEntireResponseFromString() 函数根据状态码构造返回的 http 数据流,并写回到客户端。实际上体现了对 http 协议的具体实现。我们暂时放下,追踪看看 Request 对象。
internal class Request : SimpleWorkerRequest { 。。。。。。 } 继承自 .net ,根据 MSDN 介绍: HttpWorkerRequest 的这种简单实现提供请求 URL 和查询字符串,并将输出的正文捕获到 TextWriter 中。若要实现更为丰富的功能(如提供已发送的内容和标头以及捕获以二进制数据表示的响应标头或响应正文),则应扩展 SimpleWorkerRequest 并重写适当的 HttpWorkerRequest 方法。
还是从 Process 函数入手看看 ( 代码较长 ) :
{
ReadAllHeaders(); // 阅读处所有的 http 请求头
if (_headerBytes == null || _endHeadersOffset < 0 ||
_headerByteStrings == null || _headerByteStrings.Count == 0) {
_conn.WriteErrorAndClose(400);
return; // 如果读取 http 头错误就返回
}
ParseRequestLine(); // 处理 request 行输入
// Check for bad path
if (IsBadPath()) { // 防止用户请求 bad 路径
_conn.WriteErrorAndClose(400);
return;
}
// Limit to local requests only
if (!_conn.IsLocal) {
_conn.WriteErrorAndClose(403);
return;
}
// Check if the path is not well formed or is not for the current app
bool isClientScriptPath = false;
String clientScript = null;
if (!_host.IsVirtualPathInApp(_path, out isClientScriptPath, out clientScript)) {
_conn.WriteErrorAndClose(404); // 检验 url 请求是否属于应用程序路径范围内,如果不是,报 404 错误。
return;
}
ParseHeaders(); // 解析 http 请求头
ParsePostedContent(); // 解析 post 方法的内容
if (_verb == "POST" && _contentLength > 0 && _preloadedContentLength < _contentLength) { // 如果是 post 方法,需要等待 post 数据完成才继续那么调用 conn 的等待方法 Write100Continue 直到 post 完成
_conn.Write100Continue();
}
// special case for client script
if (isClientScriptPath) { // 如果请求的是脚本路径,那么直接读取文件(也就是 .js 文件,按照文本文件来看待)
_conn.WriteEntireResponseFromFile(_host.PhysicalClientScriptPath + clientScript, false);
return;
}
// special case for directory listing
if (ProcessDirectoryListingRequest()) { // 如果是请求目录 list ,则处理后返回
return;
}
PrepareResponse(); // 准备响应内容
// Hand the processing over to HttpRuntime
HttpRuntime.ProcessRequest(this); // 通过 HttpRuntime 的方法执行 asp.net 的内容,驱动所有 ASP.NET Web 处理执行。
}
针对该函数细节,逐个分析以下函数:
ReadAllHeaders
ParseRequestLine
ParsePostedContent
ProcessDirectoryListingRequest
PrepareResponse
因为他们处理一次 http request 。
private void ReadAllHeaders() {
_headerBytes = null;
do {
if (!TryReadAllHeaders())
break; // something bad happened
}
while (_endHeadersOffset < 0); // found \r\n\r\n
} 该函数不断调用 TryReadAllHeaders ,仔细看看这个 TryReadAllHeaders:
private bool TryReadAllHeaders() {
// read the first packet (up to 32K)
byte[] headerBytes = _conn.ReadRequestBytes(maxHeaderBytes); // 从 connection 读取最大数据 32*1024 字节。
if (headerBytes == null || headerBytes.Length == 0)
return false; // 如果读取数据失败,返回错误,调用此函数者应当检查数据读取是否完整
if (_headerBytes != null) { // previous partial read 以下将当前读取的数据累加
int len = headerBytes.Length + _headerBytes.Length;
if (len > maxHeaderBytes)
return false;
byte[] bytes = new byte[len];
// 注意调用了快速的 Buffer.BlockCopy 方法,不过我认为可以更好的处理读取数据问题,因为一再的产生 byte 数组显然不是很好的方法。
Buffer.BlockCopy(_headerBytes, 0, bytes, 0, _headerBytes.Length);
Buffer.BlockCopy(headerBytes, 0, bytes, _headerBytes.Length, headerBytes.Length);
_headerBytes = bytes;
}
else {
_headerBytes = headerBytes;
}
// start parsing 下面准备解析请求行
_startHeadersOffset = -1;
_endHeadersOffset = -1;
_headerByteStrings = new ArrayList();
// find the end of headers ByteParser 是自定义的工具类,此时我们只要知道是帮助字节数组转换方便得到行, ByteString 也是一个工具类,帮助将字节数组转换为字符串
ByteParser parser = new ByteParser(_headerBytes);
for (;;) {
ByteString line = parser.ReadLine();
if (line == null)
break;
if (_startHeadersOffset < 0) {
_startHeadersOffset = parser.CurrentOffset;
}
if (line.IsEmpty) {
_endHeadersOffset = parser.CurrentOffset;
break;
}
_headerByteStrings.Add(line);
}
return true;
}
如何处理分解行呢?
private void ParseRequestLine() {
ByteString requestLine = (ByteString)_headerByteStrings[0];
ByteString[] elems = requestLine.Split(' '); // 我们知道每一个 header 同 header 值之间都有一个必然的空格,例如 cassini 返回的 http 响应的头:
HTTP/1.1 404 Not Found
// 判断 header 是否读取正确,一般请求头第一行应该是例如: GET /pub/WWW/ HTTP/1.1
if (elems == null || elems.Length < 2 || elems.Length > 3) {
return;
}
_verb = elems[0].GetString(); // 读取 http 请求方法
ByteString urlBytes = elems[1];
_url = urlBytes.GetString(); // 获取请求的 url
if (elems.Length == 3) // 确定 http 请求的协议版本
_prot = elems[2].GetString(); // 目前仅有 HTTP/1.1 或者 HTTP/1.0
else
_prot = "HTTP/1.0";
// query string
int iqs = urlBytes.IndexOf('?'); // 请求的参数获取 字节数组表示
if (iqs > 0)
_queryStringBytes = urlBytes.Substring(iqs+1).GetBytes();
else
_queryStringBytes = new byte[0];
iqs = _url.IndexOf('?'); // 取得 path 和 参数的字符串表示
if (iqs > 0) {
_path = _url.Substring(0, iqs);
_queryString = _url.Substring(iqs+1);
}
else {
_path = _url;
_queryStringBytes = new byte[0];
}
// url-decode path 开始 url 解码,这个 MS 之前犯的著名 URL 解码错误就在此处了
if (_path.IndexOf('%') >= 0) {
_path = HttpUtility.UrlDecode(_path); // 调用 .net 的工具方法
}
// path info 以下获取 path
int lastDot = _path.LastIndexOf('.');
int lastSlh = _path.LastIndexOf('/');
if (lastDot >= 0 && lastSlh >= 0 && lastDot < lastSlh) {
int ipi = _path.IndexOf('/', lastDot);
_filePath = _path.Substring(0, ipi);
_pathInfo = _path.Substring(ipi);
}
else {
_filePath = _path;
_pathInfo = String.Empty;
}
_pathTranslated = MapPath(_filePath); // 映射路径,将文件映射到具体的磁盘路径
}
处理完 http header 后,开始处理 http 的请求正文,看看 ParsePostedContent
private void ParsePostedContent() {
_contentLength = 0;
_preloadedContentLength = 0;
String contentLengthValue = _knownRequestHeaders[HttpWorkerRequest.HeaderContentLength]; // 察看头部中的定义的长度
if (contentLengthValue != null) {
try {
_contentLength = Int32.Parse(contentLengthValue);
}
catch {
}
}
// 以下检查各个长度数据是否异常
if (_headerBytes.Length > _endHeadersOffset) {
_preloadedContentLength = _headerBytes.Length - _endHeadersOffset;
if (_preloadedContentLength > _contentLength && _contentLength > 0)
_preloadedContentLength = _contentLength; // don't read more than the content-length 注意不要读取过多的数据
_preloadedContent = new byte[_preloadedContentLength];
Buffer.BlockCopy(_headerBytes, _endHeadersOffset, _preloadedContent, 0, _preloadedContentLength); // 拷贝数据
}
}
以上将 http 请求的 content 数据字节拷贝到 _preloadedContent 成员变量中去。
接下来的 ProcessDirectoryListingRequest 处理不带文件名的请求,也就是直接请求浏览某个目录。略。
处理完毕后,准备构造响应数据,看看 PrepareResponse
_headersSent = false; // 准备回写到 connection 中去
_responseStatus = 200;
_responseHeadersBuilder = new StringBuilder();
_responseBodyBytes = new ArrayList();
其他的,我们应当注意
HttpRuntime.ProcessRequest(this); 隐含的调用关系。 HttpRuntime.ProcessRequest 会在需要回写数据的时候调用相关的函数,这些函数被 cassini 注释。
我们要明白这些重载的函数会在 asp.net 处理过程中被调用。
///////////////////////////////////////////////////////////////////////////////////////////////
//
// Implementation of HttpWorkerRequest
//
///////////////////////////////////////////////////////////////////////////////////////////////
public override String GetUriPath() { // 返回请求的 URI 的虚拟路径。
return _path;
}
public override String GetQueryString() { // 返回请求 URL 中指定的查询字符串。
return _queryString;
}
public override byte[] GetQueryStringRawBytes() { // 在派生类中被重写时,以字节数组的形式返回响应查询字符串。
return _queryStringBytes;
}
public override String GetRawUrl() { // 返回附加了查询字符串的请求标头中包含的 URL 路径。
return _url;
}
public override String GetHttpVerbName() { // 返回请求标头的指定成员。
return _verb;
}
public override String GetHttpVersion() { // 提供对请求的 HTTP 版本(如 “HTTP/1.1” )的访问。
return _prot;
}
public override String GetRemoteAddress() {// 提供对请求标头的指定成员的访问。
return _conn.RemoteIP;
}
public override int GetRemotePort() {// 提供对请求标头的指定成员的访问。
return 0;
}
public override String GetLocalAddress() {//
return _conn.LocalIP;
}
public override int GetLocalPort() {
return _host.Port;
}
public override String GetFilePath() {// 在派生类中被重写时,返回所请求的 URI 的物理路径。
return _filePath;
}
public override String GetFilePathTranslated() {// 返回请求的 URI 的物理文件路径(并将其从虚拟路径翻译成物理路径:例如,从 “/proj1/page.aspx” 翻译成 “c:\dir\page.aspx” )
return _pathTranslated;
}
public override String GetPathInfo() {// 返回具有 URL 扩展的资源的其他路径信息。即对于路径 /virdir/page.html/tail , GetPathInfo 值为 /tail 。
return _pathInfo;
}
public override String GetAppPath() {// 返回当前正在执行的服务器应用程序的虚拟路径。
return _host.VirtualPath;
}
public override String GetAppPathTranslated() {// 返回当前正在执行的服务器应用程序的 UNC 翻译路径。
return _host.PhysicalPath;
}
public override byte[] GetPreloadedEntityBody() {// 返回 HTTP 请求正文已被读取的部分。
return _preloadedContent;
}
public override bool IsEntireEntityBodyIsPreloaded() {// 返回一个值,该值指示是否所有请求数据都可用,以及是否不需要对客户端进行进一步读取。
return (_contentLength == _preloadedContentLength);
}
public override int ReadEntityBody(byte[] buffer, int size) {// 读取客户端的请求数据(在尚未预加载时)。
int bytesRead = 0;
byte[] bytes = _conn.ReadRequestBytes(size);
if (bytes != null && bytes.Length > 0) {
bytesRead = bytes.Length;
Buffer.BlockCopy(bytes, 0, buffer, 0, bytesRead);
}
return bytesRead;
}
public override String GetKnownRequestHeader(int index) {// 返回与指定的索引相对应的标准 HTTP 请求标头。
return _knownRequestHeaders[index];
}
public override String GetUnknownRequestHeader(String name) {// 返回非标准的 HTTP 请求标头值。指定了名称
int n = _unknownRequestHeaders.Length;
for (int i = 0; i < n; i++) {
if (String.Compare(name, _unknownRequestHeaders[i][0], true, CultureInfo.InvariantCulture) == 0)
return _unknownRequestHeaders[i][1];
}
return null;
}
public override String[][] GetUnknownRequestHeaders() {// 获取所有非标准的 HTTP 标头的名称 - 值对。
return _unknownRequestHeaders;
}
public override String GetServerVariable(String name) {// 从与请求关联的服务器变量词典返回单个服务器变量。
String s = String.Empty;
switch (name) {
case "ALL_RAW":
s = _allRawHeaders;
break;
case "SERVER_PROTOCOL":
s = _prot;
break;
// more needed?
}
return s;
}
public override String MapPath(String path) {// 返回与指定虚拟路径相对应的物理路径。
String mappedPath = String.Empty;
if (path == null || path.Length == 0 || path.Equals("/")) {
// asking for the site root
if (_host.VirtualPath == "/") {
// app at the site root
mappedPath = _host.PhysicalPath;
}
else {
// unknown site root - don't point to app root to avoid double config inclusion
mappedPath = Environment.SystemDirectory;
}
}
else if (_host.IsVirtualPathAppPath(path)) {
// application path
mappedPath = _host.PhysicalPath;
}
else if (_host.IsVirtualPathInApp(path)) {
// inside app but not the app path itself
mappedPath = _host.PhysicalPath + path.Substring(_host.NormalizedVirtualPath.Length);
}
else {
// outside of app -- make relative to app path
if (path.StartsWith("/"))
mappedPath = _host.PhysicalPath + path.Substring(1);
else
mappedPath = _host.PhysicalPath + path;
}
mappedPath = mappedPath.Replace('/', '\\');
if (mappedPath.EndsWith("\\") && !mappedPath.EndsWith(":\\"))
mappedPath = mappedPath.Substring(0, mappedPath.Length-1);
return mappedPath;
}
public override void SendStatus(int statusCode, String statusDescription) {// 指定响应的 HTTP 状态代码和状态说明;例如 SendStatus(200, "Ok") 。
_responseStatus = statusCode;
}
public override void SendKnownResponseHeader(int index, String value) {// 将标准 HTTP 标头添加到响应。
if (_headersSent)
return;
switch (index) {
case HttpWorkerRequest.HeaderServer:
case HttpWorkerRequest.HeaderDate:
case HttpWorkerRequest.HeaderConnection:
// ignore these
return;
// special case headers for static file responses
case HttpWorkerRequest.HeaderAcceptRanges:
if (value == "bytes") {
_specialCaseStaticFileHeaders = true;
return;
}
break;
case HttpWorkerRequest.HeaderExpires:
case HttpWorkerRequest.HeaderLastModified:
if (_specialCaseStaticFileHeaders)
return;
break;
}
_responseHeadersBuilder.Append(GetKnownResponseHeaderName(index));
_responseHeadersBuilder.Append(": ");
_responseHeadersBuilder.Append(value);
_responseHeadersBuilder.Append("\r\n");
}
public override void SendUnknownResponseHeader(String name, String value) {// 将非标准 HTTP 标头添加到响应。
if (_headersSent)
return;
_responseHeadersBuilder.Append(name);
_responseHeadersBuilder.Append(": ");
_responseHeadersBuilder.Append(value);
_responseHeadersBuilder.Append("\r\n");
}
public override void SendCalculatedContentLength(int contentLength) {// 将 Content-Length HTTP 标头添加到响应。
if (!_headersSent) {
_responseHeadersBuilder.Append("Content-Length: ");
_responseHeadersBuilder.Append(contentLength.ToString());
_responseHeadersBuilder.Append("\r\n");
}
}
public override bool HeadersSent() {// 返回一个值,该值指示是否已为当前的请求将 HTTP 响应标头发送到客户端。
return _headersSent;
}
public override bool IsClientConnected() {// 返回一个值,该值指示客户端连接是否仍处于活动状态。
return _conn.Connected;
}
public override void CloseConnection() {// 终止与客户端的连接。
_conn.Close();
}
public override void SendResponseFromMemory(byte[] data, int length) {// 将内存块的内容添加到响应。
if (length > 0) {
byte[] bytes = new byte[length];
Buffer.BlockCopy(data, 0, bytes, 0, length);
_responseBodyBytes.Add(bytes);
}
}
public override void SendResponseFromFile(String filename, long offset, long length) {// 将文件的内容添加到响应。
if (length == 0)
return;
FileStream f = null;
try {
f = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
SendResponseFromFileStream(f, offset, length);
}
finally {
if (f != null)
f.Close();
}
}
public override void SendResponseFromFile(IntPtr handle, long offset, long length) {// 将文件的内容添加到响应。注意这个方法是多态的
if (length == 0)
return;
FileStream f = null;
try {
f = new FileStream(handle, FileAccess.Read, false);
SendResponseFromFileStream(f, offset, length);
}
finally {
if (f != null)
f.Close();
}
}
private void SendResponseFromFileStream(FileStream f, long offset, long length) {// 这个可不是重载的,看清楚了 J
const int maxChunkLength = 64*1024; //64K 作为传输块
long fileSize = f.Length;
if (length == -1)
length = fileSize - offset;
if (length == 0 || offset < 0 || length > fileSize - offset)
return;
if (offset > 0)
f.Seek(offset, SeekOrigin.Begin);
if (length <= maxChunkLength) {
byte[] fileBytes = new byte[(int)length];
int bytesRead = f.Read(fileBytes, 0, (int)length);
SendResponseFromMemory(fileBytes, bytesRead);
}
else {
byte[] chunk = new byte[maxChunkLength];
int bytesRemaining = (int)length;
while (bytesRemaining > 0) {
int bytesToRead = (bytesRemaining < maxChunkLength) ? bytesRemaining : maxChunkLength;
int bytesRead = f.Read(chunk, 0, bytesToRead);
SendResponseFromMemory(chunk, bytesRead);
bytesRemaining -= bytesRead;
// flush to release keep memory
if (bytesRemaining > 0 && bytesRead > 0)
FlushResponse(false);
}
}
}
public override void FlushResponse(bool finalFlush) {// 将所有挂起的响应数据发送到客户端。
if (!_headersSent) {
_conn.WriteHeaders(_responseStatus, _responseHeadersBuilder.ToString());
_headersSent = true;
}
for (int i = 0; i < _responseBodyBytes.Count; i++) {
byte[] bytes = (byte[])_responseBodyBytes[i];
_conn.WriteBody(bytes, 0, bytes.Length);
}
_responseBodyBytes = new ArrayList();
if (finalFlush) {
_conn.Close();
}
}
public override void EndOfRequest() {// 由运行库使用以通知 HttpWorkerRequest 当前请求的请求处理已完成。
// empty method
}
通过以上重载,我们实现了具体的 http 请求的数据获取、头分析,内容分析(如果是 post 方法)、 querystring 参数分析、处理路径映射、传输 asp.net 执行请求到运行库,同时构造 http 响应头和相应数据。
-----------------------
通过初步浏览全部代码之后,我们大致上明白了:
1 、执行流。 asp.net 程序具体是如何执行的?一个 asp.net 的应用程序的执行首先是需要一个宿主,通过建立宿主后,就建立了执行 asp.net 应用代码的能力。执行一次 asp.net 请求,需要通过 HttpRuntime.ProcessRequest(SimpleWorkerRequest) 来激发执行,而 SimpleWorkerRequest 需要被重载,将一次完整的 http request 实例化,并提供给 asp.net 运行库以便正确处理用户请求上下文关系以及用户请求数据。 SimpleWorkerRequest 被重载的函数在 asp.net 的执行过程中按照 http 协议的规范被依次调用。
2 、 HTTP 协议细节。我们实际上并没有看到完整的 http server 的执行细节,因为具体的 http 请求执行是 asp.net 运行库执行的(通过 HttpRuntime.ProcessRequest ),包括 .htm 之类的文件都是被运行库执行的,只不过运行库发现不需要执行就直接读取文件返回。这个同 IIS 下的 asp.net 执行不一样, IIS 自己会在 asp_WP.exe 之前处理自己注册的扩展名文件请求。这个结果可能一样,但是实质不一样,就是普通文件处理位置不一样。所以,我们可以在 cassini 基础之上自行处理部分属于不需要动态解析的文件,减轻 asp.net 运行库的负担,提高效率。
Cassini 是一个 asp.net 的宿主程序,并非完整的 web server 。
3 、 asp.net 是一个全新的执行环境,并非脚本之前的脚本技术, .net 自身除了基础框架外,还提供了执行 aspx 页面的能力和相应组件。学习 cassini 就是学习如何使用 .net 框架提供的这种能力。就类似你研读 c 代码学习利用 windows api 编程一样。
4 、我们可以选择 IIS 作为宿主,但是 ISAPI 启动 asp.net 守护进程之后通过管道传回响应毕竟是在两个进程间传输数据,况且, IIS 自身也是漏洞一大筐,没有其他需要,我们认为还是自己实现 asp.net 宿主并执行 asp.net 应用程序在效率上要高,况且应用程序域也提供了安全隔离。
5 、学习如何使用多线程来满足多线程企业应用,回调函数的使用也是精巧。
6 、对于自己将来实现独立于 IIS 的 web 应用开发有意义,譬如,可以采用 asp.net 开发方式开发桌面辅助应用。类似于 HTMLView 应用。我看到有人通过自己执行 asp.net 来快速产生界面运行桌面应用(方便将来迅速扩展到多用户)。
当然,我自身也有疑问:
1、 为什么采用 ThreadPool ,会不会导致并发访问阻塞?
2、 可否更多的利用自己创建独立的 asp.net 执行宿主?
3、 如何更通用化产生一个这样的工具,我相信可以更加独立化的。