欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-06-22 13:17:54
作为一个Java后端,提供HTTP服务可以说是基本技能之一,但是你真的理解HTTP协议吗?你知道如何使用HTTP服务器吗?Tomcat的底层如何支持HTTP服务?什么是著名的servlet以及如何使用它? 套接字编程是您第一次学习Java时不可回避的一章;虽然在实际的业务项目中,使用Socket的可 ......

作为一个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协议,返回数据

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

请求消息

 

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

 

响应消息

 

使用Java Socket手撸一个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服务器

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

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