Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1a04bf3

Browse files
committed
refactor response body streaming + fix ChunkedOutputStream
1 parent c17c8c8 commit 1a04bf3

14 files changed

Lines changed: 161 additions & 33 deletions

src/main/scala/server/Server.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import java.util.concurrent._
66

77
import com.typesafe.scalalogging.LazyLogging
88
import server.handler.FastCgi.FastCgiHandler
9+
import server.handler.StaticFileHandler
910
import server.http.headers.{HeaderParser, Headers}
1011
import server.http.request.Request
1112
import server.http.request.parser.{ParseRequestException, RequestLineParser, RequestParser}
@@ -25,6 +26,7 @@ object Server {
2526
}, "/test")
2627
*/
2728

29+
server.getRouter.registerHandler(new StaticFileHandler("/var/www"), "/static/")
2830
server.getRouter.registerHandler(new FastCgiHandler("/var/www/php"), "/")
2931

3032
server.start()

src/main/scala/server/handler/FastCgi/FastCgiHandler.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ class FastCgiHandler(val documentRoot: String) extends Handler with LazyLogging
2525
params.add("SCRIPT_NAME", "/test.php")
2626
params.add("REQUEST_URI", request.location)
2727
params.add("REQUEST_METHOD", request.method)
28+
params.add("SYMFONY_ENV", "dev")
29+
params.add("SYMFONY_DEBUG", "1")
2830
//params.add( "HTTP_CONNECTION" , "keep-alive" )
2931

32+
3033
for((k,v) <- request.headers) {
3134
params.add("HTTP_"+k.replace("-", "_").toUpperCase, v)
3235
}

src/main/scala/server/handler/FastCgi/ResponseReader.scala

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package server.handler.FastCgi
22

3-
import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream}
3+
import java.io.{ByteArrayOutputStream, InputStream}
44
import com.typesafe.scalalogging.LazyLogging
55
import server.http.headers.HeaderParser
66
import server.http.response.Response
7+
import server.http.response.body.ByteArrayResponseBody
78

89
class ResponseReader(recordReader: RecordReader, val headerParser: HeaderParser) extends LazyLogging {
910
private val headerBodySeparator = List(13,10,13,10) //CRLF
@@ -39,6 +40,10 @@ class ResponseReader(recordReader: RecordReader, val headerParser: HeaderParser)
3940
val headers = headerParser.parse(new String(headerBytes, "UTF-8"))
4041
val status = "[0-9]{3}".r.findFirstIn(headers.getOrElse("Status", "200")).getOrElse("200")
4142

42-
new Response(status.toInt, stdOut.slice(prev.length, stdOut.length), headers)
43+
new Response(
44+
status.toInt,
45+
new ByteArrayResponseBody(stdOut.slice(prev.length, stdOut.length)),
46+
headers
47+
)
4348
}
4449
}
Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
package server.handler
22

3-
import java.io.{ByteArrayOutputStream, OutputStream, InputStream}
4-
import java.nio.charset.{CodingErrorAction, Charset}
5-
3+
import java.io._
64
import server.http.request.Request
75
import server.http.response.Response
6+
import server.http.response.body.InputStreamResponseBody
87

98
class StaticFileHandler(val path: String) extends Handler {
10-
private val decoder = Charset.forName("UTF-8").newDecoder()
11-
decoder.onMalformedInput(CodingErrorAction.IGNORE)
129
override def handle(request: Request): Response = {
13-
val source = scala.io.Source.fromFile(path+"/"+request.location)(decoder)
14-
val content = source.mkString
15-
source.close()
16-
new Response(200)
10+
val file = new File(path+"/"+request.location)
11+
if(!file.exists()) {
12+
throw new FileNotFoundException(path+"/"+request.location)
13+
}
14+
new Response(200, new InputStreamResponseBody(new FileInputStream(file), Some(file.length())))
1715
}
18-
1916
}

src/main/scala/server/http/encoding/ChunkedOutputStream.scala

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,59 @@ package server.http.encoding
33
import java.io.{FilterOutputStream, OutputStream}
44

