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

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

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

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

[[225746]]

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

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

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

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

  1. <VirtualHost *:80> 
  2.     ServerName 1ll.co 
  3.     ProxyRequests off 
  4.     <Proxy *> 
  5.     Order deny,allow 
  6.     Allow from all 
  7.     </Proxy> 
  8.     ProxyPass / http://localhost:6600/ 
  9.     ProxyPassReverse / http://localhost:6600/ 
  10.     ProxyPassReverseCookieDomain http://localhost:6600 http://1ll.co 
  11.     ProxyPassReverseCookiePath / http://localhost:6600/ 
  12. </VirtualHost> 

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

  1. 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,没有反向代理,头一回遇到。

  1. var headers = obj.Request.Headers; 
  2. if (string.IsNullOrEmpty(_Host)) 
  3.     // 是否有反向代理 
  4.     bool poweredByProxy = false
  5.     IEnumerator keyenum = headers.GetEnumerator(); 
  6.     while (keyenum.MoveNext()) 
  7.     { 
  8.         string key = keyenum.Current.ToString(); 
  9.         if (key == "X-Forwarded-Host"
  10.         { 
  11.             _Host = headers[key]; 
  12.             poweredByProxy = true
  13.             break; 
  14.         } 
  15.     } 
  16.     // 没有反向代理,就使用默认 Host 
  17.     if (!poweredByProxy) _Host = obj.Request.Url.Host; 

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

  • DotLiquid http://dotliquidmarkup.org/

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

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

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

  1. string postListHtmlTmpl = ResourceHelper.LoadStringResource("postlist.html"); 
  2. string adminHtmlTmpl = ResourceHelper.LoadStringResource("admin.html"); 
  3. obj.Response.AsHtml(adminHtmlTmpl.AsHtmlFromTemplate(new 
  4.     RenderBody = postListHtmlTmpl.AsHtmlFromTemplate(new 
  5.     { 
  6.         PageData = pageData.ToArray(), 
  7.         NaviData = naviData, 
  8.         CurrentPage = page.ToString(), 
  9.         Error = error, 
  10.         Success = success 
  11.     }) 
  12. })); 

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

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

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

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

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

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

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

  1. public static void Redirect(this KHttpServer.IHttpListenerResponse resp, string url) 
  2.     resp.StatusCode = 302; 
  3.     resp.AppendHeader("Location", url); 
  4.     resp.Close(); 

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

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

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

