zoukankan      html  css  js  c++  java
  • 徒手用Java来写个Web服务器和框架吧<第二章:Request和Response>

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

    接上一篇,说到接受了请求,接下来就是解析请求构建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

    ...

    one=23&two=123

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

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

    1 private final RequestHeader header;
    2 private final RequestBody body;

    Header中我们有这几项

    1     private String URI;
    2     private String method;
    3     private Map<String, String> head;
    4     private Map<String, String> queryMap;

    Body中我们有这几项

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

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

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


    第一步:读取数据

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

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

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

     

    第二步:斩首HTTP请求

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

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

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

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

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

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

    这样头部就分出来了。

    第三步:读取完Body

     1         int contentLength = requestHeader.getContentLength();
     2         buffer.position(position + 4);
     3         ByteBuffer bodyBuffer = ByteBuffer.allocate(contentLength);
     4         bodyBuffer.put(buffer);
     5         while (bodyBuffer.hasRemaining()) {
     6             channel.read(bodyBuffer); //IOException
     7         }
     8         byte[] body = bodyBuffer.array();
     9         RequestBody requestBody = new RequestBody();
    10         if (body.length != 0) {
    11             requestBody.parseBody(body, requestHeader);
    12         }    

    接下来是详细的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             }    
    1 static 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 }

    Body解析

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

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

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

    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
    
    
    .....
    ------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

    ...

    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("
    ");
     7         for (Map.Entry<String, String> entry : heads.entrySet()) {
     8             sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("
    ");
     9         }
    10         sb.append("
    ");
    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) '
    ');
    16         finalData.put((byte) '
    ');
    17         finalData.flip(); // 记得这里需要flip
    18     }
    19     return finalData;
    20 }

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

  • 相关阅读:
    洛谷 P1219 八皇后【经典DFS,温习搜索】
    洛谷 P1972 [SDOI2009]HH的项链【莫队算法学习】
    hihoCoder #1015 : KMP算法【KMP裸题,板子】
    UVa 10341
    UVa 11461
    Uva
    BZOJ 3097: Hash Killer I【构造题,思维题】
    BZOJ 1207: [HNOI2004]打鼹鼠【妥妥的n^2爆搜,dp】
    BZOJ 1800: [Ahoi2009]fly 飞行棋【思维题,n^4大暴力】
    新版百度指数2013-12-23正式上线
  • 原文地址:https://www.cnblogs.com/imyijie/p/4823407.html
Copyright © 2011-2022 走看看