Embedded .NET HTTP Server

本文介绍了一个简单的.NET HTTP服务器,可嵌入任何.NET程序中,支持会话管理、基于主机请求的文件夹切换等功能,适用于状态监测及游戏框架。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

翻译至:http://www.codeproject.com/Articles/25050/Embedded-NET-HTTP-Server


介绍

现在HTTP已经无处不在。如果你想了解某些东西,没准你会通过你的浏览器和HTTP在互联网上寻找答案。如果一个无人值守的服务器应用程序可以监视,管理HTTP,那将成为一个好主意。

.NET框架的System.Web程序集对HTTP的客户端提供了很好的支持,并支持多种从一个地方转移数据到另一个地方(远程对象,web服务)的巧妙方式。其中大部分是通过HTTP在后台进行的。但框架没有提供一个简单易用的HTTP服务器。微软的IIS是为企业提供的,而不是单个应用提供web服务。


为了提供一个易于注册,易于用户管理,并为我线上游戏框架提供一个检测系统,我在这边的简单的HTTP服务器,可以嵌入任何.NET程序,它可以用来查看应用程序的状态或从浏览器内提交材料给他。


该服务器支持会话管理(通过cookie),基于主机请求开关文件夹(运行在多个域的同一个IP),维持连接,并通过它提供的请求处理程序,用磁盘上的伪标签代替动态内容。


使用

要启动HTTP服务器很简单

HttpServer http = new HttpServer(new Server(80));
http.Handlers.Add(new SubstitutingFileReader(this));


但是挑战是你将要执行针对某些URL的动态过程,支持回传,或替换某些动态内容的特定标签元素(例如,一个导航栏,或者显示当前用户私人信息的面板),在这种情况下,你需要继承SubstitutingFileReader类来指定如何更换某些标签:

public class MyHandler : SubstitutingFileReader {
 public override string GetValue(HttpRequest req, string tag){
  if(tag == "navbar") return "<!-- Navbar Begin -->" +
                "<div class=navbar>" +
                "<div class=logopanel>Test Web Application</div>" +
                "<div class=navlinks>" +
                "<a href=index.html>Home</a>" +
                "<a href=register.html>Register</a>" +
                "<a href=tos.html>Terms of Service</a>" +
                "</div>    </div>";
  else return base.GetValue(req, tag);
 }
}


会话

前面的例子引出了另一个问题:在大多数情况下,你都需要一个会话,因为HTTP本身是无状态的,你不能像典型的客户-服务器系统一样将信息保存在连接中。在动态服务器中通常的做法是使用会话对象,通过在浏览器和服务器传递这个对象来达到识别的作用。

这里有三种常见的方法在多请求中保持会话存活状态:

1、通过URL传递会话,如果你在浏览器地址栏中看见包含'?sessid=5b3426AF42' 或者其他类似,那就是每次移动到一个新页面时被来回传递的会话标识符

2、通过设置一个cookie(一块由浏览器存储的数据),浏览器来发送未来的请求

3、在某些涉及表单提交的情况下,会话可以通过隐藏字段提交其他的字段中


由于是最为常见的,我的服务器使用cookie来管理会话,在应用程序代码方面,你需要在你的处理过程方法中请求会话:

public override bool Process(HttpServer server, HttpRequest request, HttpResponse response){
  server.RequestSession(request);
  request.Session["lastpage"] = request.Page;
  return base.Process(server, request, response);
}
你可以把任何对象塞入会话对象中,这些对象在会在会话对象失效之前一直保持有效。一个典型的应用会话是管理认证和登录,允许一些登录页面只能被登录用户看见,下一节将涉及。


回传
一台HTTP服务器通常接受信息和建立信息,可以使用POST请求,或者通过附加到URL的。查询串。此信息可以通过HttpRequest的查询字段进入你的应用程序,而你通常需要回发一定数量的URL。在会话管理,你需要重写程序并插入代码管理回传。下面是一个处理一个用户登录的请求并生成一个HTML的例子:

