【輪子狂魔】拋棄IIS,打造個性的Web Server,狂魔iis
引言
此篇是《【輪子狂魔】拋棄IIS,向天借個HttpListener - 基礎篇(附帶源碼)》的續篇,也可以說是提高篇,如果你對HttpListener不甚了解的話,建議先看下基礎篇。
這次玩的東西有點多了,大致分為如下幾個方向:
1.支持靜態頁面
2.Ur映射l執行方法
3.Url映射執行Lua腳本
4.模仿MVC中的C
這些東西有什麼用?
支持靜態頁面:這個純屬玩具吧,只是基礎篇作為引子的一個簡單示例而已。
Url映射執行方法:類似Web API,可以提供一些基於Http協議的交互方式。
Url映射執行Lua腳本:與上面一樣,不同的是,在某些場景下更合適,比如業務頻繁變動、頻繁發布。Lua腳本我就不細說了,不清楚的可以百度一下,是個很好玩的東西。
模仿MVC中的C:這個實例只是想說基於HttpListener我們可以做很多事情,如果你對ASP.NET、MVC很熟悉,你可以做個整整的Web Server出來,甚至做個Web框架都可以。
那麼除了這些還可以做什麼?
其實太多了,比如反向代理、負載均衡、黑名單等等,你都可以做。如果你有興趣可以跟帖討論 ^_^
改造HttpServer支持橫向擴展
抽離出一個接口:HttpImplanter

![]()
1 interface HttpImplanter
2 {
3 void Start();
4 void Stop();
5 void MakeHttpPrefix(HttpListener server);
6 ReturnCode ProcessRequest(HttpListenerContext context);
7 byte[] CreateReturnResult(HttpListenerContext context, ReturnCode result);
8 }
View Code
改造HttpServer的一些執行細節

