Laden...

Chunksize in HTTP Protokollen richtig verarbeiten

Erstellt von Zappy vor 5 Jahren Letzter Beitrag vor 5 Jahren 992 Views
Z
Zappy Themenstarter:in
3 Beiträge seit 2018
vor 5 Jahren
Chunksize in HTTP Protokollen richtig verarbeiten

Hi@all.
Erstmal gleich vorne weg...
Mir ist durchaus bewusst, dass ich für Http-Request, Klassen wie "HttpWebRequest" oder "WebClient" nutzen sollte.

Mir geht es hier mehr um ein besseres Verständnis des HTTP-Protokolls und wie man dieses verarbeitet. Daher spiele ich z.Z. ein bisschen mit der TcpClient-Klasse herum. Allerdings bin ich jetzt auf ein Problem gestoßen...

Und zwar versuche ich z.Z. Internetseiten auszulesen, welche in Chunks aufgeteilt sind. Der aufbau ist mir soweit klar. Zuerst wird der Header der Seite gesendet. danach kommt die Chunk-Size als Hexadezimal-Zahl gefolgt vom eigentlichen Chunk (natürlich getrennt durch Zeilenumbrüche).

Jetzt habe ich mir mal etwas Code zusammen geschustert, um den ersten Chunk einer internetseite auszulesen (ich bitte darum über die Code-Qualität hinweg zu sehen, der ist nur zu testzwecken und daher absolut quick&dirty):

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace HTTPChunkReader
{
    class Program
    {
        static void Main(string[] args)
        {
            // ein bisschen Initialisierungs-Gedöhns
            TcpClient client = new TcpClient("blog.cowchimp.com", 80);
            NetworkStream stream = client.GetStream();

            string request = "GET /chunk-scatter-http-chunked-response-analysis-tool/ HTTP/1.1\n";
            request += @"Host: blog.cowchimp.com\n";
            request += @"User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0\n";
            request += @"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n";
            request += @"Accept-Language: en-GB,en;q=0.5\n";
            request += @"Accept-Encoding: gzip, deflate\n";
            request += @"Connection: keep-alive\n";
            request += @"Upgrade-Insecure-Requests: 1\n\n";

            stream.Write(Encoding.ASCII.GetBytes(request), 0, Encoding.ASCII.GetBytes(request).Length);
            StreamReader reader = new StreamReader(stream);

            // einfach nur alle Response-Headerzeilen abzählen und ausgeben
            Console.WriteLine(reader.ReadLine());
            Console.WriteLine(reader.ReadLine());
            Console.WriteLine(reader.ReadLine());
            Console.WriteLine(reader.ReadLine());
            Console.WriteLine(reader.ReadLine());
            Console.WriteLine(reader.ReadLine());
            Console.WriteLine(reader.ReadLine());
            Console.WriteLine(reader.ReadLine());
            Console.WriteLine(reader.ReadLine());
            Console.WriteLine(reader.ReadLine());
            Console.WriteLine(reader.ReadLine());
            Console.WriteLine(reader.ReadLine());

            // Chunksize in Dezimalzahl umwandeln und ausgeben
            string hexChunkSize = reader.ReadLine();
            int decChunkSizeInt = Convert.ToInt32(hexChunkSize, 16);
            Console.WriteLine("ChunkSize (HEX): " + hexChunkSize);
            Console.WriteLine("ChunkSize (DEC): " + decChunkSizeInt);
            
            // x bytes auslesen
            // (wobei "x" für die Chunksize steht)
            for (int i = 0; i < decChunkSizeInt; i++)
            {

                Console.Write(i + ", ");
                stream.ReadByte();

            }

            // Hier müssten jetzt eigentlich die Werte 13, 10, ?, ?, ? ausgegeben werden,
            // wobei "?" für die nächsten Hex-Chunksize-Characters (natürlich als Bytes) bzw. einem weitern Zeilenumbruch (13, 10) stehen.
            Console.WriteLine("\n");
            Console.WriteLine(stream.ReadByte());
            Console.WriteLine(stream.ReadByte());
            Console.WriteLine(stream.ReadByte());
            Console.WriteLine(stream.ReadByte());
            Console.WriteLine(stream.ReadByte());

            // Fertig
            Console.WriteLine("\n\n--- END ---");
            Console.ReadKey();

        }
    }
}

Die letzten 5 Bytes die ich mit diesem Code von der Internetseite erhalte und ausgebe, sollten eigentlich (wie schon im Kommentar beschrieben) "13, 10, ?, ?, ?" sein, wobei "?" für die nächsten Hex-Chunksize-Characters (natürlich als Bytes) bzw. einem weitern Zeilenumbruch (13, 10) stehen.

Allerdings ist das leider nicht der Fall.
Ich hoffe, dass jemand etwas Ahnung davon hat, versteht was ich meine und mir erklären kann, wo genau das Problem liegt?

3.170 Beiträge seit 2006
vor 5 Jahren

