文章目录
Http 和 Https都是应用层协议
URL
url中对应的path不同的时候,获取到的页面也是不同的
url中的服务器的IP确定一个服务器
url中服务器的端口来确定这个主机上的哪个进程
url中的path来确定这个进程中所管理的哪个资源/文件
在搜狗搜索引擎中搜索"蛋糕",可以得到如下URL
https://www.sogou.com/web?
query=%E8%9B%8B%E7%B3%95
&_asf=www.sogou.com
&_ast=&w=01015002
&p=40040108
&ie=utf8
&from=index-nologin
&s_from=index
&oq=
&ri=0
&sourceid=sugg
&suguuid=
&sut=0
&sst0=1597306823875
&lkt=0%2C0%2C0
&sugsuv=001009F56F1247DE5E814028B9A38220
&sugtime=1597306823875
查询字符串中使用 & 把这些内容区分为若干个键值对
每个键值对的键和值之间用 = 分割
此处的键值对的内容外人是不知道的,这是负责此处的程序员自己定义的。
不同服务器上使用的查询字符串中的键值对的内容也是不同的。
这是从百度搜索引擎中去搜索"蛋糕"字样的到的url
https://www.baidu.com/s?
wd=%E8%9B%8B%E7%B3%95
&rsv_spt=1
&rsv_iqid=0xb4446a1300043e82
&issp=1
&f=8
&rsv_bp=1
&rsv_idx=2
&ie=utf-8
&tn=62095104_33_oem_dg
&rsv_enter=1
&rsv_dl=tb
&rsv_sug3=7
&rsv_sug1=5
&rsv_sug7=100
&rsv_sug2=0
&rsv_btype=i
&inputT=1542
&rsv_sug4=1542
#ch1 定位到一个html中的具体位置,(用来实现"回到顶部"功能)
查询词:用户在搜索框中输入的内容
query=%E8%9B%8B%E7%B3%95
urlencode:把url中包含的中文和一些特殊符号进行了转义,转译为
“% + 16进制数字” 的形式。
为什么要转义?
url中包含了一些特殊用途的符号,例如:"/","&","%"…,这些特殊符号有可能会导致浏览器解析出错。
urlencode 的逆向操作 urldencode。
HTTP原理
HTTP协议主要需要了解协议报文格式。
需要借助专门的"抓包"工具(fiddler),查看HTTP具体的协议内容
fiddler
fillder界面主要有三部分
左侧:抓到的包的列表
右上侧:这个包的请求内容
右下侧:这个包的响应内容
HTTP请求与响应
HTTP请求格式
1、首行
方法 (GET)
url
版本号
方法、URL和版本号之间使用空格分隔开
2、协议头(header)
若干个键值对
键和值之间使用 ": " 分隔开
此处的键值对可以是用户自定义的,但是大部分都是HTTP中已有的,具有特定含义的内容
3、空行
header的结束标记
4、正文(body)
可能是空(GET)
也可能是非空(POST)
HTTP 响应格式
1、首行
HTTP/1.1 200 OK
版本号 HTTP/1.1
状态码 200
状态码描述信息 OK
2、协议头(header)
若干个键值对
键和值之间使用 ": " 分隔开
3、空行
header的结束标记
4、正文(body)
响应的正文最常见的就是HTML,表示了一个网页具体是长什么样子的。
HTTP的方法
关于GET和POST
1.GET用于从服务器获取资源,POST用于给服务器提交数据
现在很少严格遵守,两者都可以用来从服务器获取资源和给服务器提交数据。
2.GET传输的数据量限制小(URL长度有限),POST传输的数据量大
很久之前,URL的长度有限,但是现在的URL可能会很长
3.POST比GET更安全
POST只是把密码什么的放在body中,密码不出现在URL(地址栏)中
GET和POST的区别
GET一般把数据放在url中
POST一般把数据放在body中
HTTP状态码
重定向:访问一个一面的时候自动跳转到另一个页面。
HTTP常见header
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度(字节)
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
Cookie 就相当于是一个字符串。
HTTP的特点:无状态(两次HTTP请求之间无关联),从业务上要建立起这样的关联,就需要使用Cookie
Cookie就相当于是保存在浏览器中的一个字符串,这个字符串是通过服务器返回的响应的Set-Cookie字段中过来的,后续再访问该服务器,就会自动带上Cookie字段。
以登录为例,一次登陆中通常涉及两个请求
第一次交互:
请求
响应
第二次交互:
Cookie关联的意义:
HTTP服务器
HTTP服务器V1
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HttpServerV1 {
/**
* Http 底层要基于TCP实现,需要按照TCP的基本格式来开发
*/
private ServerSocket serverSocket = null;
public HttpServerV1(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
//1.获取连接
Socket clientSocket = serverSocket.accept();
//2.处理连接(使用短链接的方式实现)
executorService.execute(new Runnable() {
@Override
public void run() {
process(clientSocket);
}
});
}
}
private void process(Socket clientSocket) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
//严格按照HTTP协议来操作
//1.读取请求并解析
// a) 解析首行
String firstLine = br.readLine();
String[] firstLineTokens = firstLine.split(" ");
String method = firstLineTokens[0];
String url = firstLineTokens[1];
String version = firstLineTokens[2];
// b) 解析header
Map<String, String> headers = new HashMap<>(16);
String line = "";
//readLine 读取的一行内容,是会自动去掉换行符的。对于空行来说,去掉了换行符,就变成空字符串
while ((line = br.readLine()) != null && line.length() != 0) {
//不能使用 : 来切分,像referer字段,里面的内容可能包含:
String[] headerTokens = line.split(": ");
headers.put(headerTokens[0], headerTokens[1]);
}
// c) 解析body
//请求解析完毕,加上一个日志,观察请求的内容是否正确
System.out.printf("%s %s %s\n", method, url, version);
for (Map.Entry<String, String> entry : headers.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
System.out.println();
//2.根据请求计算响应
String response = "";
if (url.equals("/ok")) {
bw.write(version + " 200 OK\n");
response = "<h1>hello</h1>";
} else if (url.equals("/notfound")) {
bw.write(version + " 404 Not Found\n");
response = "<h1>not fount</h1>";
} else if (url.equals("/seeother")) {
bw.write(version + " 303 See Other\n");
bw.write("Location: http://www.baidu.com\n");
response = "";
} else {
bw.write(version + "200 ok\n");
response = "<h1>default</h1>";
}
//3.把响应写回到客户端
bw.write("Content-Type: text/html\n");
//此处的长度不能携程response.length(),得到的是字符的数量
bw.write("Content-Length: " + response.getBytes().length + "\n");
bw.write("\n");
bw.write(response);
bw.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
HttpServerV1 serverV1 = new HttpServerV1(9090);
serverV1.start();
}
}
V1版本比较简陋,只是在TCP的基础上,根据HTTP协议进行解析,然后再根据HTTP协议计算响应。
HTTP 服务器V2
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HttpServerV2 {
private ServerSocket serverSocket = null;
public HttpServerV2(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
executorService.execute(new Runnable() {
@Override
public void run() {
process(clientSocket);
}
});
}
}
public void process(Socket clientSocket) {
try {
//1.读取并解析请求
HttpRequest request = HttpRequest.build(clientSocket.getInputStream());
System.out.println("request: " + request);
HttpResponse response = HttpResponse.build(clientSocket.getOutputStream());
//2.根据请求计算响应
response.setHeader("Content-Type", "text/html");
if (request.getUrl().startsWith("/hello")) {
response.setStatus(200);
response.setMessage("OK");
response.writeBody("<h1>hello</h1>");
} else if (request.getUrl().startsWith("/calc")) {
//根据参数内容进行计算
String aStr = request.getParameter("a");
String bStr = request.getParameter("b");
int a = Integer.parseInt(aStr);
int b = Integer.parseInt(bStr);
response.setStatus(200);
response.setMessage("OK");
response.writeBody("<h1> ret = " + (a+b) + "</h1>");
} else if (request.getUrl().startsWith("/cookieUser")) {
response.setStatus(200);
response.setMessage("OK");
response.setHeader("Set-Cookie", "user=zh");
response.writeBody("<h1>set cookieUser</h1>");
} else if (request.getUrl().startsWith("/cookieTime")) {
response.setStatus(200);
response.setMessage("OK");
response.setHeader("Set-Cookie", "time=" +
(System.currentTimeMillis() / 1000));
response.writeBody("<h1>set cookieTime</h1>");
} else {
response.setStatus(200);
response.setMessage("OK");
response.writeBody("<h1>default</h1>");
}
//3.把响应写回到客户端
response.flush();
} catch (IOException | NullPointerException e) {
e.printStackTrace();
} finally {
try {
//这个操作会同时关闭 getInputStream 和 getOutputStream对象
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
HttpServerV2 httpServerV2 = new HttpServerV2(9090);
httpServerV2.start();
}
}
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class HttpRequest {
private String method;
private String url;
private String version;
private Map<String, String> headers = new HashMap<>(16);
private Map<String, String> parameters = new HashMap<>(16);
/**
* 请求的构造逻辑,也用工厂模式来构造
* build 的过程就是解析请求的过程
* 1.解析首行
* 2.解析url中的参数
* 3.解析 header
* 4.解析body
*
* 这个过程本质上就是在”反序列化“
* 反序列化:把字节序列恢复为对象的过程
* @param inputStream 从socket中获取到的InputStream
* @return
*/
public static HttpRequest build(InputStream inputStream) throws IOException {
HttpRequest request = new HttpRequest();
//此处的逻辑中,不能把br写到try中;一旦写进去就意味着br会被关闭,
//会影响到 clientSocket的状态 等到整个请求处理完了,再统一关闭
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
//1.解析首行
String firstLine = br.readLine();
String[] firstLineTokens = firstLine.split(" ");
request.method = firstLineTokens[0];
request.url = firstLineTokens[1];
request.version = firstLineTokens[2];
//2.解析url中的参数
int pos = request.url.indexOf("?");
if (pos != -1) {
//看看url中是否有? 如果没有,就说明不带参数,也就不需要解析
//此处的 parameters 是希望包含整个 参数 部分的内容
// /index.html?a=10&b=20
//parameters 的结果就是相当于是 a=10&b=20
String parameters = request.url.substring(pos + 1);
parseKV(parameters, request.parameters);
//3.解析 header
String line = "";
while ((line = br.readLine()) != null && line.length() != 0) {
String[] headerTokens = line.split(": ");
request.headers.put(headerTokens[0], headerTokens[1]);
}
//4.解析body 暂时不考虑
}
return request;
}
private static void parseKV(String input, Map<String, String> output) {
//1.先按照&切分成若干键值对
String[] kvTokens = input.split("&");
//2.针对切分结果再分别进行按照 = 切分,就得到了键和值
for (String kv : kvTokens) {
String[] ret = kv.split("=");
output.put(ret[0], ret[1]);
}
}
//给这个类构造一些getter方法 不需要setter方法,因为请求对象内容是从网络
//上解析来的,用户不应该修改
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getVersion() {
return version;
}
public String getHeaders(String key) {
return headers.get(key);
}
public String getParameter(String key) {
return parameters.get(key);
}
@Override
public String toString() {
return "HttpRequest{" +
"method='" + method + '\'' +
", url='" + url + '\'' +
", version='" + version + '\'' +
", headers=" + headers +
", parameters=" + parameters +
'}';
}
}
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class HttpResponse {
private String version = "HTTP/1.1";
/**
* 状态码
*/
private int status;
/**
* 状态码描述信息
*/
private String message;
private Map<String, String> headers = new HashMap<>(16);
private StringBuilder body = new StringBuilder();
/**
* 当需要把响应写回给客户端的时候,就要往outputStream中写
*/
private OutputStream outputStream = null;
public static HttpResponse build(OutputStream outputStream) {
HttpResponse response = new HttpResponse();
response.outputStream = outputStream;
//除了outputStream之外,其他的暂时无法确定
return response;
}
public void setVersion(String version) {
this.version = version;
}
public void setStatus(int status) {
this.status = status;
}
public void setMessage(String message) {
this.message = message;
}
public void setHeader(String key, String val) {
headers.put(key, val);
}
public void writeBody(String content) {
body.append(content);
}
/**
* 以上的设置属性的操作都是在内存中操作
* 还需要一个专门的方法,把这些属性 按照Http协议,写到socket中
*/
public void flush() throws IOException {
BufferedWriter bw = new BufferedWriter(new
OutputStreamWriter(outputStream));
bw.write(version + " " + status + " " + message + "\n");
headers.put("Content-Length", body.toString().getBytes().length + "");
for (Map.Entry<String, String> entry : headers.entrySet()) {
bw.write(entry.getKey() + ": " + entry.getValue() + "\n");
}
bw.write("\n");
bw.write(body.toString());
bw.flush();
}
}
HTTP服务器V3
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HttpServerV3 {
static class User {
// 保存用户的相关信息
public String userName;
public int age;
public String school;
}
private ServerSocket serverSocket = null;
// session 会话. 指的就是同一个用户的一组访问服务器的操作, 归类到一起, 就是一个会话.
// 记者来采访你, 记者问的问题就是一个请求, 你回答的内容, 就是一个响应. 一次采访过程中
// 涉及到很多问题和回答(请求和响应), 这一组问题和回答, 就可以称为是一个 "会话" (整个采访的过程)
// sessions 中就包含很多会话. (每个键值对就是一个会话)
private HashMap<String, User> sessions = new HashMap<>();
public HttpServerV3(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
executorService.execute(new Runnable() {
@Override
public void run() {
process(clientSocket);
}
});
}
}
public void process(Socket clientSocket) {
// 处理核心逻辑
try {
// 1. 读取请求并解析
HttpRequest request = HttpRequest.build(clientSocket.getInputStream());
HttpResponse response = HttpResponse.build(clientSocket.getOutputStream());
// 2. 根据请求计算响应
// 此处按照不同的 HTTP 方法, 拆分成多个不同的逻辑
if ("GET".equalsIgnoreCase(request.getMethod())) {
doGet(request, response);
} else if ("POST".equalsIgnoreCase(request.getMethod())) {
doPost(request, response);
} else {
// 其他方法, 返回一个 405 这样的状态码
response.setStatus(405);
response.setMessage("Method Not Allowed");
}
// 3. 把响应写回到客户端
response.flush();
} catch (IOException | NullPointerException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void doGet(HttpRequest request, HttpResponse response) throws IOException {
// 1. 能够支持返回一个 html 文件.
if (request.getUrl().startsWith("/index.html")) {
String sessionId = request.getCookie("sessionId");
User user = sessions.get(sessionId);
if (sessionId == null || user == null) {
// 说明当前用户尚未登陆, 就返回一个登陆页面即可.
// 这种情况下, 就让代码读取一个 index.html 这样的文件.
// 要想读文件, 需要先知道文件路径. 而现在只知道一个 文件名 index.html
// 此时这个 html 文件所属的路径, 可以自己来约定(约定某个 d:/...) 专门放 html .
// 把文件内容写入到响应的 body 中
response.setStatus(200);
response.setMessage("OK");
response.setHeader("Content-Type", "text/html; charset=utf-8");
InputStream inputStream = HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// 按行读取内容, 把数据写入到 response 中
String line = null;
while ((line = bufferedReader.readLine()) != null) {
response.writeBody(line + "\n");
}
bufferedReader.close();
} else {
// 用户已经登陆, 无需再登陆了.
response.setStatus(200);
response.setMessage("OK");
response.setHeader("Content-Type", "text/html; charset=utf-8");
response.writeBody("<html>");
response.writeBody("<div>" + "您已经登陆了! 无需再次登陆! 用户名: " + user.userName + "</div>");
response.writeBody("<div>" + user.age + "</div>");
response.writeBody("<div>" + user.school + "</div>");
response.writeBody("</html>");
}
}
}
private void doPost(HttpRequest request, HttpResponse response) {
// 2. 实现 /login 的处理
if (request.getUrl().startsWith("/login")) {
// 读取用户提交的用户名和密码
String userName = request.getParameter("username");
String password = request.getParameter("password");
// System.out.println("userName: " + userName);
// System.out.println("password: " + password);
// 登陆逻辑就需要验证用户名密码是否正确.
// 此处为了简单, 咱们把用户名和密码在代码中写死了.
// 更科学的处理方式, 应该是从数据库中读取用户名对应的密码, 校验密码是否一致.
if ("zh".equals(userName) && "123".equals(password)) {
// 登陆成功
response.setStatus(200);
response.setMessage("OK");
response.setHeader("Content-Type", "text/html; charset=utf-8");
// 原来登陆成功, 是给浏览器写了一个 cookie, cookie 中保存的是用户的用户名.
// response.setHeader("Set-Cookie", "userName=" + userName);
// 现有的对于登陆成功的处理. 给这次登陆的用户分配了一个 session
// (在 hash 中新增了一个键值对), key 是随机生成的. value 就是用户的身份信息
// 身份信息保存在服务器中, 此时也就不再有泄露的问题了
// 给浏览器返回的 Cookie 中只需要包含 sessionId 即可
String sessionId = UUID.randomUUID().toString();
User user = new User();
user.userName = "zh";
user.age = 20;
user.school = "陕科大";
sessions.put(sessionId, user);
response.setHeader("Set-Cookie", "sessionId=" + sessionId);
response.writeBody("<html>");
response.writeBody("<div>欢迎您! " + userName + "</div>");
response.writeBody("</html>");
} else {
// 登陆失败
response.setStatus(403);
response.setMessage("Forbidden");
response.setHeader("Content-Type", "text/html; charset=utf-8");
response.writeBody("<html>");
response.writeBody("<div>登陆失败</div>");
response.writeBody("</html>");
}
}
}
public static void main(String[] args) throws IOException {
HttpServerV3 serverV3 = new HttpServerV3(9090);
serverV3.start();
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class HttpRequest {
private String method;
private String url;
private String version;
private Map<String, String> headers = new HashMap<>();
// url 中的参数和 body 中的参数都放到这个 parameters hash 表中.
private Map<String, String> parameters = new HashMap<>();
private Map<String, String> cookies = new HashMap<>();
private String body;
public static HttpRequest build(InputStream inputStream) throws IOException {
HttpRequest request = new HttpRequest();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// 1. 处理首行
String firstLine = bufferedReader.readLine();
String[] firstLineTokens = firstLine.split(" ");
request.method = firstLineTokens[0];
request.url = firstLineTokens[1];
request.version = firstLineTokens[2];
// 2. 解析 url
int pos = request.url.indexOf("?");
if (pos != -1) {
String queryString = request.url.substring(pos + 1);
parseKV(queryString, request.parameters);
}
// 3. 循环处理 header 部分
String line = "";
while ((line = bufferedReader.readLine()) != null && line.length() != 0) {
String[] headerTokens = line.split(": ");
request.headers.put(headerTokens[0], headerTokens[1]);
}
// 4. 解析 cookie
String cookie = request.headers.get("Cookie");
if (cookie != null) {
// 把 cookie 进行解析
parseCookie(cookie, request.cookies);
}
// 5. 解析 body
if ("POST".equalsIgnoreCase(request.method)
|| "PUT".equalsIgnoreCase(request.method)) {
// 这两个方法需要处理 body, 其他方法暂时不考虑
// 需要把 body 读取出来.
// 需要先知道 body 的长度. Content-Length 就是干这个的.
// 此处的长度单位是 "字节"
int contentLength = Integer.parseInt(request.headers.get("Content-Length"));
// 注意体会此处的含义~~
// 例如 contentLength 为 100 , body 中有 100 个字节.
// 下面创建的缓冲区长度是 100 个 char (相当于是 200 个字节)
// 缓冲区不怕长. 就怕不够用. 这样创建的缓冲区才能保证长度管够~~
char[] buffer = new char[contentLength];
int len = bufferedReader.read(buffer);
request.body = new String(buffer, 0, len);
// body 中的格式形如: username=tanglaoshi&password=123
parseKV(request.body, request.parameters);
}
return request;
}
private static void parseCookie(String cookie, Map<String, String> cookies) {
// 1. 按照 分号空格 拆分成多个键值对
String[] kvTokens = cookie.split("; ");
// 2. 按照 = 拆分每个键和值
for (String kv : kvTokens) {
String[] result = kv.split("=");
cookies.put(result[0], result[1]);
}
}
private static void parseKV(String queryString, Map<String, String> parameters) {
// 1. 按照 & 拆分成多个键值对
String[] kvTokens = queryString.split("&");
// 2. 按照 = 拆分每个键和值
for (String kv : kvTokens) {
String[] result = kv.split("=");
parameters.put(result[0], result[1]);
}
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getVersion() {
return version;
}
public String getBody() {
return body;
}
public String getParameter(String key) {
return parameters.get(key);
}
public String getHeader(String key) {
return headers.get(key);
}
public String getCookie(String key) {
return cookies.get(key);
}
}
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class HttpResponse {
private String version = "HTTP/1.1";
private int status;
private String message;
private Map<String, String> headers = new HashMap<>();
private StringBuilder body = new StringBuilder();
private OutputStream outputStream = null;
public static HttpResponse build(OutputStream outputStream) {
HttpResponse response = new HttpResponse();
response.outputStream = outputStream;
return response;
}
public void setVersion(String version) {
this.version = version;
}
public void setStatus(int status) {
this.status = status;
}
public void setMessage(String message) {
this.message = message;
}
public void setHeader(String key, String value) {
headers.put(key, value);
}
public void writeBody(String content) {
body.append(content);
}
public void flush() throws IOException {
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write(version + " " + status + " " + message + "\n");
headers.put("Content-Length", body.toString().getBytes().length + "");
for (Map.Entry<String, String> entry : headers.entrySet()) {
bufferedWriter.write(entry.getKey() + ": " + entry.getValue() + "\n");
}
bufferedWriter.write("\n");
bufferedWriter.write(body.toString());
bufferedWriter.flush();
}
}