![]()
1 /// <summary>
2 /// 可接收Http請求的服務器
3 /// </summary>
4 class HttpServer
5 {
6 Thread _httpListenThread;
7
8 /// <summary>
9 /// HttpServer是否已經啟動
10 /// </summary>
11 volatile bool _isStarted = false;
12
13 /// <summary>
14 /// 線程是否已經結束
15 /// </summary>
16 volatile bool _terminated = false;
17 volatile bool _ready = false;
18 volatile bool _isRuning = false;
19 HttpImplanter _httpImplanter;
20
21 public void Start(HttpImplanter httpImplanter)
22 {
23 if (!HttpListener.IsSupported)
24 {
25 Logger.Exit("不支持HttpListener!");
26 }
27
28 if (_isStarted)
29 {
30 return;
31 }
32 _isStarted = true;
33 _ready = false;
34 _httpImplanter = httpImplanter;
35
36 RunHttpServerThread();
37
38 while (!_ready) ;
39 }
40
41 private void RunHttpServerThread()
42 {
43 _httpListenThread = new Thread(new ThreadStart(() =>
44 {
45 HttpListener server = new HttpListener();
46 try
47 {
48 _httpImplanter.MakeHttpPrefix(server);
49 server.Start();
50 }
51 catch (Exception ex)
52 {
53 Logger.Exit("無法啟動服務器監聽,請檢查網絡環境。");
54 }
55
56 _httpImplanter.Start();
57
58 IAsyncResult result = null;
59 while (!_terminated)
60 {
61 while (result == null || result.IsCompleted)
62 {
63 result = server.BeginGetContext(new AsyncCallback(ProcessHttpRequest), server);
64 }
65 _ready = true;
66 Thread.Sleep(10);
67 }
68
69 server.Stop();
70 server.Abort();
71 server.Close();
72 _httpImplanter.Stop();
73 }
74 ));
75
76 _httpListenThread.IsBackground = true;
77 _httpListenThread.Start();
78 }
79
80 private void ProcessHttpRequest(IAsyncResult iaServer)
81 {
82 HttpListener server = iaServer.AsyncState as HttpListener;
83 HttpListenerContext context = null;
84 try
85 {
86 context = server.EndGetContext(iaServer);
87 Logger.Info("接收請求" + context.Request.Url.ToString());
88 //判斷上一個操作未完成,即返回服務器正忙,並開啟一個新的異步監聽
89 if (_isRuning)
90 {
91 Logger.Info("正在處理請求,已忽略請求" + context.Request.Url.ToString());
92 RetutnResponse(context, _httpImplanter.CreateReturnResult(context, new ReturnCode((int)CommandResult.ServerIsBusy, EnumHelper.GetEnumDescription(CommandResult.ServerIsBusy))));
93 server.BeginGetContext(new AsyncCallback(ProcessHttpRequest), server);
94 return;
95 }
96
97 _isRuning = true;
98 server.BeginGetContext(new AsyncCallback(ProcessHttpRequest), server);
99 }
100 catch
101 {
102 Logger.Warning("服務器已關閉!");
103 return;
104 }
105
106 string scriptName = new UrlHelper(context.Request.Url).ScriptName;
107 byte[] resultBytes = null;
108 if (scriptName.ToLower().EndsWith(".html")||scriptName == "favicon.ico")
109 {
110 string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", scriptName);
111 if (File.Exists(filePath))
112 {
113 resultBytes = File.ReadAllBytes(filePath);
114 }
115 else
116 {
117 resultBytes = _httpImplanter.CreateReturnResult(context, new ReturnCode((int)CommandResult.FileNotExists, EnumHelper.GetEnumDescription(CommandResult.FileNotExists)));
118 }
119 }
120 else
121 {
122 ReturnCode result = _httpImplanter.ProcessRequest(context);
123 resultBytes = _httpImplanter.CreateReturnResult(context, result);
124 }
125 RetutnResponse(context, resultBytes);
126 _isRuning = false;
127 }
128
129 private static void RetutnResponse(HttpListenerContext context, byte[] resultBytes)
130 {
131 context.Response.ContentLength64 = resultBytes.Length;
132 System.IO.Stream output = context.Response.OutputStream;
133 try
134 {
135 output.Write(resultBytes, 0, resultBytes.Length);
136 output.Close();
137 }
138 catch
139 {
140 Logger.Warning("客戶端已經關閉!");
141 }
142 }
143
144 public void Stop()
145 {
146 if (!_isStarted)
147 {
148 return;
149 }
150
151 _terminated = true;
152 _httpListenThread.Join();
153
154 _isStarted = false;
155 }
156
157 }
View Code
Url映射執行方法
1.繼承HttpImplanter
2.增加監聽前綴,用於過濾Url
3.創建返回結果
3.1 解析Url
3.2 定位訪問的類
3.3 執行Url所表示的方法

![]()
1 public class MethodHttpServer : HttpImplanter
2 {
3 #region HttpImplanter 成員
4
5 public void Start()
6 {
7 //nothing to do
8 }
9
10 public void Stop()
11 {
12 //nothing to do
13 }
14
15 public void MakeHttpPrefix(System.Net.HttpListener server)
16 {
17 server.Prefixes.Clear();
18 server.Prefixes.Add("http://localhost:8081/");
19 Logger.Info("MethodHttpServer添加監聽前綴:http://localhost:8081/");
20 }
21
22 public ReturnCode ProcessRequest(System.Net.HttpListenerContext context)
23 {
24 return new ReturnCode((int)CommandResult.Success, EnumHelper.GetEnumDescription(CommandResult.Success));
25 }
26
27 public byte[] CreateReturnResult(System.Net.HttpListenerContext context, ReturnCode result)
28 {
29 string responseString = string.Empty;
30 UrlHelper urlHelper = new UrlHelper(context.Request.Url);
31 var type = Type.GetType("HttpListenerDemo.Test." + urlHelper.ScriptName);
32 if (type != null)
33 {
34 object obj = Activator.CreateInstance(type);
35 responseString = obj.GetType().GetMethod(urlHelper.Parameters["method"]).Invoke(obj, new object[] { urlHelper.Parameters["param"] }) as string;
36 }
37
38 return System.Text.Encoding.UTF8.GetBytes(responseString);
39 }
40
41 #endregion
42 }
View Code
測試方法

![]()
1 public class TestMethod
2 {
3 public string Test(string msg)
4 {
5 return "TestMethod:" + msg;
6 }
7 }
View Code

