使用Java NIO实现一个HTTP服务器

引言

一直以来都想写一个自己的http服务器,这次趁着稍微空闲了一些,赶紧码了一个mini版的。在这里跟大家分享一下编写的整个思路,总体来说,整个应用非常简单,目前也只是实现了最基本的静态资源访问,但对于想学习Http协议的同学来说,应该还是有所帮助

HTTP协议

先简单介绍一下HTTP协议,HTTP协议是构建于TCP/IP协议之上的一个应用层协议,并且是无连接无状态的。

http请求报文

http的请求报文由三部分组成

  • 状态行 <method> <request-URL> <version> method包含GETPOSTPUTDELETE

  • 请求头

  • 消息主体

下面是典型的http请求报文示例


GET /static/home.html HTTP/1.1

Host: localhost:8080

Connection: keep-alive

Cache-Control: max-age=0

Upgrade-Insecure-Requests: 1

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3

Accept-Encoding: gzip, deflate, br

Accept-Language: zh-CN,zh;q=0.9

http响应报文

响应报文跟请求报文类似,也由三部分组成

  • 状态行 这里需要关注下响应的各个状态所代表的含义,以及浏览器识别这些状态码会相应的做哪些事情

  • 响应头

  • 响应正文


HTTP/1.1 200 OK

Server: cloud http v1.0

Content-Type: text/html;charset=UTF-8

<html>...

动手

前面简单介绍了一下http协议的基本信息,下面就是源码实现了,我这里直接使用了Java NIO作为底层通信支撑,在实际的生产代码中,大多会选择封装良好的netty来作为稳定的底层通信

这里先放上项目源码 CloudHttp

定义Request和Response

  • Request

public class Request {

 /**

 * method

 */

 private String method;

 /**

 * http协议版本

 */

 private String httpVersion;

 /**

 * 请求uri

 */

 private String uri;

 /**

 * 请求相对路径

 */

 private String path;

 /**

 * 请求头信息

 */

 private Map<String, String> headers;

 /**

 * 请求参数

 */

 private Map<String, String> attribute;

}

  • Response

public class Response {

 private Integer code;

 private String protocol = "HTTP/1.1";

 private String msg;

 private Map<String, String> headers;

 private ByteArrayOutputStream outPutStream = new ByteArrayOutputStream();

 public Response() {

 this.code = HttpCode.STATUS_200.getCode();

 this.msg = HttpCode.STATUS_200.getMsg();

 headers = new HashMap<>();

 headers.put("Content-Type", "text/html;charset=UTF-8");

 headers.put("Server", "cloud http v1.0");

 }

}

  • 定义请求响应解析类 HttpParser

public class HttpParser {

 private final static Logger logger = LoggerFactory.getLogger(HttpParser.class);

 /**

 * <p>解析http请求体</p>

 *

 * @param buffers

 * @return

 */

 public static Request decodeReq(byte [] buffers) {

 Request request = new Request();

 if (buffers != null) {

 String resString = new String(buffers);

 logger.info(resString);

 String[] headers = resString.trim().split("\r\n");

 if (headers.length > 0) {

 String firstline = headers[0];

 // 按空格分割字符串

  // 解析 method uri 协议版本

 String mainInfo [] = firstline.split("\\s+");

 request.setMethod(mainInfo[0]);

 try {

 request.setUri(URLDecoder.decode(mainInfo[1], "UTF-8"));

 } catch (UnsupportedEncodingException e) {

 logger.error("error_HttpParser_URLDecode, uri = {}", mainInfo[1], e);

 }

 request.setHttpVersion(mainInfo[2]);

 // 解析header

 Map<String, String> headersMap = new HashMap<>();

 for (int i = 1; i < headers.length; i++) {

 String entryStr = headers[i];

 String entry [] = entryStr.trim().split(":");

 headersMap.put(entry[0].trim(), entry[1].trim());

 }

 request.setHeaders(headersMap);

 // 解析参数

 String uri = request.getUri();

 Map<String, String> attribute = new HashMap<>();

 request.setPath(uri);

 if (StringUtils.isNotEmpty(uri)) {

 int indexOfParam = uri.indexOf("?");

 if (indexOfParam > 0) {

 // 设置path

 request.setPath(uri.substring(0, indexOfParam));

 String queryString = uri.substring(indexOfParam + 1, uri.length());

 String paramEntrys [] = queryString.split("&");

 for (String paramEntry : paramEntrys) {

 String [] entry = paramEntry.split("=");

 if (entry.length > 0) {

 String key = entry[0];

 String value = entry.length > 1 ? entry[1] : "";

 attribute.put(key, value);

 }

 }

 }

 }

 request.setAttribute(attribute);

 }

 }

 return request;

 }

 /**

 * <p>返回http响应字节流</p>

 *

 * @param response

 * @return

 */

