Web服務器是Web資源的宿主,它需要處理用戶端浏覽器的請求,並指定對應的Web資源返回給用戶,這些資源不僅包括HTML文件,JS腳本,JPG圖片等,還包括由軟件生成的動態內容。為了滿足上述需求,一個完整的Web服務器工作流程:
1) 服務器獲得浏覽器通過TCP/IP連接向服務器發送的http請求數據包。
2) HTTP請求經過Web服務器的HTTP解析引擎分析得出請求方法、資源地址等信息,然後開始處理。
3) 對於靜態請求,則在服務器上查詢請求url路徑下文件,並返回(如果未找到則返回404 No Found)。
4) 涉及動態請求,如CGI, AJAX, ASP等,則根據http方法,采取不同處理。
Web服務器的核心由C# Socket通訊,http解析引擎,靜態資源文件查找,動態數據接收和發送4部分組成,本節因為個人編寫進度原因主要實現前3個部分(即能夠查詢靜態資源的Web服務器),動態數據處理因為涉及的處理方式CGI,AJAX,ASP的方法不同,後續完成後在總結相關知識。
1. C# Socket通訊
C# Socket通過對TCP/IP協議進行封裝,用於實現滿足TCP通訊的API。在B/S架構中,服務器端的處理和C/S連接基本相同,主要工作包含:創建Socket套接字,監聽連接,建立連接,獲得請求,處理並返回數據,關閉連接等。
程序入口函數,采用輪詢方式實現對客戶端請求的監聽。
//創建監聽線程
Thread Listen_thread = new Thread(socket_listen);
Listen_thread.IsBackground = false;
Listen_thread.Start();
監聽線程,創建Socket套接字,綁定並監聽指定端口,等待連接建立,連接建立後,考慮到網頁請求高並發的特性,采用另開線程的方式來處理建立的連接,從而實現並發服務器模式。
Socket server_socket = null;
//監聽的IP地址和端口 作為服務器,綁定的只能是本機Ip地址或者環回地址(不能與系統其它進程端口沖突)
//如果綁定為本節IP地址,局域網下其它設備可以通過http://host:port來訪問當前服務器
string host = "127.0.0.1";
int port = 3000;
IPAddress ip = IPAddress.Parse(host);
IPEndPoint ipe = new IPEndPoint(ip, port);
//新建Socket套接字,綁定在指定的端口並開始監聽
server_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
server_socket.Bind(ipe);
server_socket.Listen(100);
Console.WriteLine("Server Binding at " + host + ":" + port.ToString() +"...");
Console.WriteLine("Wait for connect....");
while (true)
{
Socket CurrentSocket;
//三次握手成功,新建一個連接
CurrentSocket = server_socket.Accept();
Console.WriteLine("New TCP Socket Create...");
//單開一個線程用來處理服務器收發, 並發服務器模式
ParameterizedThreadStart tStart = new ParameterizedThreadStart(socket_process);
Thread process_thread = new Thread(tStart);
process_thread.IsBackground = false;
process_thread.Start(CurrentSocket);
}
連接處理,socket通訊處理主要負責接收連接產生的數據,並將http引擎處理後數據提交給客戶端浏覽器。
Socket CurrentSocket = (Socket)obj;
try
{
string recvStr = "";
byte[] recvBytes = new byte[2000];
int length;
//獲得當前Socket連接傳輸的數據,並轉換為ASCII碼格式
length = CurrentSocket.Receive(recvBytes, recvBytes.Length, 0);
recvStr = Encoding.ASCII.GetString(recvBytes, 0, length);
//http引擎處理,返回獲得數據
byte[] bs = http_engine(recvStr, length);
//通過socket發送引擎處理後數據
CurrentSocket.Send(bs, bs.Length, 0);
Console.WriteLine("File Send Finish, Socket Close....\r\n");
//關閉socket連接
CurrentSocket.Close();
}
catch (Exception exception)
{
Console.WriteLine(exception);
CurrentSocket.Close();
}
C# Socket通訊架構的實現和C/S結構沒有什麼區別,如果了解過Socket可以輕松實現上述socket通訊架構。 不過下面這部分將講述Web服務器的實現核心--http解析引擎,這也是B/S架構和C/S架構中服務器端最大的區別。
2. http解析引擎
Web服務器主要實現對浏覽器請求數據包的處理,並返回指定的http資源或者數據,這些都是由http解析引擎實現,在寫http解析引擎之前,我們要知曉接收到的數據才能進行後續的處理,這裡提供通過WireShark抓取的http請求包:
雖然HTTP請求包的內容很多,但因為目前實現的功能較少,所以關注的只有http報文起始行就可以,而首部字段可以直接丟棄不處理,後續如果使用認證機制,如白名單,黑名單過濾,帳號/密碼保護,資源權限管理等,首部仍然要處理。
對於http報文起始行, 內部以space隔開,並以'\r\n'作為結尾與首部隔開。其中GET:HTTP方法, '/' :資源路徑url, HTTP/1.1:協議版本,參照http權威指南的內容, HTTP協議的常見方法有GET, PUT, DELETE, POST, HEAD這5種,本節中的靜態服務器主要涉及到GET方法。了解了需要如何解析HTTP請求報文後,我們先定義一個HTTP報文解析結構,用於存儲到解析的信息。
public class HTTPPrase
{
//http方法
public string http_method;
//http資源
public string url;
//http版本號
public string version;
//url解析的請求網頁類型
public string type;
};
下面我們就要開始利用C#提供的String方法來截取http報文來實現上述結構體內參數的初始化。
int pos;
//根據\r\n截斷,獲取http報文首部並轉換為小寫,方便後續處理
//Get / HTTP/1.1/r/n
pos = str.IndexOf("\r\n");
string str_head = str.Substring(0, pos);
str_head = str_head.ToLower();
//根據' '來截斷起始行,並賦值給對應參數
string[] arr = Regex.Split(str_head, @"\s+");
HTTPServer.HTTPPrase http_head = new HTTPServer.HTTPPrase();
http_head.http_method = arr[0]; // "Get"
http_head.url = arr[1]; // "/"
http_head.version = arr[2]; // "HTTP/1.1"
//判斷是否有通過ajax要求獲得或者提交的動態數據
http_head.ajax_status = str_head.IndexOf(".ajax") != -1 ? true : false;
byte[] bs = http_head.ajax_status == true ? ajax_process(http_head, str) : static_process(http_head, str);
return bs;
下面就可以把數據提交給後端接口,進行處理。因為動態網頁處理需要網頁端和後端相互的配合,工作量較大,因此本節主要闡述靜態網頁請求的實現。局域網Web請求一般是通過ip+port的模式直接訪問服務器端,所以第一個接收到的請求的url為‘/',這時我們需要將它映射到服務端定位的訪問主頁,目前設置為index.html,對於其它請求,url的值一般是'/xxx/xxx.js", '/xxx/xxx.jpg"等,而在服務器中讀取時我們需要定義絕對地址,所以還要在前面添加資源存儲的根地址,目前將程序當前所在文件夾+html作為資源的根地址,而且操作系統存儲的數據路徑為\xxx\xxx.js,所以對於請求中url數據還要替換為'\\'(為了保證轉義符能夠轉變為路徑符,需要用'\\'表示實際的'\'),此外為了後續的http響應報文中返回正確的Content-Type字段,還有截取'.'後字段,來獲取請求文件的類型。
//獲得當前程序所在的文件夾
string url_str = System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
if (string.Compare(head.url, "/") == 0)
{
//對於首個請求127.0.0.1:3000/ 返回index.html
url_str += "html\\index.html";
head.type = "html";
}
else
{
//其它請求 如/spring.js 替換為 ...\html\spring.js便於C#查詢文件路徑
url_str =url_str + "html\\" + head.url.Substring(1);
url_str = url_str.Replace('/', '\\');
int pos = url_str.IndexOf('.');
//獲得當前請求的網頁類型
head.type = url_str.Substring(pos + 1);
}
到此為止,完成了整個http解析的過程,包括http方法, url資源地址獲得並轉換為windows系統路徑,協議版本獲得三個部分。對於靜態網頁請求,後續就比較簡單,查詢系統路徑下資源,通過文件流打開,並以字符流的形式放置在內存中,作為http響應報文的正文部分。
//以文件流的方式打開指定路徑內文件
using (FileStream fs = new FileStream(url_str, FileMode.Open, FileAccess.Read))
{
//StreamReader temp = new StreamReader(fs, Encoding.Default);
int fslen = (int)fs.Length;
byte[] fbyte = new byte[fslen];
int r = fs.Read(fbyte, 0, fslen);
fs.Close();
//......
}
文件打開成功後,我們就要生成http響應報文了,http響應報文和請求報文相同,也由三部分構成。
狀態碼:主要為客戶端提供一種理解事務處理結果的便捷方式。主要實現的有:
HTTP/1.1 200 OK 請求沒有問題,實體的主體部分包含請求的資源
HTTP/1.1 400 Bad Request 通知客戶端它發送了一個錯誤的請求
HTTP/1.1 401 Unauthorized 與適當的首部一同返回,通知客戶端進行相應的認證
HTTP/1.1 404 No Found 說明服務器無法找到請求的URL
響應首部:為客戶端提供額外的關於服務器的消息,本項目中實現比較簡單:
Content-type:CurrentType\r\n
Server:C# Web\r\n
Content-Length:CurrentLength\r\n
Connection: close
其中Contenet-type需要根據我們上文獲得的type類型來替換,這裡闡述常見的替換規則。
Content-Length字段是http響應報文正文的長度,即我們獲得資源的總長度(上文中fslen), 最後將狀態碼,響應首部和正文數據整合在一起通過socket發送到客戶端,就實現了靜態服務器的全部過程。
string HTTP_Current_Head = HTTPServer.HTTP_OK_Head.Replace("CurrentLength", Convert.ToString(fslen));
//根據不同url需要返回不同的首部類型 具體對比詳見http://tool.oschina.net/commons
switch (head.type)
{
case "jpg":
HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "application/x-jpg");
break;
case "png":
HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "image/png");
break;
case "html":
HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "text/html");
break;
case "gif":
HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "image/gif");
break;
case "js":
HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "application/x-javascript");
break;
case "asp":
HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "text/asp");
break;
default:
HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "text/html");
break;
}
send_str = HTTPServer.HTTP_OK_Start + HTTP_Current_Head;
byte[] head_byte = new byte[send_str.Length];
head_byte = Encoding.UTF8.GetBytes(send_str);
//字符串流合並,生成發送文件
//之前采用的是byte[]->string, string合並, string->byte[],這種方法讀取圖片亂碼
//因此修改為,string合並, string->byte[], byte[]合並方式,讀取圖片成功
byte[] send_byte = new byte[send_str.Length + fbyte.Length];
Buffer.BlockCopy(head_byte, 0, send_byte, 0, head_byte.Length);
Buffer.BlockCopy(fbyte, 0, send_byte, head_byte.Length * sizeof(byte), fbyte.Length);
Console.WriteLine("File Send....");
return send_byte;
到現在為止,一個簡單的靜態web服務器就實現了,將希望訪問的資源文件放入當前程序文件夾/html/下, 並將首頁定義為index.html, 點開服務器程序,浏覽器中輸入http://127.0.0.1:3000, 就可以查看返回的網頁。
具體程序參考:Web服務器下載