码迷,mamicode.com
首页 > 编程语言 > 详细

使用Java Socket手撸一个http服务器

时间:2019-01-04 12:19:06      阅读:191      评论:0      收藏:0      [点我收藏+]

标签:根据   参数   single   线程池   定义   buffere   oid   https   客户端   

作为一个Java后端,提供HTTP服务可以说是基本技能之一,但是你真的理解HTTP协议吗?你知道如何使用HTTP服务器吗?Tomcat的底层如何支持HTTP服务?什么是著名的servlet以及如何使用它?

套接字编程是您第一次学习Java时不可回避的一章;虽然在实际的业务项目中,使用Socket的可能性基本上是零,但该博客将主要介绍如何使用Socket来实现简单的HTTP服务器功能,提供常见的GoAP/POST请求支持,然后在PRO中了解HTTP协议。塞斯。

I. Http服务器从0到1

因为我们的目标是构建一个带有套接字的HTTP服务器,所以我们需要首先确认两点:如何使用套接字;如何使用HTTP协议以及如何解析数据;下面分别解释。

1. socket编程基础

这里我们主要使用服务器套接字绑定端口,提供TCP服务,基本上使用姿态比较简单,一般的例行程序如下

  • 创建ServerSocket对象,绑定监听端口
  • 通过accept()方法监听客户端请求
  • 连接建立后,通过输入流读取客户端发送的请求信息
  • 通过输出流向客户端发送乡音信息
  • 关闭相关资源

对应的伪代码如下:

ServerSocket serverSocket = new ServerSocket(port, ip)
serverSocket.accept();
// 接收请求数据
socket.getInputStream();

// 返回数据给请求方
out = socket.getOutputStream()
out.print(xxx)
out.flush();;

// 关闭连接
socket.close()

2. http协议

我们上面的serversocket是TCP协议,HTTP协议本身是TCP协议之上的一层,对于我们创建一个HTTP服务器来说,最需要注意的只有两点。

  • 请求的数据怎么按照http的协议解析出来
  • 如何按照http协议,返回数据

所以我们需要知道数据格式的规范了

请求消息

 

技术分享图片

 

响应消息

 

技术分享图片

 

以上两张图片,先有直观的图像,然后开始对焦。

无论是请求消息还是相应的消息,都可以分为三个部分,这大大简化了我们的后续处理。

  • 第一行:状态行
  • 第二行到第一个空行:header(请求头/相应头)
  • 剩下所有:正文

3. http服务器设计

现在我们来谈谈重点。基于套接字创建HTTP服务器不是一个大问题。我们需要注意以下几点。

  • 对请求数据进行解析
  • 封装返回结果

a. 请求数据解析

我们从套接字获取所有数据,并将其解析为相应的HTTP请求。首先,我们定义一个请求对象并在其中存储一些基本的HTTP信息。接下来,我们将重点从套接字中提取所有数据,并将其封装为请求对象。

 1 @Data
 2 public static class Request {
 3     /**
 4      * 请求方法 GET/POST/PUT/DELETE/OPTION...
 5      */
 6     private String method;
 7     /**
 8      * 请求的uri
 9      */
10     private String uri;
11     /**
12      * http版本
13      */
14     private String version;
15 
16     /**
17      * 请求头
18      */
19     private Map<String, String> headers;
20 
21     /**
22      * 请求参数相关
23      */
24     private String message;
25 }

根据前面的HTTP协议,解析过程如下。让我们先看看请求行的解析过程。

请求行包含三个基本元素:请求方法+uri+http版本,用空格分隔,所以解析代码如下

 1 /**
 2  * 根据标准的http协议,解析请求行
 3  *
 4  * @param reader
 5  * @param request
 6  */
 7 private static void decodeRequestLine(BufferedReader reader, Request request) throws IOException {
 8     String[] strs = StringUtils.split(reader.readLine(), " ");
 9     assert strs.length == 3;
10     request.setMethod(strs[0]);
11     request.setUri(strs[1]);
12     request.setVersion(strs[2]);
13 }

