MicroHttpServer - 用100行C#寫一個HTTP Server

有個點子,想在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好威呀!

歡迎推文分享:
Published 14 August 2010 09:23 AM 由 Jeffrey
Filed under: , ,
Views: 38,277



意見

# 圍觀路人A said on 13 August, 2010 08:29 PM

.NET好威呀!

# 小賤健 said on 13 August, 2010 08:41 PM

黑大也好威啊... m(_._)m

# 小熊子 said on 13 August, 2010 09:30 PM

能否用 WebDev.WebServer 就可以有一個帶著走的 IIS ?

# dmwc said on 14 August, 2010 09:46 AM

To:小熊子

WebDev.WebServer 是可以帶著走的,不過有些Dll要先註冊好,或是那台電腦要先裝過 .Net SDK

另外 WebDev 有些 httpHandlers 會沒作用,沒辦法完全取代 IIS ,不過偶爾頂著用還 OK

# 路人喵 said on 14 August, 2010 01:15 PM

好像自己寫個socket server的感覺哦~~

# 閒人A said on 21 January, 2011 06:32 PM

-O-

黑大,怎麽處理post method呢....

# Jeffrey said on 21 January, 2011 09:17 PM

to 閒人A, POST時,Client端送回的內容也可以在CompactRequest裡用StreamReader讀出來,不過要處理的東西就多了,這種情境下還要不要自己做輪子是個好問題 ^__^

# 閒人A said on 23 January, 2011 04:00 PM

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);

               }

           }

       }

# 閒人A said on 23 January, 2011 04:33 PM

貌似正則更便捷...

^( ◕‿‿ ◕ )^

     ︶︶

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();

               }

           }

       }

# Jeffrey said on 23 January, 2011 05:57 PM

to 閒人A, GJ! QueryString的參數值會有URLEncoding,如果會傳中文或空白等特殊符號,記得要還原,.NET有個HttpUtility.ParseQueryString,我猜可以借來省工: blog2.darkthread.net/post-2010-01-06-parsequerystring.aspx

# litfal said on 20 August, 2013 04:12 PM

雖然是舊文章了,不過當時也有HttpListener可以用吧?

msdn.microsoft.com/.../system.net.httplistener%28v=vs.80%29.aspx

正在找製作網頁controller,遠端控制本機程式的好方法。

寫過mvc後,覺得要重新做route、action link、網頁樣板等這些輪子好麻煩...

# Jeffrey said on 21 August, 2013 09:29 PM

to litfal, 如果屬於API性質,可以參考Self-Hosting ASP.NET Web API ( blog2.darkthread.net/post-2013-06-04-self-host-web-api.aspx )

# Litfal said on 24 August, 2013 08:16 AM

Self-Hosting WCF我有考慮過,可惜就是有UI需求。

不然就是要做出靜態網頁介面,然後僅呼叫相應的Web API,但既囉嗦、可攜性也很差。

mvc的Razor+model 寫網頁介面真的很方便。

雖然可以依model與action需求自己去刻response, 但不直覺而且蠻辛苦的。

不知道是不是這部分的需求少,找不到類似的輪子可以用。

目前看到有這功能而且web介面最完整且精緻的,就屬emule了。

# 熊大 said on 28 May, 2015 10:21 AM

用chrome會當掉@@" 但是IE不會.

# hank said on 29 August, 2017 05:55 AM

請問如果有鑲入一個form那回傳值該如何接收?

# Jeffrey said on 29 August, 2017 08:35 AM

to hank, 表單回傳結果會走 POST Method 跟 application/x-www-form-urlencoded 編碼,稍稍複雜一些,如果不堅持一定要自己來,我推薦改用 NancyFx blog2.darkthread.net/post-2016-10-16-nancyfx.aspx 一樣輕巧,但節省很多時間。

# 路人 said on 11 October, 2017 09:34 PM

這可以用其他電腦連嗎

# Jeffrey said on 12 October, 2017 01:22 AM

to 路人, 可以。另外,如果不堅持從頭到尾徒手打造,推薦改用NancyFx blog2.darkthread.net/post-2016-10-16-nancyfx.aspx

# wcc said on 01 November, 2017 10:27 PM

給需要的人參考:

簡單的支援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;

}

                   }

               }

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<August 2010>
SunMonTueWedThuFriSat
25262728293031
1234567
891011121314
15161718192021
22232425262728
2930311234
 
RSS
創用 CC 授權條款
【廣告】
twMVC
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication