简单实现 http Server (版本1、2)

x33g5p2x  于2022-02-07 转载在 其他  
字(8.7k)|赞(0)|评价(0)|浏览(213)

版本1

代码实现:

  1. public class HttpServerV1 {
  2. /*
  3. * http 底层要基于 tcp 来实现
  4. * 需要按照 tcp 的基本格式来进行开发
  5. * */
  6. private ServerSocket serverSocket = null;
  7. public HttpServerV1(int port) throws IOException {
  8. serverSocket = new ServerSocket(port);
  9. }
  10. public void start() throws IOException {
  11. System.out.println("服务器启动...");
  12. ExecutorService executorService = Executors.newCachedThreadPool();
  13. while (true){
  14. // 1.获取连接
  15. Socket clientSocket = serverSocket.accept();
  16. // 2.处理连接 (使用短连接方式)
  17. executorService.execute(new Runnable() {
  18. @Override
  19. public void run() {
  20. process(clientSocket);
  21. }
  22. });
  23. }
  24. }
  25. private void process(Socket clientSocket) {
  26. // 由于 http 是一个文本协议,仍然使用字符流来处理
  27. try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
  28. BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
  29. // 下面的操作要按照 http 协议的格式来进行操作
  30. // 1.读取请求并解析
  31. // a)解析首行 三个部分按照空格切分
  32. String fistLine = bufferedReader.readLine();
  33. String[] fistLineTokens = fistLine.split(" ");
  34. String method = fistLineTokens[0];
  35. String url = fistLineTokens[1];
  36. String version = fistLineTokens[2];
  37. // b)解析 header 按行读取 按照冒号空格分割键值对
  38. Map<String,String> headers = new HashMap<>();
  39. String line = " ";
  40. while ((line = bufferedReader.readLine()) != null && line.length() != 0){
  41. String[] headerTokens = line.split(": ");
  42. headers.put(headerTokens[0],headerTokens[1]);
  43. }
  44. // c)解析 body (暂时不考虑)
  45. // 打印日志
  46. System.out.printf("%s %s %s\n",method,url,version);
  47. for (Map.Entry<String,String> entry : headers.entrySet()) {
  48. System.out.println(entry.getKey() + ": " + entry.getValue() + "\n");
  49. }
  50. System.out.println();
  51. // 2.根据请求计算响应
  52. // 不管是什么请求,都返回一个 hello 这样的 html
  53. String resp = "<h1>hello</h1>";
  54. // 3.把响应写回客户端
  55. bufferedWriter.write(version + "200 OK\n");
  56. bufferedWriter.write("Content-Type: text/html\n");
  57. bufferedWriter.write("Content-Length: " + resp.getBytes().length + "\n"); //此处的长度是以为字节为单位的
  58. bufferedWriter.write("\n");
  59. bufferedWriter.write(resp); // body
  60. //刷新缓冲区, 此处的flash 没有,问题也不大,
  61. // 紧接着 bufferedWriter 对象就要关闭了 close 时,就会自动触发刷新操作
  62. bufferedWriter.flush();
  63. } catch (IOException e) {
  64. e.printStackTrace();
  65. } finally {
  66. try {
  67. clientSocket.close();
  68. } catch (IOException e) {
  69. e.printStackTrace();
  70. }
  71. }
  72. }
  73. }

测试:

  1. public static void main(String[] args) throws IOException {
  2. HttpServerV1 serverV1 = new HttpServerV1(6060);
  3. serverV1.start();
  4. }

使用 fiddler 查看:

浏览器访问咱们自己的服务器时,就会构造一个 http 请求

服务器收到请求后,就返回一个响应:

把代码改一下:让不同的需求,来看到不同的响应

修改部分代码:

  1. String resp = null;
  2. if(url.equals("/OK")){
  3. bufferedWriter.write(version + "200 OK\n");
  4. resp = "<h1>hello</h1>";
  5. }
  6. else if(url.equals("/notfound")){
  7. bufferedWriter.write(version + "404 Not Found\n");
  8. resp = "<h1>not found</h1>";
  9. }
  10. // 重定向情况
  11. else if(url.equals("/seeother")){
  12. bufferedWriter.write(version + " 303 See Other\n");
  13. bufferedWriter.write("Location: http://www.sogou.com\n");
  14. resp = "";
  15. }
  16. else{
  17. bufferedWriter.write(version + "200 OK\n");
  18. resp = "<h1>default</h1>";
  19. }

运行程序,连接服务器:

default:

重定向:

版本2

1.整理代码格式,让代码更规范
2.解析 URL 中包含的参数(键值对),能够方便的处理用户传过来的参数
3.演示 Cookie 的工作流程

request 类:

  1. /*
  2. * 专门表示 Http 请求
  3. * 表示一个 http 请求,并解析
  4. * */
  5. public class HttpRequest {
  6. private String method;
  7. private String url;
  8. private String version;
  9. private Map<String,String> headers = new HashMap<>();
  10. // 表示 url中的参数
  11. private Map<String,String> parameters = new HashMap<>();
  12. // 请求的构造逻辑,仍使用工厂模式来构造
  13. // 此处的参数就是从 socket 中获取到的 InputStream 对象
  14. // 这个过程本质上是在 "反序列化" 把一个比特流,转换成一个结构化数据
  15. public static HttpRequest build(InputStream inputStream) throws IOException {
  16. HttpRequest request = new HttpRequest();
  17. // 此处的逻辑,不能把 bufferedReader 写入到 try() 中
  18. // 一旦写进去之后就意味着 bufferedReader就会被关闭,会影响到 clientSocket 的状态
  19. // 等到最后整个请求处理完了, 再统一关闭
  20. BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
  21. // 此处的 build 的过程就是解析请求的过程
  22. //1.解析首行
  23. String firstLine = bufferedReader.readLine();
  24. String[] firstLineTokens = firstLine.split(" ");
  25. request.method = firstLineTokens[0];
  26. request.url = firstLineTokens[1];
  27. request.version = firstLineTokens[2];
  28. //2.解析 url 中的参数
  29. int pos = request.url.indexOf("?");
  30. if(pos != -1){
  31. // 检查url中是否有 ? ,若没有,说明不带参数,就不需要解析了
  32. // pos 表示 ? 的下标 /index.html?a=10&bb=20
  33. // parameters 的结果就相当于是 a=10&bb=20
  34. String parameters = request.url.substring(pos + 1);
  35. // 切分的最终结果,key-a,value-10 ; key-b,value-20
  36. parseKV(parameters,request.parameters);
  37. }
  38. //3.解析header
  39. String line = "";
  40. while ((line = bufferedReader.readLine()) != null && line.length() != 0){
  41. String[] headerTokens = line.split(": ");
  42. request.headers.put(headerTokens[0],headerTokens[1]);
  43. }
  44. //4.解析 body(暂时不考虑)
  45. return request;
  46. }
  47. private static void parseKV(String input, Map<String, String> output) {
  48. // 1.先按照 & 切分成若干组键值对
  49. String[] kvTokens = input.split("&");
  50. // 2.针对切分结果,再分别进行 按照 = 切分,得到键和值
  51. for (String kv : kvTokens) {
  52. String[] result = kv.split("=");
  53. output.put(result[0],result[1]);
  54. }
  55. }
  56. // 给这个类构造一些 getter方法 (不要搞 setter 方法)
  57. // 请求对象的内容应该从网络上解析来的,用户不应该修改
  58. public String getMethod() {
  59. return method;
  60. }
  61. public String getUrl() {
  62. return url;
  63. }
  64. public String getVersion() {
  65. return version;
  66. }
  67. public String getHeader(String key) {
  68. return headers.get(key);
  69. }
  70. public String getParameter(String key) {
  71. return parameters.get(key);
  72. }
  73. @Override
  74. public String toString() {
  75. return "HttpRequest{" +
  76. "method='" + method + '\'' +
  77. ", url='" + url + '\'' +
  78. ", version='" + version + '\'' +
  79. ", headers=" + headers +
  80. ", parameters=" + parameters +
  81. '}';
  82. }
  83. }

