Handle range requests with HttpListener

2020-06-28 02:48发布

问题:

I've written a custom HTTP server that works fine for everything until the browser makes a request with a byte range. When attempting to load video (seemingly for files above a certain size because this doesn't happen every time), the browser makes a request for the video file with this header:

method: GET
/media/mp4/32.mp4
Connection - keep-alive
Accept - */*
Accept-Encoding - identity;q=1/*;q=0
Accept-Language - en-us/en;q=0.8
Host - localhost:20809
Referer - ...
Range - bytes=0-
User-Agent - Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.170 Safari/537.36

so the server sends the requested file... Then, immediately afterwards, it makes this request:

method: GET
/media/mp4/32.mp4
Connection - keep-alive
Accept - */*
Accept-Encoding - identity;q=1/*;q=0
Accept-Language - en-us/en;q=0.8
Host - localhost:20809
Referer - ...
Range - bytes=40-3689973
User-Agent - Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.170 Safari/537.36

So I write the requested bytes to the output stream but it always errors out on the second request with an error. It's almost like the server is still trying to send the file when the browser sends another request.

The I/O operation has been aborted because of either a thread exit or an application request

Here is the code that handles the range request:

public void StartServer()
{
    _server = new HttpListener();
    _server.Prefixes.Add("http://localhost:" + _port.ToString() + "/");

    LogWebserver("Listening...");

    _server.Start();
    th = new Thread(new ThreadStart(startlistener));
    th.Start();
}
private void startlistener()
{
    while (true)
    {
        ////blocks until a client has connected to the server
        ProcessRequest();
    }
}
private void ProcessRequest()
{
    var result = _server.BeginGetContext(ListenerCallback, _server);  
    result.AsyncWaitHandle.WaitOne();
}
private void ListenerCallback(IAsyncResult result)
{
    var context = _server.EndGetContext(result);
    HandleContext(context);
}
private void HandleContext(HttpListenerContext context)
{

    HttpListenerRequest req = context.Request;
...stuff...
   using (HttpListenerResponse resp = context.Response)
   {
        .... stuff....
        byte[] buffer = File.ReadAllBytes(localFile);
    if (mime.ToString().Contains("video") || mime.ToString().Contains("audio"))
    {

        resp.StatusCode = 206;
        resp.StatusDescription = "Partial Content";
        int startByte = -1;
        int endByte = -1;
        int byteRange = -1;
        if (req.Headers.GetValues("Range") != null)
        {
            string rangeHeader = req.Headers.GetValues("Range")[0].Replace("bytes=", "");
            string[] range = rangeHeader.Split('-');
            startByte = int.Parse(range[0]);
            if (range[1].Trim().Length > 0)   int.TryParse(range[1], out endByte);                                
            if (endByte == -1) endByte = buffer.Length;
        }
        else
        {
            startByte = 0;
            endByte = buffer.Length;
        }
        byteRange = endByte - startByte;
        resp.ContentLength64 = byteRange;
        resp.Headers.Add("Accept-Ranges", "bytes");
        resp.Headers.Add("Content-Range", string.Format("bytes {0}-{1}/{2}", startByte, byteRange - 1, byteRange));
        resp.Headers.Add("X-Content-Duration", "0.0");
        resp.Headers.Add("Content-Duration", "0.0");

       resp.OutputStream.Write(buffer, startByte, byteRange);/* this is where it gives the IO error */
            resp.OutputStream.Close();

            resp.Close();  
    }
    else
    {
        resp.ContentLength64 = buffer.Length;
        resp.OutputStream.Write(buffer, 0, buffer.Length);
        resp.OutputStream.Close();
        resp.Close();
    }

   }
}

I've tried simply ignoring the request with the range in it, but while no error is thrown, the browser throw an error because the video wasn't downloaded.

How do I handle these range requests and avoid the IO error?

回答1:

I solved the problem, but it involved scrapping HttpListener and using TcpListener instead.

I used the code here as a basis for the server: http://www.codeproject.com/Articles/137979/Simple-HTTP-Server-in-C

And then I modified the handleGETRequest function with this:

if (mime.ToString().Contains("video") || mime.ToString().Contains("audio"))
{


    using (FileStream fs = new FileStream(localFile, FileMode.Open))
    {
        int startByte = -1;
        int endByte = -1;
        if (p.httpHeaders.Contains("Range"))
        {
            string rangeHeader = p.httpHeaders["Range"].ToString().Replace("bytes=", "");
            string[] range = rangeHeader.Split('-');
            startByte = int.Parse(range[0]);
            if (range[1].Trim().Length > 0) int.TryParse(range[1], out endByte);
            if (endByte == -1) endByte = (int)fs.Length;
        }
        else
        {
            startByte = 0;
            endByte = (int)fs.Length;
        }
        byte[] buffer = new byte[endByte - startByte];
        fs.Position = startByte;
        int read = fs.Read(buffer,0, endByte-startByte);
        fs.Flush();
        fs.Close();
        p.outputStream.AutoFlush = true;
        p.outputStream.WriteLine("HTTP/1.0 206 Partial Content");
        p.outputStream.WriteLine("Content-Type: " + mime);
        p.outputStream.WriteLine("Accept-Ranges: bytes");
        int totalCount = startByte + buffer.Length;
        p.outputStream.WriteLine(string.Format("Content-Range: bytes {0}-{1}/{2}", startByte, totalCount - 1, totalCount));
        p.outputStream.WriteLine("Content-Length: " + buffer.Length.ToString());
        p.outputStream.WriteLine("Connection: keep-alive");
        p.outputStream.WriteLine("");
        p.outputStream.AutoFlush = false;

        p.outputStream.BaseStream.Write(buffer, 0, buffer.Length);
        p.outputStream.BaseStream.Flush();
    }                   
}
else
{
    byte[] buffer = File.ReadAllBytes(localFile);
    p.outputStream.AutoFlush = true;
    p.outputStream.WriteLine("HTTP/1.0 200 OK");
    p.outputStream.WriteLine("Content-Type: " + mime);
    p.outputStream.WriteLine("Connection: close");
    p.outputStream.WriteLine("Content-Length: " + buffer.Length.ToString());
    p.outputStream.WriteLine("");

    p.outputStream.AutoFlush = false;
    p.outputStream.BaseStream.Write(buffer, 0, buffer.Length);
    p.outputStream.BaseStream.Flush();

}

Using TcpListener avoids whatever stupid problem was causing the I/O error on the response stream.



回答2:

I know its late, but i think there are some misstakes at your solution. Your code will not work with Safari.

  • Send ranges only if requested.
  • endbyte is not correct, should be +1
  • the 2. totalcount is wrong at Content-Range, it should be the bytes of
    the whole file

Safari always request 2 bytes to check, unfortunatly without infos if failed. Other browsers are more tolerant.