【问题标题】:Java Implement byte range serving without servlets using only Java apiJava 仅使用 Java api 实现不使用 servlet 的字节范围服务
【发布时间】:2016-01-15 20:51:28
【问题描述】:

我已经用 Java 编写了一个 http 服务器已经有一段时间了(现在快两年了?),但我仍然在让字节范围请求工作时遇到问题。我只使用套接字的输入和输出流进行原始字节数据传输(即文件下载/上传),并使用PrintWriter 向连接的客户端发送响应头/字符串(例如HTTP/1.1 200 OK 等)。

我不希望使用任何 servlet 或 API(例如 HTTPURLConnection 或其他)。我想做“香草风格”。

我能够很好地和快速地提供普通网页(例如文件浏览、上传和下载、看电影、听音乐、查看 pdf 文件、文本、图片、gif 文件等),所以是不是问题。

但是,每当我尝试实现字节范围请求时,服务器都会完美接收客户端的请求,解析给定的数据,准备文件输入流以进行发送,然后当我将数据发送给客户端时,客户端总是丢弃与software caused connection abort: socket write error 的连接。(对于以下情况,我需要字节范围请求:观看一小时长的视频,然后该当的 wifi 信号消失,您不想从第一格重新观看整个视频,恢复一个 '暂停下载”等)

这真的让我很困惑,而且我确实搜索过服务字节范围请求的 java 示例,当我尝试实现它们时所有这些都失败了。我什至尝试从头开始并对其进行测试,并且产生了相同的结果。以下是与我要完成的工作相关的代码 sn-ps:

发送和接收普通网页(工作正常,这是一个例子):

    ...
        OutputStream outStream = client.getOutputStream();
        PrintWriter out = new PrintWriter(new OutputStreamWriter(outStream, StandardCharsets.UTF_8), true);
        out.println("HTTP/1.1 200 OK");
        out.println("Content-Type: text/html; charset=\"UTF-8\"");
        String responseStr = "<!DOCTYPE html><html><head><title>Hello, world!</title></head><body><string>This is a simple example webpage!</string></body></html>";
        if(acceptEncoding.contains("gzip") && responseStr.length() > 33) {
            out.println("Content-Encoding: gzip");
            byte[] r = StringUtil.compressString(responseStr, "UTF-8");
            out.println("Content-Length: " + r.length);
            out.println("");
            if(protocol.equalsIgnoreCase("GET")) {
                outStream.write(r);
            }
        } else {
            out.println("Content-Length: " + responseStr.length());
            out.println("");
            if(request.protocol.equalsIgnoreCase("GET")) {
                out.println(responseStr);
            }
        }
        out.flush();
        //followed by closing resources, etc.
    ...

字节范围服务(客户端请求数据解析后):

public static final long copyInputStreamToOutputStream(InputStream input, OutputStream output, long startBytes, long endBytes) throws IOException {
    byte[] buffer = new byte[20480];//1024
    long count = 0;
    int read;

    input.skip(startBytes);
    long toRead = (endBytes - startBytes) + 1;

    while((read = input.read(buffer)) > 0) {
        if((toRead -= read) > 0) {
            output.write(buffer, 0, read);//Socket error happens on this line mostly
            output.flush();
        } else {
            output.write(buffer, 0, (int) toRead + read);//but also on this line sometimes
            output.flush();
            break;
        }
        count += read;
    }
    return count;
}

对于任何感兴趣的人,在此基本代码上运行的服务器在线 redsandbox.no-ip.org(指向我的服务器),目前我使用 Accept-Ranges: none 而不是 @987654328 禁用了字节请求@,但我可以再次打开它来测试它。

如果我需要添加更多代码,请告诉我!感谢您的时间。 或者,如果您想查看我服务器的完整代码,可以在 github 上查看:https://github.com/br45entei/JavaWebServer

【问题讨论】:

  • 我喜欢这样的问题,因为我也相信可以以更有效的方式重写标准库的代码以满足特定的要求和/或限制列表(尤其是当您的程序必须在高负载下生存或满足高吞吐量数据流的需求)。
  • 你的代码有提交到某个 github repo 吗?
  • 很遗憾,没有,但如果您愿意,我可以尝试快速完成。
  • 处理所有代码可能更容易。当然,如果你不介意让你的代码开源。
  • Inputstream.skip(long) 不保证 javadoc 跳过 n 个字节,检查返回值。但我认为本地文件输入流是相当一致的。还要确保以前的堆栈调用函数都没有使用整数,它可能会溢出。调试打印开始+结束+跳过的值以确保。顺便说一句,您的代码风格很好,理解它没有问题。

标签: java http server range byte


【解决方案1】:

这个读写循环能正常工作吗,看起来很精简?这出乎我的意料,因此可能会发现轻微的语法错误。我期待 startIdx+endIdx 都是包容性指标。

public static final long copyInputToOutput(InputStream input, OutputStream output, long startIdx, long endIdx) throws IOException {
    final long maxread = 24*1024;
    byte[] buffer = new byte[(int)maxread];
    input.skip(startIdx);
    long written=0;
    long remaining = endIdx-startIdx+1;
    while(remaining>0) {
        int read = input.read(buffer, 0, (int)Math.min(maxread, remaining));
        output.write(buffer, 0, read);
        remaining -= read;
        written += read;
    }
    output.flush();
    return written;
}

【讨论】:

  • 我一定会试一试,如果有效,我会接受!
  • 完美运行。也没有语法错误。干得好,谢谢!附言我认为每次 write(...) 后刷新会更好,因为它不会让客户端在服务器上等待准备数据,尤其是对于非常大的文件。
  • 您可以随时刷新,但是当内部操作系统网络缓冲区已满时,无论如何都会刷新。发生这种情况时,它只是一个未知的大小。
  • 如果您有兴趣,这里是最终结果(添加了与 swt 窗口相关的代码):github.com/br45entei/JavaWebServer/commit/…
  • 这是 (int)Math.min(maxread_int, remaining_long) 是否映射到 Math.min(long, long) 或 Math.min(int, int) 函数。小心 64bit long->32bit int 隐式转换,很难找到 sleeper bug。
【解决方案2】:

所以我弄清楚了我的问题,部分感谢Whome。首先,我没有向客户端发送正确的Content-Length: 标头,其次,我的endbytes 变量计算不正确。这是工作代码:

public static final long copyInputStreamToOutputStream(InputStream input, OutputStream output, long startBytes, long endBytes, final long contentLength, boolean debug) throws IOException {
    byte[] buffer = new byte[20480];//1024
    long count = 0;
    int read;
    long skipped = input.skip(startBytes);//I tested this quite a few times with different files and it does indeed skip the desired amount of bytes.
    final long length = (endBytes - startBytes) + 1;
    if(debug) {
        println("Start bytes: " + startBytes + "; End bytes: " + endBytes + "; Skipped bytes: " + skipped + "; Skipped equals startBytes: " + (skipped == startBytes) + "; Length: " + length + "; Total file size: " + contentLength);
    }
    long toRead = length;
    while((read = input.read(buffer)) != -1) {
        count += read;
        long bytesLeft = length - count;
        if(debug) {
            println("read: " + read + "; toRead: " + toRead + "; length: " + length + "; count: " + count + "; bytesLeft: " + bytesLeft);
        }
        toRead -= read;
        if(bytesLeft >= buffer.length) {//Changed the if statement here so that we actually send the last amount of data to the client instead of whatever is leftover in the buffer
            output.write(buffer, 0, read);
            output.flush();
        } else {
            output.write(buffer, 0, read);//Send the currently read bytes
            output.flush();
            bytesLeft = length - count;//recalculate the remaining byte count
            read = input.read(buffer, 0, (int) bytesLeft);//read the rest of the required bytes
            count += read;
            if(debug) {
                println("[Last one!]read: " + read + "; toRead: " + toRead + "; length: " + length + "; count: " + count + "; bytesLeft: " + bytesLeft);
            }
            output.write(buffer, 0, read);//and send them over
            output.flush();
            break;
        }
    }
    return count;
}

由于在我的例子中endBytes 变量比它需要的多一个,我试图通过调整它周围的代码来进行补偿。但是,我需要简单地从中减去一个。带有buffer.length 的 if 语句是为了确保将最后一个字节发送到客户端。在我没有它的测试中,客户端(谷歌浏览器)挂起并等待剩余的字节,但从未收到它们,然后当 30 秒超时关闭连接时,网页重置。我还没有用其他浏览器对此进行测试,但我认为它应该可以工作。

【讨论】:

    猜你喜欢
    • 2016-09-06
    • 2022-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-22
    • 1970-01-01
    相关资源
    最近更新 更多