当前位置: 移动技术网 > IT编程>开发语言>Java > 使用Java Socket手撸一个http服务器

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

2019年01月04日  | 移动技术网IT编程  | 我要评论

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

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

请求消息

 

request headers

 

响应消息

 

respones headers

 

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

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

  • 第一行:状态行
  • 第二行到第一个空行: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请求如下

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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网