 public static byte[] encodeResHeader(Response response) {

 StringBuilder resBuild = new StringBuilder();

 resBuild.append(response.getProtocol() + " " + response.getCode() + " " + response.getMsg());

 resBuild.append("\r\n");

 Map<String, String> headers = response.getHeaders();

 headers.entrySet().forEach(entry -> {

 resBuild.append(entry.getKey());

 resBuild.append(": ");

 resBuild.append(entry.getValue());

 resBuild.append("\r\n");

 });

 resBuild.append("\r\n");

 String resString = resBuild.toString();

 byte [] bytes = null;

 try {

 bytes = resString.getBytes("UTF-8");

 } catch (UnsupportedEncodingException e) {

 logger.error("error_encodeResHeader", e);

 }

 return bytes;

 }

}

使用Java nio 启动服务器

  • NioServer

public class NioServer implements Server, Runnable {

 private final static Logger logger = LoggerFactory.getLogger(NioServer.class);

 private Thread serverThread;

 private Integer port;

  private  boolean running = false;

 private static volatile NioServer server;

 private CloudService cloudService;

 private ServerSocketChannel serverSocketChannel;

 private ByteBuffer readBuffer = ByteBuffer.allocate(8192);

 public static final ExecutorService requestWork = Executors.newCachedThreadPool();

 public static NioServer getServerInstance () {

 if (server == null) {

 synchronized (NioServer.class) {

 if (server == null) {

 server = new NioServer();

 }

 }

 }

 return server;

 }

 public NioServer() {

 this.cloudService = new CloudService();

 port = Integer.valueOf(CloudHttpConfig.getValue("port", "8080"));

 }

 @Override

  public  synchronized  void start() {

 if (running) {

 logger.info("服务器已经启动");

 return;

 }

 serverThread = new Thread(this);

 serverThread.start();

 this.running = true;

 }

 @Override

 public void stop() {

 try {

 serverSocketChannel.close();

 serverThread.stop();

 } catch (IOException e) {

 logger.error("error_NioServer_stop", e);

 }

 }

 @Override

 public void run() {

 try {

  //打开ServerSocketChannel通道

 serverSocketChannel = ServerSocketChannel.open();

  //得到ServerSocket对象

 serverSocketChannel.socket().bind(new InetSocketAddress(port));

 Selector selector = Selector.open();

 serverSocketChannel.configureBlocking(false);

 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

 while (true) {

 selector.select();

 Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();

 SelectionKey key = null;

 while (selectedKeys.hasNext()) {

 key = selectedKeys.next();

 selectedKeys.remove();

 if (!key.isValid()) {

 continue;

 }

 if (key.isAcceptable()) {

 accept(key);

 }

 if (key.isReadable()) {

 read(key);

 }

 if (key.isWritable()) {

 write(key);

 }

 }

 }

 } catch (IOException e) {

 logger.error("error_NioServer_run", e);

 }

 }

 private void accept(SelectionKey key) {

 try {

 ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();

 SocketChannel socketChannel = serverSocketChannel.accept();

 socketChannel.configureBlocking(false);

 socketChannel.register(key.selector(), SelectionKey.OP_READ);

 } catch (IOException e) {

 logger.error("error_NioServer_accept", e);

 }

 }

 private Request read(SelectionKey key) {

 Request request = null;

 SocketChannel socketChannel = (SocketChannel) key.channel();

 try {

 int readNum = socketChannel.read(readBuffer);

 if (readNum == -1) {

 socketChannel.close();

 key.cancel();

 return null;

 }

 readBuffer.flip();

 byte [] buffers = new byte[readBuffer.limit()];

 readBuffer.get(buffers);

 readBuffer.clear();

 request = HttpParser.decodeReq(buffers);

 requestWork.execute(new RequestWorker(request, key, cloudService));

 } catch (IOException e) {

 logger.error("error_NioServer_read", e);

 }

 return request;

 }

 private void write(SelectionKey key) throws IOException {

 ByteBuffer buffer = (ByteBuffer) key.attachment();

 if(buffer == null || !buffer.hasRemaining()) {

 return;

 }

 SocketChannel socketChannel = (SocketChannel) key.channel();

 socketChannel.write(buffer);

 if(!buffer.hasRemaining()){

 key.interestOps(SelectionKey.OP_READ);

 buffer.clear();

 }

 socketChannel.close();

 }

}

处理请求的 RequestWork类

对于每一个进入的请求。都会单独启动一个线程来进行处理

  • RequestWorker

public class RequestWorker implements Runnable {

 private final static Logger logger = LoggerFactory.getLogger(RequestWorker.class);

 private Request request;

 private SelectionKey key;

 private SocketChannel channel;

 private CloudService cloudService;

 public RequestWorker(Request request, SelectionKey key, CloudService cloudService) {

 this.request = request;

 this.key = key;

 this.channel = (SocketChannel) key.channel();

 this.cloudService = cloudService;

 }

 @Override

