写一个HTTP服务器中遇到的一些问题

前不久,手写了个服务器,并不难,还是基于 HttpListener ,敲简单!

当然还是基于最早写的一个 Server 雏形,项目名为 Kserver,KServer 当初是为了当初自己想用 C# 实现 WebDav 的一些想法,后来也没有继续写下去,工程量太大了,有兴趣的朋友可以看看 IETF RFC4918 中的协议定义尝试实现一把,会很愉快的。

说说我的 Kserver 的调用,基本上三两行代码的事情。

int port = 6600;
KServer kServer = new KServer(port);
kServer.OnRequest += KServer_OnRequest;
kServer.OnError += KServer_OnError;
kServer.Start();
Console.WriteLine("listening on port {0} ...", port);

在 KServer_OnRequest 中处理正常的 HTTP 请求,在 KServer_OnError 中处理程序错误,通常这里返回 HTTP 500 给客户端。

说一个坑爹的事情,这个程序启动后占用 6600 端口,然后在 Apache 配置了反向代理。

<VirtualHost *:80>
    ServerName 1ll.co
    ProxyRequests off
    <Proxy *>
    Order deny,allow
    Allow from all
    </Proxy>
    ProxyPass / http://localhost:6600/
    ProxyPassReverse / http://localhost:6600/
    ProxyPassReverseCookieDomain http://localhost:6600 http://1ll.co
    ProxyPassReverseCookiePath / http://localhost:6600/
</VirtualHost>

但是写 Cookie 始终不成功,写 Cookie 的关键代码如下:

resp.AppendHeader("Set-Cookie", name + "=" + value + "; path=/; domain=" + host + "; expires=" + expireGMT);

resp 是 KHttpServer.IHttpListenerResponse 的实现,继承于 HttpListenerResponse,我设置 Host 为 req.Url.Host。这个在本机是不会有问题的,单独在服务器中使用 80 端口也不会有问题,有问题的是即便通过反向代理,获取 Headers 中 的 Host 值始终还是 localhost,要通过 X-Forwarded-Host 才可以,这个大学时好歹了解过,平时开发全部基于 IIS,没有反向代理,头一回遇到。

var headers = obj.Request.Headers;
if (string.IsNullOrEmpty(_Host))
{
    // 是否有反向代理
    bool poweredByProxy = false;
    IEnumerator keyenum = headers.GetEnumerator();
    while (keyenum.MoveNext())
    {
        string key = keyenum.Current.ToString();
        if (key == "X-Forwarded-Host")
        {
            _Host = headers[key];
            poweredByProxy = true;
            break;
        }
    }
    // 没有反向代理,就使用默认 Host
    if (!poweredByProxy) _Host = obj.Request.Url.Host;
}

接下来就是模板引擎了,不用 Razor 了,说真的对 Razor 渐渐的没啥好感了,感觉挺笨重,所以选用了 DotLiquid,用 Liquid 做模板引擎的应用可以说是非常多了。

DotLiquid
http://dotliquidmarkup.org/

于是扩展了 String 类,增加了 Html 模板文件渲染 Html 的方法:

 public static string AsHtmlFromTemplate(this string tmpl, object model)
 {
     string html = Template.Parse(tmpl).Render(Hash.FromAnonymousObject(model));
     return html;
 }

然后包含模板页渲染的写法就变成酱婶了。

string postListHtmlTmpl = ResourceHelper.LoadStringResource("postlist.html");
string adminHtmlTmpl = ResourceHelper.LoadStringResource("admin.html");
obj.Response.AsHtml(adminHtmlTmpl.AsHtmlFromTemplate(new
{
    RenderBody = postListHtmlTmpl.AsHtmlFromTemplate(new
    {
        PageData = pageData.ToArray(),
        NaviData = naviData,
        CurrentPage = page.ToString(),
        Error = error,
        Success = success
    })
}));

RenderBody 是模仿 Razor 搞的个关键字,表示是子页显示内容的区域。

对于字体、脚本(第三方)、图片这些静态资源,我的想法是既然不会有大的变动,就让他永久缓存在浏览器好了。

obj.Response.AppendHeader("Cache-Control", "max-age=315360000");

其他的就是处理 POST ,处理 Cookie 了。HttpListenerRequest 是没法获取 Form 表单的值的,只能读取 InputStream 中的值,然后自己根据键值对获取了。Cookie 是不能简单的通过键值对分割,查询值按照等号分割没关系,因为 Value 都是编码了的,不会含有等号,但是 Cookie 中是可能会有等号的,比如 Base64 编码过的值里,大部分都有。

同样,获取 Cookie 的方法也木有,自己从 Header 里找吧,滑稽。

public static string GetCookie(this KHttpServer.IHttpListenerRequest req, string name)
{
    System.Collections.Specialized.NameValueCollection headers = req.Headers;
    string cookies = headers["Cookie"];
    if (cookies == null || cookies.Length < 1) return null;
    var dict = cookies.AsCookieParameters();
    if (!dict.ContainsKey(name)) return null;
    return dict[name];
}

接下来模拟登陆成功后的跳转,用过 Asp.net 的知道有个 Response.Redirect ,不过 HttpListenerRequest 肯定是没有这个方法的,可以通过设置 Header 302 重定向就行了,为啥是 302 不是 301,自己想吧。

public static void Redirect(this KHttpServer.IHttpListenerResponse resp, string url)
{
    resp.StatusCode = 302;
    resp.AppendHeader("Location", url);
    resp.Close();
}

对于较大的页面,也许还是希望用 Gzip 压缩一下,需要设置 Content-Encoding 为 Gzip。

resp.AppendHeader("Content-Encoding", "gzip");

我这里处理比较简单,是不管客户端的 Accept-Type 的,不过现代浏览器基本都支持了。

