在遙遠的Unix時代,爲了解決傳輸層的編程問題,從 4.2BSD Unix開始,Unix提供了類似於文件操作的網絡操作方式——Socket。通過Socket,程序員就可以像操作文件一樣通過打開、寫入、讀取、關閉等操作完成網絡編程。這使得網絡編程可以統一到文件操作之下。通過Socket幫助程序員解決網絡傳輸層的問題,而系統中的網絡系統負責處理網絡內部的複雜操作,這樣程序員就可以比較容易地編寫網絡應用程序。需要注意的是,應用層的協議需要針對網絡程序專門處理,Socket不負責應用層的協議,僅僅負責傳輸層的協議。
當然,網絡畢竟不是簡單的文件,所以,在使用Socket的時候,程序員還是需要設置一些網絡相關的細節問題參數。
當通過Socket開發網絡應用程序的時候,首先需要考慮所使用的網絡類型,注意包括以下三個方面:
1)Socket類型,使用網絡協議的類別,IPv4的類型爲PF_INET。
2)數據通信的類型,常見的數據包(SOCK_DGRAM)、數據流(SOCK_STREAM)。
3)使用的網絡協議,比如:TCP協議
在同一個網絡地址上,爲了區分使用相同協議的不同應用程序,可以爲不同的應用程序分配一個數字編號,這個編號成爲網絡端口號(port)。端口號是一個兩字節的證書,取值範圍從0~65535.IANA(Internet Assgned Number Authority,互聯網地址分配機構)維護了一個有端口分配列表,這些端口分爲三類,第一類範圍是0~1023,稱爲總所周知的端口,有IANA進行控制和分配,有特定的網絡程序使用,例如TCP協議使用80號端口來完成HTTP協議的傳輸。第二類的範圍是1024~49151,稱爲登記端口,這些端口不由IANA控制,但是IANA維護了一個登記的列表,如果沒有在IANA登記的話,也不應該在程序中使用。但是,大多數的系統中,在沒有衝突的情況下,也可以由用戶程序使用。第三輪的範圍是49152~65535,稱爲動態或者私有端口,這些端口可以由普通用戶使用。
對於一個網絡應用程序來說,通過地址、協議和端口號可以唯一地確定網絡上的一個應用程序。其中地址和端口的組合成爲端點(EndPoint)。每個Socket需要綁定到一個端點上與其他端點進行通信。
在.NET中,System.Net命名空間提供了網絡編程的大多數數據類型以及常用操作,其中常用的類型如下:
IPAddress類用來表示一個IP地址
IPEndPoint類用來表示一個IP地址和一個端口的組合,成爲網絡的端點
System.Net.Sockets命名空間中提供了基於Socket編程的數據類型
Socket類封裝了Socket的操作。
常用操作如下:
Listen:設置基於連接通信的Socket進入監聽狀態,並設置等待隊列的長度
Accept:等待一個新的連接,當連接到達的時候,返回一個針對新連接的Socket對象。通過這個新的Socket對象,可以與新連接通信
Receive:通過Socket接受字節數據,保存到一個字節數組中,返回實際接受的字節數。
Send:通過Socket發送預先保存在字節數組中的數據。
下面的代碼演示瞭如何通過Socket編程創建一個簡單的Web服務器。這個服務器通過49152端口提供訪問,向瀏覽器返回一個固定的靜態網頁。在這個示例中,請求的消息由瀏覽器生成,併發送到服務器,這個程序將簡單地顯示請求的信息。迴應的消息由服務器程序生成,通過Socket傳輸層返回給瀏覽器。
static void Main(string[] args)
{
//獲得本機的 loopback 網絡地址
IPAddress address = IPAddress.Loopback;
//創建可以訪問的端點
IPEndPoint endPoint = new IPEndPoint(address, 49152);
//創建Socket,使用IPv4地址,傳輸控制協議,雙向、可靠、基於鏈接的字節流
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//將socket綁定到端點上
socket.Bind(endPoint);
//設置連接隊列的長度
socket.Listen(10);
Console.WriteLine("開始監聽,端口號:{0}",endPoint.Port);
while (true)
{
//開始監聽,這個方法會阻塞線程的執行,直到接受到一個客戶端的連接請求
Socket client = socket.Accept();
//輸出客戶端地址
Console.WriteLine(client.RemoteEndPoint);
//準備讀取客戶端請求的數據讀取的數據將保存在一個數組中
byte[] buffer=new byte[4096];
//接受數據並獲取數據長度
int length = client.Receive(buffer, 4096, SocketFlags.None);
//將請求的數據翻譯爲UTF-8
System.Text.Encoding utf8 = System.Text.Encoding.UTF8;
string requestString = utf8.GetString(buffer, 0, length);
//顯示請求的消息
Console.WriteLine(requestString);
//迴應的狀態行
string statusLine = "HTTP/1.1 200 OK \r\n";
byte[] statusLineBytes = utf8.GetBytes(statusLine);
//準備發送到客戶端的網頁
string responseBody = "<html><head><title>From Socket Server</title></head><body><h1>Hello,world</h1></body></html>";
byte[] responseBodyBytes = utf8.GetBytes(responseBody);
//迴應頭部
string responseHeader = string.Format("Content-Type:text/html;charset=UTF-8\r\nContent-Length:{0}\r\n",responseBody.Length);
byte[] responseHeaderBytes = utf8.GetBytes(responseHeader);
//向客戶端發送狀態信息
client.Send(statusLineBytes);
//向客戶端發送迴應頭
client.Send(responseHeaderBytes);
//頭部與內容的分隔行
client.Send(new byte[] { 13, 10 });
//向客戶端發送內容部分
client.Send(responseBodyBytes);
//端口與客戶端的連接
client.Close();
if (Console.KeyAvailable)
break;
}
//關閉服務器
socket.Close();
}
開啓服務,請求localhost:49152 模擬成功
-----------------------------------------------------------------
基於TcpListener的Web服務器
爲了簡化基於TCP協議的監聽程序,.NET在System.Net.Sockets命名空間中提供了TcpListener類,使用它,在構造函數中傳遞一組網絡端點信息就可以準備好監聽參數,而不再需要設置使用的網絡協議等細節,調用Start方法之後,監聽工作就開始了。AcceptTcpClient方法將阻塞進程,直到一個客戶端的連接到達監聽器,這個方法將返回一個代表客戶端連接的代理對象,它的類型爲TcpClient,我們可以通過對它與客戶端進行通信。
在輸入輸出部分,通過TcpClient對象可以得到一個用戶輸入和輸出的網絡流對性NetworkStream,這是一個派生自Stream對象的字節流對象,對Socket的輸入和輸出進行了封裝,這樣,我們可以通過常用的字節流操作來完成網絡的輸入和輸出。
static void Main(string[] args)
{
//取得本機的loopback網絡地址
IPAddress address = IPAddress.Loopback;
//創建可以訪問的端點
IPEndPoint endPoint = new IPEndPoint(address, 49152);
//創建TCP監聽器
TcpListener newserver = new TcpListener(endPoint);
//啓動監聽
newserver.Start();
Console.WriteLine("開始監聽");
while (true)
{
//等待客戶端連接
TcpClient client = newserver.AcceptTcpClient();
Console.WriteLine("已經建立連接");
//得到一個網絡流
NetworkStream ns = client.GetStream();
System.Text.Encoding utf8 = System.Text.Encoding.UTF8;
byte[] request=new byte[4096];
int length = ns.Read(request, 0, 4096);
//請求信息
string requestString = utf8.GetString(request, 0, length);
Console.WriteLine(requestString);
//狀態行
string statusLine = "HTTP/1.1 200 OK\r\n";
byte[] statusLineBytes = utf8.GetBytes(statusLine);
//網頁內容
string responseBody = "<html><head><title></title></head><body>Hello<body></html>";
byte[] responseBodyBytes = utf8.GetBytes(responseBody);
//迴應的頭部信息
string responseHeader = string.Format("Content-Type:text/html;charset=UTF-8\r\nContent-Length:{0}\r\n",responseBody.Length);
byte[] responseHeaderBytes = utf8.GetBytes(responseHeader);
//輸入狀態行
ns.Write(statusLineBytes, 0, statusLineBytes.Length);
//輸出迴應頭部
ns.Write(responseHeaderBytes, 0, responseHeaderBytes.Length);
//迴應頭部與內容之間的空行
ns.Write(new byte[]{13,10},0,2);
//輸出內容
ns.Write(responseBodyBytes, 0, responseBodyBytes.Length);
//關閉客戶端連接
client.Close();
if (Console.KeyAvailable)
break;
}
//關閉服務器
newserver.Stop();
}
----------------------------------------------
基於HttpListener的Web服務器
爲了進一步簡化HTTP協議的監聽器,.NET在命名空間System.NET中提供了HttpListener類。伴隨這個對象,.NET提供了一系列相關對象封裝了HTTP的處理工作。注意,這個類使用Http.sys系統組件完成工作,所以,只有在Windows XP SP2 或者 Server 2003以上的操作系統中才能使用。
HttpListener類進一步簡化了監聽工作,僅需通過字符串的方法提供監聽的地址、端口號以及虛擬路徑,就可以開始監聽工作。
開始監聽後,GetContext方法將阻塞線程,當客戶端的請求到達之後,HttpListener返回一個HttpListenerContext對性愛那個最爲處理客戶端請求的總代理,通過代理對象的Request屬性,我們可以得到一個類型爲HttpListenerRequest的代表請求參數的對象,這個對象將大多數請求參數進行了對象化,所以,我們可以通過它提供的一系列屬性來獲取請求參數。例如HttpListenerRequest的HttpMethod屬性就提供了請求的方法類型。通過代理的Response屬性,可以得到一個類型爲HttpListenerResponse的迴應處理對象,這個對象將回應的數據和操作進行了封裝,使得我們大幅度簡化了迴應的編程工作了。
static void Main(string[] args)
{
//檢查系統是否支持
if (!HttpListener.IsSupported)
{
throw new System.InvalidOperationException("使用HttpListener必須爲Windows XP SP2 或 Server2003 以上系統");
}
//注意前綴必須以 / 正斜槓結尾
string[] prefixes = new string[] {"http://localhost:49152/" };
//創建監聽器
HttpListener listener = new HttpListener();
//增加監聽的前綴
foreach (string s in prefixes)
{
listener.Prefixes.Add(s);
}
//開始監聽
listener.Start();
Console.WriteLine("監聽中");
while (true)
{
//注意:GetContext方法將阻塞線程,知道請求到達
HttpListenerContext context = listener.GetContext();
//獲取對象
HttpListenerRequest request = context.Request;
Console.WriteLine("{0}{1}HTTP/1.1",request.HttpMethod,request.RawUrl);
Console.WriteLine("Accept:{0}",string.Join(",",request.AcceptTypes));
Console.WriteLine("Accept-Language:{0}",string.Join(",",request.UserLanguages));
Console.WriteLine("User-Agent:{0}",request.UserAgent);
Console.WriteLine("Accept-Encoding:{0}",request.Headers["Accept-Encoding"]);
Console.WriteLine("Connection:{0}",request.KeepAlive?"Keep-Alive":"close");
Console.WriteLine("Host:{0}",request.UserHostName);
Console.WriteLine("Pragma:{0}",request.Headers["Pragma"]);
//取得迴應對象
HttpListenerResponse response = context.Response;
//構造 迴應內容
string responseString = @"<html><head><title>aaa</title></head><body>Hello</body></html>";
response.ContentLength64 = System.Text.Encoding.UTF8.GetByteCount(responseString);
response.ContentType = "text/html;charset=UTF-8";
System.Text.Encoding utf8 = System.Text.Encoding.UTF8;
System.IO.Stream output = response.OutputStream;
byte[] responseStringBytes=utf8.GetBytes(responseString);
//輸出迴應
output.Write(responseStringBytes, 0, responseStringBytes.Length);
if (Console.KeyAvailable)
break;
}
listener.Stop();
}
在使用HttpListener的時候,常用的請求和迴應參數都變成了對象的屬性,大幅度降低了變成的工作量。但是,大多數的參數還是需要通過Headers索引器來訪問的,就像上例中的Accept-Encoding請求參數,我們就不能直接通過屬性訪問。