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

时间:2022-11-04 13:33:05

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

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

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

先上图

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

 // 2015年09月30日 更新请求的解析部分

项目地址: 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

Content-Type: application/x-www-form-urlencoded

Content-Length: 14

...

\r\n

one=23&two=123

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

首部和消息体肯定是要分开解析的。那么我们的Request对象包含一个RequestHeader 和 RequestBody

 private final RequestHeader header;
private final RequestBody body;

Header中我们有这几项

     private String URI;
private String method;
private Map<String, String> head;
private Map<String, String> queryMap;

Body中我们有这几项

     private Map<String, String> formMap;
private Map<String, MIMEData> mimeMap;

formMap是x-www-form-urlencoded数据(exp. user=123&key=4563),mimeMap是form-data格式上传的数据,包括文件一类的。MIMEData就是保存着类型,文件名,数据。

好,现在可以开始进行下一步处理了。


第一步:读取数据

 ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();

创建一个缓冲区,然后读取数据。然后调整一下position的位置。不然顺着已写入的位置继续往下读是完全没有数据的。

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

第二步:斩首HTTP请求

因为我们并不知道请求有多长,读到多少为止,但是这一次读取几乎肯定是读完了头部的。

所以我们得先把头部解析出来,然后再根据Content-Length的值或者没有Content-Length来确定还要继续读多少。

先把已读到的数据拿到再说

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

然后找到 两个\r\n同时出现的地方,那就是我们要找的头部的尾端。

         int position = BytesUtil.indexOf(bytes, "\r\n\r\n");
if (position == -1) {
throw new IllegalRequestException("请求不合法");
}
byte[] head = Arrays.copyOf(bytes, position);
RequestHeader requestHeader = new RequestHeader();
requestHeader.parseHeader(head); //IOException

这样头部就分出来了。

第三步:读取完Body

         int contentLength = requestHeader.getContentLength();
buffer.position(position + 4);
ByteBuffer bodyBuffer = ByteBuffer.allocate(contentLength);
bodyBuffer.put(buffer);
while (bodyBuffer.hasRemaining()) {
channel.read(bodyBuffer); //IOException
}
byte[] body = bodyBuffer.array();
RequestBody requestBody = new RequestBody();
if (body.length != 0) {
requestBody.parseBody(body, requestHeader);
}

接下来是详细的header和body的解析


Header解析

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

头基本就是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的Query String。

1             Map<String, String> queryMap = Collections.emptyMap();
2 int index = path.indexOf('?');
3 if (index != -1) {
4 queryMap = new HashMap<>();
5 Request.parseParameters(path.substring(index + 1), queryMap);
6 path = path.substring(0, index);
7 }
 static void parseParameters(String s, Map<String, String> requestParameters) {
String[] paras = s.split("&");
for (String para : paras) {
String[] split = para.split("=");
requestParameters.put(split[0], split[1]);
}
}

Body解析

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

先判断Content-Type再进行对应的解析。

        if (contentType.contains("application/x-www-form-urlencoded")) {
try {
String bodyMsg = new String(body, "utf-8");
parseParameters(bodyMsg, formMap);
} catch (UnsupportedEncodingException e) {
logger.log(Level.SEVERE, "基本不可能出现的错误 编码方法不支持");
throw new RuntimeException(e);
}
} else if (contentType.contains("multipart/form-data")) {
int boundaryValueIndex = contentType.indexOf("boundary=");
String bouStr = contentType.substring(boundaryValueIndex + 9); // 9是 `boundary=` 长度
mimeMap = parseFormData(body, bouStr);
}

x-www-form-urlencoded 的内容是这样的

one=23&two=123

multipart/form-data 的内容是这样的

------WebKitFormBoundaryIwVsTjLkjugAgonI
Content-Disposition: form-data; name="photo"; filename="15-5.jpeg"
Content-Type: image/jpeg
\r\n
.....
------WebKitFormBoundaryIwVsTjLkjugAgonI
Content-Disposition: form-data; name="desc"
some words
------WebKitFormBoundaryIwVsTjLkjugAgonI

这个解析复杂一些,不过都是一些简单的操作。具体看源码。

这样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就行。

 private ByteBuffer finalData = null;
