Socketprogrammering i C#

Tillbaka till filarkivet

Filer

consWebserv.zip (7.92 kB)

Verktyg

Kommentarer (2)
Utskriftsvänligt format

Betyg

markedunmaked 5.2

Den här serien tar upp socketprogrammering i C# där fokus inte ligger på just nätverksprogrammeringen utan på hur man kan nyttja den i praktiken. Vi kommer att gå igenom hur man kodar en Webserver, en Irc-klient och lite annat. Häng med! Smiley

Navigation:

Text - Del 1 - Koda en Webserver

Webserverprogrammering i C#

Oavsett vad man gör så kommer man slutligen till ett läge då man känner av ensamheten. Behovet av att kommunicera med medmänniskor är medfött och lika livsnödvändigt som att andas. Detta gäller även personer som tillbringar sin tid framför datorn, därför kan det vara bra att kunna göra program som tar hänsyn till det här ytterst grundläggande behovet.

Nätverksprogrammering kan vara hur invecklad som helst, men man kan också göra användbara saker relativt enkelt. I exemplet jag kommer visa på används C# för att bygga en enkel webserver. Att skriva om den till något annat språk eller annan plattform är busenkelt. Man bör ha lite kunskaper i C# för att hänga med, men om man behärskar språk som C++ eller java borde man inte ha något problem att förstå principen.

Serverklassen:

Först behöver vi en serverklass som hanterar allt dataskickande.

De bibliotek vi behöver är följande:

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



I själva klassen används bara två stycken medlemmsvariabler, en för att hantera sockets och en för att helt enkelt hålla koll på i vilken katalog webfilerna finns.

private Socket m_Socket;
private string m_Root;

public server(Socket s, string loc)
{
m_Socket = s;
    m_Root = loc + "\";
}


Det som är det absolut viktigaste i själva webservern, förmågan att skicka data till webläsaren, eller kanske mer korrekt, klienten, är faktiskt den simplaste biten. Vi tar bara en array med bytes, kontrollerar så att det finns en connection och sen fläskar vi bara ut datan i nätverket i sann högnivå anda.

private void SendToBrowser(byte[] data)
{
if( m_Socket.Connected )
    {
        if(m_Socket.Send(data,data.Length,0) == 1)
        {
        // Socket error                Console.WriteLine("Kunde inte skicka data pga socketfel..");
        }
}
}

För att göra det till en webserver måste den ju kunna kommunicera med själva webläsaren också, inte bara kunna fläska ut massa nonsensdata. Det gör vi med hjälp av http protokollet. Utan att göra en djupdykning i http så kan jag säga att först och främst skickas en header tillhörande varje fil som transporteras över protokollet. Såhär kan ex en header se ut tillhörande en 530 bytes stor htmlfil:


HTTP/1.1 200 OK
Content-Type: text/html
AcceptRange: bytes
Content-Length: 530

Nåväl, för att skicka iväg en header till en förfrågad fil så sätter man helt enkelt bara ihop den på enklaste sätt och sen skickar iväg den:

private void SendHeader(string httpVersion,string mimetype,int totBytes,string statusCode)
{
string buffer;
buffer = httpVersion + statusCode + "
";
    buffer = buffer + "Server: HqspyServer
";
    buffer = buffer + "Content-Type: " + mimetype + "
";
    buffer = buffer + "AcceptRange: bytes
";
    buffer = buffer + "Content-Length: " + totBytes + "

";
    byte[] data = Encoding.ASCII.GetBytes(buffer);
    SendToBrowser(data);
}


Mimetypen är vilken typ av fil det är. En htmlfil har ex mimetypen: text/html, eftersom den räknas som en typ av text. För att kunna översätta filnamn till mimetyper behöver vi en funktion som kontrollerar filändrelsen och returnerar en mimetyp. En operation som är superlätt med C#:s strängfunktion .EndsWith().

private string GetMimetype(string file)
{
if( file.EndsWith(".htm") ||file.EndsWith(".html"))
        return "text/html";
    else if( file.EndsWith(".jpg") || file.EndsWith(".jpeg"))
        return "image/jpeg";
    else if( file.EndsWith(".gif"))
        return "image/gif";
    else if( file.EndsWith(".txt"))
        return "text/plain";
    else
        return "application/octet-stream";
}


Då är alla verktyg vi behöver klara. Nu ska vi bara ta reda på vilken sida som klienten vill titta på och skicka den till honom. Till att börja med så tar vi emot all data klienten har skickat till oss från bufferten så vi kan börja analysera den. Till det behöver vi en lokal buffer som vi kan översätta till en sträng.

