http Server
版本1
代码实现:
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) {
// 由于 http 是一个文本协议,仍然使用字符流来处理
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
// 下面的操作要按照 http 协议的格式来进行操作
// 1.读取请求并解析
// a)解析首行 三个部分按照空格切分
String fistLine = bufferedReader.readLine();
String[] fistLineTokens = fistLine.split(" ");
String method = fistLineTokens[0];
String url = fistLineTokens[1];
String version = fistLineTokens[2];
// b)解析 header 按行读取 按照冒号空格分割键值对
Map<String,String> headers = new HashMap<>();
String line = " ";
while ((line = bufferedReader.readLine()) != null && line.length() != 0){
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() + "\n");
}
System.out.println();
// 2.根据请求计算响应
// 不管是什么请求,都返回一个 hello 这样的 html
String resp = "<h1>hello</h1>";
// 3.把响应写回客户端
bufferedWriter.write(version + "200 OK\n");
bufferedWriter.write("Content-Type: text/html\n");
bufferedWriter.write("Content-Length: " + resp.getBytes().length + "\n"); //此处的长度是以为字节为单位的
bufferedWriter.write("\n");
bufferedWriter.write(resp); // body
//刷新缓冲区, 此处的flash 没有,问题也不大,
// 紧接着 bufferedWriter 对象就要关闭了 close 时,就会自动触发刷新操作
bufferedWriter.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(6060);
serverV1.start();
}
使用 fiddler 查看:
浏览器访问咱们自己的服务器时,就会构造一个 http 请求
服务器收到请求后,就返回一个响应:
把代码改一下:让不同的需求,来看到不同的响应
修改部分代码:
String resp = null;
if(url.equals("/OK")){
bufferedWriter.write(version + "200 OK\n");
resp = "<h1>hello</h1>";
}
else if(url.equals("/notfound")){
bufferedWriter.write(version + "404 Not Found\n");
resp = "<h1>not found</h1>";
}
// 重定向情况
else if(url.equals("/seeother")){
bufferedWriter.write(version + " 303 See Other\n");
bufferedWriter.write("Location: http://www.sogou.com\n");
resp = "";
}
else{
bufferedWriter.write(version + "200 OK\n");
resp = "<h1>default</h1>";
}
运行程序,连接服务器:
default:
重定向:
版本2
1.整理代码格式,让代码更规范
2.解析 URL 中包含的参数(键值对),能够方便的处理用户传过来的参数
3.演示 Cookie 的工作流程
request 类:
/*
* 专门表示 Http 请求
* 表示一个 http 请求,并解析
* */
public class HttpRequest {
private String method;
private String url;
private String version;
private Map<String,String> headers = new HashMap<>();
// 表示 url中的参数
private Map<String,String> parameters = new HashMap<>();
// 请求的构造逻辑,仍使用工厂模式来构造
// 此处的参数就是从 socket 中获取到的 InputStream 对象
// 这个过程本质上是在 "反序列化" 把一个比特流,转换成一个结构化数据
public static HttpRequest build(InputStream inputStream) throws IOException {
HttpRequest request = new HttpRequest();
// 此处的逻辑,不能把 bufferedReader 写入到 try() 中
// 一旦写进去之后就意味着 bufferedReader就会被关闭,会影响到 clientSocket 的状态
// 等到最后整个请求处理完了, 再统一关闭
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// 此处的 build 的过程就是解析请求的过程
//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){
// 检查url中是否有 ? ,若没有,说明不带参数,就不需要解析了
// pos 表示 ? 的下标 /index.html?a=10&bb=20
// parameters 的结果就相当于是 a=10&bb=20
String parameters = request.url.substring(pos + 1);
// 切分的最终结果,key-a,value-10 ; key-b,value-20
parseKV(parameters,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.解析 body(暂时不考虑)
return request;
}
private static void parseKV(String input, Map<String, String> output) {
// 1.先按照 & 切分成若干组键值对
String[] kvTokens = input.split("&");
// 2.针对切分结果,再分别进行 按照 = 切分,得到键和值
for (String kv : kvTokens) {
String[] result = kv.split("=");
output.put(result[0],result[1]);
}
}
// 给这个类构造一些 getter方法 (不要搞 setter 方法)
// 请求对象的内容应该从网络上解析来的,用户不应该修改
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getVersion() {
return version;
}
public String getHeader(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 +
'}';
}
}
response 类:
/*
* 专门表示 Http 响应
* 表示一个 http 响应,负责构造
* */
public class HttpResponse {
private String version = "HTTP/1.1";
private int status; // 状态码
private String message; // 状态码描述性信息
private Map<String,String> headers = new HashMap<>();
// 使用StringBuilder 方便进行拼接
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;
}
// 提供一些 setter 方法
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);
}
// 以上设置属性操作,都是在内存中
// 还需要一个专门de方法,把这些属性,按照 Http 协议写到socket中
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();
}
}
主类:
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());
response.setHeader("Content-Type","text/html");
// 2.根据请求计算响应
if(request.getUrl().startsWith("/hello")) {
response.setStatus(200);
response.setMessage("OK");
response.writeBody("<h1>hello</h1>");
}
else{
response.setStatus(200);
response.setMessage("OK");
response.writeBody("<h1>default</h1>");
}
// 3.把响应写回客户端
response.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 这个操作会同时关闭 getInputStream 和 getOutputStream 对象
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
HttpServerV2 serverV2 = new HttpServerV2(6060);
serverV2.start();
}
}
测试结果:
添加参数计算:
else if(request.getUrl().startsWith("/calc")){
// 根据参数的内容进行计算
// 先获取到 a 和 b 两个参数的值
String aStr = request.getParameter("a");
String bStr = request.getParameter("b");
int a = Integer.parseInt(aStr);
int b = Integer.parseInt(bStr);
int result = a + b;
response.setStatus(200);
response.setMessage("OK");
response.writeBody("<h1>result = " + result + "</h1>");
}
测试结果:
基本流程:
1.服务器启动
2.在 process 方法中,要处理一次请求
- a) 读取请求并解析
HttpRequest request = HttpRequest.build(clientSocket.getInputStream());
build 方法:解析请求,严格遵守 http 协议
-
b) 根据请求计算响应 (主要业务逻辑)
不唯一,需根据具体请求,来决定代码怎样实现 - c) 把响应写回到客户端
response.flush();
通过 flush 操作,把 response 中设置好的属性写到 socket 中,这个过程也要遵守 Http 协议,本质上是一个 序列化 的过程