public ByteBuffer getByteBuffer() {
if (finalData == null) {
heads.put("Content-Length", String.valueOf(content.length));
StringBuilder sb = new StringBuilder();
sb.append(HTTP_VERSION).append(" ").append(status.getCode()).append(" ").append(status.getMessage()).append("\r\n");
for (Map.Entry<String, String> entry : heads.entrySet()) {
sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n");
}
sb.append("\r\n");
byte[] head = sb.toString().getBytes(CHARSET);
finalData = ByteBuffer.allocate(head.length + content.length + 2);
finalData.put(head);
finalData.put(content);
finalData.put((byte) '\r');
finalData.put((byte) '\n');
finalData.flip(); // 记得这里需要flip
}
return finalData;
}

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

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

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

    因为有个不会存在大量连接的小的Web服务器需求,不至于用上重量级服务器,于是自己动手写一个服务器. 同时也提供了一个简单的Web框架.能够简单的使用了. 大体的需求包括 能够处理HTTP协议. 能够提 ...

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

    徒手用Java来写个Web服务器和框架吧<第一章:NIO篇> 徒手用Java来写个Web服务器和框架吧<第二章:Request和Response> 这一章先把Web框架的功能说 ...

  3. java写的web服务器

    经常用Tomcat,不知道的以为Tomcat很牛,其实Tomcat就是用java写的,Tomcat对jsp的支持做的很好,那么今天我们用java来写一个web服务器 //首先得到一个server, S ...

  4. 用java写一个web服务器

    一.超文本传输协议 Web服务器和浏览器通过HTTP协议在Internet上发送和接收消息.HTTP协议是一种请求-应答式的协议——客户端发送一个请求,服务器返回该请求的应答.HTTP协议使用可靠的T ...

  5. 用C写一个web服务器(二) I&sol;O多路复用之epoll

    .container { margin-right: auto; margin-left: auto; padding-left: 15px; padding-right: 15px } .conta ...

  6. 转:C&num;写的WEB服务器

    转:http://www.cnblogs.com/x369/articles/79245.html 这只是一个简单的用C#写的WEB服务器,只实现了get方式的对html文件的请求,有兴趣的朋友可以在 ...

  7. 各种容器与服务器的区别与联系 Servlet容器 WEB容器 Java EE容器 应用服务器 WEB服务器 Java EE服务器

    转自:https://blog.csdn.net/tjiyu/article/details/53148174 各种容器与服务器的区别与联系 Servlet容器 WEB容器 Java EE容器 应用服 ...

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

    刚刚看了node.js文档里的一个小例子,就是用 node.js 写一个web服务器的小例子 上代码 (*^▽^*) //helloworld.js// 使用node.js写一个服务器 const h ...

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

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

随机推荐

  1. 获取json数据

    通过异步获取json来展示数据表格,性能提高不少.实例如下: 前台: <!DOCTYPE html> <html xmlns="http://www.w3.org/1999 ...

  2. 解决ThinkPHP Call to a member function assign&lpar;&rpar; on a non-object

    <ignore_js_op> assign是tp模板输出变量的一个方法.没有object只能说没实例化...<ignore_js_op> 经过几番思索,终于发现了.原来是Act ...

  3. 16061701(地图灯光编译Beast报错)

    [目标] 地图灯光编译报错 [思路] 1 我自己测c2_cwd_rt 附件为当时log 2 ExampleGame\BeastCache\PersistentCache 3 重新删除掉BeastCac ...

  4. 链表操作,空间复杂度要求为O&lpar;1&rpar;

    对于O(1)的空间复杂度要求,不能对链表进行复制等操作,双指针法对处理该类问题比较有效. 同时由于链表头结点的特殊性,可以考虑引入一个空的头结点来辅助操作.

  5. SVN与CVS的区别大全&lpar;转载&rpar;

    本节讲解SVN与CVS的区别,主要包括是否更好的冲突标识与处理,是否有更多的本地/离线操作以及元数据管理问题. 更好的冲突标识与处理     通过是否进行更好的冲突标识与处理看SVN与CVS的区别:C ...

  6. CSS溢出文本省略(text-overflow)

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  7. ionic start 创建ionic项目报错,及解决过程

    问题描述: 前一次创建利用命令行创建ionic项目一次性成功,第二次没有运行: $ npm install -g ionic cordova 直接运行: ionic start ionicDemo 出 ...

  8. AI学习吧

    一:AI学习吧 项目描述 系统使用前后端分离的模式,前端使用vue框架,后端使用restframework实现. 项目需求 公司开发AI学习吧,由于公司需要一款线上学习平台,要开发具有线上视频学习.支 ...

  9. linux操作2

    第2天 linux操作系统的目录结构 bin   #可执行程序的安装目录,命令boot #系统启动引导目录dev #设备目录,deviceetc #软件配置文件目录home #用户的家目录lib #系 ...

  10. MySQL5&period;5安装图解

    MySQL5.5安装图解... ====================== 第一部分:去官网下载MySQL安装包... MySQL下载官网:https://dev.mysql.com/downloa ...