java nio 写一个完整的http服务器 支持文件上传 chunk传输 gzip 压缩 使用过程 和servlet差不多

时间:2021-02-19 13:04:13

java nio 写一个完整的http服务器  支持文件上传   chunk传输    gzip 压缩     

也仿照着 netty处理了NIO的空轮询BUG       

本项目并不复杂 代码不多 我没有采用过多的设计模式    和套娃   使其看着比较简单易懂   

起因:

想自己写一个web服务器  不使用tomcat 有时候想轻量级一点   代码量很少

成果:

现在已经支持文件上传下载,分块传输协议  gzip压缩  使用过程和java Servlet差不多 我封装两个对象 一个HttpRequest 一个Response  可以仿照这servlet来对http做出响应与接收  后续有空 可能会对原生的Servlet做出支持。

图:下面是我给出的示例

首先这是我写的按照我的规则  写的一个普通servlet(不是javax 里面的那个 是我自己写的一个接口)

java nio 写一个完整的http服务器  支持文件上传 chunk传输  gzip 压缩  使用过程 和servlet差不多

访问效果:

java nio 写一个完整的http服务器  支持文件上传 chunk传输  gzip 压缩  使用过程 和servlet差不多

我也写了几个 默认的servlet

分别处理 404  500   和   icon   和静态资源请求

下面是  静态资源的servlet

import com.pps.web.constant.ContentTypeEnum;
import com.pps.web.constant.PpsWebConstant;
import com.pps.web.data.HttpRequest;
import com.pps.web.data.Response;
import com.pps.web.servlet.model.PpsHttpServlet; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map; /**
* 讲静态资源的默认处理
* @author Pu PanSheng, 2021/12/20
* @version OPRA v1.0
*/
public class DefaultStaticResourceServlet extends PpsHttpServlet { private volatile Map<String, String> resource; public void scanFile(String root){
//检索资源目录
String context =(String) serverParams.get("context");
String mapUrl=(String) serverParams.get(PpsWebConstant.RESOUCE_MAPPING_DIR_KEY);
String dir=(String) serverParams.get(PpsWebConstant.RESOUCE_DIR_KEY);
File file=new File(root);
if(file==null){
return;
}
for (File listFile : file.listFiles()) {
if(listFile.isDirectory()){
scanFile(listFile.getPath());
}else if(listFile.isFile()){
String path = listFile.getPath();
String s = context + mapUrl;
path=path.replace(dir,s);
path=path.replace(File.separator,"/");
resource.put(path, listFile.getAbsolutePath());
}
}
}
@Override
public boolean isMatch(String url) { if(resource==null){
synchronized (this) {
resource = new HashMap<>();
scanFile((String) serverParams.get(PpsWebConstant.RESOUCE_DIR_KEY));
}
}
return resource.containsKey(url); } @Override
public void get(HttpRequest request, Response response) { String url = request.getUrl();
String u = resource.get(url);
try {
int i = url.lastIndexOf(".");
String suffix = url.substring(i + 1);
ContentTypeEnum contentTypeEnum=null;
for (ContentTypeEnum value : ContentTypeEnum.values()) {
String type = value.getType();
if(type.endsWith(suffix)||suffix.equals(value.getDesc())){
contentTypeEnum=value;
break;
}
}
//下载
if(contentTypeEnum==null){
contentTypeEnum=ContentTypeEnum.applicationstream;
}
response.setContentType(contentTypeEnum.getType()); try (FileInputStream fileInputStream = new FileInputStream(u)) { byte[] bu = new byte[1024];
int read = fileInputStream.read(bu);
while (read != -1) {
response.write(bu, 0, read);
read = fileInputStream.read(bu);
}
response.flush(); } } catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("静态资源映射过程出错");
} }
}

静态资源需要配置一下参数 :

/**
* 静态资源映射设置
*/
webServer.setStaticResourceDir("c:\\test");
webServer.setResourceMapping("/resource");

对应电脑情况

java nio 写一个完整的http服务器  支持文件上传 chunk传输  gzip 压缩  使用过程 和servlet差不多

效果:

java nio 写一个完整的http服务器  支持文件上传 chunk传输  gzip 压缩  使用过程 和servlet差不多


好了下面上硬菜:怎么实现的 用原生的java nio 类实现这种效果:

预备知识:

1  了解reactor模式

2  熟悉http报文规范  因为后面要解析http报文

3 了解nio socket等知识

