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