您的位置:首页 > 产品设计 > UI/UE

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

2015-09-20 14:26 411 查看
徒手用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/postHTTP/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

privatefinalRequestHeaderheader;
privatefinalRequestBodybody;


Header中我们有这几项

privateStringURI;
privateStringmethod;
privateMap<String,String>head;
privateMap<String,String>queryMap;


Body中我们有这几项

privateMap<String,String>formMap;
privateMap<String,MIMEData>mimeMap;


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

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

第一步:读取数据

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


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

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

第二步:斩首HTTP请求

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

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

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

intremaining=buffer.remaining();
byte[]bytes=newbyte[remaining];
buffer.get(bytes);


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

intposition=BytesUtil.indexOf(bytes,"\r\n\r\n");
if(position==-1){
thrownewIllegalRequestException("请求不合法");
}
byte[]head=Arrays.copyOf(bytes,position);
RequestHeaderrequestHeader=newRequestHeader();
requestHeader.parseHeader(head);//IOException


这样头部就分出来了。

第三步:读取完Body

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


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

Header解析

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

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

BufferedReaderreader=newBufferedReader(newStringReader(newString(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的QueryString。

1Map<String,String>queryMap=Collections.emptyMap();
2intindex=path.indexOf('?');
3if(index!=-1){
4queryMap=newHashMap<>();
5Request.parseParameters(path.substring(index+1),queryMap);
6path=path.substring(0,index);
7}


staticvoidparseParameters(Strings,Map<String,String>requestParameters){
String[]paras=s.split("&");
for(Stringpara:paras){
String[]split=para.split("=");
requestParameters.put(split[0],split[1]);
}
}


Body解析

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

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

if(contentType.contains("application/x-www-form-urlencoded")){
try{
StringbodyMsg=newString(body,"utf-8");
parseParameters(bodyMsg,formMap);
}catch(UnsupportedEncodingExceptione){
logger.log(Level.SEVERE,"基本不可能出现的错误编码方法不支持");
thrownewRuntimeException(e);
}
}elseif(contentType.contains("multipart/form-data")){
intboundaryValueIndex=contentType.indexOf("boundary=");
StringbouStr=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"
somewords
------WebKitFormBoundaryIwVsTjLkjugAgonI


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

这样Request就出来了。

创造响应构建Response对象

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

先看一个简化的Http响应


HTTP/1.1200OK

Date:Sun,20Sep201505:04:55GMT

Server:Apache

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

Content-Length:100

\r\n

...


Response头

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

首先协议版本固定为HTTP/1.1

响应码我们写个枚举类Status

Date要是rfc822格式

Content-Type和Content-Length根据内容定

Response的成员变量只需

privateStatusstatus;
privateMap<String,String>heads;
privatebyte[]content;

先来看看Date

Date域

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

SimpleDateFormatsimpleDateFormat=newSimpleDateFormat("EEE,dMMMyyyyHH:mm:sszzz",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就行。

privateByteBufferfinalData=null;
publicByteBuffergetByteBuffer(){
if(finalData==null){
heads.put("Content-Length",String.valueOf(content.length));
StringBuildersb=newStringBuilder();
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
}
returnfinalData;
}


这里使用了一个finalData保存最后的结果,一旦调用就不可修改了,同时防止重复读取时发送同一个内容。不然的话每读一次hasRemaining都为true。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐
章节导航