changeset 2008:bba3e529e346 default tip

chunked encoding
author Franklin Schmidt <fschmidt@gmail.com>
date Wed, 27 Aug 2025 01:14:17 -0600
parents 408f7dd7e503
children
files src/goodjava/io/IoUtils.java src/goodjava/webserver/ChunkedOutputStream.java src/goodjava/webserver/Connection.java src/goodjava/webserver/Response.java src/goodjava/webserver/ResponseOutputStream.java src/goodjava/webserver/examples/Chunked.java src/goodjava/webserver/examples/Example.java src/goodjava/webserver/handlers/ContentTypeHandler.java src/goodjava/webserver/handlers/FileHandler.java src/goodjava/webserver/handlers/LogHandler.java src/luan/modules/Boot.luan src/luan/modules/IoLuan.java src/luan/modules/http/Http.luan src/luan/modules/http/LuanHandler.java src/luan/modules/url/LuanUrl.java website/src/examples/chunked.html.luan
diffstat 16 files changed, 214 insertions(+), 25 deletions(-) [+]
line wrap: on
line diff
--- a/src/goodjava/io/IoUtils.java	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/goodjava/io/IoUtils.java	Wed Aug 27 01:14:17 2025 -0600
@@ -81,7 +81,7 @@
 	public static void copyAll(InputStream in,OutputStream out)
 		throws IOException
 	{
-		byte[] a = new byte[8192];
+		byte[] a = new byte[32768];
 		int n;
 		while( (n=in.read(a)) != -1 ) {
 			out.write(a,0,n);
@@ -92,7 +92,7 @@
 	public static void copyAll(Reader in,Writer out)
 		throws IOException
 	{
-		char[] a = new char[8192];
+		char[] a = new char[32768];
 		int n;
 		while( (n=in.read(a)) != -1 ) {
 			out.write(a,0,n);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/ChunkedOutputStream.java	Wed Aug 27 01:14:17 2025 -0600
@@ -0,0 +1,88 @@
+package goodjava.webserver;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+
+public class ChunkedOutputStream extends ByteArrayOutputStream {
+	private static final byte[] end = "0\r\n\r\n".getBytes();
+	private static final int OPEN = 1;
+	private static final int CLOSING = 2;
+	private static final int CLOSED = 3;
+
+	private final Response response;
+	private int status = OPEN;
+	private int i = 0;
+
+	public ChunkedOutputStream(Response response) {
+		if(response==null) throw new NullPointerException();
+		this.response = response;
+		response.headers.put("Transfer-Encoding","chunked");
+		response.body = new ChunkedInputStream();
+	}
+
+	@Override public synchronized void close() {
+		if( status == OPEN ) {
+			status = CLOSING;
+			notifyAll();
+		}
+	}
+
+	@Override public synchronized void write(int b) {
+		super.write(b);
+		notifyAll();
+	}
+
+	@Override public synchronized void write(byte b[], int off, int len) {
+		super.write(b,off,len);
+		notifyAll();
+	}
+
+	@Override public synchronized void reset() {
+		super.reset();
+		i = 0;
+	}
+
+	private class ChunkedInputStream extends InputStream {
+
+		@Override public int read() {
+			throw new UnsupportedOperationException();
+		}
+
+		@Override public int read(byte[] b,int off,int len) throws IOException {
+			synchronized(ChunkedOutputStream.this) {
+				if( i == count ) {
+					if( status == CLOSED )
+						return -1;
+					if( status == CLOSING ) {
+						System.arraycopy(end,0,b,off,end.length);
+						status = CLOSED;
+						return end.length;
+					}
+					try {
+						ChunkedOutputStream.this.wait();
+					} catch(InterruptedException e) {
+						throw new RuntimeException(e);
+					}
+					return read(b,off,len);
+				}
+				int offOld = off;
+				int left = count - i;
+				int extra = Integer.toHexString(left).length() + 4;
+				int n = Math.min( len - extra, left );
+				byte[] hex = Integer.toHexString(n).getBytes();
+				System.arraycopy( hex, 0, b, off, hex.length );
+				off += hex.length;
+				b[off++] = '\r';  b[off++] = '\n';
+				System.arraycopy(buf,i,b,off,n);
+				off += n;
+				b[off++] = '\r';  b[off++] = '\n';
+				i += n;
+				if( i == count )
+					ChunkedOutputStream.this.reset();
+				return off - offOld;
+			}
+		}
+	}
+}
--- a/src/goodjava/webserver/Connection.java	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/goodjava/webserver/Connection.java	Wed Aug 27 01:14:17 2025 -0600
@@ -129,12 +129,11 @@
 				response = Response.errorResponse(Status.BAD_REQUEST,msg);
 			}
 			response.headers.put("Connection","close");
-			response.headers.put("Content-Length",Long.toString(response.body.length));
 			byte[] header = response.toHeaderString().getBytes();
 	
 			OutputStream out = socket.getOutputStream();
 			out.write(header);
-			IoUtils.copyAll(response.body.content,out);
+			IoUtils.copyAll(response.body,out);
 			out.close();
 			socket.close();
 		} catch(IOException e) {
--- a/src/goodjava/webserver/Response.java	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/goodjava/webserver/Response.java	Wed Aug 27 01:14:17 2025 -0600
@@ -16,20 +16,15 @@
 	{
 		headers.put("Server","goodjava");
 	}
-	private static final Body empty = new Body(0,new InputStream(){
-		public int read() { return -1; }
-	});
-	public volatile Body body = empty;
-
-	public static class Body {
-		public final long length;
-		public final InputStream content;
-	
-		public Body(long length,InputStream content) {
-			this.length = length;
-			this.content = content;
+	private final InputStream empty = new InputStream() {
+		@Override public int read() {
+			return -1;
 		}
-	}
+		@Override public void close() {
+			headers.put("Content-Length","0");
+		}
+	};
+	public volatile InputStream body = empty;
 
 
 	public void addHeader(String name,String value) {
--- a/src/goodjava/webserver/ResponseOutputStream.java	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/goodjava/webserver/ResponseOutputStream.java	Wed Aug 27 01:14:17 2025 -0600
@@ -5,7 +5,6 @@
 import java.io.IOException;
 
 
-// plenty of room for improvement
 public class ResponseOutputStream extends ByteArrayOutputStream {
 	private final Response response;
 
@@ -17,6 +16,7 @@
 	@Override public void close() throws IOException {
 		super.close();
 		int size = size();
-		response.body = new Response.Body( size, new ByteArrayInputStream(buf,0,size) );
+		response.headers.put("Content-Length",Long.toString(size));
+		response.body = new ByteArrayInputStream(buf,0,size);
 	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/goodjava/webserver/examples/Chunked.java	Wed Aug 27 01:14:17 2025 -0600
@@ -0,0 +1,38 @@
+package goodjava.webserver.examples;
+
+import java.io.Writer;
+import java.io.OutputStreamWriter;
+import java.io.IOException;
+import java.util.Date;
+import goodjava.webserver.Handler;
+import goodjava.webserver.Request;
+import goodjava.webserver.Response;
+import goodjava.webserver.ChunkedOutputStream;
+import goodjava.webserver.Server;
+
+
+public class Chunked implements Handler {
+
+	public Response handle(Request request) {
+		Response response = new Response();
+		response.headers.put( "Content-Type", "text/html; charset=utf-8" );
+		final Writer writer = new OutputStreamWriter( new ChunkedOutputStream(response) );
+		new Thread(new Runnable(){public void run(){
+			try {
+				String s = new Date().toString();
+				for( int i=1; i<=10; i++ ) {
+					writer.write(s+" "+i+"<br>\n");
+					writer.flush();
+					Thread.sleep(1000);
+				}
+				writer.close();
+			} catch(IOException e) {
+				throw new RuntimeException(e);
+			} catch(InterruptedException e) {
+				throw new RuntimeException(e);
+			}
+		}}).start();
+		return response;
+	}
+
+}
--- a/src/goodjava/webserver/examples/Example.java	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/goodjava/webserver/examples/Example.java	Wed Aug 27 01:14:17 2025 -0600
@@ -45,6 +45,7 @@
 		map.put( "/headers", new Headers() );
 		map.put( "/params", new Params() );
 		map.put( "/cookies", new Cookies() );
+		map.put( "/chunked", new Chunked() );
 		Handler mapHandler = new MapHandler(map);
 		FileHandler fileHandler = new FileHandler();
 		Handler dirHandler = new DirHandler(fileHandler);
--- a/src/goodjava/webserver/handlers/ContentTypeHandler.java	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/goodjava/webserver/handlers/ContentTypeHandler.java	Wed Aug 27 01:14:17 2025 -0600
@@ -39,6 +39,7 @@
 		map.put( "ico", "image/x-icon" );
 		map.put( "mov", "video/quicktime" );
 		map.put( "mp3", "audio/mpeg" );
+		map.put( "wav", "audio/wav" );
 		// add more as need
 	}
 
--- a/src/goodjava/webserver/handlers/FileHandler.java	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/goodjava/webserver/handlers/FileHandler.java	Wed Aug 27 01:14:17 2025 -0600
@@ -56,8 +56,8 @@
 					} catch(ParseException e) {}
 				}
 				response.headers.put("Last-Modified",lastModified);
-
-				response.body = new Response.Body( file.length(), new FileInputStream(file) );
+				response.headers.put("Content-Length",Long.toString(file.length()));
+				response.body = new FileInputStream(file);
 				return response;
 			}
 			return null;
--- a/src/goodjava/webserver/handlers/LogHandler.java	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/goodjava/webserver/handlers/LogHandler.java	Wed Aug 27 01:14:17 2025 -0600
@@ -67,7 +67,7 @@
 			return null;
 		String ip = (String)request.headers.get("x-real-ip");
 		//String agent = (String)request.headers.get("user-agent");
-		logger.info( ip + " \"" + request.method + " " + request.rawPath + "\" " + response.status.code + " " + response.body.length );
+		logger.info( ip + " \"" + request.method + " " + request.rawPath + "\" " + response.status.code + " " + response.headers.get("Content-Length") );
 		return response;
 	}
 }
--- a/src/luan/modules/Boot.luan	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/luan/modules/Boot.luan	Wed Aug 27 01:14:17 2025 -0600
@@ -87,6 +87,7 @@
 	local this = {}
 	this.java = writer
 	this.write = writer.write
+	this.flush = writer.flush
 	this.close = writer.close
 
 	function this.write_from(reader)
--- a/src/luan/modules/IoLuan.java	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/luan/modules/IoLuan.java	Wed Aug 27 01:14:17 2025 -0600
@@ -68,6 +68,10 @@
 				}
 			}
 