Hallo,

das liegt daran, dass Du zuerst mit dem StreamReader auf den Stream losgehst, und anschließend versuchst, direkt einzelne Bytes aus dem Stream zu lesen.

Der StreamReader verwendet einen internen Lesepuffer, den er erst mal füllt. Dadurch liest er Dir dann schon einige Bytes unterm Hintern weg, und wenn Du die einzelnen Bytes lesen willst, ist der NetworkStream schon ein Stück weiter, als Du es erwartest.

Du kannst das mit dem StreamReader machen, dann musst Du aber alles damit lesen.
Dann brauchst Du aber auch noch einen StreamWriter, der dir das wieder sauber wegschreibt in einen MemoryStream.
Achtung: Du brauchst dann ein 8-Bit Encoding im StreamReader und das gleiche nochmal im StreamWriter, mit einem Multibyte-Encoding wird das nix (und mit ASCII auch nicht), vor allem weil Du mit Accept-Encoding: gzip, deflate mit Sicherheit Binärdaten erhältst. Du kannst z.B. Encoding.Default verwenden (nicht zu verwechseln mit dem standardmäßigen Encoding des StreamReaders, das ist nämlich UTF8).

Also sowas würde funktioieren:

           StreamReader reader = new StreamReader(stream, Encoding.Default);

           //... deine ganze Header-Ausgabe

            string hexChunkSize = reader.ReadLine();
            int decChunkSizeInt = Convert.ToInt32(hexChunkSize, 16);
            Console.WriteLine("ChunkSize (HEX): " + hexChunkSize);
            Console.WriteLine("ChunkSize (DEC): " + decChunkSizeInt);
            
            MemoryStream ms = new MemoryStream();
            StreamWriter writer = new StreamWriter(ms, Encoding.Default);
            while (decChunkSizeInt > 0)
            {
                var content = new char[decChunkSizeInt];
                reader.ReadBlock(content, 0, decChunkSizeInt);
                writer.Write(content);
                
                // am Ende des Chunks das CRLF lesen
                reader.ReadLine();

                // und ab in die nächste Runde
                hexChunkSize = reader.ReadLine();
                decChunkSizeInt = Convert.ToInt32(hexChunkSize, 16);
                Console.WriteLine("ChunkSize (HEX): " + hexChunkSize);
                Console.WriteLine("ChunkSize (DEC): " + decChunkSizeInt);
            }
            writer.Flush();
            // jezt stehen im MemoryStream genau die Nutzdaten, allerdings binär wegen gzip/deflate,   
            // die musst Du dann noch auspacken

Die bessere Lösung:
Besser ist es meiner Ansicht nach aber, die Daten gar nicht als Text zu verhackstücken, sondern direkt alles binär aus dem Stream zu lesen - weil es sich ja auch um Binärdaten handelt.
Dazu brauchst Du erst mal eine Hilfsmethode, die ein Zeilenende erkennt. Nach HTTP-Spezifikation kannst Du Dich dabei auf CRLF verlassen:

        static byte[] ReadLine(NetworkStream stream)
        {
            MemoryStream ms = new MemoryStream();
            int b;
            while((b = stream.ReadByte()) != -1)
            {
                if (b == 13)
                {
                    b = stream.ReadByte();
                    if (b == 10 || b == -1)
                        break;
                }
                ms.WriteByte((byte)b);
            }
            return ms.ToArray();
        }

Diese kannst Du dann nutzen:

            // die Ausgabe der Header erfolgt dann mit
            Console.WriteLine(Encoding.ASCII.GetString(ReadLine(stream)));
            //... und die anderen Zeilen dazu

            // Chunksize in Dezimalzahl umwandeln und ausgeben
            string hexChunkSize = Encoding.ASCII.GetString(ReadLine(stream));
            int decChunkSizeInt = Convert.ToInt32(hexChunkSize, 16);
            Console.WriteLine("ChunkSize (HEX): " + hexChunkSize);
            Console.WriteLine("ChunkSize (DEC): " + decChunkSizeInt);
 
            MemoryStream ms = new MemoryStream();
            while(decChunkSizeInt > 0)
            {
                byte[] buffer = new byte[decChunkSizeInt];
                stream.Read(buffer, 0, buffer.Length);
                ms.Write(buffer, 0, buffer.Length);

                // am Ende des Chunks das CRLF lesen
                ReadLine(stream);

                // und ab in die nächste Runde
                hexChunkSize = Encoding.ASCII.GetString(ReadLine(stream));
                decChunkSizeInt = Convert.ToInt32(hexChunkSize, 16);
                Console.WriteLine("ChunkSize (HEX): " + hexChunkSize);
                Console.WriteLine("ChunkSize (DEC): " + decChunkSizeInt);
            }
            // jezt stehen im MemoryStream genau die Nutzdaten, allerdings binär wegen gzip/deflate,   
            // die musst Du dann noch auspacken

Gruß, MarsStein

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca