陳鍾誠

Version 1.0

C# 網路程式:HTTP 程式設計 (1) – Server

HTTP 協定簡介

HTTP 協定是 Web 的基礎,Web 是一個典型的 Client-Server 架構,主要由 HTTP+URL+HTML 所組成。

Web Server (網站伺服器) 是 WWW 網路的基礎, 1991 年 Tim Burner Lee 發明 HTML 與 URL 後,就自己寫了第一個 Web Server,導至後來 Web 網路的興起,因此、Tim Burner Lee被尊稱為 WWW 之父,然而、對於一般程式設計人員而言, Web Server 的設計方法卻是個難解的謎,本篇文章將以一個簡單而完整的 Java 程式,讓大家了解 Web Server 的設計原理。

一個最簡單的 Web Server 之功能包含下列三個步驟:

  • 步驟一 : 接收瀏覽器所傳來的網址。
  • 步驟二 : 取出相對應的檔案。
  • 步驟三 : 將檔案內容傳回給瀏覽器。

然而、在這個接收與傳回的過程中,所有的資訊都必須遵照固定的格式,規範這個接收/傳送格式的協定,稱為超文字傳送協定 (Hyper Text Transfer Protocol),簡稱為 HTTP 協定。

HTTP 協定格式的基礎,乃是建構在網址 URL 上的傳輸方式,早期只能用來傳送簡單的 HTML 檔案,後來經擴充後也可以傳送 其他類型的檔案,包含 影像、動畫、簡報、Word 文件等。

在本文中,我們將先簡介 HTTP 協定的訊息內容,然後在介紹如何以 Java 語言實作 HTTP 協定,以建立一個簡單的 Web Server。

HTTP 表頭:

當你在瀏覽器上打上網址(URL)後,瀏覽器會傳出一個HTTP訊息給對應的 Web Server,Web Server 再接收到這個訊息後, 根據網址取出對應的檔案,並將該檔案以 HTTP 格式的訊息傳回給瀏覽器,以下是這個過程的一個範例。

豬小弟上網,在瀏覽器中打上 http://ccc.kmit.edu.tw/index.htm,於是、瀏覽器傳送下列訊息給 ccc.kmit.edu.tw 這台電腦。

GET /index.htm HTTP/1.0
Accept: image/gif, image/jpeg, application/msword, */*
Accept-Language: zh-tw
User-Agent: Mozilla/4.0
Content-Length: 
Host: ccc.kmit.edu.tw
Cache-Control: max-age=259200
Connection: keep-alive

當 ccc.kmit.edu.tw 電腦上的 Web Server 程式收到上述訊息後,會取出指定的路徑 /index.htm ,然後根據預設的網頁根目錄 (假設為 c:\web),合成一個 c:\web\index.htm 的絕對路徑,接著從磁碟機中取出該檔案,並傳回下列訊息給豬小弟的瀏覽器。

HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 438

<html>
  ....
</html>

其中第一行 HTTP/1.0 200 OK 代表該網頁被成功傳回,第二行 Content-Type: text/html 代表傳回文件為 HTML 檔案, Content-Length: 438 代表該 HTML 檔案的大小為 438 位元組。

HelloServer: 永遠傳回 Hello 的 WebServer

檔案 HelloServer.cs

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.IO;

public class HttpServer
{
    public static void Main()
    {
    	IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 80);

    	Socket newsock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

    	newsock.Bind(ipep);
    	newsock.Listen(10);
    
    	while(true)
    	{
    	 Socket client = newsock.Accept();
    	 IPEndPoint clientep = (IPEndPoint) client.RemoteEndPoint;
    	
    	 // create a new thread and then receive message.
    	 HttpListener listener = new HttpListener(client);
    	 Thread thread = new Thread(new ThreadStart(listener.run));
    	 thread.Start();
    	}
//	  newsock.Close();
   }
}

public class HttpListener {
    Socket socket;

    public HttpListener(Socket s)
    {
    	socket = s;
    }

    public void run() 
    {
    	String msg = "Hello!";
    	String helloMsg = @"HTTP/1.0 200 OK\nContent-Type: text/plain\nContgent-Length: "+msg.Length+"\n\n"+msg;

    	NetworkStream stream = new NetworkStream(socket);
    	StreamReader reader = new StreamReader(stream);
    	String header = "";
    	while (true) 
    	{
    	 String line = reader.ReadLine();
    	 Console.WriteLine(line);
    	 if (line.Trim().Length==0)
    	 break;
    	 header += line+"\n";
    	}
    	socket.Send(Encoding.UTF8.GetBytes(helloMsg));
    	socket.Close();
    }
}

完整的 Web Server

專案程式下載:https://dl.dropbox.com/u/101584453/cs/code/WebServer.zip

請注意:此範例必須在專案中加入 System.Web 套件引用。

根據 HTTP 協定,我們以 C# 實作了一個 Web Server 程式 (WebServer.cs),該程式是利用一個稱為 Socket 的物件來實作的,這個物件位於 C# 的網路函式庫 System.Net 中。

Socket 是根據博克萊 (U.C.Berkley) 大學早期發展的 Socket 概念寫成的,其設計理念是是將網路傳輸類比成檔案讀取與寫入 (傳送的動作被視為是寫入/接收的動作被視為是讀取),如此、傳送與接收就簡化為程式人員比較容易懂的 讀取與寫入,降低了網路程式的學習困難度。

要使用 Socket 的方式寫網路程式,首先要登記網路的埠號 (port),將該 port 占領下來,以防止其他程式使用該 port 而互相干擾,HTTP 協定所預設使用的是 port 80。

一但完成登記,就可以開始接受瀏覽器的請求,並根據請求回傳檔案訊息,以下程式為其 (接收/傳送) 程序的核心程式。

這個最簡單版以 Socket 的方式,不斷讀取資料直到發現有一空白行即結束,然而、這樣的程式是過度簡化的結果,無法處理有 POST 訊息的狀況,然而、何謂 POST 訊息呢 ?

所謂 POST 訊息、乃是 HTML 為了傳遞較大量的填表資料,所發展出來的一種訊息格式,以下是POST訊息的一個範例:

POST /getMsg.asp HTTP/1.0
Accept: image/gif, image/jpeg, application/msword, */*
Accept-Language: zh-tw
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/4.0
Content-Length: 66
Host: ccc.kmit.edu.tw
Cache-Control: max-age=259200
Connection: keep-alive

user=ccc&password=1234&msg=Hello+%21+%0D%0AHow+are+you+%21%0D%0A++

其中的HTTP的訊息開頭以 POST 取代原來的 GET ,並且多了一個 Content-Length:66 的欄位,該欄位指示了訊息結尾會有 66 個位元組的填表資料,這些資料會被編碼成特輸的格式以利在網路上傳遞。

一但取得了瀏覽器傳來的 GET 或 POST 訊息後,我們就可以根據其訊息,決定回應的方式,在 WebServer.java 中,我們只是單純的將對應的檔案取出,並附在回應的訊息表頭後傳回,其程式碼如下。

以上就是 Web Server 的簡單設計方式,若想了解更多細節,請直接閱讀 WebServer.java 檔並執行之,執行時請安裝好 Visual Studio 後,並於 WebServer.cs 的存檔路徑上打 csc WebServer.cs, 之後再打 WebServer 即可啟動,其執行畫面如下:

檔案: WebServer.cs

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.IO;
using System.Web;

public class WebServer
{
   public static void Main()
   {
      IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 8085);

      Socket newsock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

      newsock.Bind(ipep);
      newsock.Listen(10);

      while(true)
      {
         Socket client = newsock.Accept();
         IPEndPoint clientep = (IPEndPoint) client.RemoteEndPoint;

         // create a new thread and then receive message.
         HttpListener listener = new HttpListener(client);
         Thread thread = new Thread(new ThreadStart(listener.run));
         thread.Start();
      }
   }
}

class HttpListener 
{
    String[] map ={"mpeg=video/mpeg", "mpg=video/mpeg", "wav=audio/x-wav", "jpg=image/jpeg", 
"gif=image/gif", "zip=application/zip", "pdf=application/pdf", "xls=application/vnd.ms-excel", 
"ppt=application/vnd.ms-powerpoint", "doc=application/msword", "htm=text/html", 
"html=text/html", "css=text/plain", "vbs=text/plain", "js=text/plain", "txt=text/plain", 
"java=text/plain"};
    Socket socket;
    NetworkStream stream;
    String header;
    String root = ".";

    public HttpListener(Socket s)
    {
        socket = s;
    }

    public void run() 
    {
        stream = new NetworkStream(socket);
        request();
        response();
        stream.Close();
    }

    public void send(String str) {
        socket.Send(Encoding.UTF8.GetBytes(str));
    }

    public static String innerText(String pText, String beginMark, String endMark)
    {
        int beginStart = pText.IndexOf(beginMark);
        if (beginStart < 0) return null;
        int beginEnd = beginStart + beginMark.Length;
        int endStart = pText.IndexOf(endMark, beginEnd);
        if (endStart < 0) return null;
        return pText.Substring(beginEnd, endStart - beginEnd);
    }

    public void request()
    {
    	try {
    	 StreamReader reader = new StreamReader(stream);
    	 header = "";
    	 while (true)
    	 {
    	 String line = reader.ReadLine();
    	 Console.WriteLine(line);
    	 if (line.Trim().Length == 0)
    	 break;
    	 header += line + "\n";
    	 }
    	} catch {
    	 Console.WriteLine("request error!");
    	}
    }

    void response() 
    {
      try 
      {
        Console.WriteLine("========response()==========");
        String path = innerText(header, "GET ", "HTTP/").Trim(); // 取得檔案路徑 : GET 版。
        HttpUtility.UrlDecode(path);
        String fullPath = root+path;
        FileInfo info = new FileInfo(fullPath);
        if (!info.Exists) 
            throw new Exception("File not found !");
        send("HTTP/1.0 200 OK\n");
        send("Content-Type: "+type(fullPath)+"\n");
        send("Content-Length: "+info.Length+"\n");
        send("\n");
        byte[] buffer = new byte[4096];
        FileStream fileStream = File.OpenRead(fullPath);
        while (true) 
        {
            int len = fileStream.Read(buffer, 0, buffer.Length);
            socket.Send(buffer, 0, len, SocketFlags.None);
            if (len < buffer.Length) break;
        }
        fileStream.Close();
      } catch {
    	try {
    	 send("HTTP/1.0 404 Error\n");
    	 send("\n");
    	} catch {
    	 Console.WriteLine("Send Error Msg fail!");
    	}
      }
    }

    String type(String path)
    {
        String type = "*/*";
        path = path.ToLower();
        for (int i = 0; i < map.Length; i++)
        {
            String[] tokens = map[i].Split('=');
            String ext = tokens[0], mime = tokens[1];
            if (path.EndsWith("." + ext)) type = mime;
        }
        return type;
    }
}

參考文獻