response 类:

  1. /*
  2. * 专门表示 Http 响应
  3. * 表示一个 http 响应,负责构造
  4. * */
  5. public class HttpResponse {
  6. private String version = "HTTP/1.1";
  7. private int status; // 状态码
  8. private String message; // 状态码描述性信息
  9. private Map<String,String> headers = new HashMap<>();
  10. // 使用StringBuilder 方便进行拼接
  11. private StringBuilder body = new StringBuilder();
  12. // 写回给客户端时需要用到
  13. // 代码把响应写回到客户端时,就往 outputStream 中写
  14. private OutputStream outputStream = null;
  15. public static HttpResponse build(OutputStream outputStream){
  16. HttpResponse response = new HttpResponse();
  17. response.outputStream = outputStream;
  18. // 除了outputStream外,其他的属性内容,暂时无法确定,要根据代码的具体逻辑,来决定
  19. return response;
  20. }
  21. // 提供一些 setter 方法
  22. public void setVersion(String version) {
  23. this.version = version;
  24. }
  25. public void setStatus(int status) {
  26. this.status = status;
  27. }
  28. public void setMessage(String message) {
  29. this.message = message;
  30. }
  31. public void setHeader(String key,String value) {
  32. headers.put(key,value);
  33. }
  34. public void writeBody(String content) {
  35. body.append(content);
  36. }
  37. // 以上设置属性操作,都是在内存中
  38. // 还需要一个专门de方法,把这些属性,按照 Http 协议写到socket中
  39. public void flush() throws IOException {
  40. BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
  41. // 构造首行
  42. bufferedWriter.write(version + " " + status + " " + message + "\n");
  43. headers.put("Content-Length",body.toString().getBytes().length +"");
  44. for (Map.Entry<String,String> entry : headers.entrySet()){
  45. bufferedWriter.write(entry.getKey() + ": " + entry.getValue() + "\n");
  46. }
  47. bufferedWriter.write("\n");
  48. bufferedWriter.write(body.toString());
  49. bufferedWriter.flush();
  50. }
  51. }

主类:

  1. public class HttpServerV2 {
  2. private ServerSocket serverSocket = null;
  3. public HttpServerV2(int port) throws IOException {
  4. serverSocket = new ServerSocket(port);
  5. }
  6. public void start() throws IOException {
  7. System.out.println("服务器启动...");
  8. ExecutorService executorService = Executors.newCachedThreadPool();
  9. while (true) {
  10. Socket clientSocket = serverSocket.accept();
  11. executorService.execute(new Runnable() {
  12. @Override
  13. public void run() {
  14. process(clientSocket);
  15. }
  16. });
  17. }
  18. }
  19. public void process(Socket clientSocket) {
  20. try {
  21. // 1.读取并解析请求
  22. HttpRequest request = HttpRequest.build(clientSocket.getInputStream());
  23. System.out.println("request: " + request);
  24. HttpResponse response = HttpResponse.build(clientSocket.getOutputStream());
  25. response.setHeader("Content-Type","text/html");
  26. // 2.根据请求计算响应
  27. if(request.getUrl().startsWith("/hello")) {
  28. response.setStatus(200);
  29. response.setMessage("OK");
  30. response.writeBody("<h1>hello</h1>");
  31. }
  32. else{
  33. response.setStatus(200);
  34. response.setMessage("OK");
  35. response.writeBody("<h1>default</h1>");
  36. }
  37. // 3.把响应写回客户端
  38. response.flush();
  39. } catch (IOException e) {
  40. e.printStackTrace();
  41. } finally {
  42. try {
  43. // 这个操作会同时关闭 getInputStream 和 getOutputStream 对象
  44. clientSocket.close();
  45. } catch (IOException e) {
  46. e.printStackTrace();
  47. }
  48. }
  49. }
  50. public static void main(String[] args) throws IOException {
  51. HttpServerV2 serverV2 = new HttpServerV2(6060);
  52. serverV2.start();
  53. }
  54. }

测试结果:

添加参数计算:

  1. else if(request.getUrl().startsWith("/calc")){
  2. // 根据参数的内容进行计算
  3. // 先获取到 a 和 b 两个参数的值
  4. String aStr = request.getParameter("a");
  5. String bStr = request.getParameter("b");
  6. int a = Integer.parseInt(aStr);
  7. int b = Integer.parseInt(bStr);
  8. int result = a + b;
  9. response.setStatus(200);
  10. response.setMessage("OK");
  11. response.writeBody("<h1>result = " + result + "</h1>");
  12. }

测试结果:

基本流程:

1.服务器启动

2.在 process 方法中,要处理一次请求

  • a) 读取请求并解析
  1. HttpRequest request = HttpRequest.build(clientSocket.getInputStream());

build 方法:解析请求,严格遵守 http 协议

  • b) 根据请求计算响应 (主要业务逻辑)
    不唯一,需根据具体请求,来决定代码怎样实现
  • c) 把响应写回到客户端
  1. response.flush();

通过 flush 操作,把 response 中设置好的属性写到 socket 中,这个过程也要遵守 Http 协议,本质上是一个 序列化 的过程

相关文章