基于 Java NIO 实现简单的 HTTP 服务器
1.简介
本文是上一篇文章实践篇,在上一篇文章中,我分析了选择器 Selector 的原理。本篇文章,我们来说说 Selector 的应用,如标题所示,这里我基于 Java NIO 实现了一个简单的 HTTP 服务器。在接下来的章节中,我会详细讲解 HTTP 服务器实现的过程。另外,本文所对应的代码已经上传到 GitHub 上了,需要的自取,仓库地址为 toyhttpd。好了,废话不多说,进入正题吧。
2. 实现
本节所介绍的 HTTP 服务器是一个很简单的实现,仅支持 HTTP 协议极少的特性。包括识别文件后缀,并返回相应的 Content-Type。支持200、400、403、404、500等错误码等。由于支持的特性比较少,所以代码逻辑也比较简单,这里罗列一下:
- 处理请求,解析请求头
- 响应请求,从请求头中获取资源路径, 检测请求的资源路径是否合法
- 根据文件后缀匹配 Content-Type
- 读取文件数据,并设置 Content-Length,如果文件不存在则返回404
- 设置响应头,并将响应头和数据返回给浏览器。
接下来我们按照处理请求和响应请求两步操作,来说说代码实现。先来看看核心的代码结构,如下:
/**
* TinyHttpd
*
* @author code4wt
* @date 2018-03-26 22:28:44
*/
public class TinyHttpd {
private static final int DEFAULT_PORT = 8080;
private static final int DEFAULT_BUFFER_SIZE = 4096;
private static final String INDEX_PAGE = "index.html";
private static final String STATIC_RESOURCE_DIR = "static";
private static final String META_RESOURCE_DIR_PREFIX = "/meta/";
private static final String KEY_VALUE_SEPARATOR = ":";
private static final String CRLF = "\r\n";
private int port;
public TinyHttpd() {
this(DEFAULT_PORT);
}
public TinyHttpd(int port) {
this.port = port;
}
public void start() throws IOException {
// 初始化 ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("localhost", port));
ssc.configureBlocking(false);
// 创建 Selector
Selector selector = Selector.open();
// 注册事件
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
int readyNum = selector.select();
if (readyNum == 0) {
continue;
}
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey selectionKey = it.next();
it.remove();
if (selectionKey.isAcceptable()) {
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 处理请求
request(selectionKey);
selectionKey.interestOps(SelectionKey.OP_WRITE);
} else if (selectionKey.isWritable()) {
// 响应请求
response(selectionKey);
}
}
}
}
private void request(SelectionKey selectionKey) throws IOException {...}
private Headers parseHeader(String headerStr) {...}
private void response(SelectionKey selectionKey) throws IOException {...}
private void handleOK(SocketChannel channel, String path) throws IOException {...}
private void handleNotFound(SocketChannel channel) {...}
private void handleBadRequest(SocketChannel channel) {...}
private void handleForbidden(SocketChannel channel) {...}
private void handleInternalServerError(SocketChannel channel) {...}
private void handleError(SocketChannel channel, int statusCode) throws IOException {...}
private ByteBuffer readFile(String path) throws IOException {...}
private String getExtension(String path) {...}
private void log(String ip, Headers headers, int code) {}
}
上面的代码是 HTTP 服务器的核心类的代码结构。其中 request 负责处理请求,response 负责响应请求。handleOK 方法用于响应正常的请求,handleNotFound 等方法用于响应出错的请求。readFile 方法用于读取资源文件,getExtension 则是获取文件后缀。
2.1 处理请求
处理请求的逻辑比较简单,主要的工作是解析消息头。相关代码如下:
private void request(SelectionKey selectionKey) throws IOException {
// 从通道中读取请求头数据
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
channel.read(buffer);
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
String headerStr = new String(bytes);
try {
// 解析请求头
Headers headers = parseHeader(headerStr);
// 将请求头对象放入 selectionKey 中
selectionKey.attach(Optional.of(headers));
} catch (InvalidHeaderException e) {
selectionKey.attach(Optional.empty());
}
}
private Headers parseHeader(String headerStr) {
if (Objects.isNull(headerStr) || headerStr.isEmpty()) {
throw new InvalidHeaderException();
}
// 解析请求头第一行
int index = headerStr.indexOf(CRLF);
if (index == -1) {
throw new InvalidHeaderException();
}
Headers headers = new Headers();
String firstLine = headerStr.substring(0, index);
String[] parts = firstLine.split(" ");
/*
* 请求头的第一行必须由三部分构成,分别为 METHOD PATH VERSION
* 比如:
* GET /index.html HTTP/1.1
*/
if (parts.length < 3) {
throw new InvalidHeaderException();
}
headers.setMethod(parts[0]);
headers.setPath(parts[1]);
headers.setVersion(parts[2]);
// 解析请求头属于部分
parts = headerStr.split(CRLF);
for (String part : parts) {
index = part.indexOf(KEY_VALUE_SEPARATOR);
if (index == -1) {
continue;
}
String key = part.substring(0, index);
if (index == -1 || index + 1 >= part.length()) {
headers.set(key, "");
continue;
}
String value = part.substring(index + 1);
headers.set(key, value);
}
return headers;
}
简单总结一下上面的代码逻辑,首先是从通道中读取请求头,然后解析读取到的请求头,最后将解析出的 Header 对象放入 selectionKey 中。处理请求的逻辑很简单,不多说了。
2.2 响应请求
看完处理请求的逻辑,接下来再来看看响应请求的逻辑。代码如下:
private void response(SelectionKey selectionKey) throws IOException {
SocketChannel channel = (SocketChannel) selectionKey.channel();
// 从 selectionKey 中取出请求头对象
Optional<Headers> op = (Optional<Headers>) selectionKey.attachment();
// 处理无效请求,返回 400 错误
if (!op.isPresent()) {
handleBadRequest(channel);
channel.close();
return;
}
String ip = channel.getRemoteAddress().toString().replace("/", "");
Headers headers = op.get();
// 如果请求 /meta/ 路径下的资源,则认为是非法请求,返回 403 错误
if (headers.getPath().startsWith(META_RESOURCE_DIR_PREFIX)) {
handleForbidden(channel);
channel.close();
log(ip, headers, FORBIDDEN.getCode());
return;
}
try {
handleOK(channel, headers.getPath());
log(ip, headers, OK.getCode());
} catch (FileNotFoundException e) {
// 文件未发现,返回 404 错误
handleNotFound(channel);
log(ip, headers, NOT_FOUND.getCode());
} catch (Exception e) {
// 其他异常,返回 500 错误
handleInternalServerError(channel);
log(ip, headers, INTERNAL_SERVER_ERROR.getCode());
} finally {
channel.close();
}
}
// 处理正常的请求
private void handleOK(SocketChannel channel, String path) throws IOException {
ResponseHeaders headers = new ResponseHeaders(OK.getCode());
// 读取文件
ByteBuffer bodyBuffer = readFile(path);
// 设置响应头
headers.setContentLength(bodyBuffer.capacity());
headers.setContentType(ContentTypeUtils.getContentType(getExtension(path)));
ByteBuffer headerBuffer = ByteBuffer.wrap(headers.toString().getBytes());
// 将响应头和资源数据一同返回
channel.write(new ByteBuffer[]{headerBuffer, bodyBuffer});
}
// 处理请求资源未发现的错误
private void handleNotFound(SocketChannel channel) {
try {
handleError(channel, NOT_FOUND.getCode());
} catch (Exception e) {
handleInternalServerError(channel);
}
}
private void handleError(SocketChannel channel, int statusCode) throws IOException {
ResponseHeaders headers = new ResponseHeaders(statusCode);
// 读取文件
ByteBuffer bodyBuffer = readFile(String.format("/%d.html", statusCode));
// 设置响应头
headers.setContentLength(bodyBuffer.capacity());
headers.setContentType(ContentTypeUtils.getContentType("html"));
ByteBuffer headerBuffer = ByteBuffer.wrap(headers.toString().getBytes());
// 将响应头和资源数据一同返回
channel.write(new ByteBuffer[]{headerBuffer, bodyBuffer});
}
上面的代码略长,不过逻辑仍然比较简单。首先,要判断请求头存在,以及资源路径是否合法。如果都合法,再去读取资源文件,如果文件不存在,则返回 404 错误码。如果发生其他异常,则返回 500 错误。如果没有错误发生,则正常返回响应头和资源数据。这里只贴了核心代码,其他代码就不贴了,大家自己去看吧。
2.3 效果演示
分析完代码,接下来看点轻松的吧。下面贴一张代码的运行效果图,如下:
3.总结
本文所贴的代码是我在学习 Selector 过程中写的,核心代码不到 300 行。通过动手写代码,也使得我加深了对 Selector 的了解。在学习 JDK 的过程中,强烈建议大家多动手写代码。通过写代码,并踩一些坑,才能更加熟练运用相关技术。这个是我写 NIO 系列文章的一个感触。
好了,本文到这里结束。谢谢阅读!
本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处
作者:coolblog
本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
基于 Java NIO 实现简单的 HTTP 服务器的更多相关文章
- 基于Java Mina框架的部标808服务器设计和开发
在开发部标GPS平台中,部标808GPS服务器是系统的核心关键,决定了部标平台的稳定性和行那个.Linux服务器是首选,为了跨平台,开发语言选择Java自不待言. 我们为客户开发的部标服务器基于Min ...
- 基于Java Mina框架的部标jt808服务器设计和开发
在开发部标GPS平台中,部标jt808GPS服务器是系统的核心关键,决定了部标平台的稳定性和行那个.Linux服务器是首选,为了跨平台,开发语言选择Java自不待言.需要购买jt808GPS服务器源码 ...
- Java NIO: Non-blocking Server 非阻塞网络服务器
本文翻译自 Jakob Jenkov 的 Java NIO: Non-blocking Server ,原文地址:http://tutorials.jenkov.com/java-nio/non-bl ...
- java实现一个简单的Web服务器
注:本段内容来源于<JAVA 实现 简单的 HTTP服务器> 1. HTTP所有状态码 状态码 状态码英文名称 中文描述 100 Continue 继续.客户端应继续其请求 101 Swi ...
- JAX-WS 学习一:基于java的最简单的WebService服务
JAVA 1.6 之后,自带的JAX-WS API,这使得我们可以很方便的开发一个基于Java的WebService服务. 基于JAVA的WebService 服务 1.创建服务端WebService ...
- 基于python创建一个简单的HTTP-WEB服务器
背景 大多数情况下主机资源只有开发和测试相关人员可以登录直接操作,且有些特定情况"答辩.演示.远程"等这些场景下是无法直接登录主机的.web是所有终端用户都可以访问了,解决了人员权 ...
- 基于java NIO 的服务端与客户端代码
在对java NIO selector 与 Buffer Channel 有一定的了解之后,我们进行编写java nio 实现的 客户端与服务端例子: 服务端: public class NIOC ...
- 基于TcpListener实现最简单的http服务器
最近实现一套简单的网络程序.为了查看程序内部的变量,方便调试.就在想搞一个最最简单的方式.第一个想到写文件,日志.这个不实时,而且打开麻烦,pass .于是想到用网络输出.本来是想写成c/s模式,想着 ...
- 基于java mail实现简单的QQ邮箱发送邮件
刚学习到java邮件相关的知识,先写下这篇博客,方便以后翻阅学习. -----------------------------第一步 开启SMTP服务 在 QQ 邮箱里的 设置->账户里开启 S ...
随机推荐
- python中Django 使用方法简述
Django是由Python写成的免费而且开源的Web应用框架--一堆零件的组成,可以帮助我们轻松的开发网站.这些零件都包括常用的:登录(注册,登入,登出),网站后台管理,表单,文件上传等.可以帮助我 ...
- git基本使用(搭建Git服务器)
我操作的是阿里的云服务器Linux系统的.系统不一样可能指令也不一样: 简要说明: git是分布式版本控制系统,也就是说每个开发人员的本地库和远程的库都是一样的. 基本思路: 1.在远程服务器上的一个 ...
- java 文本图片字体模糊优化处理
昨天发现虽然图片生成字体够清楚了,但是会出现少量锯齿,所以调整了下,代码如下: g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, Renderin ...
- 【python学习笔记】2.列表和元组
# 第二章:列表和元组 序列中,每个元素都有个一个序号,序号以0开始,最后一个元素序号为-1,倒数第二个-2 序列类型包括,列表,元组,字符串,unicode字符串,buffer, xrange ...
- 使用IIS Server Farms搭建应用服务负载均衡
当公司的业务扩大, 伴随着大量的请求,应用服务器的承受能力已经不能满足不断增长的业务需求,使用IIS Server Farms搭建应负载均衡的方式,把请求分发给不同的应用服务器进行处理,这个时候就降低 ...
- Redis分布式锁的正确实现方式
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- CSS( Cascading Style Sheets )简书
(注:带*号的属性是CSS3新增属性)一.基本规则1.css通常存储在样式表(style)中,用于定义如何显示HTML元素:2.css主要由两个部分构成:选择器和一条或多条声明. 选择器通常是需要改变 ...
- Linux如此“自私”?
Linux如此“自私”? “如果当时我真的知道从头建立一个操作系统的难度,肯定是不会有勇气去做的.”1991年8月25日,随着林纳斯·托瓦兹(Linus Torvalds)这句“天真”的描述,Linu ...
- 笔记:Spring Cloud Feign 其他配置
请求压缩 Spring Cloud Feign 支持对请求与响应进行GZIP压缩,以减少通信过程中的性能损耗,我们只需要通过下面二个参数设置,就能开启请求与响应的压缩功能,yml配置格式如下: fei ...
- 教你怎么样在大陆直接使用google搜索
一.环境准备 我们需要一个nginx的模块来进行设置,ngx_http_google_filter_module.前提我们是有一个海外的VPS,并且可以访问谷歌,我的VPS是亿速云香港的. 首先先感受 ...