.NETC#我爱编程

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

2018-03-24  本文已影响72人  天兵公园

前不久,手写了个服务器,并不难,还是基于 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 了,滑稽 →_→

上一篇下一篇

猜你喜欢

热点阅读