 public void run() {

 Response response = new Response();

 try {

 cloudService.doService(request, response);

 } catch (ViewNotFoundException e) {

 response.setCode(HttpCode.STATUS_404.getCode());

 response.setMsg(HttpCode.STATUS_404.getMsg());

 ByteArrayOutputStream outputStream = response.getOutPutStream();

 InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("404.html");

 byte bytes [] = new byte [1024];

 int len;

 try {

 while ((len = inputStream.read(bytes)) > -1) {

 outputStream.write(bytes, 0, len);

 }

 } catch (IOException e1) {

 logger.error("error_requestWork_write404", e);

 }

 }

 byte [] resHeader = HttpParser.encodeResHeader(response);

 byte [] body = response.getOutPutStream().toByteArray();

 ByteBuffer byteBuffer = ByteBuffer.allocate(resHeader.length + body.length);

 byteBuffer.put(resHeader);

 byteBuffer.put(body);

 byteBuffer.flip();

  // 将输出流绑定至附件

 key.attach(byteBuffer);

  // 注册写事件

 key.interestOps(SelectionKey.OP_WRITE);

 key.selector().wakeup();

 }

}

定义Servlet接口

  • CloudServlet

public interface CloudServlet {

 /**

 * 判断当前请求是否匹配 handler

 *

 * @param request

 * @return

 */

 boolean match(Request request);

 /**

 * 执行初始化

 */

 void init(Request request, Response response);

 /**

 * 执行请求

 *

 * @param request

 * @param response

 */

 void doService(Request request, Response response);

}

  • StaticViewServlet 定义一个处理静态资源的StaticViewServlet实现CloudServlet接口

该静态资源处理器会自动拦截static路径下的请求


public class StaticViewServlet implements CloudServlet {

 private final static Logger logger = LoggerFactory.getLogger(StaticViewServlet.class);

 private String staticRootPath;

 public static Pattern p = Pattern.compile("^/static/\\S+");

 @Override

 public boolean match(Request request) {

 String path = request.getPath();

 Matcher matcher = p.matcher(path);

 return matcher.matches();

 }

 @Override

 public void init(Request request, Response response) {

 staticRootPath = CloudHttpConfig.getValue("static.resource.path");

 }

 @Override

 public void doService(Request request, Response response) {

 String path = request.getPath();

 String fileRelativePath = path.substring(8);

 String absolutePath = staticRootPath + "/" + fileRelativePath;

 RandomAccessFile randomAccessFile = null;

 try {

 randomAccessFile = new RandomAccessFile(absolutePath, "r");

 FileChannel fileChannel = randomAccessFile.getChannel();

 ByteBuffer htmBuffer = ByteBuffer.allocate((int)fileChannel.size());

 fileChannel.read(htmBuffer);

 htmBuffer.flip();

 byte [] htmByte = new byte[htmBuffer.limit()];

 htmBuffer.get(htmByte);

 response.getOutPutStream().write(htmByte);

 } catch (FileNotFoundException e) {

 throw new ViewNotFoundException();

 } catch (IOException e) {

 logger.error("error_StaticViewServlet_doService 异常", e);

 }

 }

}

定义 CloudService

CloudService是用于盛放Servlet的容器


public class CloudService {

 private List<CloudServlet> cloudServlets = new ArrayList<>();

 public CloudService() {

 cloudServlets.add(new StaticViewServlet());

 cloudServlets.add(new MappingUrlServlet());

 }

 public void doService(Request request, Response response) {

 CloudServlet servlet = doSelect(request);

 servlet.init(request, response);

 servlet.doService(request, response);

 }

 private CloudServlet doSelect(Request request) {

 for (CloudServlet cloudServlet : cloudServlets) {

 if (cloudServlet.match(request)) {

 return cloudServlet;

 }

 }

 return new MappingUrlServlet();

 }

}

最后 编写启动类


public class CloudHttpServer {

 /**

 * 启动服务器

 */

 public static void startServer() {

 NioServer.getServerInstance().start();

 }

 public static void main(String[] args) {

 startServer();

 }

}

测试

  1. 启动服务器

  2. 浏览器访问 http://localhost:8080/static/home.html

响应页面

image.png

尾言

本文只是一个示例,仅仅实现了静态资源访问。做为自娱自乐的项目 +——=
博客原文戳这里

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • http协议有http0.9,http1.0,http1.1和http2三个版本,但是现在浏览器使用的是htt...
    一现_阅读 1,855评论 0 3
  • Web 页面的实现 Web 基于 HTTP 协议通信 客户端(Client)的 Web 浏览器从 Web 服务器端...
    毛圈阅读 1,072评论 0 2
  • 本文整理自MIN飞翔博客 [1] 1. 概念 协议是指计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或...
    HoyaWhite阅读 2,634评论 2 20
  • 身在广东,在这经济较为发达的美食美景之乡,我为之骄傲!同时,也深感无奈! 广东的传统思想:儿孙满堂,多子多福!于是...
    王梓烨妈妈阅读 404评论 2 3
  • 毫无疑问这是个物欲横流的时代。能够用金钱购买的东西实在是太多了,加上信息的高速传播,媒体电视互联网的诱惑,使得更多...
    长不大的屁屁侦探阅读 293评论 0 2