有個點子,想在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; } } }

Post a comment