本web服务采用reactor模式    和netty类似   会有一个专门处理连接的bosser  和 处理普通的读写的worker

当配置 bosser等于 0的时候 worker也会承担 bosser的职责  处理socket连接

当配置bosser大于0  那么bosser 会处理连接  把连接好的socket分发到 worker众多线程里面

所有 根据参数的不同 reactor模式也会发生变化

下面作者带着大家看下我写的服务器实现

首先定义工作者worker:

它是非常核心的一个类

他来处理Socket事件  其实现了 Runnable对象  把他提交到线程池 它的run 方法将会执行  继而会一直轮询处理Socket的事件

后面将详细介绍 怎么处理Socket 不同的事件的:

package com.pps.web;

import com.pps.web.constant.PpsWebConstant;
import com.pps.web.hander.EventHander; import java.io.IOException;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingDeque; /**
* @author Pu PanSheng, 2021/12/17
* @version OPRA v1.0
*/
public class Worker implements Runnable{ protected Selector selector; /**
* 测试bug的计数器限制
*/ private int testBugSize=PpsWebConstant.TEST_BUG_COUNT; private BlockingDeque<Runnable> task; private Executor executor; private WebServer webServer; public Worker(Executor executor){
try {
this.selector=Selector.open();
this.executor=executor;
this.task=new LinkedBlockingDeque<>(); } catch (IOException e) {
e.printStackTrace();
}
} void init(WebServer webServer){
this.webServer=webServer;
} protected void registerEvent(SelectableChannel selectableChannel, int type) throws ClosedChannelException {
//如果该Selector 此时已经被阻塞在select()中了 那么这里会被阻塞住 请注意
if(selectableChannel.isOpen()) {
selectableChannel.register(chooseSelector(), type);
}
} /**
* 向工作者 注册 同步执行任务
* @param runnable
*/
public void registerTask(Runnable runnable){ task.addLast(runnable); } /**
* 向工作者 注册 异步执行任务
* @param runnable
*/
public void registerAsyncTask(Runnable runnable){ executor.execute(runnable); } protected Selector chooseSelector(){
return selector;
} protected void wakeUp(){
selector.wakeup();
} @Override
public void run() { EventHander instance = EventHander.getInstance(webServer); int count=0;
long startTime=System.nanoTime();
int timeOut= PpsWebConstant.TIMEOUT;
while (true){ try { //同步任务执行
runTask(); /**
* 如果不设置超时时间 那么如果线程已经运行了 且阻塞在select() 上面
* 这个时候再注册事件 那么注册线程会一直阻塞在注册方法上
* 远无法注册成功
* 所以必须加个超时时间
*/
int select = selector.select(timeOut); if(select<=0){ /**
* nio bug 当某些因为poll和epoll对于突然中断的连接socket
* 会对返回的eventSet事件集合置为POLLHUP或者POLLERR,eventSet事件集合发生了变化,
* 这就导致Selector会被唤醒,进而导致CPU 100%问题。
* 根本原因就是JDK没有处理好这种情况,
* 比如SelectionKey中就没定义有异常事件的类型。
*
* 所以需要处理下这种情况:
*/
count++; if(count>testBugSize) { long endTime = System.nanoTime();
long distance = endTime - startTime; //如果小于正常情况下 该限制次数下的事件间隔 说明触发了bug
if(distance<((testBugSize+1)*1000*1000*timeOut)){ //重建selector
Selector newSelector = Selector.open();
for (SelectionKey key : selector.keys()) {
key.channel().register(newSelector, key.interestOps());
} this.selector=newSelector; } count=0;
startTime=System.nanoTime(); } continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ try { SelectionKey next = iterator.next(); if(!next.isValid()){
next.cancel();
continue;
} SelectableChannel channel = next.channel(); if (next.isReadable()) { SocketChannel channelRead = (SocketChannel) channel;
instance.read(channelRead, next, this);
registerEvent(channelRead,SelectionKey.OP_READ); }else if(next.isAcceptable()){ ServerSocketChannel connectChannel = (ServerSocketChannel)channel;
SocketChannel accept = connectChannel.accept();
accept.configureBlocking(false);
registerEvent(accept,SelectionKey.OP_READ); }else if(next.isConnectable()){ }else if (next.isWritable()) { //应当不会出现这种情况 } }catch(Exception e){ e.printStackTrace();
}
finally {
iterator.remove();
startTime=System.nanoTime();
count=0;
} } } catch (Exception e) {
e.printStackTrace();
} } } private void runTask() { while (!task.isEmpty()){
Runnable poll = task.poll();
if(poll!=null){
poll.run();
}
} }
}

对于socket 连接事件处理:

