1. 程式人生 > >你也可以寫個伺服器 - C# Socket學習2

你也可以寫個伺服器 - C# Socket學習2

續上篇《你也可以寫個聊天程式 - C# Socket學習1》

前言

這裡說的伺服器是Web伺服器,是類似IIS、Tomcat之類的,用來響應瀏覽器請求的服務。

Socket模擬瀏覽器的Url Get請求

首先瀏覽器的請求是HTTP協議。我們上一篇說過,HTTP是短連線,用完就斷開,是無狀態的。所以我們在等待響應的時候不需要另外開個執行緒迴圈等待。
也就是我們只需要通過Socket和伺服器建立連線,然後傳送請求,然後接收伺服器的響應,這樣就完成了一次請求。
可是,我們一般訪問網頁的時候都是通過域名,沒有IP也沒有埠,怎麼和伺服器建立連線了。這裡就需要用到我們上篇介紹的幾個類了:

//根據DNS獲取域名繫結的IP
foreach (var address in Dns.GetHostEntry("www.baidu.com").AddressList)
{
    Console.WriteLine($"百度IP:{address}");
}

//字串轉IP地址
IPAddress ipAddress = IPAddress.Parse("192.168.1.101");

//通過IP和埠構造IPEndPoint物件,用於遠端連線
//通過IP可以確定一臺電腦,通過埠可以確定電腦上的一個程式
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 80);

對於HTTP沒有顯示埠預設都是80 (為了簡單這裡就先不考慮HTTPS了)
知道了IP和埠,連線是可以建立了,為了得到正確的響應,我們應該給伺服器傳送什麼訊息呢?這裡就需要用到HTTP協議了。
具體協議這裡就不說了,我們先F12看看瀏覽器的請求報文,然後依葫蘆畫瓢試試,以http://fanyi-pro.baidu.com為例。(現在找個非HTTPS的地址也是不容易了)

然後我們程式碼實現如下:

void ...()
{
    //得到主機資訊
    IPHostEntry ipInfo = Dns.GetHostEntry(new Uri("http://fanyi-pro.baidu.com").Host);
    //取得IPAddress[]
    IPAddress[] ipAddr = ipInfo.AddressList;
    //得到伺服器ip
    IPAddress ip = ipAddr[0];
    //組合遠端終結點
    IPEndPoint ipEndPoint = new IPEndPoint(ip, 80);
    //建立Socket 例項
    Socket socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    
    //嘗試連線
    socketClient.Connect(ipEndPoint);
    //傳送請求
    Send(socketClient);
    //接收伺服器的響應 
    Receive(socketClient); 
}

//接收來自服務端的訊息
void Receive(Socket socketClient)
{
    byte[] data = new byte[1024 * 1024];
    while (true)
    {
        //讀取客戶端傳送過來的資料
        int readLeng = socketClient.Receive(data, 0, data.Length, SocketFlags.None);
        textBox2.AppendText($"{socketClient.RemoteEndPoint}:{Encoding.UTF8.GetString(data, 0, readLeng)}\r\n");
    }
}

//傳送訊息到服務端
void Send(Socket socketClient)
{
    //為了方便演示,僅用請求報文的前兩行即可。(切記:需要嚴格按照報文格式。如,最後需要連續兩次換行)
    var msg = $"GET / HTTP/1.1\r\nHost: {new Uri(textBox1.Text).Host}\r\n\r\n";
    socketClient.Send(Encoding.UTF8.GetBytes(msg));
}

整個流程也就是:

  • 1、dns服務把域名解析成ip
  • 2、通過ip和埠和伺服器建立連線(三次握手)
  • 3、獲取html文件
  • 4、根據文件裡面的連結(js、css、img)再重複以上過程

【注意】:傳送報文的時候需要嚴格按照報文格式。如,最後需要連續兩次換行、行末不能有空格等。

效果圖:

用Socket實現Web伺服器

Web伺服器的實現和我們上一篇的Socket聊天服務端其實也差不多。
不同之處就在於,解析請求報文,然後按HTTP協議回覆標準的響應報文(我這裡為了簡單,就沒有按標準的協議來玩,僅僅只是實現了表面的效果)
程式碼如下:

