原文连接:使用Java Socket手撸一个http服务器

作为一个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()方法监听客户端请求
  • 连接建立后,通过输入流读取客户端发送的请求信息
  • 通过输出流向客户端发送乡音信息
  • 关闭相关资源

对应的伪代码如下:

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. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

一灰灰blog

知识星球

使用Java Socket手撸一个http服务器的更多相关文章

  1. 通过 Netty、ZooKeeper 手撸一个 RPC 服务

    说明 项目链接 微服务框架都包括什么? 如何实现 RPC 远程调用? 开源 RPC 框架 限定语言 跨语言 RPC 框架 本地 Docker 搭建 ZooKeeper 下载镜像 启动容器 查看容器日志 ...

  2. 手撸一个SpringBoot-Starter

    1. 简介 通过了解SpringBoot的原理后,我们可以手撸一个spring-boot-starter来加深理解. 1.1 什么是starter spring官网解释 starters是一组方便的依 ...

  3. 手撸一个springsecurity,了解一下security原理

    手撸一个springsecurity,了解一下security原理 转载自:www.javaman.cn 手撸一个springsecurity,了解一下security原理 今天手撸一个简易版本的sp ...

  4. 五分钟,手撸一个Spring容器!

    大家好,我是老三,Spring是我们最常用的开源框架,经过多年发展,Spring已经发展成枝繁叶茂的大树,让我们难以窥其全貌. 这节,我们回归Spring的本质,五分钟手撸一个Spring容器,揭开S ...

  5. 【手撸一个ORM】MyOrm的使用说明

    [手撸一个ORM]第一步.约定和实体描述 [手撸一个ORM]第二步.封装实体描述和实体属性描述 [手撸一个ORM]第三步.SQL语句构造器和SqlParameter封装 [手撸一个ORM]第四步.Ex ...

  6. 第二篇-用Flutter手撸一个抖音国内版,看看有多炫

    前言 继上一篇使用Flutter开发的抖音国际版 后再次撸一个国内版抖音,大部分功能已完成,主要是Flutter开发APP速度很爽,  先看下图 项目主要结构介绍 这次主要的改动在api.dart 及 ...

  7. C#基于Mongo的官方驱动手撸一个Super简易版MongoDB-ORM框架

    C#基于Mongo的官方驱动手撸一个简易版MongoDB-ORM框架 如题,在GitHub上找了一圈想找一个MongoDB的的ORM框架,未偿所愿,就去翻了翻官网(https://docs.mongo ...

  8. Golang:手撸一个支持六种级别的日志库

    Golang标准日志库提供的日志输出方法有Print.Fatal.Panic等,没有常见的Debug.Info.Error等日志级别,用起来不太顺手.这篇文章就来手撸一个自己的日志库,可以记录不同级别 ...

  9. Tomcat源码分析 (一)----- 手写一个web服务器

    作为后端开发人员,在实际的工作中我们会非常高频地使用到web服务器.而tomcat作为web服务器领域中举足轻重的一个web框架,又是不能不学习和了解的. tomcat其实是一个web框架,那么其内部 ...

随机推荐

  1. dev richEditControl控件 设置文字 字体 大小

    Document doc = NoticeContentRichEditControl.Document; doc.BeginUpdate(); doc.Text = "需要设置格式的文字& ...

  2. 第二次作业 APP分析

    第一部分 调研, 评测 1.下载软件并使用. 今天我要分析的软件app是UC浏览器这个软件,UC浏览器的用户群体还是挺多的,作为一款主流之一的浏览器APP,整体的用户体验还是很好的.简洁的界面还有中间 ...

  3. 51nod 1443 路径和树(最短路)

    题目链接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1443 1443 路径和树 题目来源: CodeForces ...

  4. 20165318 2017-2018-2 《Java程序设计》第四周学习总结

    20165318 2017-2018-2 <Java程序设计>第四周学习总结 IDEA安装 按照娄老师Intellj IDEA 简易教程,我下载了IDEA但是由于没有注册学校邮箱,我没办法 ...

  5. linux增加自己的可执行目录 $PATH

    Linux中有三种方法可以添加用户的路径到系统路径PATH, 以添加 /etc/apache/bin 为例: 方法一: 直接在命令行中输入: #PATH=$PATH:/etc/apache/bin 这 ...

  6. jQuery全选反选插件

    (function($){ $.fn.check = function(options){ var options = $.extend({ element : "input[name='n ...

  7. virtualbox+vagrant学习-3-Vagrant Share-5-Security

    Security 可以理解,分享你vagrant环境引发了一些安全问题. vagrant share的主要安全机制是通过隐藏的安全性以及SSH的加密密钥.此外,还有几个配置选项可用来帮助控制访问和管理 ...

  8. 【转】打包2个10g文件 测试

    微博上kevin_prajna提了一个问题:“求Linux下一打包工具,需求:能把两个10G的文件打包成一个文件,时间在1分钟之内能接受!”. 暂且作答一下吧.首先问题是求解工具,那么我们忽略IO问题 ...

  9. PHP的Reflection反射机制

    更多内容推荐微信公众号,欢迎关注: 原文地址: http://www.nowamagic.net/php/php_Reflection.php PHP5添加了一项新的功能:Reflection.这个功 ...

  10. java 泛型数组列表

    如下代码: package com.company; import java.lang.reflect.Array; import java.util.ArrayList; public class ...