从第二行到第一行的请求头解析为请求头,请求头格式清晰,如key:value,实现如下。

 1 /**
 2  * 根据标准http协议,解析请求头
 3  *
 4  * @param reader
 5  * @param request
 6  * @throws IOException
 7  */
 8 private static void decodeRequestHeader(BufferedReader reader, Request request) throws IOException {
 9     Map<String, String> headers = new HashMap<>(16);
10     String line = reader.readLine();
11     String[] kv;
12     while (!"".equals(line)) {
13         kv = StringUtils.split(line, ":");
14         assert kv.length == 2;
15         headers.put(kv[0].trim(), kv[1].trim());
16         line = reader.readLine();
17     }
18 
19     request.setHeaders(headers);
20 }

最后,对文本的解析,这篇文章需要注意的是,文本可能是空的,也可能是数据;当有数据时,我们如何取出所有的数据?

首先看下面的具体实现

 1 /**
 2  * 根据标注http协议,解析正文
 3  *
 4  * @param reader
 5  * @param request
 6  * @throws IOException
 7  */
 8 private static void decodeRequestMessage(BufferedReader reader, Request request) throws IOException {
 9     int contentLen = Integer.parseInt(request.getHeaders().getOrDefault("Content-Length", "0"));
10     if (contentLen == 0) {
11         // 表示没有message,直接返回
12         // 如get/options请求就没有message
13         return;
14     }
15 
16     char[] message = new char[contentLen];
17     reader.read(message);
18     request.setMessage(new String(message));
19 }

注意我上面的姿势。首先,我们根据请求头中的内容类型值获取主体的数据大小。所以我们通过创建一个如此大的char[]来获得它,我们可以读取流中的所有数据。如果数组小于实际大小,则无法完成读取。如果它很大,数组中会有一些空数据。

最后,封装上述解析以完成请求解析。

 1 /**
 2  * http的请求可以分为三部分
 3  *
 4  * 第一行为请求行: 即 方法 + URI + 版本
 5  * 第二部分到一个空行为止,表示请求头
 6  * 空行
 7  * 第三部分为接下来所有的,表示发送的内容,message-body;其长度由请求头中的 Content-Length 决定
 8  *
 9  * 几个实例如下
10  *
11  * @param reqStream
12  * @return
13  */
14 public static Request parse2request(InputStream reqStream) throws IOException {
15     BufferedReader httpReader = new BufferedReader(new InputStreamReader(reqStream, "UTF-8"));
16     Request httpRequest = new Request();
17     decodeRequestLine(httpReader, httpRequest);
18     decodeRequestHeader(httpReader, httpRequest);
19     decodeRequestMessage(httpReader, httpRequest);
20     return httpRequest;
21 }

b. 请求任务HttpTask

每个请求都分配了一个任务来单独完成这项任务,即支持并发性,对于serversocket,接收到一个请求,然后创建一个http task任务来实现HTTP通信。

那么这个httptask是做什么的呢?

  • 从请求中捞数据
  • 响应请求
  • 封装结果并返回
 1 public class HttpTask implements Runnable {
 2     private Socket socket;
 3 
 4     public HttpTask(Socket socket) {
 5         this.socket = socket;
 6     }
 7 
 8     @Override
 9     public void run() {
10         if (socket == null) {
11             throw new IllegalArgumentException("socket can‘t be null.");
12         }
13 
14         try {
15             OutputStream outputStream = socket.getOutputStream();
16             PrintWriter out = new PrintWriter(outputStream);
17 
18             HttpMessageParser.Request httpRequest = HttpMessageParser.parse2request(socket.getInputStream());
19             try {
20                 // 根据请求结果进行响应,省略返回
21                 String result = ...;
22                 String httpRes = HttpMessageParser.buildResponse(httpRequest, result);
23                 out.print(httpRes);
24             } catch (Exception e) {
25                 String httpRes = HttpMessageParser.buildResponse(httpRequest, e.toString());
26                 out.print(httpRes);
27             }
28             out.flush();
29         } catch (IOException e) {
30             e.printStackTrace();
31         } finally {
32             try {
33                 socket.close();
34             } catch (IOException e) {
35                 e.printStackTrace();
36             }
37         }
38     }
39 }