+			public void flush() {
+				out.flush();
+			}
+
 			public void close() {
 				out.close();
 			}
@@ -87,6 +91,10 @@
 				}
 			}
 
+			public void flush() throws IOException {
+				out.flush();
+			}
+
 			public void close() throws IOException {
 				out.close();
 			}
--- a/src/luan/modules/http/Http.luan	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/luan/modules/http/Http.luan	Wed Aug 27 01:14:17 2025 -0600
@@ -25,10 +25,12 @@
 local format_time = Time.format or error()
 local parse_time = Time.parse or error()
 local Boot = require "luan:Boot.luan"
+local Thread = require "luan:Thread.luan"
 local LuanJava = require "java:luan.Luan"
 local Request = require "java:goodjava.webserver.Request"
 local Response = require "java:goodjava.webserver.Response"
 local ResponseOutputStream = require "java:goodjava.webserver.ResponseOutputStream"
+local ChunkedOutputStream = require "java:goodjava.webserver.ChunkedOutputStream"
 local Status = require "java:goodjava.webserver.Status"
 local ServerSentEvents = require "java:goodjava.webserver.ServerSentEvents"
 local OutputStreamWriter = require "java:java.io.OutputStreamWriter"
@@ -182,6 +184,35 @@
 		return Boot.binary_writer(response.writer)
 	end
 
