写一个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 了,滑稽 →_→