徒手用Java来写个Web服务器和框架吧<第二章:Request和Response>

徒手用Java来写个Web服务器和框架吧<第一章:NIO篇>

接上一篇,说到接受了请求,接下来就是解析请求构建Request对象,以及创建Response对象返回。

多有纰漏还请指出。省略了很多生产用的服务器需要处理的过程,仅供参考。可能在不断的完整中修改文章内容。

先上图

项目地址: https://github.com/csdbianhua/Telemarketer



首先看看如何解析请求

解析请求 构建Request对象

这部分对应代码在这里,可以对照查看

一个HTTP的GET请求大概如下所示。

GET / HTTP/1.1

Host: 123.45.67.89

Connection: keep-alive

Cache-Control: max-age=0

...

一个HTTP的POST请求大概如下

POST /post HTTP/1.1

Host: 123.45.67.89

Connection: keep-alive

Cache-Control: max-age=0

...

\r\n

one=23&two=123

请求行以一个方法符号开头,以空格分开,后面跟着请求的URI和协议的版本。 接下来就是一系列的头域,我们先不管每个的作用,先把他们提取出来保存到Request对象里再说。 每行结尾都有一个\r\n,并且除了作为结尾的\r\n外,不允许出现单独的\r或\n字符。 而post方法有个消息体,与HTTP头之间由一个\r\n隔开。

那么我们的Request对象就应该有这样几个成员

1 private String path;
2 private String method;
3 private Map<String, String> head;
4 private Map<String, String> requestParameters;
5 private byte[] messageBody;

其中path是请求的URI、method是请求的方法、head是各个头域、requestParameter是请求参数(比如123.45.67.89/?one=23&two=123,参数就是 one=23和two=123。或者post的表单里),由于messageBody里面可能有二进制数据,就统一先用byte[]表示

好,现在可以开始啃这块NIO读取的ByteBuffer了。


第一步:斩首HTTP请求

首先我们得分开头部和身子。

既然读取的是ByteBuffer,那么要读取的话首先当然是调整一下position的位置。不然顺着已写入的位置继续往下读是完全没有数据的。 那么这样

if (buffer.position() != 0) { buffer.flip(); }

flip()的作用不用多说了吧 看源代码就做了这么几件事 limit = position; position = 0; mark = -1;

接下来读取它的byte数组

1     int remaining = buffer.remaining();
2     byte[] bytes = new byte[remaining];
3     buffer.get(bytes);

然后就开始循环找到 两个\r\n同时出现的地方,那就是我们要砍的脖子的位置。

 1     for (int i = 0; i < remaining; i++) {
 2         if (bytes[i] == ‘\r‘ && bytes[i + 1] == ‘\n‘) {
 3             position = i;
 4             i += 2;
 5         }
 6         if (i + 1 < remaining && bytes[i] == ‘\r‘ && bytes[i + 1] == ‘\n‘) {
 7             break;
 8         }
 9     }
10     buffer.rewind();
11     byte[] head = new byte[position];
12     buffer.get(head, 0, position);
13     byte[] body = null;
14     if (remaining - position > 4) {
15         buffer.position(position + 4);
16         body = new byte[remaining - position - 4];
17         buffer.get(body, 0, remaining - position - 4);
18     }

同样记得在get完之后还要重新读得话得 rewind() 将position 归零。 注意这一句   buffer.position(position + 4)   因为都是从buffer.position开始读的。而buffer.position此时已经到了头的末尾,想要读body只要让position前进四位即可。

到这里就斩首完了,成了一个字节数组的头 byte[] head 和字节数组的身子byte[] head

接下来才是真正关键的解析的时候。


第二步:解剖两团字节

头基本就是UTF-8编码了,直接br读就行。

BufferedReader reader = new BufferedReader(new StringReader(new String(head,"UTF-8")));

读第一行 用空格分开,第一个就是请求方法,第二个就是uri。 注意要使用  URLDecoder.decode(lineOne[1], "utf-8");  进行解码uri,因为会可能会包括%20等转义字符。

接下来读取每一行 String[]  keyValue = line.split(":"); 再去掉空格添加到headMap里  headMap.put(keyValue[0].trim(), keyValue[1].trim());  头就读完了。

然后是参数,先弄Get的参数,再弄Post表单的参数。获得了那串字符串( one=23&two=123 )后就用这个方法解析出来。

1 void parseParameters(String s, Map<String, String> requestParameters) {
2     String[] paras = s.split("&");
3     for (String para : paras) {
4         String[] split = para.split("=");
5         requestParameters.put(split[0], split[1]);
6     }
7 }

比如Get的

1 String[] pathPart = path.split("\\?");
2 path = pathPart[0];
3 if (pathPart.length == 2) {
4     parseParameters(pathPart[1], requestParameters);
5 }

怎么判断是不是Post表单呢,可以看看Content-Type里是不是 application/x-www-form-urlencoded

headMap.containsKey("Content-Type") && headMap.get("Content-Type").contains("application/x-www-form-urlencoded")

这样Request就出来了。


创造响应 构建Response对象

这部分对应代码在这里,可以对照查看

先看一个简化的Http响应

HTTP/1.1 200 OK

Date: Sun, 20 Sep 2015 05:04:55 GMT

Server: Apache

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

Content-Length: 100

\r\n

...

Response头

先不考虑其他设置Cookie等头域,浏览器主要想知道HTTP协议版本、返回码、内容种类和内容长度。 那我们就考虑这几项先。

  1. 首先协议版本固定为 HTTP/1.1
  2. 响应码我们写个枚举类Status
  3. Date 要是rfc822格式
  4. Content-Type 和 Content-Length 根据内容定

Response的成员变量只需

private Status status;
private Map<String, String> heads;
private byte[] content;

先来看看Date

Date域

使用一个SimpleDateFormat格式化时间成rfc822,注意要将Locale设置成English。

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);

但是这样时区不对,那我们再设置一下时区 static { simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); }

Content-Type域

如果是文本类型需要用户指定,比如Json。 使用 URLConnection.getFileNameMap().getContentTypeFor(path) 即可获得文件路径对应的MIME类型。同时如果是文本类型,需要写出charset。

if (contentType.startsWith("text")) {
    contentType += "; charset=" + charset;
}

Content-Length域

设置成content.length就好了。

Response体

如果内容是文件 Files.readAllBytes(FileSystems.getDefault().getPath(path)); 就可以读取所有的字节。 如果内容是文本,直接编码成UTF-8就好了。当然一般来说是Json文本,那么Content-Type需要设置为application/json; charset=utf-8 。这个可以用户指定。

返回ByteBuffer

由于最后写入SocketChannel需要ByteBuffer,那么我们需要将响应变成ByteBuffer。按格式写好转换成ByteBuffer就行。

 1 private ByteBuffer finalData = null;
 2 public ByteBuffer getByteBuffer() {
 3     if (finalData == null) {
 4         heads.put("Content-Length", String.valueOf(content.length));
 5         StringBuilder sb = new StringBuilder();
 6         sb.append(HTTP_VERSION).append(" ").append(status.getCode()).append(" ").append(status.getMessage()).append("\r\n");
 7         for (Map.Entry<String, String> entry : heads.entrySet()) {
 8             sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n");
 9         }
10         sb.append("\r\n");
11         byte[] head = sb.toString().getBytes(CHARSET);
12         finalData = ByteBuffer.allocate(head.length + content.length + 2);
13         finalData.put(head);
14         finalData.put(content);
15         finalData.put((byte) ‘\r‘);
16         finalData.put((byte) ‘\n‘);
17         finalData.flip(); // 记得这里需要flip
18     }
19     return finalData;
20 }

这里使用了一个finalData保存最后的结果,一旦调用就不可修改了,同时防止重复读取时发送同一个内容。不然的话每读一次 hasRemaining 都为true。

时间: 2024-11-03 05:42:40

徒手用Java来写个Web服务器和框架吧<第二章:Request和Response>的相关文章

徒手用Java来写个Web服务器和框架吧&lt;第三章:Service的实现和注册&gt;

徒手用Java来写个Web服务器和框架吧<第一章:NIO篇> 徒手用Java来写个Web服务器和框架吧<第二章:Request和Response> 这一章先把Web框架的功能说一些,有个雏形. 先是制作一个Service,并绑定到一个正则地址.用到了注解和反射. 项目地址: Telemarketer Service的定义 Telemarketer的Service是一个服务,请求了跟它关联的地址,那就由它来为你服务. 它对外只需一个方法.并且对这个方法的要求大概只有输入一个Reque

