Recentment he tingut que fer un mini-webserver, multithread per tal que faci d'sparring d'una altra aplicació. Algo fàcil de fer servir, ràpid de configurar, i que respongui a les peticions un contingut fàcil de modificar. Així que he fet al Listener. No té suport per SSL, encara que no és difícil afegir-li. Té un formulari que li fa d'interface d'usuari i dues classes principals.
- HttpServer: Aquesta classes es dedica a tenir un Socket que escolta peticions entrants. Per cada petició entrant instancia una HttpProcessor que s'encarrega de servir la petició en el seu propi thread.
- HttpProcessor: Aquesta classes s'encarrega d'atendre una petició. La parseja i li serveix el que demana, ja sigui un GET o un POST.
El codi més interessant del HttpServer és:
public void Listen() {
IPAddress localhost = IPAddress.Any;
listener = new TcpListener(localhost, port);
listener.Start();
ThreadPool.SetMaxThreads(NumThreads, NumThreads);
while (is_active) {
if (listener.Pending())
{
TcpClient client = listener.AcceptTcpClient();
HttpProcessor processor = new HttpProcessor(client, this);
ThreadPool.QueueUserWorkItem(processor.process);
}
else
{
Thread.Sleep(100);
}
}
}
El HttpProcessor per la seva banda:
class HttpProcessor : IDisposable
{
public TcpClient socket { get; set; }
public HttpServer server { get; set; }
private BufferedStream inputStream;
private StreamWriter outputStream;
private bool isDisposing = false;
private string http_contenttype { get; set; }
private string http_method { get; set; }
private string http_url { get; set; }
private string http_protocol_versionstring { get; set; }
private Dictionary<string, string> httpHeaders = new Dictionary<string, string>();
private Dictionary<string, string> urlParameters = new Dictionary<string, string>();
private static int MAX_POST_SIZE = 10 * 1024 * 1024;
public void Dispose()
{
if (!isDisposing)
{
isDisposing = true;
if (inputStream != null)
{
inputStream.Dispose();
inputStream = null;
}
if (outputStream != null)
{
if (outputStream.BaseStream != null)
{
try
{
outputStream.Dispose();
}
catch
{
}
}
outputStream = null;
}
if (socket != null)
{
socket.Close();
socket = null;
}
}
}
public HttpProcessor(TcpClient s, HttpServer srv)
{
this.socket = s;
this.server = srv;
}
private string streamReadLine(BufferedStream PinputStream)
{
int next_char = 0;
string data = "";
int i = 0;
while (i < 10000 && next_char != -1)
{
next_char = PinputStream.ReadByte();
if (next_char != -1)
{
if (next_char == '\n')
{
break;
}
else
{
if (next_char != '\r')
{
data += Convert.ToChar(next_char);
}
}
}
i++;
}
return data;
}
public void process(Object threadContext)
{
try
{
inputStream = new BufferedStream(socket.GetStream());
outputStream = new StreamWriter(socket.GetStream(), Encoding.Default);
parseRequest();
readHeaders();
server.IFace.SetResultsValue("URL: " + http_url);
server.IFace.SetResultsValue("Method: " + http_method);
server.IFace.SetResultsValue("Headers ---------");
foreach(KeyValuePair<string, string> Item in httpHeaders)
{
server.IFace.SetResultsValue(Item.Key + ": " + Item.Value);
}
switch (http_method)
{
case "GET":
EscriureResposta();
break;
case "POST":
server.IFace.SetResultsValue("--------------- POST -------------");
server.IFace.SetResultsValue(ReadPostContents());
EscriureResposta();
break;
case "HEAD":
case "PUT":
case "DELETE":
case "TRACE":
case "CONNECT":
server.IFace.SetResultsValue("Verb HTTP no suportat: " + http_method);
break;
case "UNKNOWN":
server.IFace.SetResultsValue("Verb HTTP desconegut: " + http_method);
break;
}
server.IFace.SetResultsValue("CLOSING------");
Dispose();
}
catch (Exception e)
{
server.IFace.SetResultsValue("Exception: " + e.ToString());
Thread.CurrentThread.Join(10000);
}
}
private void EscriureResposta()
{
HttpCodes Codes = new HttpCodes();
string Capcalera = Codes.Success(http_contenttype, "");
string Contingut = server.IFace.GetResponseValue();
Capcalera += Environment.NewLine + "Content-Length: " + Contingut.ToCharArray().Length;
outputStream.Write(Capcalera);
outputStream.WriteLine("");
outputStream.WriteLine("");
outputStream.Write(Contingut);
outputStream.Flush();
outputStream.Close();
}
private void parseRequest()
{
string request = streamReadLine(inputStream);
request = request.Trim();
if (!string.IsNullOrWhiteSpace(request))
{
string[] tokens = request.Split(' ');
if (tokens.Length != 3)
{
throw new Exception("Invalid HTTP request line: ->" + request + "<-");
}
http_method = tokens[0].ToUpper();
http_url = tokens[1];
if (http_url.Contains("?")) parseURLParameters();
http_protocol_versionstring = tokens[2];
}
else
{
http_method = "UNKNOWN";
}
}
private void parseURLParameters()
{
string[] URL = http_url.Split('?');
if(URL.Length != 2)
{
server.IFace.SetResultsValue("ERROR: URL no valida: " + http_url);
}
string[] Parameters = URL[1].Split('&');
string[] Parameter;
http_url = URL[0];
foreach (string item in Parameters)
{
Parameter = item.Split('=');
if(Parameter.Length != 2)
{
server.IFace.SetResultsValue("ERROR: Parametre no valid: " + Parameter);
}
urlParameters.Add(Parameter[0], Parameter[1]);
}
}
private void readHeaders()
{
String line;
while ((line = streamReadLine(inputStream)) != null)
{
line = line.Trim();
if (line.Equals(""))
{
return;
}
int separator = line.IndexOf(':');
if (separator == -1)
{
throw new Exception("Invalid HTTP header line: ->" + line + "<-");
}
String name = line.Substring(0, separator);
int pos = separator + 1;
while ((pos < line.Length) && (line[pos] == ' '))
{
pos++;
}
string value = line.Substring(pos, line.Length - pos);
httpHeaders.Add(name, value);
}
}
private const int BUF_SIZE = 4096;
private string ReadPostContents()
{
int content_len = 0;
MemoryStream ms = new MemoryStream();
StreamReader SR = null;
string PostData = "";
if (httpHeaders.ContainsKey("Content-Length"))
{
content_len = Convert.ToInt32(httpHeaders["Content-Length"]);
if (content_len > MAX_POST_SIZE)
{
throw new Exception(String.Format("POST Content-Length({0}) too big for this simple server", content_len));
}
byte[] buf = new byte[BUF_SIZE];
int to_read = content_len;
while (to_read > 0)
{
int numread = this.inputStream.Read(buf, 0, Math.Min(BUF_SIZE, to_read));
if (numread == 0)
{
if (to_read == 0)
{
break;
}
else
{
throw new Exception("POST Client disconnected during post");
}
}
to_read -= numread;
ms.Write(buf, 0, numread);
}
ms.Seek(0, SeekOrigin.Begin);
}
SR = new StreamReader(ms);
PostData = SR.ReadToEnd();
return PostData;
}
private Dictionary<string, string> ProcessPostValues(string PostData)
{
Dictionary<string, string> PostValues = new Dictionary<string, string>();
string EncodedHTTP;
string DecodedString;
string[] Splitter;
string[] ItemSplitter;
Splitter = PostData.Split('&');
for (int i = 0; i < Splitter.Length; i++)
{
ItemSplitter = Splitter[i].Split('=');
if (ItemSplitter.Length == 2)
{
EncodedHTTP = ItemSplitter[1];
DecodedString = WebUtility.UrlDecode(EncodedHTTP);
PostValues.Add(ItemSplitter[0], DecodedString);
}
}
return PostValues;
}
}
També hi ha una classe de support que encapsula els codis HTTP, i el formulari principal fa servir la classe de configuració.
class HttpCodes
{
public string SuccessJSON()
{
string Result = "";
Result = "HTTP/1.0 200 OK";
Result += Environment.NewLine + "Content-Type: application/json; charset=iso-8859-1";
Result += Environment.NewLine + "Cache-Control: no-cache, no-store, must-revalidate";
Result += Environment.NewLine + "Pragma: no-cache";
Result += Environment.NewLine + "Expires: 0";
Result += Environment.NewLine + "Connection: close";
return Result;
}
public string SuccessHTML()
{
string Result = "";
Result = "HTTP/1.0 200 OK";
Result += Environment.NewLine + "Content-Type: text/html; charset=iso-8859-1";
Result += Environment.NewLine + "Cache-Control: no-cache, no-store, must-revalidate";
Result += Environment.NewLine + "Pragma: no-cache";
Result += Environment.NewLine + "Expires: 0";
Result += Environment.NewLine + "Connection: close";
return Result;
}
public string Success(string ContentType, string Charset)
{
string Result = "";
Result = "HTTP/1.0 200 OK";
if(string.IsNullOrWhiteSpace(Charset))
{
Result += Environment.NewLine + "Content-Type: " + ContentType;
}
else
{
Result += Environment.NewLine + "Content-Type: " + ContentType + "; charset=" + Charset;
}
Result += Environment.NewLine + "Cache-Control: no-cache, no-store, must-revalidate";
Result += Environment.NewLine + "Pragma: no-cache";
Result += Environment.NewLine + "Expires: 0";
Result += Environment.NewLine + "Connection: close";
return Result;
}
public string SuccessText()
{
string Result = "";
Result = "HTTP/1.0 200 OK";
Result += Environment.NewLine + "Content-Type: text/plain; charset=iso-8859-1";
Result += Environment.NewLine + "Cache-Control: no-cache, no-store, must-revalidate";
Result += Environment.NewLine + "Pragma: no-cache";
Result += Environment.NewLine + "Expires: 0";
Result += Environment.NewLine + "Connection: close";
return Result;
}
public string Error500()
{
string Result = "";
Result = "HTTP/1.0 500 Internal Server Error";
Result += Environment.NewLine + "Content-Type: text/plain; charset=iso-8859-1";
Result += Environment.NewLine + "Cache-Control: no-cache, no-store, must-revalidate";
Result += Environment.NewLine + "Pragma: no-cache";
Result += Environment.NewLine + "Expires: 0";
Result += Environment.NewLine + "Connection: close";
return Result;
}
public string SuccessIMG(string IMG)
{
string Result = "";
Result = "HTTP/1.0 200 OK";
Result += Environment.NewLine + "Content-Type: image/" + IMG;
Result += Environment.NewLine + "Cache-Control: no-cache, no-store, must-revalidate";
Result += Environment.NewLine + "Pragma: no-cache";
Result += Environment.NewLine + "Expires: 0";
Result += Environment.NewLine + "Connection: close";
return Result;
}
public string SuccessCSS()
{
string Result = "";
Result = "HTTP/1.0 200 OK";
Result += Environment.NewLine + "Content-Type: text/css";
Result += Environment.NewLine + "Cache-Control: no-cache, no-store, must-revalidate";
Result += Environment.NewLine + "Pragma: no-cache";
Result += Environment.NewLine + "Expires: 0";
Result += Environment.NewLine + "Connection: close";
return Result;
}
public string SuccessJS()
{
string Result = "";
Result = "HTTP/1.0 200 OK";
Result += Environment.NewLine + "Content-Type: application/x-javascript";
Result += Environment.NewLine + "Cache-Control: no-cache, no-store, must-revalidate";
Result += Environment.NewLine + "Pragma: no-cache";
Result += Environment.NewLine + "Expires: 0";
Result += Environment.NewLine + "Connection: close";
return Result;
}
public string FileNotFound404()
{
string Result = "";
Result = "HTTP/1.0 404 File not found";
Result += Environment.NewLine + "Content-Type: text/plain";
Result += Environment.NewLine + "Cache-Control: no-cache, no-store, must-revalidate";
Result += Environment.NewLine + "Pragma: no-cache";
Result += Environment.NewLine + "Expires: 0";
Result += Environment.NewLine + "Connection: close";
return Result;
}
public string Redirect(string URL)
{
string Result = "";
Result = "HTTP/1.1 302 Found";
Result += Environment.NewLine + "Location: " + URL;
Result += Environment.NewLine + "Connection: close";
return Result;
}
}
El Listener té un germà gran que fa més coses, si algun dia l'acabo ja el publicaré |