55
class ChunkedOutputStream(out: OutputStream, chunkSize: Int) extends FilterOutputStream(out) {
6-
override def write(b: Array[Byte]): Unit = {
7-
for(i <- 0 to b.length-1 by chunkSize) {
8-
val curChunkLength = math.min(b.length-i, chunkSize)
9-
writeChunkLength(curChunkLength)
10-
super.write(b.slice(i, i+curChunkLength))
6+
private val buffer = new Array[Byte](chunkSize)
7+
private var currentBufferPos = 0
8+
private val CRLF = Array[Byte](13, 10)
9+
10+
override def write(b: Array[Byte], start: Int, length: Int): Unit = {
11+
if(length <= 0) {
12+
return
13+
}
14+
if(currentBufferPos + length > chunkSize) {
15+
val remainingInBuffer = chunkSize - currentBufferPos
16+
System.arraycopy(b, start, buffer, currentBufferPos, remainingInBuffer)
17+
currentBufferPos = chunkSize
18+
flushBuffer()
19+
write(b, start+remainingInBuffer, length-remainingInBuffer)
20+
}
21+
else {
22+
System.arraycopy(b, start, buffer, currentBufferPos, length)
23+
currentBufferPos += length
24+
if(currentBufferPos == chunkSize) {
25+
flushBuffer()
26+
}
27+
}
28+
}
29+
30+
override def write(i: Int): Unit = {
31+
buffer(currentBufferPos) = i.toByte
32+
currentBufferPos += 1
33+
if(currentBufferPos == chunkSize) {
34+
flushBuffer()
35+
}
36+
}
37+
38+
private def flushBuffer(): Unit = {
39+
if(currentBufferPos > 0) {
40+
writeChunkLength(currentBufferPos)
41+
out.write(buffer, 0, currentBufferPos)
1142
writeCRLF()
43+
currentBufferPos = 0
1244
}
1345
}
1446

1547
private def writeChunkLength(l: Int): Unit = {
16-
super.write(l.toHexString.getBytes("UTF-8"))
48+
out.write(l.toHexString.getBytes("UTF-8"))
1749
writeCRLF()
1850
}
1951

2052
private def writeCRLF(): Unit = {
21-
super.write(13)
22-
super.write(10)
53+
out.write(CRLF, 0, CRLF.length)
2354
}
2455

25-
override def flush(): Unit = {
56+
def finish(): Unit = {
57+
flushBuffer()
2658
writeChunkLength(0)
2759
writeCRLF()
28-
super.flush()
2960
}
3061
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package server.http.request
22

3+
import server.http.HttpProtocol
34
import server.http.headers.Headers
45

56
class Request(val method: String, val location: String, val protocol: String, val headers: Headers, val body: String = "") {
67
def keepAlive = {
7-
headers.getOrElse(Headers.CONNECTION, "").equalsIgnoreCase("keep-alive")
8+
protocol == HttpProtocol.HTTP_1_1 && headers.getOrElse(Headers.CONNECTION, "").equalsIgnoreCase("keep-alive")
89
}
910
}
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
package server.http.response
22

3+
import java.io.OutputStream
4+
35
import server.http.headers.Headers
6+
import server.http.response.body.ResponseBody
7+
8+
class Response(val status: Int, val body: ResponseBody = null, val headers: Headers = new Headers) {
9+
if(status == 304 && hasBody) {
10+
throw new Exception("response with status 304 must not have a body")
11+
}
412

5-
class Response(val status: Int, val body: Array[Byte] = null, val headers: Headers = new Headers) {
613
def hasBody = {
7-
body != null && body.nonEmpty
14+
body != null
15+
}
16+
17+
def writeBody(outputStream: OutputStream): Unit = {
18+
if(body != null) {
19+
body.write(outputStream)
20+
}
821
}
922
}

src/main/scala/server/http/response/ResponseWriter.scala

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ package server.http.response
22

33
import java.io._
44
import java.util.zip.GZIPOutputStream
5+
import server.http.encoding.ChunkedOutputStream
56
import server.http.headers.Headers
67
import server.http.{HttpProtocol, HttpMethod}
7-
import server.http.encoding.ChunkedOutputStream
88
import server.http.request.Request
99

1010
class ResponseWriter {
@@ -18,9 +18,9 @@ class ResponseWriter {
1818
//TODO: get correct status message
1919
stringBuilder.append(request.protocol+" "+response.status+" OK"+CRLF)
2020

21-
//TODO: only in case there is a response body with chunking otherwise we have to close it!
21+
//TODO: also make sure we close it if we don't have a content length and no chunks
2222
response.headers += "Connection" -> {
23-
if(false && request.protocol == HttpProtocol.HTTP_1_1 && request.keepAlive) {
23+
if(request.keepAlive) {
2424
"Keep-Alive"
2525
}
2626
else {
@@ -36,12 +36,16 @@ class ResponseWriter {
3636
var bodyOutputStream = outputStream
3737

3838
//TODO: make configurable if chunked should be used [+ do not use for HEAD requests or 304 response]
39-
if(request.protocol == HttpProtocol.HTTP_1_1) {
39+
if(response.hasBody && response.body.getLength.isDefined) {
40+
response.headers += "Content-Length" -> response.body.getLength.get.toString
41+
}
42+
else if( request.protocol == HttpProtocol.HTTP_1_1) {
4043
response.headers += Headers.TRANSFER_ENCODING -> "chunked"
41-
bodyOutputStream = new ChunkedOutputStream(bodyOutputStream, 4096)
44+
bodyOutputStream = new ChunkedOutputStream(bodyOutputStream, 2048)
4245
}
4346

4447
/*
48+
//TODO: make work ;P
4549
//TODO: consider priorities
4650
if(request.headers.getOrElse(Headers.ACCEPT_ENCODING, "").contains("gzip")) {
4751
response.headers += "Content-Encoding" -> "gzip"
@@ -57,7 +61,12 @@ class ResponseWriter {
5761
outputStream.write(stringBuilder.toString().getBytes(encoding))
5862

5963
if(request.method != HttpMethod.HEAD && response.hasBody) {
60-
bodyOutputStream.write(response.body)
64+
response.writeBody(bodyOutputStream)
65+
}
66+
67+
bodyOutputStream match {
68+
case c: ChunkedOutputStream => c.finish()
69+
case _ =>
6170
}
6271

6372
bodyOutputStream.flush()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package server.http.response.body
2+
3+
import java.io.OutputStream
4+
5+
class ByteArrayResponseBody(data: Array[Byte]) extends ResponseBody {
6+
override def write(outputStream: OutputStream): Unit = {
7+
outputStream.write(data)
8+
}
9+
10+
override def getLength: Option[Long] = {
11+
Some(data.length)
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package server.http.response.body
2+
3+
import java.io.{InputStream, OutputStream}
4+
import scala.sys.process.BasicIO
5+
6+
class InputStreamResponseBody(inputStream: InputStream, length: Option[Long] = None) extends ResponseBody {
7+
override def write(outputStream: OutputStream) = {
8+
BasicIO.transferFully(inputStream, outputStream)
9+
}
10+
override def getLength: Option[Long] = {
11+
length
12+
}
13+
}

0 commit comments

Comments
 (0)