Url映射執行Lua腳本
使用Lua前要填坑
首先需要注意的是使用Lua有個坑,我使用的是Lua51.dll,而這個是 .net 2.0版本,而我的程序是 4.0。且這個類庫是32位,而我的系統是64位。所以遇到了2個問題。
1.需要修改app.config,增加startup節點,讓程序運行時以.net 2.0來加載lua51和LuaInterface這兩個程序集。

![]()
1 <startup useLegacyV2RuntimeActivationPolicy="true">
2 <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
3 <runtime>
4 <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
5 <dependentAssembly>
6 <assemblyIdentity name="Lua51" publicKeyToken="32ab4ba45e0a69a1" culture="neutral"/>
7 <codeBase version="v2.0.50727" href="Lua51.dll"/>
8 </dependentAssembly>
9 <dependentAssembly>
10 <assemblyIdentity name="LuaInterface" publicKeyToken="32ab4ba45e0a69a1" culture="neutral"/>
11 <codeBase version="v2.0.50727" href="LuaInterface.dll"/>
12 </dependentAssembly>
13 </assemblyBinding>
14 </runtime>
15 </startup>
View Code
2.設置項目屬性->生成->目標平台-> x86
此時,坑已經填完,開始正式撸代碼了。
實現調用Lua和Url與Lua之間的映射
LuaApiRegister:這個類用於注冊Lua虛擬機,告訴Lua虛擬機如果尋找提供給Lua使用的API。

![]()
1 public class LuaApiRegister
2 {
3 private Lua _luaVM = null;
4
5 public LuaApiRegister(object luaAPIClass)
6 {
7 _luaVM = new Lua();//初始化Lua虛擬機
8 BindClassToLua(luaAPIClass);
9 }
10
11 private void BindClassToLua(object luaAPIClass)
12 {
13 foreach (MethodInfo mInfo in luaAPIClass.GetType().GetMethods())
14 {
15 foreach (Attribute attr in Attribute.GetCustomAttributes(mInfo, false))
16 {
17 if (!attr.ToString().StartsWith("System.") && !attr.ToString().StartsWith("__"))
18 {
19 _luaVM.RegisterFunction((attr as LuaFunction).getFuncName(), luaAPIClass, mInfo);
20 }
21 }
22 }
23 }
24
25 public void ExecuteFile(string luaFileName)
26 {
27 Logger.Info("開始執行腳本:" + luaFileName);
28 _luaVM.DoFile(luaFileName);
29 }
30
31 public void ExecuteString(string luaCommand)
32 {
33 try
34 {
35 _luaVM.DoString(luaCommand);
36 }
37 catch (Exception e)
38 {
39 Console.WriteLine("執行lua腳本指令:" + e.ToString());
40 }
41 }
42 }
43
44 public class LuaFunction : Attribute
45 {
46 private String FunctionName;
47
48 public LuaFunction(String strFuncName)
49 {
50 FunctionName = strFuncName;
51 }
52
53 public String getFuncName()
54 {
55 return FunctionName;
56 }
57 }
View Code
LuaScriptEngineer:這個類將會映射Url解析後的指令如何執行Lua腳本。其中包括定位Lua文件、設置變量、執行腳本和回收資源等。

