使用Java Socket手撸一个http服务器
作为一个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请求如下
接下来我们看下打印出返回头的情况
上一篇: Synchronized 实现原理