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、 如何更通用化产生一个这样的工具,我相信可以更加独立化的。