public class PostbackHandler : SubstitutingFileHandler {
    public override bool Process(HttpServer server, 
                    HttpRequest request, HttpResponse response){
        if((request.Page.Length > 8) && 
           (request.Page.Substring(request.Page.Length - 8) == "postback")){
            // Postback. Action depends on the page parameter
            server.RequestSession(request);
            string target = request.Page.Substring(0, request.Page.Length - 8) + 
                            request.Query["page"] as string;
            if(target == "/login") Login(request, response);
            else {
                response.Content = "Unknown postback target "+target;
                response.ReturnCode = 404;
            }
            return true;
        }
        // Session management, special processing of GET requests etc
        base.Process(server, request, response);
    }

    void Login(HttpRequest req, HttpResponse resp){
        // Authenticate
        if( (((string)req.Query["f1"]) != "test") ||
            (((string)req.Query["f2"]) != "password") ){
            resp.MakeRedirect("/login.html?error=1&redirect="+req.Query["redirect"]);
            return;
        }
        // Add to session and redirect
        req.Session["user"] = new string[]{"test", 
                                            "password", "A Test User"};
        resp.MakeRedirect((string)req.Query["redirect"]);
    }

    public override string GetValue(HttpRequest req, string tag){
        if(tag == "navbar") return "<i>insert navbar</i>";
        else if(tag == "loginerror")
            return ((string)req.Query["error"] == "1") ?
              "<p class=error>The user name or password " + 
              "you provided was incorrect.</p>" : "";
        else if(tag == "redirect") return "" + req.Query["redirect"];
        else return base.GetValue(req, tag);
    }
}
这是用来回发的HTML文件
<!-- login.html -->
<html>
<head>
<title>Test App: Log In</title>
<link rel=stylesheet href=my.css>
</head>

<body>

<%navbar>

<!-- Navbar End -->
<div id=content>
<h1>Log In</h1>
<p>The page you were trying to view requires you to be logged in. 
   Please enter your details below to be redirected.</p>

<%loginerror>

<div class=loginpanel>
<form action="postback?page=login&redirect=<%redirect>" method=POST>
<table class=login>
<tr><td align=right>Username:</td><td><input name="f1" value=""></td></tr>
<tr><td align=right>Password:</td><td><input type="password" name="f2" value=""></td></tr>
<tr><td colspan=2 align=center><input type=submit value="   Log in!   "></td></tr>
</table>
</form>
</div>

</div></body></html>
请注意,这个HTML文件包含三个伪标签(navbar,loginerror,redirect),这是为了处理GetValue方法而定义。


更多的关于认证和会员区

在上面回传的例子中,登录功能被放在一个数组中塞入会话中,包含当前登录用户的信息。我推荐使用这种技术,无论是放在数组,哈希表,或者会话自定义的UserInfo类中,会话中包含了你需要了解的当前登录用户一切。

if((request.Page.Length > 9) && (request.Page.Substring(0, 9) == "/members/")){
    server.RequestSession(request);
    if(request.Session["user"] == null){
        response.MakeRedirect("/login.html?redirect="+request.Page);
        return true;
    }
}
现在,任何试图访问/menbers下URL的未登录用户都会被重定向到一个登录页面。当然如果在本文之前使用登录代码和HTML,那么当你登录成功后,你将被重定向回你最初尝试访问的页面。


多个处理程序
在多数情况下,一个处理程序就够了,但是如果你愿意,也可以有多个处理程序(IHttpHandler接口的实例)添加到HttpServer的处理程序列表;例如,你可以有一个单独的处理程序应对回传和保护文件夹,而不是增加分支机构的流程方法。


他是如何工作的?

如果你有兴趣在你的应用程序中使用HTTP服务器,你可以返回顶部,点击链接下载源码,但是大多数人会有兴趣它的内部工作原理,这个实现使用了我自己的socket库,但是与直接运行在.NET的socket上类似,或者其他语言的socket

HTTP头

搜索互联网将迅速转向HTTP标准,包括定义的所有有效头字段,还有很多你希望看到的细节
GET /path/page.html?query=value HTTP/1.1
Host: www.test.com
Header-Field: value