对相应内容进行压缩:

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

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

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

  1. /// <summary> 
  2. /// 分页处理类 
  3. /// </summary> 
  4. public class PageNumber 
  5.     /// <summary> 
  6.     /// 是否显示[首页] 
  7.     /// </summary> 
  8.     public bool ShowFirstPage { get; set; } 
  9.  
  10.     /// <summary> 
  11.     /// 是否显示[末页] 
  12.     /// </summary> 
  13.     public bool ShowEndPage { get; set; } 
  14.  
  15.     /// <summary> 
  16.     /// 翻页Url前缀 
  17.     /// </summary> 
  18.     public string UrlPrefix { get; set; } 
  19.  
  20.     public PageNumber() 
  21.     { 
  22.         ShowFirstPage = true
  23.         ShowEndPage = true
  24.         UrlPrefix = ""
  25.     } 
  26.  
  27.     /// <summary> 
  28.     /// 获取分页,返回数据,如[["1","首页","/page/1"]] 
  29.     /// </summary> 
  30.     /// <param name="page">当前页</param> 
  31.     /// <param name="pages">总页数</param> 
  32.     /// <returns></returns
  33.     public List<string[]> GetPageNumbers(int page, int pages) 
  34.     { 
  35.  
  36.         List<string[]> plists = new List<string[]>(); 
  37.  
  38.         //最多显示多少个页码   
  39.         int _pageNum = 5; 
  40.         //当前页面小于1 则为1   
  41.         page = page < 1 ? 1 : page; 
  42.         //当前页大于总页数 则为总页数   
  43.         page = page > pages ? pages : page; 
  44.         //页数小当前页 则为当前页   
  45.         pages = pages < page ? page : pages; 
  46.  
  47.         //计算开始页   
  48.         int _start = page - (int)Math.Floor((double)_pageNum / 2); 
  49.         _start = _start < 1 ? 1 : _start; 
  50.         //计算结束页   
  51.         int _end = page + (int)Math.Floor((double)_pageNum / 2); 
  52.         _end = _end > pages ? pages : _end; 
  53.  
  54.         //当前显示的页码个数不够最大页码数,在进行左右调整   
  55.         int _curPageNum = _end - _start + 1; 
  56.         //左调整   
  57.         if (_curPageNum < _pageNum && _start > 1) 
  58.         { 
  59.             _start = _start - (_pageNum - _curPageNum); 
  60.             _start = _start < 1 ? 1 : _start; 
  61.             _curPageNum = _end - _start + 1; 
  62.         } 
  63.         //右边调整   
  64.         if (_curPageNum < _pageNum && _end < pages) 
  65.         { 
  66.             _end = _end + (_pageNum - _curPageNum); 
  67.             _end = _end > pages ? pages : _end; 
  68.         } 
  69.  
  70.         if (ShowFirstPage) 
  71.             plists.Add(new string[] { """首页", string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + "1" }); 
  72.  
  73.         if (page > 1) 
  74.         { 
  75.             plists.Add(new string[] { (page - 1).ToString(), "上页", string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + (page - 1).ToString() }); 
  76.         } 
  77.         for (int i = _start; i <= _end; i++) 
  78.         { 
  79.             plists.Add(new string[] { i.ToString(), i.ToString(), string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + i.ToString() }); 
  80.         } 
  81.         if (page < _end) 
  82.         { 
  83.             plists.Add(new string[] { (page + 1).ToString(), "下页" , string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + (page + 1).ToString() }); 
  84.         } 
  85.  
  86.         if (ShowEndPage) 
  87.             plists.Add(new string[] { """末页", string.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix + (pages).ToString() }); 
  88.  
  89.         return plists; 
  90.     } 

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

  • SimpleMDE https://simplemde.com/

效果如下图:

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

  1. function upload(){ 
  2.     var sid = 'hTyx6Tm9Ikl06Ap'
  3.     var forms = $('#form_' + sid).length; 
  4.     if (forms > 0) { 
  5.         $('#form_' + sid).remove(); 
  6.     } 
  7.     var fhtml = '<form action="图片上传接口" method="post" enctype="multipart/form-data" style="display:none;" id="form_' + sid + '">'
  8.     fhtml += '<input id="input_' + sid + '" type="file" name="file">'
  9.     fhtml += '<input type="submit" value="upload" />'
  10.     fhtml += '</form>'
  11.     $('body').append(fhtml); 
  12.     $('#input_' + sid).change(function () { 
  13.         $('#form_' + sid).ajaxSubmit({ 
  14.             success: function (data) { 
  15.             alert(data); 
  16.             } 
  17.         }); 
  18.     }).click(); 

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

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

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

责任编辑:武晓燕 来源: 天兵公园
相关推荐

2016-10-18 22:10:02

HTTP推送HTML

2022-01-16 08:04:44

集群部署canal

2020-07-29 08:03:26

Celery异步项目

2011-03-08 14:28:03

proftpdGentoo

2012-12-19 11:40:13

思科路由器

2017-07-03 17:20:55

Android软键盘控制开发问题

2021-11-15 15:43:28

Windows 11升级微软

2018-06-12 15:39:41

容器部署云平台

2009-06-12 10:25:42

Webservices

2017-10-13 12:23:17

苹果

2009-08-06 16:01:30

C#接口成员

2009-06-10 21:46:02

JavaScript与

2010-05-04 15:59:05

Oracle字符集

2010-09-17 15:41:46

网络协议分析软件

2011-03-28 16:59:16

nagios监控服务器

2018-03-01 10:45:25

HTTP服务器程序

2009-10-10 08:36:18

2021-06-06 16:15:57

地区接口项目

2019-04-24 15:06:37

Http服务器协议

2021-10-21 06:52:17

Vue3组件 API
点赞
收藏

51CTO技术栈公众号