MicroHttpServer - 用100行C#寫一個HTTP Server
19 |
有個點子,想在WinForm上跑程式模擬出Web Server功能,讓Browser或程式可以透過HTTP協定與其溝通。既然想到,就動手做看看囉!
HTTP Server絕大部分的核心功能,其實都可用.NET搞定: 用TcpListener接受特定Port連入的TCP連線,取得NetworkStream,以StreamReader、StreamWriter讀取及寫入資料... .NET BCL真是應有盡有!相較之下,以前那種基礎元件跟函式庫都得自己張羅的時代,只能用茹毛飲血來形容。
有了BCL的加持,配合兩個自訂類別封裝Request、Response,只花了不到100行C#,就組出一個可以接受HTTP Request,傳回結果的超迷你HTTP Server!
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace DarkHttpServer
{
//Reuquest物件
public class CompactRequest
{
public string Method, Url, Protocol;
public Dictionary<string, string> Headers;
//傳入StreamReader,讀取Request傳入的內容
public CompactRequest(StreamReader sr)
{
//第一列格式如: GET /index.html HTTP/1.1
string firstLine = sr.ReadLine();
string[] p = firstLine.Split(' ');
Method = p[0];
Url = (p.Length > 1) ? p[1] : "NA";
Protocol = (p.Length > 2) ? p[2] : "NA";
//讀取其他Header,格式為HeaderName: HeaderValue
string line = null;
Headers = new Dictionary<string, string>();
while (!string.IsNullOrEmpty(line = sr.ReadLine()))
{
int pos = line.IndexOf(":");
if (pos > -1)
Headers.Add(line.Substring(0, pos),
line.Substring(pos + 1));
}
}
}
//Response物件
public class CompactResponse
{
//預設200, 404, 500三種回應
public class HttpStatus
{
public static string Http200 = "200 OK";
public static string Http404 = "404 Not Found";
public static string Http500 = "500 Error";
}
public string StatusText = HttpStatus.Http200;
public string ContentType = "text/plain";
//可回傳Response Header
public Dictionary<string, string> Headers
= new Dictionary<string, string>();
//傳回內容,以byte[]表示
public byte[] Data = new byte[] { };
}
//簡陋但堪用的HTTP Server
public class MicroHttpServer
{
private Thread serverThread;
TcpListener listener;
//呼叫端要準備一個函數,接收CompactRequest,回傳CompactResponse
public MicroHttpServer(int port,
Func<CompactRequest, CompactResponse> reqProc)
{
IPAddress ipAddr = IPAddress.Parse("127.0.0.1");
listener = new TcpListener(ipAddr, port);
//另建Thread執行
serverThread = new Thread(() =>
{
listener.Start();
while (true)
{
Socket s = listener.AcceptSocket();
NetworkStream ns = new NetworkStream(s);
//解讀Request內容
StreamReader sr = new StreamReader(ns);
CompactRequest req = new CompactRequest(sr);
//呼叫自訂的處理邏輯,得到要回傳的Response
CompactResponse resp = reqProc(req);
//傳回Response
StreamWriter sw = new StreamWriter(ns);
sw.WriteLine("HTTP/1.1 {0}", resp.StatusText);
sw.WriteLine("Content-Type: " + resp.ContentType);
foreach (string k in resp.Headers.Keys)
sw.WriteLine("{0}: {1}", k, resp.Headers[k]);
sw.WriteLine("Content-Length: {0}", resp.Data.Length);
sw.WriteLine();
sw.Flush();
//寫入資料本體
s.Send(resp.Data);
//結束連線
s.Shutdown(SocketShutdown.Both);
ns.Close();
}
});
serverThread.Start();
}
public void Stop()
{
listener.Stop();
serverThread.Abort();
}
}
}
好了,有了MicroHttpServer類別,我們來寫一個小小的Console Application,做一個將特定目錄下JPG圖檔以網頁方式呈現的迷你Web Server當作應用範例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace DarkHttpServer
{
class Program
{
static string path = @"C:\temp\arts";
static void Main(string[] args)
{
MicroHttpServer mhs = new MicroHttpServer(1688,
(req) =>
{
if (req.Url == "/")
return ListPhoto(req);
else if (req.Url.EndsWith(".jpg"))
return GetJpeg(
Path.Combine(path, req.Url.TrimStart('/')));
else return new CompactResponse()
{
StatusText = CompactResponse.HttpStatus.Http500
};
});
Console.Write("Press any key to stop...");
Console.Read();
mhs.Stop();
}
//列出圖檔,組成網頁傳回
static CompactResponse ListPhoto(CompactRequest req)
{
StringBuilder sb = new StringBuilder();
sb.Append(@"
<html><head><title>Index</title>
<style type='text/css'>img {
width: 160px; height: 120px; float: left;
margin: 10px;
}</style>
</head><body>
");
sb.Append("");
foreach (string file in
Directory.GetFiles(path, "*.jpg"))
sb.AppendFormat("<img src='{0}' />",
Path.GetFileName(file));
sb.Append("</body></html>");
return new CompactResponse()
{
ContentType = "text/html",
Data = Encoding.UTF8.GetBytes(sb.ToString())
};
}
//取得圖檔
static CompactResponse GetJpeg(string file)
{
if (File.Exists(file))
return new CompactResponse()
{
ContentType = "image/jpeg",
Data = File.ReadAllBytes(file)
};
else //找不到檔案時傳回HTTP 404
return new CompactResponse()
{
StatusText = CompactResponse.HttpStatus.Http404
};
}
}
}
用IE連上httq://localhost:1688,薑薑薑薑! 小閃光的手工藝品在他爹的"手工藝品"上被展示出來了。(這證明我的手也很巧呀! XD)
請大家跟我一起高呼: .NET好威呀!
Comments
# by 圍觀路人A
.NET好威呀!
# by 小賤健
黑大也好威啊... m(_._)m
# by 小熊子
能否用 WebDev.WebServer 就可以有一個帶著走的 IIS ?
# by dmwc
To:小熊子 WebDev.WebServer 是可以帶著走的,不過有些Dll要先註冊好,或是那台電腦要先裝過 .Net SDK 另外 WebDev 有些 httpHandlers 會沒作用,沒辦法完全取代 IIS ,不過偶爾頂著用還 OK
# by 路人喵
好像自己寫個socket server的感覺哦~~
# by 閒人A
-O- 黑大,怎麽處理post method呢....
# by Jeffrey
to 閒人A, POST時,Client端送回的內容也可以在CompactRequest裡用StreamReader讀出來,不過要處理的東西就多了,這種情境下還要不要自己做輪子是個好問題 ^__^
# by 閒人A
to 黑大,因爲軟体要提供HTTP介面給PHP讀取... 所以我在做輪子 T_T 剛剛根據您的mhs寫了讀取QueryString的fun...調試中 Method = p[0]; parseUriQuery(p[1]); public void parseUriQuery(string QueryString) { int Index = QueryString.IndexOf("?"); if (Index > 0) { string name, value; int start, max; QueryString = QueryString.Substring(Index + 1); max = QueryString.Length; start = 1; while (start > 0) { start -= 1; Index = QueryString.IndexOf("=", start); name = QueryString.Substring(start, Index - start); start = QueryString.IndexOf("&", Index) + 1; if (start > 0) { value = QueryString.Substring(Index + 1, start - Index - 2); start += 1; } else { value = QueryString.Substring(Index + 1); } Console.WriteLine("{0}:{1}", name, value); } } }
# by 閒人A
貌似正則更便捷... ^( ◕‿‿ ◕ )^ ︶︶ public static void parseUriQuery(string QueryString) { int Index = QueryString.IndexOf("?"); if (Index > 0) { Regex r = new Regex(@"[\?\&]([^\?\&]+)=([^\?\&]+)", RegexOptions.IgnoreCase); Match m = r.Match(QueryString); while (m.Success) { Console.WriteLine("{0}:{1}", m.Groups[1].ToString(), m.Groups[2].ToString()); m = m.NextMatch(); } } }
# by Jeffrey
to 閒人A, GJ! QueryString的參數值會有URLEncoding,如果會傳中文或空白等特殊符號,記得要還原,.NET有個HttpUtility.ParseQueryString,我猜可以借來省工: http://blog.darkthread.net/post-2010-01-06-parsequerystring.aspx
# by litfal
雖然是舊文章了,不過當時也有HttpListener可以用吧? http://msdn.microsoft.com/zh-tw/library/system.net.httplistener%28v=vs.80%29.aspx 正在找製作網頁controller,遠端控制本機程式的好方法。 寫過mvc後,覺得要重新做route、action link、網頁樣板等這些輪子好麻煩...
# by Jeffrey
to litfal, 如果屬於API性質,可以參考Self-Hosting ASP.NET Web API ( http://blog.darkthread.net/post-2013-06-04-self-host-web-api.aspx )
# by Litfal
Self-Hosting WCF我有考慮過,可惜就是有UI需求。 不然就是要做出靜態網頁介面,然後僅呼叫相應的Web API,但既囉嗦、可攜性也很差。 mvc的Razor+model 寫網頁介面真的很方便。 雖然可以依model與action需求自己去刻response, 但不直覺而且蠻辛苦的。 不知道是不是這部分的需求少,找不到類似的輪子可以用。 目前看到有這功能而且web介面最完整且精緻的,就屬emule了。
# by 熊大
用chrome會當掉@@" 但是IE不會.
# by hank
請問如果有鑲入一個form那回傳值該如何接收?
# by Jeffrey
to hank, 表單回傳結果會走 POST Method 跟 application/x-www-form-urlencoded 編碼,稍稍複雜一些,如果不堅持一定要自己來,我推薦改用 NancyFx http://blog.darkthread.net/post-2016-10-16-nancyfx.aspx 一樣輕巧,但節省很多時間。
# by 路人
這可以用其他電腦連嗎
# by Jeffrey
to 路人, 可以。另外,如果不堅持從頭到尾徒手打造,推薦改用NancyFx http://blog.darkthread.net/post-2016-10-16-nancyfx.aspx
# by wcc
給需要的人參考: 簡單的支援POST (content-type: application/json): if (Headers.ContainsKey("content-length")) { int ContentLength = 0; if (int.TryParse(Headers["content-length"], out ContentLength) && ContentLength > 0) { char[] c = new char[ContentLength]; sr.Read(c, 0, ContentLength); //Console.WriteLine(c); string contents = new string(c); log.Debug("Content: " + contents); if (Headers["content-type"]=="application/json") { Body = contents; } } }