+	function response.write_chunked_text(fn)
+		response.writer and error "writer already set"
+		response.writer = "done"
+		local writer = ChunkedOutputStream.new(response.java)
+		writer = OutputStreamWriter.new(writer)
+		writer = Boot.text_writer(writer)
+		Thread.run(function()
+			try
+				fn(writer)
+			finally
+				writer.close()
+			end
+		end)
+	end
+
+	function response.write_chunked_binary(fn)
+		response.writer and error "writer already set"
+		response.writer = "done"
+		local writer = ChunkedOutputStream.new(response.java)
+		writer = Boot.binary_writer(writer)
+		Thread.run(function()
+			try
+				fn(writer)
+			finally
+				writer.close()
+			end
+		end)
+	end
+
 	return response
 end
 
@@ -194,7 +225,7 @@
 		value = LuanJava.toJava(value)
 		java.headers.put(name,value)
 	end
-	response.writer and response.writer.close()
+	response.writer and response.writer~="done" and response.writer.close()
 	return java
 end
 
--- a/src/luan/modules/http/LuanHandler.java	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/luan/modules/http/LuanHandler.java	Wed Aug 27 01:14:17 2025 -0600
@@ -19,7 +19,6 @@
 import goodjava.webserver.Status;
 import goodjava.webserver.Server;
 import goodjava.webserver.Handler;
-import goodjava.webserver.ResponseOutputStream;
 import luan.Luan;
 import luan.LuanTable;
 import luan.LuanFunction;
--- a/src/luan/modules/url/LuanUrl.java	Mon Jul 28 23:47:43 2025 -0600
+++ b/src/luan/modules/url/LuanUrl.java	Wed Aug 27 01:14:17 2025 -0600
@@ -27,9 +27,12 @@
 import luan.modules.IoLuan;
 import luan.modules.StringLuan;
 import luan.modules.Utils;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
 
 
 public final class LuanUrl extends IoLuan.LuanIn {
+	private static final Logger logger = LoggerFactory.getLogger(LuanUrl.class);
 
 	private static enum Method { GET, POST, DELETE, PUT }
 
@@ -280,7 +283,11 @@
 		throws IOException, LuanException, AuthException
 	{
 		try {
-			return httpCon.getInputStream();
+//			return httpCon.getInputStream();
+			InputStream in = httpCon.getInputStream();
+//			System.err.println("Content-Length = "+httpCon.getHeaderField("Content-Length"));
+//			System.err.println("Transfer-Encoding = "+httpCon.getHeaderField("Transfer-Encoding"));
+			return in;
 //		} catch(FileNotFoundException e) {
 //			throw e;
 		} catch(IOException e) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/website/src/examples/chunked.html.luan	Wed Aug 27 01:14:17 2025 -0600
@@ -0,0 +1,21 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local range = Luan.range or error()
+local Thread = require "luan:Thread.luan"
+local sleep = Thread.sleep or error()
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+
+
+return function()
+	Http.response.write_chunked_text(function(writer)
+		Io.stdout = writer
+		for i in range(1,10) do
+%>
+line <%=i%><br>
+<%
+			writer.flush()
+			sleep(1000)
+		end
+	end)
+end