前不久,手寫(xiě)了個(gè)服務(wù)器,并不難,還是基于 HttpListener ,敲簡(jiǎn)單!
當然還是基于最早寫(xiě)的一個(gè) Server 雛形,項目名為 Kserver,KServer 當初是為了當初自己想用 C# 實(shí)現 WebDav 的一些想法,后來(lái)也沒(méi)有繼續寫(xiě)下去,工程量太大了,有興趣的朋友可以看看 IETF RFC4918 中的協(xié)議定義嘗試實(shí)現一把,會(huì )很愉快的。
說(shuō)說(shuō)我的 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 給客戶(hù)端。
說(shuō)一個(gè)坑爹的事情,這個(gè)程序啟動(dòng)后占用 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>
但是寫(xiě) Cookie 始終不成功,寫(xiě) Cookie 的關(guān)鍵代碼如下:
- resp.AppendHeader("Set-Cookie", name + "=" + value + "; path=/; domain=" + host + "; expires=" + expireGMT);
resp 是 KHttpServer.IHttpListenerResponse 的實(shí)現,繼承于 HttpListenerResponse,我設置 Host 為 req.Url.Host。這個(gè)在本機是不會(huì )有問(wèn)題的,單獨在服務(wù)器中使用 80 端口也不會(huì )有問(wèn)題,有問(wèn)題的是即便通過(guò)反向代理,獲取 Headers 中 的 Host 值始終還是 localhost,要通過(guò) X-Forwarded-Host 才可以,這個(gè)大學(xué)時(shí)好歹了解過(guò),平時(shí)開(kāi)發(fā)全部基于 IIS,沒(méi)有反向代理,頭一回遇到。
- 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;
- }
- }
- // 沒(méi)有反向代理,就使用默認 Host
- if (!poweredByProxy) _Host = obj.Request.Url.Host;
- }
接下來(lái)就是模板引擎了,不用 Razor 了,說(shuō)真的對 Razor 漸漸的沒(méi)啥好感了,感覺(jué)挺笨重,所以選用了 DotLiquid,用 Liquid 做模板引擎的應用可以說(shuō)是非常多了。
于是擴展了 String 類(lèi),增加了 Html 模板文件渲染 Html 的方法:
- public static string AsHtmlFromTemplate(this string tmpl, object model)
- {
- string html = Template.Parse(tmpl).Render(Hash.FromAnonymousObject(model));
- return html;
- }
然后包含模板頁(yè)渲染的寫(xiě)法就變成醬嬸了。
- 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 搞的個(gè)關(guān)鍵字,表示是子頁(yè)顯示內容的區域。
對于字體、腳本(第三方)、圖片這些靜態(tài)資源,我的想法是既然不會(huì )有大的變動(dòng),就讓他永久緩存在瀏覽器好了。
- obj.Response.AppendHeader("Cache-Control", "max-age=315360000");
其他的就是處理 POST ,處理 Cookie 了。HttpListenerRequest 是沒(méi)法獲取 Form 表單的值的,只能讀取 InputStream 中的值,然后自己根據鍵值對獲取了。Cookie 是不能簡(jiǎn)單的通過(guò)鍵值對分割,查詢(xún)值按照等號分割沒(méi)關(guān)系,因為 Value 都是編碼了的,不會(huì )含有等號,但是 Cookie 中是可能會(huì )有等號的,比如 Base64 編碼過(guò)的值里,大部分都有。
同樣,獲取 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];
- }
接下來(lái)模擬登陸成功后的跳轉,用過(guò) Asp.net 的知道有個(gè) Response.Redirect ,不過(guò) HttpListenerRequest 肯定是沒(méi)有這個(gè)方法的,可以通過(guò)設置 Header 302 重定向就行了,為啥是 302 不是 301,自己想吧。
- public static void Redirect(this KHttpServer.IHttpListenerResponse resp, string url)
- {
- resp.StatusCode = 302;
- resp.AppendHeader("Location", url);
- resp.Close();
- }
對于較大的頁(yè)面,也許還是希望用 Gzip 壓縮一下,需要設置 Content-Encoding 為 Gzip。
- resp.AppendHeader("Content-Encoding", "gzip");
我這里處理比較簡(jiǎn)單,是不管客戶(hù)端的 Accept-Type 的,不過(guò)現代瀏覽器基本都支持了。
對相應內容進(jìn)行壓縮:
- resp.AppendHeader("Content-Encoding", "gzip");
- byte[] data = GzipCompressor.Compress(text);
- MemoryStream ms = new MemoryStream(data);
- AsStream(resp, ms, mime);
- ms.Close();
既然是純 C#,沒(méi)有了 WebForm 和 MVC 這類(lèi)框架,分頁(yè)處理也顯得不簡(jiǎn)單了,從網(wǎng)上改造了一個(gè) PHP 寫(xiě)的分頁(yè)類(lèi),果然 PHP 是最好的語(yǔ)言!鷂→
這不是取數據時(shí)的分頁(yè),而是顯示時(shí)候的分頁(yè)。
- /// <summary>
- /// 分頁(yè)處理類(lèi)
- /// </summary>
- public class PageNumber
- {
- /// <summary>
- /// 是否顯示[首頁(yè)]
- /// </summary>
- public bool ShowFirstPage { get; set; }
- /// <summary>
- /// 是否顯示[末頁(yè)]
- /// </summary>
- public bool ShowEndPage { get; set; }
- /// <summary>
- /// 翻頁(yè)Url前綴
- /// </summary>
- public string UrlPrefix { get; set; }
- public PageNumber()
- {
- ShowFirstPage = true;
- ShowEndPage = true;
- UrlPrefix = "";
- }
- /// <summary>
- /// 獲取分頁(yè),返回數據,如[["1","首頁(yè)","/page/1"]]
- /// </summary>
- /// <param name="page">當前頁(yè)</param>
- /// <param name="pages">總頁(yè)數</param>
- /// <returns></returns>
- public List<string[]> GetPageNumbers(int page, int pages)
- {
- List<string[]> plists = new List<string[]>();
- //最多顯示多少個(gè)頁(yè)碼
- int _pageNum = 5;
- //當前頁(yè)面小于1 則為1
- page = page < 1 ? 1 : page;
- //當前頁(yè)大于總頁(yè)數 則為總頁(yè)數
- page = page > pages ? pages : page;
- //頁(yè)數小當前頁(yè) 則為當前頁(yè)
- pages = pages < page ? page : pages;
- //計算開(kāi)始頁(yè)
- int _start = page - (int)Math.Floor((double)_pageNum / 2);
- _start = _start < 1 ? 1 : _start;
- //計算結束頁(yè)
- int _end = page + (int)Math.Floor((double)_pageNum / 2);
- _end = _end > pages ? pages : _end;
- //當前顯示的頁(yè)碼個(gè)數不夠最大頁(yè)碼數,在進(jìn)行左右調整
- 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[] { "", "首頁(yè)", string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + "1" });
- if (page > 1)
- {
- plists.Add(new string[] { (page - 1).ToString(), "上頁(yè)", 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(), "下頁(yè)" , string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + (page + 1).ToString() });
- }
- if (ShowEndPage)
- plists.Add(new string[] { "", "末頁(yè)", string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + (pages).ToString() });
- return plists;
- }
- }
用 SimpleMDE 作為 Markdown 編輯器,,誰(shuí)用誰(shuí)知道,對于富文本的排版,我始終無(wú)能為力,Word 也不會(huì )用,markdown 真好用!
效果如下圖:
SimpleMDE 是沒(méi)有上傳圖片的功能,需要自己處理,不過(guò)自定義按鈕官方文檔中有,我只是做了寫(xiě)微小的工作,為按鈕加個(gè)選圖片和上傳的事件,這需要 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();
- }
如果你的接口是外部服務(wù)或者阿里云OSS,要記得設置跨域,不然報錯,這個(gè)搞過(guò)開(kāi)發(fā)的都懂得。
最初版本的后臺 Markdown 渲染用的 Github 上的 star 最多的那一個(gè) Markdig,在 CentOS 7 下 mono 環(huán)境運行報錯,換了 CommonMark 使用,這個(gè)在 Nuget 上能找到。
最終的最終,把所有資源都打包進(jìn)了資源文件,用 ILMerge 合并程序集,你的服務(wù)端就只剩下一個(gè) EXE 了,滑稽 →_→