对于请求结果的封装,给一个简单的进行演示

 1 @Data
 2 public static class Response {
 3     private String version;
 4     private int code;
 5     private String status;
 6 
 7     private Map<String, String> headers;
 8 
 9     private String message;
10 }
11 
12 public static String buildResponse(Request request, String response) {
13     Response httpResponse = new Response();
14     httpResponse.setCode(200);
15     httpResponse.setStatus("ok");
16     httpResponse.setVersion(request.getVersion());
17 
18     Map<String, String> headers = new HashMap<>();
19     headers.put("Content-Type", "application/json");
20     headers.put("Content-Length", String.valueOf(response.getBytes().length));
21     httpResponse.setHeaders(headers);
22 
23     httpResponse.setMessage(response);
24 
25     StringBuilder builder = new StringBuilder();
26     buildResponseLine(httpResponse, builder);
27     buildResponseHeaders(httpResponse, builder);
28     buildResponseMessage(httpResponse, builder);
29     return builder.toString();
30 }
31 
32 
33 private static void buildResponseLine(Response response, StringBuilder stringBuilder) {
34     stringBuilder.append(response.getVersion()).append(" ").append(response.getCode()).append(" ")
35             .append(response.getStatus()).append("\n");
36 }
37 
38 private static void buildResponseHeaders(Response response, StringBuilder stringBuilder) {
39     for (Map.Entry<String, String> entry : response.getHeaders().entrySet()) {
40         stringBuilder.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
41     }
42     stringBuilder.append("\n");
43 }
44 
45 private static void buildResponseMessage(Response response, StringBuilder stringBuilder) {
46     stringBuilder.append(response.getMessage());
47 }

c. http服务搭建

基本上,我们已经做了所有我们需要做的事情,剩下的很简单。创建serversocket,绑定端口以接收请求,然后在线程池中运行此HTTP服务。

 1 public class BasicHttpServer {
 2     private static ExecutorService bootstrapExecutor = Executors.newSingleThreadExecutor();
 3     private static ExecutorService taskExecutor;
 4     private static int PORT = 8999;
 5 
 6     static void startHttpServer() {
 7         int nThreads = Runtime.getRuntime().availableProcessors();
 8         taskExecutor =
 9                 new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100),
10                         new ThreadPoolExecutor.DiscardPolicy());
11 
12         while (true) {
13             try {
14                 ServerSocket serverSocket = new ServerSocket(PORT);
15                 bootstrapExecutor.submit(new ServerThread(serverSocket));
16                 break;
17             } catch (Exception e) {
18                 try {
19                     //重试
20                     TimeUnit.SECONDS.sleep(10);
21                 } catch (InterruptedException ie) {
22                     Thread.currentThread().interrupt();
23                 }
24             }
25         }
26 
27         bootstrapExecutor.shutdown();
28     }
29 
30     private static class ServerThread implements Runnable {
31 
32         private ServerSocket serverSocket;
33 
34         public ServerThread(ServerSocket s) throws IOException {
35             this.serverSocket = s;
36         }
37 
38         @Override
39         public void run() {
40             while (true) {
41                 try {
42                     Socket socket = this.serverSocket.accept();
43                     HttpTask eventTask = new HttpTask(socket);
44                     taskExecutor.submit(eventTask);
45                 } catch (Exception e) {
46                     e.printStackTrace();
47                     try {
48                         TimeUnit.SECONDS.sleep(1);
49                     } catch (InterruptedException ie) {
50                         Thread.currentThread().interrupt();
51                     }
52                 }
53             }
54         }
55     }
56 }

此时,一个基于socket的HTTP服务器基本上已经构建好,可以进行测试了。

4. 测试

此服务器主要基于项目快速修复。本项目主要解决应用程序内部服务访问和数据修改问题。我们在这个项目的基础上进行测试。

完成的POST请求如下

技术分享图片

接下来我们看下打印出返回头的情况

技术分享图片

使用Java Socket手撸一个http服务器

标签:根据   参数   single   线程池   定义   buffere   oid   https   客户端   

原文地址:https://www.cnblogs.com/aishangJava/p/10218402.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!