作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomcat的底层是怎么支持http服务的呢?大名鼎鼎的Servlet又是什么东西呢,该怎么使用呢?
在初学java时,socket编程是逃不掉的一章;虽然在实际业务项目中,使用这个的可能性基本为0,本篇博文将主要介绍如何使用socket来实现一个简单的http服务器功能,提供常见的get/post请求支持,并再此过程中了解下http协议
I. Http服务器从0到1 既然我们的目标是借助socket来搭建http服务器,那么我们首先需要确认两点,一是如何使用socket;另一个则是http协议如何,怎么解析数据;下面分别进行说明
1. socket编程基础 我们这里主要是利用ServerSocket来绑定端口,提供tcp服务,基本使用姿势也比较简单,一般套路如下
创建ServerSocket对象,绑定监听端口
通过accept()方法监听客户端请求
连接建立后,通过输入流读取客户端发送的请求信息
通过输出流向客户端发送乡音信息
关闭相关资源
对应的伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 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服务器设计 接下来开始进入正题,基于socket创建一个http服务器,使用socket基本没啥太大的问题,我们需要额外关注以下几点
a. 请求数据解析 我们从socket中拿到所有的数据,然后解析为对应的http请求,我们先定义个Request对象,内部保存一些基本的HTTP信息,接下来重点就是将socket中的所有数据都捞出来,封装为request对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Data public static class Request { private String method; private String uri; private String version; private Map<String, String> headers; private String message; }
根据前面的http协议介绍,解析过程如下,我们先看请求行的解析过程
请求行 ,包含三个基本要素:请求方法 + URI + http版本,用空格进行分割,所以解析代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 private static void decodeRequestLine (BufferedReader reader, Request request) throws IOException { String[] strs = StringUtils.split(reader.readLine(), " " ); assert strs.length == 3 ; request.setMethod(strs[0 ]); request.setUri(strs[1 ]); request.setVersion(strs[2 ]); }
请求头的解析 ,从第二行,到第一个空白行之间的所有数据,都是请求头;请求头的格式也比较清晰, 形如 key:value
, 具体实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private static void decodeRequestHeader (BufferedReader reader, Request request) throws IOException { Map<String, String> headers = new HashMap<>(16 ); String line = reader.readLine(); String[] kv; while (!"" .equals(line)) { kv = StringUtils.split(line, ":" ); assert kv.length == 2 ; headers.put(kv[0 ].trim(), kv[1 ].trim()); line = reader.readLine(); } request.setHeaders(headers); }
最后就是正文的解析了 ,这一块需要注意一点,正文可能为空,也可能有数据;有数据时,我们要如何把所有的数据都取出来呢?
先看具体实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private static void decodeRequestMessage (BufferedReader reader, Request request) throws IOException { int contentLen = Integer.parseInt(request.getHeaders().getOrDefault("Content-Length" , "0" )); if (contentLen == 0 ) { return ; } char [] message = new char [contentLen]; reader.read(message); request.setMessage(new String(message)); }
注意下上面我的使用姿势,首先是根据请求头中的Content-Type
的值,来获得正文的数据大小,因此我们获取的方式是创建一个这么大的char[]
来读取流中所有数据,如果我们的数组比实际的小,则读不完;如果大,则数组中会有一些空的数据;
最后将上面的几个解析封装一下 ,完成request解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static Request parse2request (InputStream reqStream) throws IOException { BufferedReader httpReader = new BufferedReader(new InputStreamReader(reqStream, "UTF-8" )); Request httpRequest = new Request(); decodeRequestLine(httpReader, httpRequest); decodeRequestHeader(httpReader, httpRequest); decodeRequestMessage(httpReader, httpRequest); return httpRequest; }
b. 请求任务HttpTask 每个请求,单独分配一个任务来干这个事情,就是为了支持并发,对于ServerSocket而言,接收到了一个请求,那就创建一个HttpTask任务来实现http通信
那么这个httptask干啥呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class HttpTask implements Runnable { private Socket socket; public HttpTask (Socket socket) { this .socket = socket; } @Override public void run () { if (socket == null ) { throw new IllegalArgumentException("socket can't be null." ); } try { OutputStream outputStream = socket.getOutputStream(); PrintWriter out = new PrintWriter(outputStream); HttpMessageParser.Request httpRequest = HttpMessageParser.parse2request(socket.getInputStream()); try { String result = ...; String httpRes = HttpMessageParser.buildResponse(httpRequest, result); out.print(httpRes); } catch (Exception e) { String httpRes = HttpMessageParser.buildResponse(httpRequest, e.toString()); out.print(httpRes); } out.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
对于请求结果的封装,给一个简单的进行演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Data public static class Response { private String version; private int code; private String status; private Map<String, String> headers; private String message; } public static String buildResponse (Request request, String response) { Response httpResponse = new Response(); httpResponse.setCode(200 ); httpResponse.setStatus("ok" ); httpResponse.setVersion(request.getVersion()); Map<String, String> headers = new HashMap<>(); headers.put("Content-Type" , "application/json" ); headers.put("Content-Length" , String.valueOf(response.getBytes().length)); httpResponse.setHeaders(headers); httpResponse.setMessage(response); StringBuilder builder = new StringBuilder(); buildResponseLine(httpResponse, builder); buildResponseHeaders(httpResponse, builder); buildResponseMessage(httpResponse, builder); return builder.toString(); } private static void buildResponseLine (Response response, StringBuilder stringBuilder) { stringBuilder.append(response.getVersion()).append(" " ).append(response.getCode()).append(" " ) .append(response.getStatus()).append("\n" ); } private static void buildResponseHeaders (Response response, StringBuilder stringBuilder) { for (Map.Entry<String, String> entry : response.getHeaders().entrySet()) { stringBuilder.append(entry.getKey()).append(":" ).append(entry.getValue()).append("\n" ); } stringBuilder.append("\n" ); } private static void buildResponseMessage (Response response, StringBuilder stringBuilder) { stringBuilder.append(response.getMessage()); }
c. http服务搭建 前面的基本上把该干的事情都干了,剩下的就简单了,创建ServerSocket
,绑定端口接收请求,我们在线程池中跑这个http服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class BasicHttpServer { private static ExecutorService bootstrapExecutor = Executors.newSingleThreadExecutor(); private static ExecutorService taskExecutor; private static int PORT = 8999 ; static void startHttpServer () { int nThreads = Runtime.getRuntime().availableProcessors(); taskExecutor = new ThreadPoolExecutor(nThreads, nThreads, 0L , TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100 ), new ThreadPoolExecutor.DiscardPolicy()); while (true ) { try { ServerSocket serverSocket = new ServerSocket(PORT); bootstrapExecutor.submit(new ServerThread(serverSocket)); break ; } catch (Exception e) { try { TimeUnit.SECONDS.sleep(10 ); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } } bootstrapExecutor.shutdown(); } private static class ServerThread implements Runnable { private ServerSocket serverSocket; public ServerThread (ServerSocket s) throws IOException { this .serverSocket = s; } @Override public void run () { while (true ) { try { Socket socket = this .serverSocket.accept(); HttpTask eventTask = new HttpTask(socket); taskExecutor.submit(eventTask); } catch (Exception e) { e.printStackTrace(); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } } } } }
到这里,一个基于socket实现的http服务器基本上就搭建完了,接下来就可以进行测试了
4. 测试 做这个服务器,主要是基于项目 quick-fix 产生的,这个项目主要是为了解决应用内部服务访问与数据订正,我们在这个项目的基础上进行测试
一个完成的post请求如下
接下来我们看下打印出返回头的情况
II. 其他 0. 项目源码
quick-fix
相关代码:
com.git.hui.fix.core.endpoint.BasicHttpServer
com.git.hui.fix.core.endpoint.HttpMessageParser
com.git.hui.fix.core.endpoint.HttpTask
一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
2. 声明 尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
3. 扫描关注 一灰灰blog
知识星球