使用Java Socket手撸一个http服务器
作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomcat的底层是怎么支持http服务的呢?大名鼎鼎的Servlet又是什么东西呢,该怎么使用呢?
在初学java时,socket编程是逃不掉的一章;虽然在实际业务项目中,使用这个的可能性基本为0,本篇博文将主要介绍如何使用socket来实现一个简单的http服务器功能,提供常见的get/post请求支持,并再此过程中了解下http协议
<!-- more -->
I. Http服务器从0到1
既然我们的目标是借助socket来搭建http服务器,那么我们首先需要确认两点,一是如何使用socket;另一个则是http协议如何,怎么解析数据;下面分别进行说明
1. socket编程基础
我们这里主要是利用ServerSocket来绑定端口,提供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服务器设计
接下来开始进入正题,基于socket创建一个http服务器,使用socket基本没啥太大的问题,我们需要额外关注以下几点
- 对请求数据进行解析
- 封装返回结果
a. 请求数据解析
我们从socket中拿到所有的数据,然后解析为对应的http请求,我们先定义个Request对象,内部保存一些基本的HTTP信息,接下来重点就是将socket中的所有数据都捞出来,封装为request对象
@Data public static class Request { /** * 请求方法 GET/POST/PUT/DELETE/OPTION... */ private String method; /** * 请求的uri */ private String uri; /** * http版本 */ private String version; /** * 请求头 */ private Map<String, String> headers; /** * 请求参数相关 */ private String message; }
根据前面的http协议介绍,解析过程如下,我们先看请求行的解析过程
请求行,包含三个基本要素:请求方法 + URI + http版本,用空格进行分割,所以解析代码如下
/** * 根据标准的http协议,解析请求行 * * @param reader * @param request */ 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
, 具体实现如下
/** * 根据标准http协议,解析请求头 * * @param reader * @param request * @throws IOException */ 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); }
最后就是正文的解析了,这一块需要注意一点,正文可能为空,也可能有数据;有数据时,我们要如何把所有的数据都取出来呢?
先看具体实现如下
/** * 根据标注http协议,解析正文 * * @param reader * @param request * @throws IOException */ private static void decodeRequestMessage(BufferedReader reader, Request request) throws IOException { int contentLen = Integer.parseInt(request.getHeaders().getOrDefault("Content-Length", "0")); if (contentLen == 0) { // 表示没有message,直接返回 // 如get/options请求就没有message return; } char[] message = new char[contentLen]; reader.read(message); request.setMessage(new String(message)); }
注意下上面我的使用姿势,首先是根据请求头中的Content-Type
的值,来获得正文的数据大小,因此我们获取的方式是创建一个这么大的char[]
来读取流中所有数据,如果我们的数组比实际的小,则读不完;如果大,则数组中会有一些空的数据;
最后将上面的几个解析封装一下,完成request解析
/** * http的请求可以分为三部分 * * 第一行为请求行: 即 方法 + URI + 版本 * 第二部分到一个空行为止,表示请求头 * 空行 * 第三部分为接下来所有的,表示发送的内容,message-body;其长度由请求头中的 Content-Length 决定 * * 几个实例如下 * * @param reqStream * @return */ 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干啥呢?
- 从请求中捞数据
- 响应请求
- 封装结果并返回
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(); } } } }
对于请求结果的封装,给一个简单的进行演示
@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服务
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
1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
2. 声明
尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
- 微博地址: 小灰灰Blog
- QQ: 一灰灰/3302797840
3. 扫描关注
一灰灰blog
知识星球
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
如何提高代码可读性、可维护性
高质量代码的三大要素: 可读性、可维护性和可变更性 做好代码规范、提高代码质量,能显著增强代码的可读性、可维护性和可变更性。努力提高代码的读写可维护性,是做好代码规范的必要非充分条件。代码规范和架构设计是软件的灵魂所在,代码质量偏低,就像是人失去了三魂七魄中的一魄,就会丧失活力,影响正常运行,增加软件交付后维护成本,出现推迟完成、超出预算、特性缺失等现象。 任何语言都需要强调编码风格的一致性。只要是团队开发,每个人都以相同的方式编写代码就是至关重要的。这样大家才能方便地互相看懂和维护对方的代码。 实际上,每家公司都会有一份(或多份)代码规范,因此提高代码的读写可维护性的关键在于是否能落实公司的相关文档,公司的技术总监、项目经理或相关代码审查机关是否具有应有的执行力。如果不能落实,那么即便代码规范画得再美,具体的代码也会丑到崩溃。 代码规范 如果不想为以后挖坑,做好代码规范是程序员和团队负责人、项目经理的必修课。如何保证当前项目开发过程中压力正常,而不是在后期面对过多的压力、以至于噩梦缠身?最简单的办法就是照看好你的代码,也就是落实好公司的代码规范工作。每天为此付出一丁点的努力,便可以避...
- 下一篇
java爬取并下载酷狗TOP500歌曲
是这样的,之前买车送的垃圾记录仪不能用了,这两天狠心买了好点的记录仪,带导航、音乐、蓝牙、4G等功能,寻思,既然有这些功能就利用起来,用4G听歌有点奢侈,就准备去酷狗下点歌听,居然都是需要办会员才能下载,而且vip一月只能下载300首,我这么穷又这么抠怎么可能冲会员,于是百度搜了下怎么免费下载,都是python爬取,虽然也会一点,但是电脑上没安装python,再安装再研究感觉有点费劲,于是就花了半小时做了这个爬虫,技术一般,只记录分析实现过程,大牛请绕行。其中用到了一些库,包括:jsoup、HttpClient、net.sf.json大家可以自行去下载jar包 1、分析是否能获得TOP500歌单 首先,打开酷狗首页查看酷狗TOP500,说好的500首,怎么就只有22首呢, 是真的只让看这些还是能找到其余的呢,于是我就看了下这TOP500的链接 https://www.kugou.com/yy/rank/home/1-8888.html?from=rank 可以看的出home后边有个1,难道这是代表第一页的意思?于是我就把1改成2,进入,果然进入了第二页,至此可以知道我们可以在网页里获取...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- CentOS6,CentOS7官方镜像安装Oracle11G
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题