public void Process()
{
Byte[] receive = new Byte[1024];
    int i = m_Socket.Receive(receive,receive.Length,0);
    string buffer = Encoding.ASCII.GetString(receive);

Det blir mycket konverterande mellan bytearrayer och rena strängar för att inte göra dataanalysen så tyngrodd. I vilket fall som helst så klarar våran lilla webserver bara av EURGETEUR förfrågningar, så det är lika bra att kolla så att ingen försöker med någonting annat.

if( buffer.Substring(0,3) != "GET")
{
/*  Error! hanterar bara GET */
    Console.WriteLine("Fel - Hanterar bara GET förfrågningar");
    m_Socket.Close();
}


Hela förfrågan är en halvlång textsträng som innehåller en del vettigt som vi vill ha ut och en massa gojja som vi helst vill slippa. Iaf för närvarande. Det som är viktigt här är att ta ut själva filrequesten och versionen på httpprotokollet. Det görs med hjälp av lite strängfunktioner. Man måste hålla tungan rätt i mun så man har koll på hur man hoppar i strängen. Det är inte alltid en dans på rosor att parsa saker och ting.

int startPos = buffer.IndexOf("HTTP",1);
string httpVersion = buffer.Substring(startPos,8);
string request = buffer.Substring(0, startPos -1); // requesten
request.Replace("\","/");
// lägg till ett "/" om det rör sig om en katalog som inte är utmärkt.
if( (request.IndexOf(".")<1) && (!request.EndsWith("/")))
request = request + "/";


Det kan ju röra sig om en fil som inte ligger i rooten på webservern, då får man försöka ta reda på det också. Det blir nästan ännu värre om ingen fil är angiven, men då kan man nog anta att det är index.htm som efterfrågas. Med lite rotande i strängen brukar det lösa sig. När man nystat upp allting så är det lika bra att passa på att ta reda på mimetypen också.

startPos = request.LastIndexOf("/") + 1;
string reqFile = request.Substring(startPos); // Filen vi ska läsa in 
string dirname = request.Substring(request.IndexOf("/"),request.IndexOf("/")-3);
if(reqFile.Length == 0)
reqFile = "index.htm";
string mimetype = this.GetMimetype(reqFile);
string filepath = m_Root + dirname + reqFile;
Console.WriteLine("Skickar fil: " + reqFile);


Nu har vi sökvägen fint uppdelad och sammansatt med rooten igen! Då är det inte mycket som återstår. Vi öppnar bara filen på vanligt sätt, läser in den och skickar iväg den. Ifall den inte finns så får vi visa ett 404 meddelande istället.

if(File.Exists(filepath) == false)
{
string sError = "<h2>Error: 404, file does not exist</h2>";
    SendHeader(httpVersion,"",sError.Length," 404 Not Found");
    Byte[] data = Encoding.ASCII.GetBytes(sError);
    SendToBrowser(data);
    m_Socket.Close();
    return;
}
else
{
FileStream fs = new FileStream(filepath,FileMode.Open,FileAccess.Read,FileShare.Rea);
    BinaryReader br = new BinaryReader(fs);
    byte[] bytes = new byte[fs.Length];
    bytes = br.ReadBytes((int)fs.Length);
    int totBytes  = (int)fs.Length;
    br.Close();
    fs.Close();
    SendHeader(httpVersion,mimetype,totBytes," 200 OK");
    SendToBrowser(bytes);
    Console.WriteLine("Skickat: " + bytes.Length + " bytes
");
}
m_Socket.Close();

}

}


Okej, då var klassen för webserven klar. Då måste vi bara ha någonting att styra den med. Det går ju att göra hur flashigt som helst med lite fantasi, men nu ska vi ta det på det enkla sättet, dvs i consolemiljö. För det skapar vi en till klass i samma namespace med valfritt namn. Den klassen kommer vara beroende av följande bibliotek:

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


För att smidigt kunna hantera många användare samtidigt så använder vi threads. Denna klass kommer endast att ha två funktioner, den vanliga statiska mainfunktionen och en funktion som som kör allting, RunServer.

class Class1
{
[STAThread]
    static void Main(string[] args)
    {
        Class1 c = new Class1();
        Thread t = new Thread(new ThreadStart(c.RunServer));
        t.Start();
    }


För att skapa kopplingar till servern måste man skapa en socket. En socket ta hand om dataflödet och har uppgifter om vilka protokoll som ska användas. För att en socket ska kunna användas krävs det att den binds samman med en EndPoint, dvs. serverns ipnummer och den port som som socketen ska lyssna på. Port 80 är att föredra då det är standardporten för http.
    
När servern sen får en koppling mot sig så startar den en ny instans av webservern som sedan får behandla ärendet och sedan gå under gjorden igen.

private void RunServer()
{    
    IPHostEntry ip = Dns.GetHostByName(Dns.GetHostName());
    IPEndPoint EP = new IPEndPoint(ip.AddressList[0],80);
    Socket s = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
    s.Bind(EP);
s.Listen(20);
    Console.WriteLine("Lyssnar..
");
            
    //Lyssna och hantera kopplingar
    while(true)
    {
        Socket newSock = s.Accept();
        Console.WriteLine("Förfrågan från: " + newSock.RemoteEndPoint + "...");
        server serv = new server(newSock,"c:\webserver");
        Thread t = new Thread(new ThreadStart( serv.Process));
        t.IsBackground = true;
        t.Priority = ThreadPriority.BelowNormal;
        t.Start();
    }
}
}


Såja.. nu är det bara att slå in de bägge klasserna i någon snygg namespace och köra.


// Petter Nordlander

Slut

Navigation:

Till toppen