使用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.BasicHttpServercom.git.hui.fix.core.endpoint.HttpMessageParsercom.git.hui.fix.core.endpoint.HttpTask
1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
2. 声明
尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
- 微博地址: 小灰灰Blog
- QQ: 一灰灰/3302797840
3. 扫描关注
一灰灰blog

知识星球

使用Java Socket手撸一个http服务器的更多相关文章
- 通过 Netty、ZooKeeper 手撸一个 RPC 服务
说明 项目链接 微服务框架都包括什么? 如何实现 RPC 远程调用? 开源 RPC 框架 限定语言 跨语言 RPC 框架 本地 Docker 搭建 ZooKeeper 下载镜像 启动容器 查看容器日志 ...
- 手撸一个SpringBoot-Starter
1. 简介 通过了解SpringBoot的原理后,我们可以手撸一个spring-boot-starter来加深理解. 1.1 什么是starter spring官网解释 starters是一组方便的依 ...
- 手撸一个springsecurity,了解一下security原理
手撸一个springsecurity,了解一下security原理 转载自:www.javaman.cn 手撸一个springsecurity,了解一下security原理 今天手撸一个简易版本的sp ...
- 五分钟,手撸一个Spring容器!
大家好,我是老三,Spring是我们最常用的开源框架,经过多年发展,Spring已经发展成枝繁叶茂的大树,让我们难以窥其全貌. 这节,我们回归Spring的本质,五分钟手撸一个Spring容器,揭开S ...
- 【手撸一个ORM】MyOrm的使用说明
[手撸一个ORM]第一步.约定和实体描述 [手撸一个ORM]第二步.封装实体描述和实体属性描述 [手撸一个ORM]第三步.SQL语句构造器和SqlParameter封装 [手撸一个ORM]第四步.Ex ...
- 第二篇-用Flutter手撸一个抖音国内版,看看有多炫
前言 继上一篇使用Flutter开发的抖音国际版 后再次撸一个国内版抖音,大部分功能已完成,主要是Flutter开发APP速度很爽, 先看下图 项目主要结构介绍 这次主要的改动在api.dart 及 ...
- C#基于Mongo的官方驱动手撸一个Super简易版MongoDB-ORM框架
C#基于Mongo的官方驱动手撸一个简易版MongoDB-ORM框架 如题,在GitHub上找了一圈想找一个MongoDB的的ORM框架,未偿所愿,就去翻了翻官网(https://docs.mongo ...
- Golang:手撸一个支持六种级别的日志库
Golang标准日志库提供的日志输出方法有Print.Fatal.Panic等,没有常见的Debug.Info.Error等日志级别,用起来不太顺手.这篇文章就来手撸一个自己的日志库,可以记录不同级别 ...
- Tomcat源码分析 (一)----- 手写一个web服务器
作为后端开发人员,在实际的工作中我们会非常高频地使用到web服务器.而tomcat作为web服务器领域中举足轻重的一个web框架,又是不能不学习和了解的. tomcat其实是一个web框架,那么其内部 ...
随机推荐
- mysql workbench的PK,NN,UQ,BIN,UN,ZF,AI
mysql workbench建表时PK.NN.UQ.BIN.UN.ZF.AI的意思,后面几个老搞不清,随记在这便于以后方便查. [intrinsic column flags] (基本字段类型标识) ...
- Extjs TreePanel API详解
转自:http://web.qhwins.com/CSS-JS-XML/2011091312092944999107.html config定义{ animate : Boolean, contain ...
- 动态代理与HOOK(与oc isa 替换)
HOOK:面向函数,解决函数调用拦截与替换的问题: 动态代理:面向对象,解决对象的动态替换问题: 动态代理的实现方案: 1.经典代理机制: 2.子类化机制:oc语言的isa替换是这额解决方案的经典案例 ...
- virtualbox+vagrant学习-3-Vagrant Share-4-Vagrant Connect
Vagrant Connect vagrant可以共享到vagrant环境的任何或每个端口,而不仅仅是SSH和HTTP.“vagrant connect”命令为连接人员提供一个静态IP,他们可以使用该 ...
- Python自动化之高级语法单例模式
方法1 共享属性;所谓单例就是所有引用(实例.对象)拥有相同的状态(属性)和行为(方法) 同一个类的所有实例天然拥有相同的行为(方法), 只需要保证同一个类的所有实例具有相同的状态(属性)即可 所有实 ...
- 一条SQL语句执行得很慢原因有哪些
一条SQL语句执行得很慢,要分两种情况: 1.大多数情况是正常,偶尔很慢 数据库在处理数据忙时候,更新或新增数据都会暂时记录到redo log日志,等空闲时把数据同步到磁盘.假设数据库一直很忙,更新又 ...
- Java日志框架中真的需要判断log.isDebugEnabled()吗?
在项目中我们经常可以看到这样的代码: if (logger.isDebugEnabled()) { logger.debug(message); } 简单来说,就是用isDebugEnabled方法判 ...
- git 代码比较工具,分支冲突解决
下载地址:https://www.scootersoftware.com/BCompare-4.2.9.23626.exe
- shadowssock+openvpn 2.2.2 成功*** -- 好文档 - 原理理解+架构
[root@iZrj9j4lxhjopzii4vhw0lZ 2.0]# grep -v '^#' vars |grep -v '^$'export EASY_RSA="`pwd`" ...
- L2-022 重排链表(链表)
题目: 给定一个单链表 L1→L2→⋯→Ln−1→Ln,请编写程序将链表重新排列为 Ln→L1→Ln−1→L2→⋯.例如:给定L为1→2→3→4→5→6 ...