通过空白行"\r\n\r\n"终止,我的socket库允许使用文本分隔符来终止消息,因此在处理连接程序的时候,可以设置一个事件处理程序,来解析头

bool ClientConnect(Server s, ClientInfo ci){
    ci.Delimiter = "\r\n\r\n";
    ci.Data = new ClientData(ci);
    ci.OnRead += new ConnectionRead(ClientRead);
    ci.OnReadBytes += new ConnectionReadBytes(ClientReadBytes);
    return true;
}
我们需要一个读取表头,读取文本信息直到遇到定界符。但我们还需要一个ReadBytes处理程序以接收内容,其不被固定的分隔符终止,并且可以包含任何字符。

<span style="font-size:18px;"></span><pre name="code" class="csharp" style="font-size:18px;">ClientData data = (ClientData)ci.Data;
if(data.state != ClientState.Header) return;
// already done; must be some text in content, which will be handled elsewhere


因为它能够接受POST内容中的空行,取代了两个字符的"\r\n"结束符。第一行包含了大量的重要信息,必须首先解析和验证

// First line: METHOD /path/url HTTP/version
string[] firstline = lines[0].Split(' ');
if(firstline.Length != 3){
  SendResponse(ci, data.req, new HttpResponse(400, 
     "Incorrect first header line "+lines[0]), true); return;
}
if(firstline[2].Substring(0, 4) != "HTTP"){
  SendResponse(ci, data.req, new HttpResponse(400, 
     "Unknown protocol "+firstline[2]), true); return;
}
data.req.Method = firstline[0];
data.req.Url = firstline[1];
data.req.HttpVersion = firstline[2].Substring(5);

该URL扫描问号,如果找到一个,将它分为页和查询字符串。如果第一行是有效的,其余的行被假定为头字段,分割然后放置到头哈希表中。有迹象表面,服务器着眼于三个特殊的头字段,主机:被放置在request.Host中并且必须存在;Cookie:被放置在Cookie哈希表中;内容长度:指定后面有多少内容。

data.req.Host = (string)data.req.Header["Host"];
if(null == data.req.Host){
  SendResponse(ci, data.req, new HttpResponse(400, "No Host specified"), true);
  return; 
}

if(null != data.req.Header["Cookie"]){
    string[] cookies = ((string)data.req.Header["Cookie"]).Split(';');
    foreach(string cookie in cookies){
        p = cookie.IndexOf('=');
        if(p > 0){
            data.req.Cookies[cookie.Substring(0, p).Trim()] = cookie.Substring(p+1);
        } else {
            data.req.Cookies[cookie.Trim()] = "";
        }
    }
}

if(null == data.req.Header["Content-Length"]) data.req.ContentLength = 0;
else data.req.ContentLength = Int32.Parse((string)data.req.Header["Content-Length"]);

最后,连接的状态被改变来指示它准备接收数据以及跳过报头的多少字节。在准备读取内容(如果有的话)。即时没有,因为我的socket库该结构没有内容,ClientReadBytes处理程序被调用来有效的处理零字节的消息。

内容
该消息没有固定的界定符,所以我们必须连接socket的二进制流,通过ClientReadBytes事件处理,ClientReadBytes在数据被接收的时候调用。即使我们在前面处理过,这边也包含头的部分,所以我们必须跳过头数据,在移除头数据之后,我们就是简单的将读到的内容附近为请求内容,如果该消息是完整的,那么查询字符串被解析并处理请求。

data.req.Content += Encoding.Default.GetString(bytes, ofs, len-ofs);
data.req.BytesRead += len - ofs;
data.headerskip += len - ofs;
if(data.req.BytesRead >= data.req.ContentLength){
    if(data.req.Method == "POST"){
        if(data.req.QueryString == "")data.req.QueryString = data.req.Content;
        else data.req.QueryString += "&" + data.req.Content;
    }
    ParseQuery(data.req);
    DoProcess(ci);
}

data.headerskip用于此连接的下一个请求,以确保该消息的内容没有被误解为下一个报头的一部分,作为一个连接保存活动状态


处理