 ServerSocketChannel connectChannel  = (ServerSocketChannel)channel;
SocketChannel accept = connectChannel.accept();
accept.configureBlocking(false);
registerEvent(accept,SelectionKey.OP_READ);

Worker的

registerEvent(accept,SelectionKey.OP_READ);

就是向当前的 work注册事件

而 BosserWorker 重载了

registerEvent(accept,SelectionKey.OP_READ);

方法
会挑选一个 工作者worker 来注册:

如下:
@Override
protected void registerEvent(SelectableChannel selectableChannel, int type) throws ClosedChannelException {

if(type == SelectionKey.OP_ACCEPT){
super.registerEvent(selectableChannel,type);
return;
}
int cU= count.addAndGet(1);
int workL=cU%workers.length;
Worker worker=workers[workL];
worker.registerEvent(selectableChannel, type);
worker.wakeUp();

}

那么 对于 read事件 是怎么处理的呢 这一块 牵扯的内容 就很多了

因为涉及到 http报文的读取 和解析 以及对一些异常的处理 特别是http报文的解析 针对http 上传文件的的报文 还有点复杂

首先read事件 会调用 EventHander 的 read 方法

public void read(SocketChannel socketChannel, SelectionKey selectionKey, Worker worker) throws IOException {
        Response response=null;
try {
//构造自己的输入流 方便后面读取

PpsInputSteram ppsInputSteram=new PpsInputSteram(socketChannel,selectionKey);
HttpRequest request= null;

//解析Http报文
request = new HttpRequest(ppsInputSteram, worker);
/**
* 假如 服务器 context 为 /
* 1 servlet / 匹配 /
* 2 servlet /key 匹配 /key
* 3 servlet /* 匹配
* 假如 服务器 context 为 /context
*
* servlet /context 匹配 url /context/context
* servlet / 匹配 url /context
* servlet /key 匹配 url /context/key
* servlet /* 匹配
*/
try {
//封装Response 方便我们的程序写Http报文响应

response=new Response(socketChannel,webServer.getServerParms());
response.setRequestHeader(request.getHeaderParam()); String matchUrl=request.getUrl(); if(matchUrl==null){
return;
}
if(matchUrl.endsWith("/")){
matchUrl=matchUrl.substring(0,matchUrl.length()-1);
}


//作者定义的规范 类似于 java 的servlet HttpServlet httpServlet = mappingServlet.get(matchUrl); if(httpServlet==null){ //是否满足静态资源
if(resourceServlet.isMatch(matchUrl)){
httpServlet=resourceServlet;
} //全局servlet是否存在
if(httpServlet==null){
httpServlet=allServlet;
} //一个都没 那么就用系统默认的了 也就是404
if(httpServlet==null){
httpServlet=defualtServlet;
} }
//这一步会调用到我们自己写的处理程序
httpServlet.get(request,response);


//一些后置任务 假如一个http请求是文件上传 那么肯定是要把这个文件放在 暂存文件里面的 当访问结束后 需要删掉它 不然会越来越多


if(request.isSavleFile()){ //删除临时文件
HttpRequest finalRequest = request;

//向工作者提交任务
worker.registerTask(()->{
for (Application__multipart_form_dataHttpBodyResolve.FileEntity fromDatum : finalRequest.getFromData()) {
String urlF = fromDatum.getInfo(PpsWebConstant.TEMP_FILE_KEY);
if (urlF != null) {
try {
Files.deleteIfExists(Paths.get(urlF));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}); } }catch (Exception e){

//如果是这种异常 那么说明客户端 关闭了连接
if(e instanceof ChannelCloseException){
selectionKey.cancel();
socketChannel.close();
}else {
//这种异常就是我们自己的程序 异常了 需要打印 和返回500错误
e.printStackTrace();
errorServlet.get(request, response);
}
} } catch (Exception e) {
if(!(e instanceof IOException)&&!(e instanceof ChannelCloseException)){
e.printStackTrace();
}
selectionKey.cancel();
socketChannel.close();
} }

以上就是非常核心的socketc处理了

下面就进入怎么到解析Http报文

看下 我的HttpRquest类  当构造它是 根据传入的socketChannel 他就会开始解析了  并把数据封装到这个对象里面

解析http报文的方法体 非常复杂  根据不同的Content-Type 需要采取不同的算法  所以笔者采用工厂模式+策略方法模式+java的SPI    能够灵活的添加自己的算法

作者默认写了 三种解析算法  分别解析

application/x-www-form-urlencoded

multipart/form-data

application/json

三种请求体的数据:

请读者自己看吧  就不一一介绍了

package com.pps.web.data;

import com.pps.web.Worker;
import com.pps.web.constant.PpsWebConstant;
import com.pps.web.exception.ChannelReadEndException;
import com.pps.web.servlet.entity.PpsInputSteram; import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map; /**
* @author Pu PanSheng, 2021/12/18
* @version OPRA v1.0
*/
public class HttpRequest { private String protocol;
private String method;
private Map<String,String> urlParams=new HashMap<>();
private Map<String,String> headerParam=new HashMap<>();
private Map<String,Object> httpBodyData=new HashMap<>();
private boolean isSavleFile;
private PpsInputSteram inputStream;
private String url;
private Worker worker; public HttpRequest(PpsInputSteram inputStream, Worker worker) throws Exception {
this.worker=worker;
this.inputStream = inputStream;
httpResolve();
inputStream.returnBuffer();
} public boolean isSavleFile() {
return isSavleFile;
} public void setSavleFile(boolean savleFile) {
isSavleFile = savleFile;
} public void addTask(Runnable task){ worker.registerTask(task); }
public InputStream getInputStream() {
return inputStream;
} void putHttpBody(String key,Object v){
httpBodyData.put(key,v);
} public List<Application__multipart_form_dataHttpBodyResolve.FileEntity>getFromData(){
Object body = httpBodyData.get("body");
if(body instanceof ArrayList){
return (List<Application__multipart_form_dataHttpBodyResolve.FileEntity>)body;
}
return new ArrayList<>(0);
} public String getBodyContent(){ Object body = httpBodyData.get("body");
return (String)body; } private void httpResolve() throws Exception { //http报文 请求行和请求头
List<String> httpReportHead=new ArrayList<>();
byte [] line=new byte[PpsWebConstant.BUFFER_INIT_LENGTH];
int index=0;
while (true) { try{
byte data = (byte) inputStream.read();
line[index]=data; if(data=='\n'&&index!=0&&line[index-1]=='\r'){ String s = new String(line, 0, index+1, "utf-8");
//如果s==\r\n 那么说明这个就是请求体和请求头的那个分隔标记 下面的字节就是请求体了
if(s.equals("\r\n")){
break;
}
httpReportHead.add(s);
for (int i = 0; i < line.length; i++) {
line[i]=0;
}
index=0;
continue;
} index++;
//扩容
if(index>=line.length){
byte[] newLine=new byte[line.length*2];
System.arraycopy(line,0,newLine,0,index);
line=newLine;
} }catch (ChannelReadEndException e){
break;
} } //解析请求头
resolveHttpHead(httpReportHead); //请求体解析
String content_type = getHeader("content-type");
if(content_type==null){
content_type=getHeader("Content-Type");
}
if(content_type!=null){
content_type=content_type.trim();
if(content_type.contains("multipart/form-data")){
content_type="multipart/form-data";
}
}
HttpBodyResolve factory = HttpAlgoFactory.getHttpBodyResoveAlgo(content_type);
if(factory!=null){
factory.resolve(this);
} } private void resolveHttpHead(List<String> httpReportHead) throws UnsupportedEncodingException { //解析请求头
if(!httpReportHead.isEmpty()) { //请求行
String requestLine= httpReportHead.get(0);
String[] lineArr = requestLine.split(" ");
setMethod(lineArr[0]);
setProtocol(lineArr[2]);
String url=lineArr[1];
int i1 = url.indexOf("?");
if(i1!=-1) {
String pureUrl = url.substring(0, i1);
String endUrl = url.substring(i1+1);
setUrl(pureUrl);
endUrl= URLDecoder.decode(endUrl, PpsWebConstant.CHAR_SET);
String[] split1 = endUrl.split("&");
for (String s : split1) {
String[] split2 = s.split("=");
if(split2.length==2){
putUrlParam(split2[0], split2[1]);
}
}
}else {
setUrl(url);
} //解析请求头
for (int i = 1; i < httpReportHead.size(); i++) { String[] split1 = httpReportHead.get(i).split(":");
if(split1.length==2){
putHeaderParam(split1[0],split1[1]);
}
} } } public String getProtocol() {
return protocol;
} public void putUrlParam(String key,String v){
urlParams.put(key,v);
}
public void putHeaderParam(String key,String v){
headerParam.put(key,v);
}
public String getHeader(String k){
return headerParam.get(k);
}
public Map<String, String> getHeaderParam() {
return headerParam;
} public void setHeaderParam(Map<String, String> headerParam) {
this.headerParam = headerParam;
} public void setProtocol(String protocol) {
this.protocol = protocol;
} public String getMethod() {
return method;
} public void setMethod(String method) {
this.method = method;
} public Map<String, String> getUrlParams() {
return urlParams;
} public void setUrlParams(Map<String, String> urlParams) {
this.urlParams = urlParams;
} public String getParam(String key){
return urlParams.get(key);
} public String getUrl() {
return url;
} public void setUrl(String url) {
this.url = url;
}
}

对于 content-type

multipart/form-data

的请求体解析:

package com.pps.web.data;

import com.pps.web.constant.ContentTypeEnum;
import com.pps.web.constant.PpsWebConstant; import java.io.*;
import java.util.*; /**
* @author Pu PanSheng, 2021/12/19
* @version OPRA v1.0
*/
public class Application__multipart_form_dataHttpBodyResolve implements HttpBodyResolve { private Map<String, Object> serverParam; @Override
public void init(Map<String, Object> serverParam) {
this.serverParam=serverParam;
} @Override
public void resolve(HttpRequest httpRequest) throws Exception { InputStream ppsInputSteram = httpRequest.getInputStream(); List<FileEntity> list=new ArrayList<>(); byte[] spline = getSpline(ppsInputSteram); while (true) { if (spline == null||spline[spline.length-1]=='-') {
break;
} FileEntity fileEntity = new FileEntity();
String desc = getDesc(ppsInputSteram);
String[] split = desc.split("\r\n");
for (String s : split) {
String[] split1 = s.split(";");
for (String s1 : split1) {
String[] split2 = s1.split("=");
if (split2.length == 2) {
String key = split2[0].trim();
String v = split2[1].trim();
fileEntity.putInfo(key, v);
}
} } //具体的内容
byte[] lineBuff = new byte[3*1024]; int indexL = 0;
while (true) { byte data = (byte) ppsInputSteram.read();
if (data != -1) {
lineBuff[indexL] = data;
//如果\r\n 那么说明下面 可能 就是其他文件了 但是还不一定
if (data == '\n' && indexL != 0 && lineBuff[indexL - 1] == '\r') { //看看下一段是否位分隔
byte[] spline2 = getSpline2(ppsInputSteram, spline.length);
boolean f = true;
if (spline2 != null && spline2.length != spline.length) {
f = false;
}
if (f && spline2 != null && spline2.length == spline.length) {
for (int i = 0; i < spline.length - 2; i++) {
if (spline2[i] != spline[i]) {
f = false;
break;
}
}
}
//是分割线 表示确实结束了 这一段的内容就是文件了
if (f) {
spline = spline2;
break;
} else {//没有结束 那么
for (int i = 0; i < spline2.length; i++) {
indexL++;
//扩容
if (indexL >= lineBuff.length) {
lineBuff = resize(lineBuff, indexL);
}
lineBuff[indexL] = spline2[i];
}
}
}
indexL++;
//扩容
if (indexL >= lineBuff.length) {
lineBuff = resize(lineBuff, indexL);
}
} else {
break;
}
} int size=indexL-1;
if(size<=0){
break;
}
String filename = fileEntity.getInfo("filename");
//说明是文件
if(filename!=null){
String tempDir =(String) serverParam.get(PpsWebConstant.TEMP_DIR_KEY);
String tempFileName=tempDir+File.separator+UUID.randomUUID().toString();
try(BufferedOutputStream fileOutputStream=new BufferedOutputStream(new FileOutputStream(tempFileName))) {
fileOutputStream.write(lineBuff, 0, size);
fileOutputStream.flush();
fileEntity.putInfo(PpsWebConstant.TEMP_FILE_KEY, tempFileName);
httpRequest.setSavleFile(true);
}
}else {
byte [] t=new byte[size];
System.arraycopy(lineBuff,0,t,0,size);
fileEntity.setData(t);
}
list.add(fileEntity);
}
httpRequest.putHttpBody("body",list); } @Override
public String getType() {
return ContentTypeEnum.multipartformdata.getType();
}
public byte[] getSpline2(InputStream inputStream, int len) throws IOException { byte [] bytes=new byte[len];
for (int i = 0; i < len; i++) {
byte data = (byte) inputStream.read();
if(data==-1){
break;
}
} return bytes;
}
public byte[] getSpline(InputStream inputStream) throws IOException { //取第一行的标志位
byte[] splitLine=new byte[1024];
int k=0;
while (true) { byte data = (byte) inputStream.read(); if(data!=-1) { splitLine[k] = data;
if (data == '\n' && k != 0 && splitLine[k - 1] == '\r') {
//----------------343434--\r\n 表示结束了
if (splitLine[k - 2] == '-' && splitLine[k - 3] == '-') {
return null;
} k++; if (k >= splitLine.length) {
splitLine = resize(splitLine, k);
} break;
} k++;
if (k >= splitLine.length) {
splitLine = resize(splitLine, k);
}
}else {
break;
} }
byte[] newL=new byte[k];
System.arraycopy(splitLine,0,newL,0,k);
splitLine=newL;
return splitLine; } public String getDesc(InputStream inputStream) throws IOException { StringBuilder stringBuilder=new StringBuilder();
byte[] lineBuff=new byte[1024];
int indexL=0;
while (true) { byte data = (byte) inputStream.read();
if(data!=-1) {
lineBuff[indexL] = data;
if (data == '\n' && indexL != 0 && lineBuff[indexL - 1] == '\r') {
String s = new String(lineBuff, 0, indexL + 1, "utf-8");
//如果s==\r\n 那么说明这个下面就是具体的请求体字节内容
if (s.equals("\r\n")) {
break;
}
stringBuilder.append(s);
for (int i = 0; i < lineBuff.length; i++) {
lineBuff[i] = 0;
}
indexL = 0;
continue;
} indexL++;
//扩容
if (indexL >= lineBuff.length) {
lineBuff = resize(lineBuff, indexL);
}
}else {
break;
}
}
return stringBuilder.toString();
}
public byte[] resize(byte[] lineBuff,int indexL){
int resizeL = lineBuff.length * 2;
if(resizeL>(Integer) serverParam.get("maxDataSize")){
throw new RuntimeException("超过服务器 最大可支持数据大小!");
}
byte[] newLine=new byte[resizeL];
System.arraycopy(lineBuff,0,newLine,0,indexL);
return newLine;
} public static class FileEntity{ private Map<String,String> info=new HashMap<>();
private byte [] data;
private InputStream inputStream;
public InputStream getInputStream() {
if(inputStream!=null){
return inputStream;
}
String url = info.get("pps-file-url");
if(url==null){
return null;
}
try {
inputStream=new FileInputStream(url);
return inputStream;
} catch (FileNotFoundException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
void putInfo(String k,String v){
info.put(k,v);
}
public String getInfo(String k){
return info.get(k);
} public void setData(byte[] data) {
this.data = data;
} public byte[] getData() {
return data;
}
}
}

其他的就不举例了    对于上传文件的请求 服务器会将这个文件存到临时目录  当我们想要拿到这个文件流时  可以调用Request的相关方法 西面是我写的一个上传的servletDemo

import com.pps.web.data.Application__multipart_form_dataHttpBodyResolve;
import com.pps.web.data.HttpRequest;
import com.pps.web.data.Response;
import com.pps.web.servlet.model.PpsHttpServlet; import java.io.InputStream;
import java.util.List; /**
* @author Pu PanSheng, 2021/12/21
* @version OPRA v1.0
*/
public class UploadServlet extends PpsHttpServlet {
@Override
public void get(HttpRequest request, Response response) { List<Application__multipart_form_dataHttpBodyResolve.FileEntity> fromData = request.getFromData(); fromData.forEach(f->{ f.getInfo("name");
String filename = f.getInfo("filename");
//说明时文件流
if(filename!=null) {
//得到文件流
InputStream inputStream = f.getInputStream(); }else {
//普通key 那么直接转成字符
byte[] data = f.getData();
String s = new String(data); } }); }
}

接下来看下 是怎么处理Response 当我们在自己的程序里 写入数据 是如何返回给服务器的

搜先我们需要的知道  我们的数据想要让浏览器任务  那么必须告诉浏览器如下信息:

什么文件类型(也就是content-Type)

数据量多大(不告诉这个  浏览器就会一直转圈 因为它不知到结束没有  但是有时候 我们并不知道我们要发送的数据有多大 那么就需要使用到分块传输 Chunck)

还有Http报文的那些规范  请求头 请求行

我们的程序应该只需要关心发送的数据 以上对于使用者应该是透明的  所以Response的重点就在这里  下面请看Response 的Write等重点方法

package com.pps.web.data;

import com.pps.web.WebServer;
import com.pps.web.constant.PpsWebConstant;
import com.pps.web.exception.ChannelCloseException;
import com.pps.web.util.BufferUtil; import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set; /**
* @author Pu PanSheng, 2021/12/18
* @version OPRA v1.0
*/
public class Response { private SocketChannel socketChannel;
private String protocol="HTTP/1.1";
private String code="200";
private String contentType="text/html";
private String charset="utf-8";
private Map<String,String> headerParams=new HashMap<>();
private Map<String,String> requestHeader=new HashMap<>(); /**
* 客户端可支持压缩格式
*/
private String[] acceptEncoding; /**
* 应用的压缩算法
*/
private ContentEncoding applyEncoding; /**
* 压缩文件支持类型
*/
private Set<String> compressContentType=new HashSet<>(); /**
* 是否开启压缩
*/
private boolean cancompress=false; private boolean flag; private Map<String, Object> serverParam; public Response(SocketChannel socketChannel,Map<String, Object> serverParam) { this.serverParam=serverParam;
this.socketChannel = socketChannel;
headerParams.put("Connection","keep-alive");
cancompress= (Boolean)serverParam
.getOrDefault(PpsWebConstant.OPEN_CONPRECESS_KEY,false);
String[] ss=(((String)serverParam
.get(PpsWebConstant.CONPRECESS_TYPE_KEY)).split(","));
for (String s : ss) {
compressContentType.add(s);
} } public void setRequestHeader(Map<String, String> requestHeader) {
String s = requestHeader.get("Accept-Encoding");
if(s==null){
s=requestHeader.get("accept-encoding");
}
if(s!=null){
acceptEncoding=s.split(",");
for (int i = 0; i < acceptEncoding.length; i++) {
acceptEncoding[i]=acceptEncoding[i].trim();
}
}
this.requestHeader = requestHeader;
} public void setCode(int code){
this.code=String.valueOf(code);
}
public void setCharset(String charset){
this.charset=charset;
}
public void setContentType(String contentType){
this.contentType=contentType;
}
public void putHeaderParam(String key,String v){
headerParams.put(key,v);
} public void write(byte [] bytes){
write(bytes,0,bytes.length);
}
public void write(String s){
write(BufferUtil.strToBytes(s,charset));
}
public void write(byte [] bytes,int offset, int len){ if(!flag){//第一次 那么需要写入 http报文头
byte[] messageChunckHead = createMessageChunckHead();
doWrite(messageChunckHead);
flag=true;
}
//写入chunk 内容
byte[] content = createChunckBody(bytes,offset,len);
doWrite(content);
} /**
* 结束发送 如果是write 那么必须要用flush 结束发送 不然浏览器无法结束
*/
public void flush(){
byte[] encoding = createEndChunckBody();
doWrite(encoding);
}
/**
* 压缩编码
* @param bytes
* @return
*/
private byte[] encoding(byte[] bytes,int offset,int len){ if(isSupportCompress()){
return applyEncoding.convert(bytes,offset,len);
}
return bytes;
} /**
* 当前应用是否支持压缩
* @return
*/
private boolean isSupportCompress(){ boolean f1=cancompress
&&applyEncoding!=null
&&acceptEncoding!=null
&&acceptEncoding.length>0;
if(f1) {
String s = headerParams.get("content-type");
if (s == null) {
s = headerParams.get("Content-Type");
}
return compressContentType.contains(s);
} return false;
}
/**
* 直接发送 一次发送完毕 只能调用一次 不必调用flush
* @param content
*/
public void writeDirect(String content){
byte[] co=createMessage(content);
doWrite(co);
}
/**
* 直接发送 一次发送完毕 只能调用一次 不必调用flush
* @param bytes
*/
public void writeDirect(byte [] bytes) {
bytes=createMessage(bytes);
doWrite(bytes);
} private void doWrite(byte [] ccc){
BufferUtil.write(ccc,(byteBuffer)->{
try {
socketChannel.write(byteBuffer);
} catch (IOException e) {
throw new ChannelCloseException(e);
}
});
} /**
* 构造 普通的响应头 带有content_length
* @param httpr
* @return
*/
private byte[] createMessage(String httpr) {
return createMessage(BufferUtil.strToBytes(httpr,charset));
} /**
* 构造 普通的响应头 带有content_length
* @param httpr
* @return
*/
private byte[] createMessage(byte [] httpr) { StringBuilder returnStr = new StringBuilder();
//请求行
appendResponLine(returnStr,String.format("%s %s ok"
,protocol
,code));//增加响应消息行 //请求头
compreHander(returnStr,true); /**
* 可能会被压缩
*/
httpr=encoding(httpr,0,httpr.length); String contentLen=String.valueOf(httpr.length); appendResponseHeader(returnStr,"Content-Type: "+contentType+";charset=" + charset);
appendResponseHeader(returnStr,String.format("Content-Length: %s",contentLen));
headerParams.forEach((k,v)->{
appendResponseHeader(returnStr,k +": "+v);
}); returnStr.append("\r\n"); //请求体
byte[] bytes = BufferUtil.strToBytes(returnStr.toString(),charset); int i = httpr.length + bytes.length;
byte[] newC=new byte[i];
System.arraycopy(bytes,0,newC,0,bytes.length);
System.arraycopy(httpr,0,newC,bytes.length,httpr.length);
return newC;
} /**
* 压缩头添加
* @param stringBuilder
*/
private void compreHander(StringBuilder stringBuilder, boolean force){ if(applyEncoding==null&&acceptEncoding!=null){
for (String s : acceptEncoding) {
ContentEncoding contentEncodingAlgo = HttpAlgoFactory.getContentEncodingAlgo(s);
if(contentEncodingAlgo !=null){
applyEncoding= contentEncodingAlgo;
break;
}
}
}
if(force&&isSupportCompress()){
appendResponseHeader(stringBuilder,String.format("content-encoding: %s",applyEncoding.support()));
}
}
/**
* 构造http 分块请求头 不带有content_length
*
* 压缩算法例如gizp 和 chunck分块传输 如何组合呢
*
* 答:
* 只能先要把发送的数据 用gzip 组合起来 然后再分块传输
* 而不是 对每一块分别进行压缩后 再发送
* @return
*/
private byte[] createMessageChunckHead() { StringBuilder returnStr = new StringBuilder();
//请求行
appendResponLine(returnStr,String.format("%s %s ok"
,protocol
,code));//增加响应消息行
//请求头
appendResponseHeader(returnStr,"Content-Type: "+contentType+";charset=" + charset);
appendResponseHeader(returnStr,String.format("Transfer-Encoding: %s","chunked")); headerParams.forEach((k,v)->{
appendResponseHeader(returnStr,k +": "+v);
}); returnStr.append("\r\n");
byte[] bytes =null;
bytes = BufferUtil.strToBytes(returnStr.toString(),charset);
return bytes; } private byte [] createChunckBody(byte [] bytes,int offset,int lenA){ String len = Integer.toHexString(lenA);
String h=len+"\r\n";
byte[] bytes1 = BufferUtil.strToBytes(h);
int i = lenA + bytes1.length;
byte[] n=new byte[i+2];
System.arraycopy(bytes1,0,n,0,bytes1.length);
System.arraycopy(bytes,offset, n,bytes1.length,lenA);
n[i]='\r';
n[i+1]='\n';
return n; }
private byte [] createEndChunckBody(){ String end="0\r\n\r\n";
byte[] bytes = BufferUtil.strToBytes(end);
return bytes; }
private void appendResponLine(StringBuilder stringBuilder,String line){
stringBuilder.append(line+"\r\n");
}
private void appendResponseHeader(StringBuilder stringBuilder,String line){
stringBuilder.append(line+"\r\n");
} }

对于http压缩算法   作者只是简单提供了gzip的压缩算法

首先我们需要知道的是 当客户端访问的时候 accept-type 包含了客户端可支持的压缩数据格式  我们获取到这个请求

然后判断 它请求的资源是否可以压缩  如果可以 我们就把资源压缩了 返回客户端

并且要在请求头带上

content-encoding

标识 标识这些数据我们压缩了 你需要解压再渲染处理

对于解压算法  作者只是简单写了下 并不成熟  因为它耗费机器cpu  有时候得不偿失  应该在一些静态资源上开启压缩    并且缓存起来    而作者的只是拿到数据马上压缩

然后返回而已   不太成熟


以上大体就介绍完毕了   我的WebServer

最后附上gitHub 链接 里面lib目录包含了jar 包 可以直接导入项目使用

pupansheng/pps-web (github.com)

记得帮我点赞哦!谢谢