此篇是《【輪子狂魔】拋棄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 CodeUrl映射執行方法
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 CodeUrl映射執行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 Code2.設置項目屬性->生成->目標平台-> 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 Code2.創建一個Controller
1 public class TestMVCController 2 { 3 public string Test() 4 { 5 return "TestMVCController"; 6 } 7 } View Code3.擴展一個 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