一旦请求被解析,它被传递给一个响应处理程序并产生结果,除去无效的情况(头部不能被正确解析)。处理有两个步骤:

首先,查询字符串被解析,如果存在的话

其次,请求被依次传递给每个处理程序,从最新添加的开始,逐一的处理它:

void DoProcess(ClientInfo ci){
    ClientData data = (ClientData)ci.Data;
    string sessid = (string)data.req.Cookies["_sessid"];
    if(sessid != null) data.req.Session = (Session)sessions[sessid];
    bool closed = Process(ci, data.req);
    data.state = closed ? ClientState.Closed : ClientState.Header;
    data.read = 0;
    HttpRequest oldreq = data.req;
    // Once processed, the connection will be used for a new request
    data.req = new HttpRequest();
    data.req.Session = oldreq.Session; // ... but session is persisted
    data.req.From = ((IPEndPoint)ci.Socket.RemoteEndPoint).Address;
}

protected virtual bool Process(ClientInfo ci, HttpRequest req){
    HttpResponse resp = new HttpResponse();
    resp.Url = req.Url;
    for(int i = handlers.Count - 1; i >= 0; i--){
        IHttpHandler handler = (IHttpHandler)handlers[i];
        if(handler.Process(this, req, resp)){
            SendResponse(ci, req, resp, resp.ReturnCode != 200);
            return resp.ReturnCode != 200;
        }
    }
    return true;
}
在Process之前,DoProcess执行一定的管理工作:

首先,它加载会话请求;在请求处理之后,它创造出一个新的HttpRequest对象来连接下一次请求。


回应

最后一个阶段发送一个响应,一旦应用程序确定要发送什么内容。包括添加一个有限的HTTP头部信息,然后添加发送的内容信息,可以是二进制或者文本。

void SendResponse(ClientInfo ci, HttpRequest req, HttpResponse resp, bool close){
    ci.Send("HTTP/1.1 " + resp.ReturnCode + Responses[resp.ReturnCode] +
            "\r\nDate: "+DateTime.Now.ToString("R")+
            "\r\nServer: RedCoronaEmbedded/1.0"+
            "\r\nConnection: "+(close ? "close" : "Keep-Alive"));
    if(resp.RawContent == null )
        ci.Send("\r\nContent-Encoding: utf-8"+
            "\r\nContent-Length: "+resp.Content.Length);
    else
        ci.Send("\r\nContent-Length: "+resp.RawContent.Length);
    if(req.Session != null) ci.Send("\r\nSet-Cookie: _sessid="+req.Session.ID+"; path=/");
    foreach(DictionaryEntry de in resp.Header) ci.Send("\r\n" + de.Key + ": " + de.Value);
    ci.Send("\r\n\r\n"); // End of header
    if(resp.RawContent != null) ci.Send(resp.RawContent);
    else ci.Send(resp.Content);
    //Console.WriteLine("** SENDING\n"+Encoding.Default.GetString(resp.Content));
    if(close) ci.Close();
}


会话管理
此服务器的一个有用的功能是会话管理。大多数会话管理的代码在DoProcess中,cookie _sessid被检查,如果它存在的话会话被加载,并在SendResponse中设置cookie。这里有两种关于session的方法:RequestSession获取一个有效会话的方法;CleanUpSessions每当任何请求被处理时调用,删除已经到期的会话

public Session RequestSession(HttpRequest req){
    if(req.Session != null){
        if(sessions[req.Session.ID] == req.Session) return req.Session;
    }
    req.Session = new Session(req.From);
    sessions[req.Session.ID] = req.Session;
    return req.Session;
}

void CleanUpSessions(){
    ICollection keys = sessions.Keys;
    ArrayList toRemove = new ArrayList();
    foreach(string k in keys){
        Session s = (Session)sessions[k];
        int time = (int)((DateTime.Now - s.LastTouched).TotalSeconds);
        if(time > sessionTimeout){
            toRemove.Add(k);
            Console.WriteLine("Removed session "+k);
        }
    }
    foreach(object k in toRemove) sessions.Remove(k);
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值