徒手用Java来写个Web服务器和框架吧&lt;第一章:NIO篇&gt;

因为有个不会存在大量连接的小的Web服务器需求,不至于用上重量级服务器,于是自己动手写一个服务器. 同时也提供了一个简单的Web框架.能够简单的使用了. 大体的需求包括 能够处理HTTP协议. 能够提供接口让使用者编写自己的服务. 会省略一些暂时影响察看的代码.还不够完善,供记录问题和解决办法之用,可能会修改许多地方. 让我们开始吧~ Project的地址 : Github 从ServerSocket开始 点这里是这部分的完整代码,可以对照察看 大家都知道HTTP协议使用的是TCP服务. 而要用

用C写一个web服务器(二) I/O多路复用之epoll

.container { margin-right: auto; margin-left: auto; padding-left: 15px; padding-right: 15px } .container::before,.container::after { content: " "; display: table } .container::after { clear: both } .container::before,.container::after { content:

转:C#写的WEB服务器

转:http://www.cnblogs.com/x369/articles/79245.html 这只是一个简单的用C#写的WEB服务器,只实现了get方式的对html文件的请求,有兴趣的朋友可以在此基础之上继续开发更多功能,小弟学c#不久,如有错漏,望请见凉!! 摘要: WWW的工作基于客户机/服务器计算模型,由Web 浏览器(客户机)和Web服务器(服务器)构成,两者之间采用超文本传送协议(HTTP)进行 通信,HTTP协议的作用原理包括四个步骤:连接,请求,应答.根据上述HTTP协议的作

一起写一个 Web 服务器

导读: 本系列深入浅出的讲述了如何用 Python 从 0 开始,写一个 web 服务器,并让其与业界流行的 web 框架协同工作,最后还进一步完善了开头的 web 服务器 demo,让其可以支持多并发请求的处理,并解决了过程当中遇到的"僵尸进程"等一系列 socket/网络编程 中的常见问题,图文并茂.循序渐进,是篇非常不错的教程,对了解整个 Web 编程理论相当有帮助,推荐一看. 作者:伯乐在线 - 高世界 翻译 1.什么是 Web 服务器,以及怎样工作的? 一起写一个 Web 服

使用node.js 文档里的方法写一个web服务器

刚刚看了node.js文档里的一个小例子,就是用 node.js 写一个web服务器的小例子 上代码 (*^▽^*) //helloworld.js// 使用node.js写一个服务器 const http=require('http'); const hostname='127.0.0.1' const port=3000; const server = http.createServer((req,res)=>{ res.statusCode=200; res.setHeader('Cont

《ASP.NET Web API 2框架揭秘》第一章 概述【样章】

<ASP.NET Web API 2框架揭秘>(详情请见<新作<ASP.NET Web API 2框架揭秘>正式出版>)以实例演示的方式介绍了很多与ASP.NET Web API相关的最佳实践,同时还提供了一系列实用性的扩展.本书详细讲解了ASP.NET Web API从接收请求到响应回复的整个流程,包括路由.Http Controller的激活.Action方法的选择与执行.参数的绑定与验证.过滤器的执行和安全等相关的机制.除此之外,本书在很多章节还从设计的角度对AS

用C写一个web服务器(四) CGI协议

* { margin: 0; padding: 0 } body { font: 13.34px helvetica, arial, freesans, clean, sans-serif; color: black; line-height: 1.4em; background-color: #F8F8F8; padding: 0.7em } p { margin: 1em 0; line-height: 1.5em } table { font-size: inherit; font: 10

如何写一个Web服务器

最近两个月的业余时间在写一个私人项目,目的是在Linux下写一个高性能Web服务器,名字叫Zaver.主体框架和基本功能已完成,还有一些高级功能日后会逐渐增加,代码放在了github.Zaver的框架会在代码量尽量少的情况下接近工业水平,而不像一些教科书上的toy server为了教原理而舍弃了很多原本server应该有的东西.在本篇文章中,我将一步步地阐明Zaver的设计方案和开发过程中遇到的困难以及相应的解决方法. 为什么要重复造轮子 几乎每个人每天都要或多或少和Web服务器打交道,比较著名