对相应内容进行压缩:

resp.AppendHeader("Content-Encoding", "gzip");
byte[] data = GzipCompressor.Compress(text);
MemoryStream ms = new MemoryStream(data);
AsStream(resp, ms, mime);
ms.Close();

既然是纯 C#,没有了 WebForm 和 MVC 这类框架,分页处理也显得不简单了,从网上改造了一个 PHP 写的分页类,果然 PHP 是最好的语言。→_→

这不是取数据时的分页,而是显示时候的分页。

/// <summary>
/// 分页处理类
/// </summary>
public class PageNumber
{
    /// <summary>
    /// 是否显示[首页]
    /// </summary>
    public bool ShowFirstPage { get; set; }

    /// <summary>
    /// 是否显示[末页]
    /// </summary>
    public bool ShowEndPage { get; set; }

    /// <summary>
    /// 翻页Url前缀
    /// </summary>
    public string UrlPrefix { get; set; }

    public PageNumber()
    {
        ShowFirstPage = true;
        ShowEndPage = true;
        UrlPrefix = "";
    }

    /// <summary>
    /// 获取分页,返回数据,如[["1","首页","/page/1"]]
    /// </summary>
    /// <param name="page">当前页</param>
    /// <param name="pages">总页数</param>
    /// <returns></returns>
    public List<string[]> GetPageNumbers(int page, int pages)
    {

        List<string[]> plists = new List<string[]>();

        //最多显示多少个页码  
        int _pageNum = 5;
        //当前页面小于1 则为1  
        page = page < 1 ? 1 : page;
        //当前页大于总页数 则为总页数  
        page = page > pages ? pages : page;
        //页数小当前页 则为当前页  
        pages = pages < page ? page : pages;

        //计算开始页  
        int _start = page - (int)Math.Floor((double)_pageNum / 2);
        _start = _start < 1 ? 1 : _start;
        //计算结束页  
        int _end = page + (int)Math.Floor((double)_pageNum / 2);
        _end = _end > pages ? pages : _end;

        //当前显示的页码个数不够最大页码数,在进行左右调整  
        int _curPageNum = _end - _start + 1;
        //左调整  
        if (_curPageNum < _pageNum && _start > 1)
        {
            _start = _start - (_pageNum - _curPageNum);
            _start = _start < 1 ? 1 : _start;
            _curPageNum = _end - _start + 1;
        }
        //右边调整  
        if (_curPageNum < _pageNum && _end < pages)
        {
            _end = _end + (_pageNum - _curPageNum);
            _end = _end > pages ? pages : _end;
        }

        if (ShowFirstPage)
            plists.Add(new string[] { "", "首页", string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + "1" });

        if (page > 1)
        {
            plists.Add(new string[] { (page - 1).ToString(), "上页", string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + (page - 1).ToString() });
        }
        for (int i = _start; i <= _end; i++)
        {
            plists.Add(new string[] { i.ToString(), i.ToString(), string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + i.ToString() });
        }
        if (page < _end)
        {
            plists.Add(new string[] { (page + 1).ToString(), "下页" , string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + (page + 1).ToString() });
        }

        if (ShowEndPage)
            plists.Add(new string[] { "", "末页", string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + (pages).ToString() });

        return plists;
    }
}

用 SimpleMDE 作为 Markdown 编辑器,,谁用谁知道,对于富文本的排版,我始终无能为力,Word 也不会用,markdown 真好用!

SimpleMDE
https://simplemde.com/

效果如下图:

SimpleMDE 是没有上传图片的功能,需要自己处理,不过自定义按钮官方文档中有,我只是做了写微小的工作,为按钮加个选图片和上传的事件,这需要 jQuery 和 jQuery.Form 的支持。

function upload(){
    var sid = 'hTyx6Tm9Ikl06Ap';
    var forms = $('#form_' + sid).length;
    if (forms > 0) {
        $('#form_' + sid).remove();
    }
    var fhtml = '<form action="图片上传接口" method="post" enctype="multipart/form-data" style="display:none;" id="form_' + sid + '">';
    fhtml += '<input id="input_' + sid + '" type="file" name="file">';
    fhtml += '<input type="submit" value="upload" />';
    fhtml += '</form>';
    $('body').append(fhtml);
    $('#input_' + sid).change(function () {
        $('#form_' + sid).ajaxSubmit({
            success: function (data) {
            alert(data);
            }
        });
    }).click();
}

如果你的接口是外部服务或者阿里云OSS,要记得设置跨域,不然报错,这个搞过开发的都懂得。

最初版本的后台 Markdown 渲染用的 Github 上的 star 最多的那一个 Markdig,在 CentOS 7 下 mono 环境运行报错,换了 CommonMark 使用,这个在 Nuget 上能找到。

最终的最终,把所有资源都打包进了资源文件,用 ILMerge 合并程序集,你的服务端就只剩下一个 EXE 了,滑稽 →_→

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容

  • width: 65%;border: 1px solid #ddd;outline: 1300px solid #...
    邵胜奥阅读 4,791评论 0 1
  • Razor 页面是 ASP.NET Core MVC 的一个新功能,它可以使基于页面的编码方式更简单高效。 若要查...
    yanshouwang阅读 7,226评论 0 5
  • 会话(Session)跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。常用的会话跟踪技术是Cookie与Se...
    chinariver阅读 5,608评论 1 49
  • Cookie技术是客户端的解决方案,Cookie就是由服务器发给客户端的特殊信息,而这些信息以文本文件的方式存放在...
    饥人谷_陆邈阅读 1,461评论 1 5
  • 刚考完科目二,一圈满分通过,还是很欣慰的。毕竟最热的夏天,最冷的冬天,我都再驾校,开着车牌270的桑塔纳,...
    真_李默阅读 147评论 0 0