void ...()
{
    //1 建立Socket物件
    Socket socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

    //2 繫結ip和埠
    IPAddress ip = IPAddress.Parse("127.0.0.1");
    IPEndPoint ipEndPoint = new IPEndPoint(ip, 80);
    socketServer.Bind(ipEndPoint);

    //3、開啟偵聽(等待客戶機發出的連線),並設定最大客戶端連線數為10
    socketServer.Listen(10); 
    
    //阻塞等待客戶端連線
    Task.Run(() => { Accept(socketServer); });
}

//4 阻塞等待客戶端連線
private static void Accept(Socket socketServer)
{
    while (true)
    {
        //阻塞等待客戶端連線
        Socket newSocket = socketServer.Accept();
        Task.Run(() => { Receive(newSocket); });
    }
}

//5 讀取客戶端傳送過來的報文
private static void Receive(Socket newSocket)
{
    byte[] data = new byte[1024 * 1024];
    while (newSocket.Connected)
    {
        //讀取客戶端傳送過來的資料
        int readLeng = newSocket.Receive(data, 0, data.Length, SocketFlags.None);
        //讀取客戶端發來的請求報文
        var requst = Encoding.UTF8.GetString(data, 0, readLeng);
        
        //解析請求報文的請求路徑(可以解析請求路徑、請求檔案、檔案型別)
        var requstFile = requst.Split("\r\n")[0].Split(" ")[1];
        //回覆客戶端響應報文
        Send(newSocket, requstFile);
    }
}

//6 回覆客戶端響應報文
private static void Send(Socket newSocket, string requstFile)
{
    //這裡如果請求的根目錄,預設顯示Index.html
    if (requstFile == "/" ) requstFile = "/Index.html";

    var msg = File.ReadAllText(Directory.GetCurrentDirectory() + requstFile);
    //把訊息內容轉成位元組陣列後傳送
    newSocket.Send(Encoding.UTF8.GetBytes(msg));
   
    //回覆響應後馬上關閉連線
    newSocket.Shutdown(SocketShutdown.Both);
    newSocket.Close();
}

效果如下:


由此我們知道了.net core為什麼可以在不需要iis的情況下,一個黑窗體就提供了對網址的訪問。其實也就是KestrelServer通過Socket繫結並監聽埠提供的服務。
【注意】:我們繫結的ip是127.0.0.1socketServer.Bind(ipEndPoint),所以我們測試的時候只能在瀏覽器輸入127.0.0.1或者localhost。如果想通過內外ip訪問,我們可以繫結任意ipIPAddress.Any。如socketServer.Bind(new IPEndPoint(IPAddress.Any, port))

為什麼不見三次握手

對於HTTP/TCP可能大家多少都聽過三次握手,可是在我們在用Socket編寫Web伺服器的時候並沒有看到相關的東西啊,這是怎麼回事。
因為我們在客戶端執行連線socketClient.Connect(ipEndPoint)的時候已經進行了三次握手

具體可細讀小坦克大佬的文章。
也就是說我們在用C#的Socket、TCP、HttpClient的時候根本就不用關注這些細節。
另外套接字有三種不同的型別:流套接字、資料報套接字和原始套接字。前兩者是標準套接字,分別對應TCP和UDP。而原始套接字則更加底層更加牛逼,普通開發人員一般接觸不到。
我們說的HTTP、TCP、UDP之類都是網路協議,那協議到底是什麼?通俗的說其實只是你我他之間的一個約定而已,大家都按規定了來那就可以說是協議。
而HTTP又是建立在TCP之上的,也就是說基礎協議之後再加約定又可以成為一種新的協議。下章我們將用Socket來實現ModbusTCP協議對暫存器讀和寫。

結束

  • 本文已同步至索引目錄:《物聯網基礎元件IoTClient開發系列》
  • 完整demo:https://github.com/zhaopeiym/BlogDemoCode/tree/master/Socket程式設計/2HTTP
  • 推薦閱讀:https://www.cnblogs.com/TankXiao/archive/2012/10/10/2711777.html