![]()
1 public class LuaScriptEngineer
2 {
3 public string _scriptRoot;
4 private string _throwMessage = "";
5 private ReturnCode _returnCode = null;
6
7 public void ExecuteScript(string scriptName, NameValueCollection parameters)
8 {
9 if (!File.Exists(Path.Combine(_scriptRoot, scriptName + ".lua")))
10 {
11 throw new FileNotFoundException();
12 }
13
14 LuaApiRegister luaHelper = new LuaApiRegister(new TestLuaApiInterface());
15
16 InitLuaGlobalParameter(luaHelper, parameters);
17
18 ExecuteFile(luaHelper, Path.Combine(_scriptRoot, scriptName + ".lua"));
19
20 }
21
22 private void InitLuaGlobalParameter(LuaApiRegister luaHelper, NameValueCollection parameters)
23 {
24 foreach (var item in parameters.AllKeys)
25 {
26 luaHelper.ExecuteString("a_" + item.Trim() + " = \"" + parameters[item].Replace("\\", "\\\\") + "\";");
27 }
28 }
29
30 private void ExecuteFile(LuaApiRegister luaHelper, string luaFileName)
31 {
32 try
33 {
34 _throwMessage = "";
35 _returnCode = null;
36 luaHelper.ExecuteFile(luaFileName);
37 }
38 catch (ReturnCode returnCode)
39 {
40 _returnCode = returnCode;
41 }
42 catch (Exception ex)
43 {
44 _throwMessage = ex.Message;
45 }
46
47 if (_returnCode != null)
48 {
49 throw _returnCode;
50 }
51 else if (string.IsNullOrEmpty(_throwMessage))
52 {
53 Logger.Info("腳本執行完畢:" + luaFileName);
54 }
55 else if (!string.IsNullOrEmpty(_throwMessage))
56 {
57 Logger.Error(_throwMessage);
58 throw new Exception(_throwMessage);
59 }
60
61 }
62
63 public void Start()
64 {
65 _scriptRoot = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Script");
66
67 Logger.Info("腳本路徑-" + _scriptRoot);
68 if (!Directory.Exists(_scriptRoot))
69 {
70 Logger.Error("腳本根路徑不存在!");
71 }
72
73 if (File.Exists(_scriptRoot + "Startup.lua"))
74 {
75 Logger.Info("開始執行初始化腳本!");
76 try
77 {
78 ExecuteScript("Startup", new NameValueCollection());
79 }
80 catch
81 {
82 Logger.Error("啟動初始化腳本失敗!");
83 }
84 }
85 }
86
87 public void Stop()
88 {
89 try
90 {
91 Logger.Info("開始執行回收資源腳本!");
92 ExecuteScript("Cleanup", new NameValueCollection());//清空所調用的資源
93 }
94 catch
95 {
96 Logger.Warning("回收資源過程出錯");
97 }
98 }
99
100 }
View Code
LuaHttpServer:這個類做了一些簡單的Url校驗和調用LuaScriptEnginner。

![]()
1 class LuaHttpServer : HttpImplanter
2 {
3 LuaScriptEngineer _luaScriptEngineer = new LuaScriptEngineer();
4
5 #region HttpImplanter 成員
6
7 public void Start()
8 {
9 _luaScriptEngineer.Start();
10 }
11
12 public void Stop()
13 {
14 _luaScriptEngineer.Stop();
15 }
16
17 public void MakeHttpPrefix(System.Net.HttpListener server)
18 {
19 server.Prefixes.Clear();
20 server.Prefixes.Add("http://localhost:8082/");
21 Logger.Info("LuaHttpServer添加監聽前綴:http://localhost:8082/");
22 }
23
24 public ReturnCode ProcessRequest(System.Net.HttpListenerContext context)
25 {
26 UrlHelper urlHelper = new UrlHelper(context.Request.Url);
27 CommandResult result = urlHelper.ParseResult;
28 if (urlHelper.ParseResult == CommandResult.Success)
29 {
30 try
31 {
32 _luaScriptEngineer.ExecuteScript(urlHelper.ScriptName, urlHelper.Parameters);
33 return new ReturnCode((int)CommandResult.Success, EnumHelper.GetEnumDescription(CommandResult.Success));
34 }
35 catch (FileNotFoundException fileNotFoundException)
36 {
37 return new ReturnCode((int)CommandResult.NoExistsMethod, EnumHelper.GetEnumDescription(CommandResult.NoExistsMethod));
38 }
39 catch (ReturnCode returnCode)
40 {
41 return returnCode;
42 }
43 catch (Exception ex)
44 {
45 return new ReturnCode((int)CommandResult.ExcuteFunctionFailed, EnumHelper.GetEnumDescription(CommandResult.ExcuteFunctionFailed));
46 }
47 }
48 return new ReturnCode((int)result, EnumHelper.GetEnumDescription(result));
49 }
50
51 public byte[] CreateReturnResult(System.Net.HttpListenerContext context, ReturnCode result)
52 {
53 string responseString = string.Format("code={0}&msg={1}&request={2}",
54 result.Code,
55 result.Message,
56 context.Request.Url.ToString()
57 );
58
59 return System.Text.Encoding.UTF8.GetBytes(responseString);
60 }
61
62 #endregion
63 }
View Code
TestLuaApiInterface:提供給Lua可以調用的接口。這裡主要是因為C#本身比Lua的可操作性要高。固定的一些功能還是要轉回C#來做,Lua只做一些業務組合。

