一、概述
NanoHttpd是使用Java实现的微型web server,是一个可嵌入应用程序的轻量级的HTTP Server
package com.example;
import java.io.IOException;
import java.util.Map;
import org.nanohttpd.NanoHTTPD;
// NOTE: If you're using NanoHTTPD < 3.0.0 the namespace is different,
// instead of the above import use the following:
// import fi.iki.elonen.NanoHTTPD;
public class App extends NanoHTTPD {
public static void main(String[] args) {
try {
new App();
} catch (IOException ioe) {
System.err.println("Couldn't start server:\n" + ioe);
}
}
public App() throws IOException {
super(8080);
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
System.out.println("\nRunning! Point your browsers to http://localhost:8080/ \n");
}
/**
* 接收并响应HTTP请求
*/
@Override
public Response serve(IHTTPSession session) {
String msg = "<html><body><h1>Hello server</h1>\n";
Map<String, String> parms = session.getParms();
if (parms.get("username") == null) {
msg += "<form action='?' method='get'>\n <p>Your name: <input type='text' name='username'></p>\n" + "</form>\n";
} else {
msg += "<p>Hello, " + parms.get("username") + "!</p>";
}
return newFixedLengthResponse(msg + "</body></html>\n");
}
}
核心
- 只有一个Java文件,提供HTTP 1.1支持。
- 没有固定的配置文件,日志记录,授权等等(如果你需要它们,自己实现。但是错误传递给java.util.logging)。
- 支持HTTPS(SSL)。
- 对cookie的基本支持。
- 支持GET和POST方法的参数解析。
- 一些内置的HEAD,POST和DELETE请求支持。不过,您可以轻松实现/定制任何HTTP方法。
- 支持文件上传。为小型上传使用内存,为大型文件使用临时文件。
- 从不缓存任何东西。
- 默认情况下,不会限制带宽,请求时间或同时连接。
- 所有标题名称都被转换为小写字母,因此它们在浏览器/客户端之间不会改变。
- 持久连接(连接“保持活动”)支持允许通过单个套接字连接提供多个请求。
WebSocket
- 经过Firefox,Chrome和IE测试。
Webserver
- 默认代码提供文件和显示(在控制台上打印)所有HTTP参数和标题。
- 支持动态内容和文件服务。
- 文件服务器支持目录列表index.html和index.htm。
- 文件服务器支持部分内容(流媒体和继续下载)。
- 文件服务器支持ETags。
- 文件服务器为没有的目录执行301重定向技巧/。
- 文件服务器还提供非常长的文件,无需内存开销。
- 包含大多数常见MIME类型的内置列表。
- 运行时扩展支持(服务特定MIME类型的扩展) - 服务于Markdown格式文件的示例扩展。只需在Web服务器类路径中包含扩展JAR即可加载扩展。
- 简单的CORS支持通过–cors参数
- 默认服务 Access-Control-Allow-Headers: origin,accept,content-type
- Access-Control-Allow-Headers通过设置系统属性设置的可能性:AccessControlAllowHeader
- 示例: -DAccessControlAllowHeader=origin,accept,content-type,Authorization
- 可能的值:
- –cors:激活CORS支持,Access-Control-Allow-Origin将被设置为*。
- –cors=some_value:Access-Control-Allow-Origin将被设置为some_value。
CORS参数的例子
- –cors=http://appOne.company.com
- –cors=”http://appOne.company.com, http://appTwo.company.com“:请注意双引号,以便将这两个URL视为单个参数的一部分。
二、Socket通信承载功能一览
可以参看(4.2.46)AndroidGodEye源码整体结构分析 中的原生写法
- 在线程或后台任务(非UI线程)中,实例化socket并设置监听端口;
开启 while (mIsRunning) {…}死循环体,监听请求
循环体内:
2.1 Socket socket = mServerSocket.accept();响应一次请求
2.2 mRequestHandler.handle(socket) 处理本次请求
2.2.1 获取请求体中的Path
2.2.2 根据 “后缀命名规则(/空,.html,.js等)“和“已支持Servlet列表(/login,/regiter…)”进行响应,前者返回本地文件数据流,后者返回java处理数据流
2.2.2 根据请求体中的参数,进行对应响应
2.2.2 html页面元素中的标签资源,会再次向服务器做请求,类似“后缀命名规则(/空,.html,.js等)逻辑
2.2.3 返回“HTTP/1.0 200 OK”,“Content-Type: ”,“Content-Length:”头部,以及对应数据体
2.3 socket.close() 结束本次请求
【图1 processon】
NanoHTTPD项目目前由四部分组成:
/core - 由一(1)个Java文件组成的全功能的HTTP(s)服务器,可随时为您自己的项目定制/继承。
/samples - 关于如何定制NanoHTTPD的简单例子。看到HelloServer.java是一款热情招呼你的杀手级应用!
/nanolets - 独立的nano应用服务器,在/core的基础上,为实现者提供一个类似于系统的servlet。 响应URL返回JAVA运算数据
/webserver - 独立的文件服务器。运行和享受。一种流行的用途似乎是从Android设备上提供文件。 响应URL返回本地资源文件数据
/websocket - Websocket的实现,也在一个Java文件中,依赖于core。
/fileupload - 整合了apache常用文件上传库。
三、核心实现NanoHTTPD
新版本的代码可能和分析的不太一样,但是逻辑是相同的
3.1 持有变量与构造函数
public final String hostname;
public final int myPort;//端口
private volatile ServerSocket myServerSocket;//socketServer
private IFactoryThrowing<ServerSocket, IOException> serverSocketFactory = new DefaultServerSocketFactory();//socketServer工厂
private IHandler<IHTTPSession, Response> httpHandler;//请求处理器
protected List<IHandler<IHTTPSession, Response>> interceptors = new ArrayList<IHandler<IHTTPSession, Response>>(4);//拦截器
protected IAsyncRunner asyncRunner;//异步任务运行器
private IFactory<ITempFileManager> tempFileManagerFactory;//临时文件操作管理器的生成工厂
public NanoHTTPD(int port) {
this(null, port);
}
public NanoHTTPD(String hostname, int port) {
this.hostname = hostname;
this.myPort = port;
setTempFileManagerFactory(new DefaultTempFileManagerFactory());
setAsyncRunner(new DefaultAsyncRunner());
// creates a default handler that redirects to deprecated serve();
this.httpHandler = new IHandler<IHTTPSession, Response>() {
@Override
public Response handle(IHTTPSession input) {
return NanoHTTPD.this.serve(input);
}
};
}
3.1.1 DefaultServerSocketFactory 服务SocketServer工厂
/**
* SocketServer工厂,用于生产ServerSocket
*/
public class DefaultServerSocketFactory implements IFactoryThrowing<ServerSocket, IOException> {
@Override
public ServerSocket create() throws IOException {
return new ServerSocket();
}
}
/**
* 工厂能力抽象: 一个“可以在生产实体期间抛出异常”的生成函数
*/
public interface IFactoryThrowing<T, E extends Throwable> {
T create() throws E;
}
nanohttpd下的sockets下还有一个SecureServerSocketFactory
/**
* Creates a new SSLServerSocket
*/
public class SecureServerSocketFactory implements IFactoryThrowing<ServerSocket, IOException> {
private SSLServerSocketFactory sslServerSocketFactory;
private String[] sslProtocols;
public SecureServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) {
this.sslServerSocketFactory = sslServerSocketFactory;
this.sslProtocols = sslProtocols;
}
@Override
public ServerSocket create() throws IOException {
SSLServerSocket ss = null;
ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket();
if (this.sslProtocols != null) {
ss.setEnabledProtocols(this.sslProtocols);
} else {
ss.setEnabledProtocols(ss.getSupportedProtocols());
}
ss.setUseClientMode(false);
ss.setWantClientAuth(false);
ss.setNeedClientAuth(false);
return ss;
}
}
3.1.2 DefaultTempFileManagerFactory 临时文件的操作管理器的生成工厂
nanohttpd下的tempfiles全部
- 临时文件操作管理器的生成工厂
/**
* 创建和清除临时文件的默认策略生成器
*/
public class DefaultTempFileManagerFactory implements IFactory<ITempFileManager> {
@Override
public ITempFileManager create() {
return new DefaultTempFileManager();
}
}
/**
* 工厂能力抽象
*/
public interface IFactory<T> {
T create();
}
- 默认的临时文件操作管理器
public interface ITempFileManager {
void clear();
public ITempFile createTempFile(String filename_hint) throws Exception;
}
/**
* 创建和清除临时文件的默认策略
* - 文件被存储在默认路径 java.io.tmpdir所指向位置
* - 临时文件的创建
* - 提供清除功能
*/
public class DefaultTempFileManager implements ITempFileManager {
private final File tmpdir;
private final List<ITempFile> tempFiles;
public DefaultTempFileManager() {
this.tmpdir = new File(System.getProperty("java.io.tmpdir"));
if (!tmpdir.exists()) {
tmpdir.mkdirs();
}
this.tempFiles = new ArrayList<ITempFile>();
}
@Override
public void clear() {
for (ITempFile file : this.tempFiles) {
try {
file.delete();
} catch (Exception ignored) {
NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored);
}
}
this.tempFiles.clear();
}
@Override
public ITempFile createTempFile(String filename_hint) throws Exception {
DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir);
this.tempFiles.add(tempFile);
return tempFile;
}
}
- 临时文件:
public interface ITempFile {
public void delete() throws Exception;
public String getName();
public OutputStream open() throws Exception;
}
public class DefaultTempFile implements ITempFile {
private final File file;
private final OutputStream fstream;
public DefaultTempFile(File tempdir) throws IOException {
this.file = File.createTempFile("NanoHTTPD-", "", tempdir);
this.fstream = new FileOutputStream(this.file);
}
@Override
public void delete() throws Exception {
NanoHTTPD.safeClose(this.fstream);
if (!this.file.delete()) {
throw new Exception("could not delete temporary file: " + this.file.getAbsolutePath());
}
}
@Override
public String getName() {
return this.file.getAbsolutePath();
}
@Override
public OutputStream open() throws Exception {
return this.fstream;
}
}
3.1.3 DefaultAsyncRunner 异步任务运行器
默认的,当有新的Request来的时候,创新一个新的线程来响应;
这个线程被设置为守护线程和对应的命名
/**
* 为requests的响应过程提供异步过程
*/
public interface IAsyncRunner {
void closeAll();
void closed(ClientHandler clientHandler);
void exec(ClientHandler code);
}
public class DefaultAsyncRunner implements IAsyncRunner {
protected long requestCount;
private final List<ClientHandler> running = Collections.synchronizedList(new ArrayList<ClientHandler>());
/**
* @return a list with currently running clients.
*/
public List<ClientHandler> getRunning() {
return running;
}
@Override
public void closeAll() {
// copy of the list for concurrency
for (ClientHandler clientHandler : new ArrayList<ClientHandler>(this.running)) {
clientHandler.close();
}
}
@Override
public void closed(ClientHandler clientHandler) {
this.running.remove(clientHandler);
}
@Override
public void exec(ClientHandler clientHandler) {
++this.requestCount;
this.running.add(clientHandler);
createThread(clientHandler).start();
}
protected Thread createThread(ClientHandler clientHandler) {
Thread t = new Thread(clientHandler);
t.setDaemon(true);
t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")");
return t;
}
}
3.1.4 IHandler响应器
我们在使用NanoHTTPD时候会重写覆盖Response serve(IHTTPSession session)方法,用于真正的处理Request
这里给一个默认的实现,返回一个 NotFound异常Reponse
// creates a default handler that redirects to deprecated serve();
this.httpHandler = new IHandler<IHTTPSession, Response>() {
@Override
public Response handle(IHTTPSession input) {
return NanoHTTPD.this.serve(input);
}
};
@Deprecated
protected Response serve(IHTTPSession session) {
return Response.newFixedLengthResponse(Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found");
}
3.2 核心start函数
- 【步骤1】使用服务SocketServer工厂生成一个 ServerSocket
- 【步骤2】开启一个线程,并将当前NanoHTTPD传递过去
public void start(final int timeout) throws IOException {
start(timeout, true);
}
public void start(final int timeout, boolean daemon) throws IOException {
//【步骤1】使用服务SocketServer工厂生成一个 ServerSocket
this.myServerSocket = this.getServerSocketFactory().create();
this.myServerSocket.setReuseAddress(true);
//【步骤2】开启一个线程,并将当前NanoHTTPD传递过去
ServerRunnable serverRunnable = createServerRunnable(timeout);
this.myThread = new Thread(serverRunnable);
this.myThread.setDaemon(daemon);
this.myThread.setName("NanoHttpd Main Listener");
this.myThread.start();
//绑定端口失败,譬如端口被占用
while (!serverRunnable.hasBinded() && serverRunnable.getBindException() == null) {
try {
Thread.sleep(10L);
} catch (Throwable e) {
// on android this may not be allowed, that's why we
// catch throwable the wait should be very short because we are
// just waiting for the bind of the socket
}
}
if (serverRunnable.getBindException() != null) {
throw serverRunnable.getBindException();
}
}
protected ServerRunnable createServerRunnable(final int timeout) {
return new ServerRunnable(this, timeout);
}
- 【步骤3】异步线程的执行
- 【步骤3.1】绑定端口和地址,重置绑定标示和绑定异常
- 【步骤3.2】开启死循环,监听request: ServerSocket.accept()
- 【步骤3.3】根据请求soket及其输入流构建处理器ClientHandler
- 【步骤3.4】ClientHandler传入异步任务运行器中执行
@Override
public void run() {
try {
httpd.getMyServerSocket().bind(httpd.hostname != null ? new InetSocketAddress(httpd.hostname, httpd.myPort) : new InetSocketAddress(httpd.myPort));
hasBinded = true;
} catch (IOException e) {
this.bindException = e;
return;
}
do {
try {
final Socket finalAccept = httpd.getMyServerSocket().accept();
if (this.timeout > 0) {
finalAccept.setSoTimeout(this.timeout);
}
final InputStream inputStream = finalAccept.getInputStream();
httpd.asyncRunner.exec(httpd.createClientHandler(finalAccept, inputStream));
} catch (IOException e) {
NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
}
} while (!httpd.getMyServerSocket().isClosed());
}
- 【步骤4】分发请求
- 【步骤4.1】获取socket的输出流
- 【步骤4.2】获取 临时文件管理器
- 【步骤4.3】NanoHTTPD、临时文件管理器ITempFileManager、输入流、输出流、ip端口,共同构建HttpSession
- 【步骤4.3】socket未关闭时,死循环执行 session.execute();
public class ClientHandler implements Runnable {
private final NanoHTTPD httpd;
private final InputStream inputStream;
private final Socket acceptSocket;
public ClientHandler(NanoHTTPD httpd, InputStream inputStream, Socket acceptSocket) {
this.httpd = httpd;
this.inputStream = inputStream;
this.acceptSocket = acceptSocket;
}
public void close() {
NanoHTTPD.safeClose(this.inputStream);
NanoHTTPD.safeClose(this.acceptSocket);
}
@Override
public void run() {
OutputStream outputStream = null;
try {
outputStream = this.acceptSocket.getOutputStream();
ITempFileManager tempFileManager = httpd.getTempFileManagerFactory().create();
HTTPSession session = new HTTPSession(httpd, tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress());
while (!this.acceptSocket.isClosed()) {
session.execute();
}
} catch (Exception e) {
// When the socket is closed by the client,
// we throw our own SocketException
// to break the "keep alive" loop above. If
// the exception was anything other
// than the expected SocketException OR a
// SocketTimeoutException, print the
// stacktrace
if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) {
NanoHTTPD.LOG.log(Level.SEVERE, "Communication with the client broken, or an bug in the handler code", e);
}
} finally {
NanoHTTPD.safeClose(outputStream);
NanoHTTPD.safeClose(this.inputStream);
NanoHTTPD.safeClose(this.acceptSocket);
httpd.asyncRunner.closed(this);
}
}
}
3.3 HttpSession
HttpSession是java里Session概念的实现,简单来说一个Session就是一次httpClient->httpServer的连接,当连接close后session就结束了,如果没结束则session会一直存在。这点从这里的代码也能看到:如果socket不close或者exec没有抛出异常(异常有可能是client段断开连接)session会一直执行exec方法
一个HttpSession中存储了一次网络连接中server应该保存的信息,比如:URI,METHOD,PARAMS,HEADERS,COOKIES等
public interface IHTTPSession {
void execute() throws IOException;
void parseBody(Map<String, String> files) throws IOException, ResponseException;
CookieHandler getCookies();
Map<String, String> getHeaders();
InputStream getInputStream();
Method getMethod();
Map<String, List<String>> getParameters();
String getQueryParameterString();
String getUri();
String getRemoteIpAddress();
String getRemoteHostName();
}
我们重点看下execute函数:
其实就是:解析Respons中的各类参数赋值到HttpSession中,把当前HttpSession传给自己复写的处理函数作为形参,调用并返回Response
- 【步骤1】读取socket数据流的前8192个字节,因为http协议中头部最长为8192
- 【步骤2】通过findHeaderEnd函数找到header数据的截止位置,并把位置保存到splitbyte内。
byte[] buf = new byte[BUFSIZE];
splitbyte = 0;
rlen = 0;
{
int read = -1;
try {
read = inputStream.read(buf, 0, BUFSIZE);
} catch (Exception e) {
safeClose(inputStream);
safeClose(outputStream);
throw new SocketException("NanoHttpd Shutdown");
}
if (read == -1) {
// socket was been closed
safeClose(inputStream);
safeClose(outputStream);
throw new SocketException("NanoHttpd Shutdown");
}
while (read > 0) {
rlen += read;
splitbyte = findHeaderEnd(buf, rlen);
if (splitbyte > 0)
break;
read = inputStream.read(buf, rlen, BUFSIZE - rlen);
}
}
//涉及函数
private int findHeaderEnd(final byte[] buf, int rlen) {
//Http协议规定header和body之间使用两个回车换行分割
int splitbyte = 0;
while (splitbyte + 3 < rlen) {
if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
return splitbyte + 4;
}
splitbyte++;
}
return 0;
}
- 【步骤3】使用unread函数将之前读出来的body pushback回去,这里使用了pushbackstream,用法比较巧妙,因为一旦读到了header的尾部就需要进入下面的逻辑来判断是否需要再读下去了,而不应该一直读,读到没有数据为止
- 【步骤4】decodeHeader,将byte的header转换为java对象
if (splitbyte < rlen) {
inputStream.unread(buf, splitbyte, rlen - splitbyte);
}
parms = new HashMap<String, String>();
if(null == headers) {
headers = new HashMap<String, String>();
}
// Create a BufferedReader for parsing the header.
BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));
// Decode the header into parms and header java properties
Map<String, String> pre = new HashMap<String, String>();
decodeHeader(hin, pre, parms, headers);
//涉及函数
//1.Http协议第一行是Method URI HTTP_VERSION
//2.后面每行都是KEY:VALUE格式的header
//3.uri需要经过URIDecode处理后才能使用
//4.uri中如果包含?则表示有param,httprequest的param一般表现为:/index.jsp?username=xiaoming&id=2
private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers)
throws ResponseException {
try {
// Read the request line
String inLine = in.readLine();
if (inLine == null) {
return;
}
StringTokenizer st = new StringTokenizer(inLine);
if (!st.hasMoreTokens()) {
throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
}
pre.put("method", st.nextToken());
if (!st.hasMoreTokens()) {
throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
}
String uri = st.nextToken();
// Decode parameters from the URI
int qmi = uri.indexOf('?');
if (qmi >= 0) {
decodeParms(uri.substring(qmi + 1), parms);
uri = decodePercent(uri.substring(0, qmi));
} else {
uri = decodePercent(uri);
}
// If there's another token, it's protocol version,
// followed by HTTP headers. Ignore version but parse headers.
// NOTE: this now forces header names lowercase since they are
// case insensitive and vary by client.
if (st.hasMoreTokens()) {
String line = in.readLine();
while (line != null && line.trim().length() > 0) {
int p = line.indexOf(':');
if (p >= 0)
headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
line = in.readLine();
}
}
pre.put("uri", uri);
} catch (IOException ioe) {
throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
}
}
- 【步骤5】处理cookie
this.cookies = new CookieHandler(this.headers);
- 【步骤6】响应处理,并生成Respons
- 过滤器调用
- 自己重写的处理函数响应
r = httpd.handle(this);
//涉及NanoHTTPD的函数
public Response handle(IHTTPSession session) {
for (IHandler<IHTTPSession, Response> interceptor : interceptors) {
Response response = interceptor.handle(session);
if (response != null)
return response;
}
return httpHandler.handle(session);
}
//涉及NanoHTTPD的变量
this.httpHandler = new IHandler<IHTTPSession, Response>() {
@Override
public Response handle(IHTTPSession input) {
return NanoHTTPD.this.serve(input);
}
};
//涉及NanoHTTPD的函数
@Deprecated
protected Response serve(IHTTPSession session) {
return Response.newFixedLengthResponse(Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found");
}
- 【步骤7】Response返回,将返回数据写入输出流中
r.send(this.outputStream);
3.4 Response响应
发送response的步骤如下:
1.设置mimeType和Time等内容。
2.创建一个PrintWriter,按照HTTP协议依次开始写入内容
3.第一行是HTTP的返回码
4.然后是content-Type
5.然后是Date时间
6.之后是其他的HTTP Header
7.设置Keep-Alive的Header,Keep-Alive是Http1.1的新特性,作用是让客户端和服务器端之间保持一个长链接。
8.如果客户端指定了ChunkedEncoding则分块发送response,Chunked Encoding是Http1.1的又一新特性。一般在response的body比较大的时候使用,server端会首先发送response的HEADER,然后分块发送response的body,每个分块都由chunk length\r\n和chunk data\r\n组成,最后由一个0\r\n结束。
/**
* Sends given response to the socket.
*/
protected void send(OutputStream outputStream) {
String mime = mimeType;
SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
try {
if (status == null) {
throw new Error("sendResponse(): Status can't be null.");
}
PrintWriter pw = new PrintWriter(outputStream);
pw.print("HTTP/1.1 " + status.getDescription() + " \r\n");
if (mime != null) {
pw.print("Content-Type: " + mime + "\r\n");
}
if (header == null || header.get("Date") == null) {
pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
}
if (header != null) {
for (String key : header.keySet()) {
String value = header.get(key);
pw.print(key + ": " + value + "\r\n");
}
}
sendConnectionHeaderIfNotAlreadyPresent(pw, header);
if (requestMethod != Method.HEAD && chunkedTransfer) {
sendAsChunked(outputStream, pw);
} else {
int pending = data != null ? data.available() : 0;
sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending);
pw.print("\r\n");
pw.flush();
sendAsFixedLength(outputStream, pending);
}
outputStream.flush();
safeClose(data);
} catch (IOException ioe) {
// Couldn't write? No can do.
}
}
9.如果没指定ChunkedEncoding则需要指定Content-Length来让客户端指定response的body的size,然后再一直写body直到写完为止
private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException {
pw.print("Transfer-Encoding: chunked\r\n");
pw.print("\r\n");
pw.flush();
int BUFFER_SIZE = 16 * 1024;
byte[] CRLF = "\r\n".getBytes();
byte[] buff = new byte[BUFFER_SIZE];
int read;
while ((read = data.read(buff)) > 0) {
outputStream.write(String.format("%x\r\n", read).getBytes());
outputStream.write(buff, 0, read);
outputStream.write(CRLF);
}
outputStream.write(String.format("0\r\n\r\n").getBytes());
}
private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException {
if (requestMethod != Method.HEAD && data != null) {
int BUFFER_SIZE = 16 * 1024;
byte[] buff = new byte[BUFFER_SIZE];
while (pending > 0) {
int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
if (read <= 0) {
break;
}
outputStream.write(buff, 0, read);
pending -= read;
}
}
}