![]()
1 public class TestLuaApiInterface
2 {
3 [LuaFunction("Test")]
4 public void Test(string msg)
5 {
6 Logger.Info("TestLuaApiInterface:" + msg);
7 }
8 }
View Code
Test.lua:這只是個測試腳本,很簡單,只執行了一下Test方法,注意這裡的參數param前面有 a_ ,是因為區別外部參數與Lua內部參數的一種手段,並非一定要這樣。這個a_也是在LuaApiScripEngineer裡自動添加的。
Test(a_param);

模仿MVC中的C
其實也不只是C,有一點點Route的東西,因為這裡需要解析Url定位Controller。並且使用的是 vNext 的方式,類名以Controller結尾即可,不用繼承任何類。
1.創建一個Route

![]()
1 public class TestMVCRoute
2 {
3 public List<string> RegisterRoutes()
4 {
5 return new List<string>() { "{controller}/{action}" };
6 }
7 }
View Code
2.創建一個Controller

![]()
1 public class TestMVCController
2 {
3 public string Test()
4 {
5 return "TestMVCController";
6 }
7 }
View Code
3.擴展一個 MVCHttpServer
需要注意的是,Start方法中處理了Route(偷懶所以只取FirstOrDefault),只是檢索出controller和action的位置而已。
CreateReturnResult方法則是真正的映射邏輯。

![]()
1 class MVCHttpServer : HttpImplanter
2 {
3 string _route = null;
4 int _controllerIndex = -1;
5 int _actionIndex = -1;
6
7 #region HttpImplanter 成員
8
9 public void Start()
10 {
11 _route = new TestMVCRoute().RegisterRoutes().FirstOrDefault();
12
13 var routes = _route.Split('/');
14 for (int i = 0; i < routes.Length; i++)
15 {
16 if (routes[i] == "{controller}")
17 {
18 _controllerIndex = i;
19 }
20 else if (routes[i] == "{action}")
21 {
22 _actionIndex = i;
23 }
24 }
25 }
26
27 public void Stop()
28 {
29 //nothing to do
30 }
31
32 public void MakeHttpPrefix(System.Net.HttpListener server)
33 {
34 server.Prefixes.Clear();
35 server.Prefixes.Add("http://localhost:8083/");
36 Logger.Info("MVCHttpServer添加監聽前綴:http://localhost:8083/");
37 }
38
39 public ReturnCode ProcessRequest(System.Net.HttpListenerContext context)
40 {
41 return new ReturnCode((int)CommandResult.Success, EnumHelper.GetEnumDescription(CommandResult.Success));
42 }
43
44 public byte[] CreateReturnResult(System.Net.HttpListenerContext context, ReturnCode result)
45 {
46 string responseString = string.Empty;
47 var splitedPath = context.Request.Url.AbsolutePath.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
48 var controllerName = splitedPath[_controllerIndex] + "Controller";
49 var actionName = splitedPath[_actionIndex];
50
51 var type = Type.GetType("HttpListenerDemo.Test." + controllerName);
52 if (type != null)
53 {
54 object obj = Activator.CreateInstance(type);
55 responseString = obj.GetType().GetMethod(actionName).Invoke(obj, null) as string;
56 }
57
58 return System.Text.Encoding.UTF8.GetBytes(responseString);
59 }
60
61 #endregion
62 }
View Code

我有一個想法
由於自己一直從事C/S方面的工作,所以我想做個Web項目,可以應用到各種技術,並且是最新的,這期間肯定會遇到重重阻礙,但這並不能影響我前進的步伐。
目前計劃使用到的技術包括並不僅限於 ASP.NET MVC 6、SignalR、Web API 3.0、Boostrap、jQuery、Redis。
當然其中一定會有一些創新或者好玩的東西出現。
如果你想加入或者你對此感興趣,歡迎聯系我。
最後,又到了大家最喜愛的公布源碼環節 ^_^
http://git.oschina.net/doddgu/WebServerDemo