├── .gitignore ├── README.md ├── doc └── ChangeLog.txt ├── pom.xml └── src ├── main └── java │ └── com │ └── xiaoleilu │ └── loServer │ ├── LoServer.java │ ├── ServerSetting.java │ ├── action │ ├── Action.java │ ├── DefaultIndexAction.java │ ├── ErrorAction.java │ └── FileAction.java │ ├── annotation │ └── Route.java │ ├── exception │ └── ServerSettingException.java │ ├── filter │ └── Filter.java │ ├── handler │ ├── ActionHandler.java │ ├── HttpChunkContentCompressor.java │ ├── Request.java │ └── Response.java │ └── listener │ └── FileProgressiveFutureListener.java └── test ├── java └── com │ └── xiaoleilu │ └── loServer │ └── example │ └── ExampleAction.java └── resources └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | /target -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## loServer 2 | 3 | 基于Netty的Http应用服务器 4 | 5 | ### 介绍 6 | 在之前公司的时候有一些小任务是这样的:写一个小Http接口处理一些任务(这个任务常常只需要几行代码)。然后我就开始写个简单的Servlet,挂在Tomcat上。随着时间的推移,Tomcat上的这种小web项目越来越多,最后要更新一个项目就要重新启动Tomcat(没有设置热启动),非常笨重。领导意思是不要写啥都挂在Tomcat上,最好写的这个功能可以独立运行提供服务。于是需求就来了,找个嵌入式的Servlet容器(记得当时年纪小,摆脱不了Servlet容器了……),貌似满足要求的就是Jetty了,也没折腾,后来就不了了之了。 7 | 8 | 现在想想面对一些压力并不大的请求,可以自己实现一个Http应用服务器来处理简单的Get和Post请求(就是请求文本,响应的也是文本或json),了解了下Http协议,发现解析起来写的东西很多,而且自己写性能也是大问题(性能这个问题为未来考虑),于是考虑用Netty,发现它有Http相关的实现,便按照Example的指示,自己实现了一个应用服务器。思想很简单,在ServerHandler中拦截请求,把Netty的Request对象转换成我自己实现的Request对象,经过用户的Action对象后,生成自己的Response,最后转换为Netty的Response返回给用户。 9 | 10 | 这个项目中使用到的Netty版本是4.X,毕竟这个版本比较稳定,而且最近也在不停更新,于是采用了。之后如果有时间,会在项目中添加更多功能,希望它最终成为一个完善的高性能的Http服务器。 11 | 12 | ### 使用方法 13 | 1. 新建一个类实现Action接口,例如我新建了一个ExampleAction 14 | 2. 调用ServerSetting.addAction("/example", ExampleAction.class);增加请求路径和Action的映射关系 15 | 3. ServerSetting.setPort(8090);设置监听端口 16 | 4. LoServer.start();启动服务 17 | 5. 在浏览器中访问http://localhost:8090/example既可 18 | 19 | ### 代码 20 | 21 | package com.xiaoleilu.loServer.example; 22 | 23 | import com.xiaoleilu.loServer.LoServer; 24 | import com.xiaoleilu.loServer.Request; 25 | import com.xiaoleilu.loServer.Response; 26 | import com.xiaoleilu.loServer.ServerSetting; 27 | 28 | /** 29 | * loServer样例程序
30 | * Action对象用于处理业务流程,类似于Servlet对象
31 | * 在启动服务器前必须将path和此Action加入到ServerSetting的ActionMap中
32 | * 使用ServerSetting.setPort方法设置监听端口,此处设置为8090(如果不设置则使用默认的8090端口) 33 | * 然后调用LoServer.start()启动服务
34 | * 在浏览器中访问http://localhost:8090/example?a=b既可在页面上显示response a: b 35 | * @author Looly 36 | * 37 | */ 38 | public class ExampleAction implements Action{ 39 | 40 | @Override 41 | public void doAction(Request request, Response response) { 42 | String a = request.getParam("a"); 43 | response.setContent("response a: " + a); 44 | } 45 | 46 | public static void main(String[] args) { 47 | ServerSetting.addAction("/example", ExampleAction.class); 48 | ServerSetting.setPort(8090); 49 | LoServer.start(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /doc/ChangeLog.txt: -------------------------------------------------------------------------------- 1 | 0.5.0 2 | 1、增加过滤器 3 | 2、升级Hutool 4 | 5 | 0.5.1 6 | 1、增加错误处理机制 -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | jar 6 | 7 | com.xiaoleilu 8 | loServer 9 | 0.5.6 10 | loServer 11 | 基于Netty的嵌入式应用服务器 12 | https://github.com/looly/loServer 13 | 14 | 15 | 16 | The Apache Software License, Version 2.0 17 | http://www.apache.org/licenses/LICENSE-2.0.txt 18 | 19 | 20 | 21 | 22 | 23 | Looly 24 | loolly@gmail.com 25 | 26 | 27 | 28 | 29 | scm:git@github.com:looly/loServer.git 30 | scm:git@github.com:looly/loServer.git 31 | git@github.com:looly/loServer.git 32 | 33 | 34 | 35 | UTF8 36 | UTF8 37 | 38 | 39 | 40 | 41 | loServer 42 | 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-compiler-plugin 48 | 3.1 49 | 50 | 1.7 51 | 1.7 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | cn.hutool 60 | hutool-all 61 | 4.6.1 62 | 63 | 64 | io.netty 65 | netty-all 66 | 4.1.42.Final 67 | 68 | 69 | org.javassist 70 | javassist 71 | 3.25.0-GA 72 | 73 | 74 | 75 | ch.qos.logback 76 | logback-classic 77 | 1.2.13 78 | test 79 | 80 | 81 | 82 | 83 | 84 | release 85 | 86 | 87 | oss 88 | https://oss.sonatype.org/content/repositories/snapshots/ 89 | 90 | 91 | oss 92 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 93 | 94 | 95 | 96 | 97 | 98 | 99 | org.apache.maven.plugins 100 | maven-source-plugin 101 | 3.0.1 102 | 103 | 104 | package 105 | 106 | jar-no-fork 107 | 108 | 109 | 110 | 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-javadoc-plugin 115 | 2.10.4 116 | 117 | 118 | package 119 | 120 | jar 121 | 122 | 123 | 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-gpg-plugin 129 | 1.6 130 | 131 | 132 | sign-artifacts 133 | verify 134 | 135 | sign 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/LoServer.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer; 2 | 3 | import com.xiaoleilu.loServer.handler.ActionHandler; 4 | 5 | import cn.hutool.core.date.DateUtil; 6 | import cn.hutool.log.Log; 7 | import cn.hutool.log.StaticLog; 8 | import io.netty.bootstrap.ServerBootstrap; 9 | import io.netty.channel.Channel; 10 | import io.netty.channel.ChannelInitializer; 11 | import io.netty.channel.ChannelOption; 12 | import io.netty.channel.EventLoopGroup; 13 | import io.netty.channel.nio.NioEventLoopGroup; 14 | import io.netty.channel.socket.SocketChannel; 15 | import io.netty.channel.socket.nio.NioServerSocketChannel; 16 | import io.netty.handler.codec.http.HttpObjectAggregator; 17 | import io.netty.handler.codec.http.HttpServerCodec; 18 | import io.netty.handler.stream.ChunkedWriteHandler; 19 | 20 | /** 21 | * LoServer starter
22 | * 用于启动服务器的主对象
23 | * 使用LoServer.start()启动服务器
24 | * 服务的Action类和端口等设置在ServerSetting中设置 25 | * @author Looly 26 | * 27 | */ 28 | public class LoServer { 29 | private static final Log log = StaticLog.get(); 30 | 31 | /** 32 | * 启动服务 33 | * @param port 端口 34 | * @throws InterruptedException 35 | */ 36 | public void start(int port) throws InterruptedException { 37 | long start = System.currentTimeMillis(); 38 | 39 | // Configure the server. 40 | final EventLoopGroup bossGroup = new NioEventLoopGroup(1); 41 | final EventLoopGroup workerGroup = new NioEventLoopGroup(); 42 | 43 | try { 44 | final ServerBootstrap b = new ServerBootstrap(); 45 | b.group(bossGroup, workerGroup) 46 | .option(ChannelOption.SO_BACKLOG, 1024) 47 | .channel(NioServerSocketChannel.class) 48 | // .handler(new LoggingHandler(LogLevel.INFO)) 49 | .childHandler(new ChannelInitializer(){ 50 | @Override 51 | protected void initChannel(SocketChannel ch) throws Exception { 52 | ch.pipeline() 53 | .addLast(new HttpServerCodec()) 54 | //把多个消息转换为一个单一的FullHttpRequest或是FullHttpResponse 55 | .addLast(new HttpObjectAggregator(65536)) 56 | //压缩Http消息 57 | // .addLast(new HttpChunkContentCompressor()) 58 | //大文件支持 59 | .addLast(new ChunkedWriteHandler()) 60 | 61 | .addLast(new ActionHandler()); 62 | } 63 | }); 64 | 65 | final Channel ch = b.bind(port).sync().channel(); 66 | log.info("***** Welcome To LoServer on port [{}], startting spend {}ms *****", port, DateUtil.spendMs(start)); 67 | ch.closeFuture().sync(); 68 | } finally { 69 | bossGroup.shutdownGracefully(); 70 | workerGroup.shutdownGracefully(); 71 | } 72 | } 73 | 74 | /** 75 | * 启动服务器 76 | */ 77 | public static void start() { 78 | try { 79 | new LoServer().start(ServerSetting.getPort()); 80 | } catch (InterruptedException e) { 81 | log.error("LoServer start error!", e); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/ServerSetting.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer; 2 | 3 | import java.io.File; 4 | import java.nio.charset.Charset; 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | import com.xiaoleilu.loServer.action.Action; 9 | import com.xiaoleilu.loServer.action.DefaultIndexAction; 10 | import com.xiaoleilu.loServer.action.ErrorAction; 11 | import com.xiaoleilu.loServer.annotation.Route; 12 | import com.xiaoleilu.loServer.exception.ServerSettingException; 13 | import com.xiaoleilu.loServer.filter.Filter; 14 | 15 | import cn.hutool.core.io.FileUtil; 16 | import cn.hutool.core.lang.Singleton; 17 | import cn.hutool.core.util.StrUtil; 18 | import cn.hutool.log.Log; 19 | import cn.hutool.log.StaticLog; 20 | 21 | /** 22 | * 全局设定文件 23 | * @author xiaoleilu 24 | * 25 | */ 26 | public class ServerSetting { 27 | private static final Log log = StaticLog.get(); 28 | 29 | //-------------------------------------------------------- Default value start 30 | /** 默认的字符集编码 */ 31 | public final static String DEFAULT_CHARSET = "utf-8"; 32 | 33 | public final static String MAPPING_ALL = "/*"; 34 | 35 | public final static String MAPPING_ERROR = "/_error"; 36 | //-------------------------------------------------------- Default value end 37 | 38 | /** 字符编码 */ 39 | private static String charset = DEFAULT_CHARSET; 40 | /** 端口 */ 41 | private static int port = 8090; 42 | /** 根目录 */ 43 | private static File root; 44 | /** Filter映射表 */ 45 | private static Map filterMap; 46 | /** Action映射表 */ 47 | private static Map actionMap; 48 | 49 | static{ 50 | filterMap = new ConcurrentHashMap(); 51 | 52 | actionMap = new ConcurrentHashMap(); 53 | actionMap.put(StrUtil.SLASH, new DefaultIndexAction()); 54 | actionMap.put(MAPPING_ERROR, new ErrorAction()); 55 | } 56 | 57 | /** 58 | * @return 获取编码 59 | */ 60 | public static String getCharset() { 61 | return charset; 62 | } 63 | /** 64 | * @return 字符集 65 | */ 66 | public static Charset charset() { 67 | return Charset.forName(getCharset()); 68 | } 69 | 70 | /** 71 | * 设置编码 72 | * @param charset 编码 73 | */ 74 | public static void setCharset(String charset) { 75 | ServerSetting.charset = charset; 76 | } 77 | 78 | /** 79 | * @return 监听端口 80 | */ 81 | public static int getPort() { 82 | return port; 83 | } 84 | /** 85 | * 设置监听端口 86 | * @param port 端口 87 | */ 88 | public static void setPort(int port) { 89 | ServerSetting.port = port; 90 | } 91 | 92 | //----------------------------------------------------------------------------------------------- Root start 93 | /** 94 | * @return 根目录 95 | */ 96 | public static File getRoot() { 97 | return root; 98 | } 99 | /** 100 | * @return 根目录 101 | */ 102 | public static boolean isRootAvailable() { 103 | if(root != null && root.isDirectory() && root.isHidden() == false && root.canRead()){ 104 | return true; 105 | } 106 | return false; 107 | } 108 | /** 109 | * @return 根目录 110 | */ 111 | public static String getRootPath() { 112 | return FileUtil.getAbsolutePath(root); 113 | } 114 | /** 115 | * 根目录 116 | * @param root 根目录绝对路径 117 | */ 118 | public static void setRoot(String root) { 119 | ServerSetting.root = FileUtil.mkdir(root); 120 | log.debug("Set root to [{}]", ServerSetting.root.getAbsolutePath()); 121 | } 122 | /** 123 | * 根目录 124 | * @param root 根目录绝对路径 125 | */ 126 | public static void setRoot(File root) { 127 | if(root.exists() == false){ 128 | root.mkdirs(); 129 | }else if(root.isDirectory() == false){ 130 | throw new ServerSettingException(StrUtil.format("{} is not a directory!", root.getPath())); 131 | } 132 | ServerSetting.root = root; 133 | } 134 | //----------------------------------------------------------------------------------------------- Root end 135 | 136 | //----------------------------------------------------------------------------------------------- Filter start 137 | /** 138 | * @return 获取FilterMap 139 | */ 140 | public static Map getFilterMap() { 141 | return filterMap; 142 | } 143 | /** 144 | * 获得路径对应的Filter 145 | * @param path 路径,为空时将获得 根目录对应的Action 146 | * @return Filter 147 | */ 148 | public static Filter getFilter(String path){ 149 | if(StrUtil.isBlank(path)){ 150 | path = StrUtil.SLASH; 151 | } 152 | return getFilterMap().get(path.trim()); 153 | } 154 | /** 155 | * 设置FilterMap 156 | * @param filterMap FilterMap 157 | */ 158 | public static void setFilterMap(Map filterMap) { 159 | ServerSetting.filterMap = filterMap; 160 | } 161 | 162 | /** 163 | * 设置Filter类,已有的Filter类将被覆盖 164 | * @param path 拦截路径(必须以"/"开头) 165 | * @param filter Action类 166 | */ 167 | public static void setFilter(String path, Filter filter) { 168 | if(StrUtil.isBlank(path)){ 169 | path = StrUtil.SLASH; 170 | } 171 | 172 | if(null == filter) { 173 | log.warn("Added blank action, pass it."); 174 | return; 175 | } 176 | //所有路径必须以 "/" 开头,如果没有则补全之 177 | if(false == path.startsWith(StrUtil.SLASH)) { 178 | path = StrUtil.SLASH + path; 179 | } 180 | 181 | ServerSetting.filterMap.put(path, filter); 182 | } 183 | 184 | /** 185 | * 设置Filter类,已有的Filter类将被覆盖 186 | * @param path 拦截路径(必须以"/"开头) 187 | * @param filterClass Filter类 188 | */ 189 | public static void setFilter(String path, Class filterClass) { 190 | setFilter(path, (Filter)Singleton.get(filterClass)); 191 | } 192 | //----------------------------------------------------------------------------------------------- Filter end 193 | 194 | //----------------------------------------------------------------------------------------------- Action start 195 | /** 196 | * @return 获取ActionMap 197 | */ 198 | public static Map getActionMap() { 199 | return actionMap; 200 | } 201 | /** 202 | * 获得路径对应的Action 203 | * @param path 路径,为空时将获得 根目录对应的Action 204 | * @return Action 205 | */ 206 | public static Action getAction(String path){ 207 | if(StrUtil.isBlank(path)){ 208 | path = StrUtil.SLASH; 209 | } 210 | return getActionMap().get(path.trim()); 211 | } 212 | /** 213 | * 设置ActionMap 214 | * @param actionMap ActionMap 215 | */ 216 | public static void setActionMap(Map actionMap) { 217 | ServerSetting.actionMap = actionMap; 218 | } 219 | 220 | /** 221 | * 设置Action类,已有的Action类将被覆盖 222 | * @param path 拦截路径(必须以"/"开头) 223 | * @param action Action类 224 | */ 225 | public static void setAction(String path, Action action) { 226 | if(StrUtil.isBlank(path)){ 227 | path = StrUtil.SLASH; 228 | } 229 | 230 | if(null == action) { 231 | log.warn("Added blank action, pass it."); 232 | return; 233 | } 234 | //所有路径必须以 "/" 开头,如果没有则补全之 235 | if(false == path.startsWith(StrUtil.SLASH)) { 236 | path = StrUtil.SLASH + path; 237 | } 238 | 239 | ServerSetting.actionMap.put(path, action); 240 | } 241 | 242 | /** 243 | * 增加Action类,已有的Action类将被覆盖
244 | * 所有Action都是以单例模式存在的! 245 | * @param path 拦截路径(必须以"/"开头) 246 | * @param actionClass Action类 247 | */ 248 | public static void setAction(String path, Class actionClass) { 249 | setAction(path, (Action)Singleton.get(actionClass)); 250 | } 251 | 252 | /** 253 | * 增加Action类,已有的Action类将被覆盖
254 | * 自动读取Route的注解来获得Path路径 255 | * @param action 带注解的Action对象 256 | */ 257 | public static void setAction(Action action) { 258 | final Route route = action.getClass().getAnnotation(Route.class); 259 | if(route != null){ 260 | final String path = route.value(); 261 | if(StrUtil.isNotBlank(path)){ 262 | setAction(path, action); 263 | return; 264 | } 265 | } 266 | throw new ServerSettingException("Can not find Route annotation,please add annotation to Action class!"); 267 | } 268 | 269 | /** 270 | * 增加Action类,已有的Action类将被覆盖
271 | * 所有Action都是以单例模式存在的! 272 | * @param actionClass 带注解的Action类 273 | */ 274 | public static void setAction(Class actionClass) { 275 | setAction((Action)Singleton.get(actionClass)); 276 | } 277 | //----------------------------------------------------------------------------------------------- Action start 278 | 279 | } 280 | -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/action/Action.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.action; 2 | 3 | import com.xiaoleilu.loServer.handler.Request; 4 | import com.xiaoleilu.loServer.handler.Response; 5 | 6 | /** 7 | * 请求处理接口
8 | * 当用户请求某个Path,则调用相应Action的doAction方法 9 | * @author Looly 10 | * 11 | */ 12 | public interface Action { 13 | public void doAction(Request request, Response response); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/action/DefaultIndexAction.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.action; 2 | 3 | import com.xiaoleilu.loServer.handler.Request; 4 | import com.xiaoleilu.loServer.handler.Response; 5 | 6 | /** 7 | * 默认的主页Action,当访问主页且没有定义主页Action时,调用此Action 8 | * @author Looly 9 | * 10 | */ 11 | public class DefaultIndexAction implements Action{ 12 | 13 | @Override 14 | public void doAction(Request request, Response response) { 15 | response.setContent("Welcome to LoServer."); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/action/ErrorAction.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.action; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | 6 | import com.xiaoleilu.loServer.handler.Request; 7 | import com.xiaoleilu.loServer.handler.Response; 8 | 9 | import cn.hutool.core.util.StrUtil; 10 | import cn.hutool.log.Log; 11 | import cn.hutool.log.StaticLog; 12 | import io.netty.handler.codec.http.HttpResponseStatus; 13 | 14 | /** 15 | * 错误堆栈Action类 16 | * @author Looly 17 | * 18 | */ 19 | public class ErrorAction implements Action{ 20 | private static final Log log = StaticLog.get(); 21 | 22 | public final static String ERROR_PARAM_NAME = "_e"; 23 | 24 | private final static String TEMPLATE_ERROR = "LoServer - Error report

HTTP Status {} - {}


{}


LoServer

"; 25 | 26 | @Override 27 | public void doAction(Request request, Response response) { 28 | Object eObj = request.getObjParam(ERROR_PARAM_NAME); 29 | if(eObj == null){ 30 | response.sendError(HttpResponseStatus.NOT_FOUND, "404 File not found!"); 31 | return; 32 | } 33 | 34 | if(eObj instanceof Exception){ 35 | Exception e = (Exception)eObj; 36 | log.error("Server action internal error!", e); 37 | final StringWriter writer = new StringWriter(); 38 | // 把错误堆栈储存到流中 39 | e.printStackTrace(new PrintWriter(writer)); 40 | String content = writer.toString().replace("\tat", "    \tat"); 41 | content = content.replace("\n", "
\n"); 42 | content = StrUtil.format(TEMPLATE_ERROR, 500, request.getUri(), content); 43 | 44 | response.sendServerError(content); 45 | } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/action/FileAction.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.action; 2 | 3 | import java.io.File; 4 | import java.io.UnsupportedEncodingException; 5 | import java.net.URLDecoder; 6 | import java.text.SimpleDateFormat; 7 | import java.util.Date; 8 | import java.util.Locale; 9 | import java.util.regex.Pattern; 10 | 11 | import com.xiaoleilu.loServer.ServerSetting; 12 | import com.xiaoleilu.loServer.handler.Request; 13 | import com.xiaoleilu.loServer.handler.Response; 14 | 15 | import cn.hutool.core.date.DatePattern; 16 | import cn.hutool.core.date.DateUtil; 17 | import cn.hutool.core.io.FileUtil; 18 | import cn.hutool.core.util.ReUtil; 19 | import cn.hutool.core.util.StrUtil; 20 | import cn.hutool.log.Log; 21 | import cn.hutool.log.StaticLog; 22 | import io.netty.handler.codec.http.HttpHeaderNames; 23 | import io.netty.handler.codec.http.HttpResponseStatus; 24 | 25 | /** 26 | * 默认的主页Action,当访问主页且没有定义主页Action时,调用此Action 27 | * 28 | * @author Looly 29 | * 30 | */ 31 | public class FileAction implements Action { 32 | private static final Log log = StaticLog.get(); 33 | 34 | private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*"); 35 | private static final SimpleDateFormat HTTP_DATE_FORMATER = new SimpleDateFormat(DatePattern.HTTP_DATETIME_PATTERN, Locale.US); 36 | 37 | @Override 38 | public void doAction(Request request, Response response) { 39 | if (false == Request.METHOD_GET.equalsIgnoreCase(request.getMethod())) { 40 | response.sendError(HttpResponseStatus.METHOD_NOT_ALLOWED, "Please use GET method to request file!"); 41 | return; 42 | } 43 | 44 | if(ServerSetting.isRootAvailable() == false){ 45 | response.sendError(HttpResponseStatus.NOT_FOUND, "404 Root dir not avaliable!"); 46 | return; 47 | } 48 | 49 | final File file = getFileByPath(request.getPath()); 50 | log.debug("Client [{}] get file [{}]", request.getIp(), file.getPath()); 51 | 52 | // 隐藏文件,跳过 53 | if (file.isHidden() || !file.exists()) { 54 | response.sendError(HttpResponseStatus.NOT_FOUND, "404 File not found!"); 55 | return; 56 | } 57 | 58 | // 非文件,跳过 59 | if (false == file.isFile()) { 60 | response.sendError(HttpResponseStatus.FORBIDDEN, "403 Forbidden!"); 61 | return; 62 | } 63 | 64 | // Cache Validation 65 | String ifModifiedSince = request.getHeader(HttpHeaderNames.IF_MODIFIED_SINCE.toString()); 66 | if (StrUtil.isNotBlank(ifModifiedSince)) { 67 | Date ifModifiedSinceDate = null; 68 | try { 69 | ifModifiedSinceDate = DateUtil.parse(ifModifiedSince, HTTP_DATE_FORMATER); 70 | } catch (Exception e) { 71 | log.warn("If-Modified-Since header parse error: {}", e.getMessage()); 72 | } 73 | if(ifModifiedSinceDate != null) { 74 | // 只对比到秒一级别 75 | long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000; 76 | long fileLastModifiedSeconds = file.lastModified() / 1000; 77 | if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { 78 | log.debug("File {} not modified.", file.getPath()); 79 | response.sendNotModified(); 80 | return; 81 | } 82 | } 83 | } 84 | 85 | response.setContent(file); 86 | } 87 | 88 | /** 89 | * 通过URL中的path获得文件的绝对路径 90 | * 91 | * @param httpPath Http请求的Path 92 | * @return 文件绝对路径 93 | */ 94 | public static File getFileByPath(String httpPath) { 95 | // Decode the path. 96 | try { 97 | httpPath = URLDecoder.decode(httpPath, "UTF-8"); 98 | } catch (UnsupportedEncodingException e) { 99 | throw new Error(e); 100 | } 101 | 102 | if (httpPath.isEmpty() || httpPath.charAt(0) != '/') { 103 | return null; 104 | } 105 | 106 | // 路径安全检查 107 | if (httpPath.contains("/.") || httpPath.contains("./") || httpPath.charAt(0) == '.' || httpPath.charAt(httpPath.length() - 1) == '.' || ReUtil.isMatch(INSECURE_URI, httpPath)) { 108 | return null; 109 | } 110 | 111 | // 转换为绝对路径 112 | return FileUtil.file(ServerSetting.getRoot(), httpPath); 113 | } 114 | } -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/annotation/Route.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.annotation; 2 | 3 | import java.lang.annotation.Inherited; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | 7 | /** 8 | * 注解,用于自定义访问的URL路径
9 | * 值可以是一个请求路径,如果需要指定HTTP方法,在前面加方法名用":"分隔既可
10 | * @author loolly 11 | * 12 | */ 13 | @Retention(RetentionPolicy.RUNTIME)/*保留的时间长短*/ 14 | @Inherited/*只用于class,可被子类继承*/ 15 | public @interface Route { 16 | String value(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/exception/ServerSettingException.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.exception; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | 5 | /** 6 | * 设置异常 7 | * @author xiaoleilu 8 | */ 9 | public class ServerSettingException extends RuntimeException{ 10 | private static final long serialVersionUID = -4134588858314744501L; 11 | 12 | public ServerSettingException(Throwable e) { 13 | super(e); 14 | } 15 | 16 | public ServerSettingException(String message) { 17 | super(message); 18 | } 19 | 20 | public ServerSettingException(String messageTemplate, Object... params) { 21 | super(StrUtil.format(messageTemplate, params)); 22 | } 23 | 24 | public ServerSettingException(String message, Throwable throwable) { 25 | super(message, throwable); 26 | } 27 | 28 | public ServerSettingException(Throwable throwable, String messageTemplate, Object... params) { 29 | super(StrUtil.format(messageTemplate, params), throwable); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/filter/Filter.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.filter; 2 | 3 | import com.xiaoleilu.loServer.handler.Request; 4 | import com.xiaoleilu.loServer.handler.Response; 5 | 6 | /** 7 | * 过滤器接口
8 | * @author Looly 9 | * 10 | */ 11 | public interface Filter { 12 | 13 | /** 14 | * 执行过滤 15 | * @param request 请求对象 16 | * @param response 响应对象 17 | * @return 如果返回true,则继续执行下一步内容,否则中断 18 | */ 19 | public boolean doFilter(Request request, Response response); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/handler/ActionHandler.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.handler; 2 | 3 | import java.io.IOException; 4 | 5 | import com.xiaoleilu.loServer.ServerSetting; 6 | import com.xiaoleilu.loServer.action.Action; 7 | import com.xiaoleilu.loServer.action.ErrorAction; 8 | import com.xiaoleilu.loServer.action.FileAction; 9 | import com.xiaoleilu.loServer.filter.Filter; 10 | 11 | import cn.hutool.core.lang.Singleton; 12 | import cn.hutool.log.Log; 13 | import cn.hutool.log.StaticLog; 14 | import io.netty.channel.ChannelHandlerContext; 15 | import io.netty.channel.SimpleChannelInboundHandler; 16 | import io.netty.handler.codec.http.FullHttpRequest; 17 | 18 | /** 19 | * Action处理单元 20 | * 21 | * @author Looly 22 | */ 23 | public class ActionHandler extends SimpleChannelInboundHandler { 24 | private static final Log log = StaticLog.get(); 25 | 26 | @Override 27 | protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws Exception { 28 | 29 | final Request request = Request.build(ctx, fullHttpRequest); 30 | final Response response = Response.build(ctx, request); 31 | 32 | try { 33 | //do filter 34 | boolean isPass = this.doFilter(request, response); 35 | 36 | if(isPass){ 37 | //do action 38 | this.doAction(request, response); 39 | } 40 | } catch (Exception e) { 41 | Action errorAction = ServerSetting.getAction(ServerSetting.MAPPING_ERROR); 42 | request.putParam(ErrorAction.ERROR_PARAM_NAME, e); 43 | errorAction.doAction(request, response); 44 | } 45 | 46 | //如果发送请求未被触发,则触发之,否则跳过。 47 | if(false ==response.isSent()){ 48 | response.send(); 49 | } 50 | } 51 | 52 | @Override 53 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 54 | if(cause instanceof IOException){ 55 | log.warn("{}", cause.getMessage()); 56 | }else{ 57 | super.exceptionCaught(ctx, cause); 58 | } 59 | } 60 | 61 | //---------------------------------------------------------------------------------------- Private method start 62 | /** 63 | * 执行过滤 64 | * @param request 请求 65 | * @param response 响应 66 | * @param 是否继续 67 | */ 68 | private boolean doFilter(Request request, Response response) { 69 | //全局过滤器 70 | Filter filter = ServerSetting.getFilter(ServerSetting.MAPPING_ALL); 71 | if(null != filter){ 72 | if(false == filter.doFilter(request, response)){ 73 | return false; 74 | } 75 | } 76 | 77 | //自定义Path过滤器 78 | filter = ServerSetting.getFilter(request.getPath()); 79 | if(null != filter){ 80 | if(false == filter.doFilter(request, response)){ 81 | return false; 82 | } 83 | } 84 | 85 | return true; 86 | } 87 | 88 | /** 89 | * 执行Action 90 | * @param request 请求对象 91 | * @param response 响应对象 92 | */ 93 | private void doAction(Request request, Response response){ 94 | Action action = ServerSetting.getAction(request.getPath()); 95 | if (null == action) { 96 | //查找匹配所有路径的Action 97 | action = ServerSetting.getAction(ServerSetting.MAPPING_ALL); 98 | if(null == action){ 99 | // 非Action方法,调用静态文件读取 100 | action = Singleton.get(FileAction.class); 101 | } 102 | } 103 | 104 | action.doAction(request, response); 105 | } 106 | //---------------------------------------------------------------------------------------- Private method start 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/handler/HttpChunkContentCompressor.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.handler; 2 | 3 | import cn.hutool.log.StaticLog; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelPromise; 6 | import io.netty.channel.FileRegion; 7 | import io.netty.handler.codec.http.DefaultHttpResponse; 8 | import io.netty.handler.codec.http.HttpContentCompressor; 9 | 10 | /** 11 | * 解决大文件传输与Gzip压缩冲突问题 12 | * @author Looly 13 | * 14 | */ 15 | public class HttpChunkContentCompressor extends HttpContentCompressor { 16 | @Override 17 | public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { 18 | StaticLog.debug("Write object [{}]", msg); 19 | 20 | if (msg instanceof FileRegion || msg instanceof DefaultHttpResponse) { 21 | //文件传输不经过Gzip压缩 22 | ctx.write(msg, promise); 23 | }else{ 24 | super.write(ctx, msg, promise); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/handler/Request.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.handler; 2 | 3 | import java.io.IOException; 4 | import java.net.InetSocketAddress; 5 | import java.nio.charset.Charset; 6 | import java.util.Date; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Map.Entry; 11 | import java.util.Set; 12 | 13 | import cn.hutool.core.convert.Convert; 14 | import cn.hutool.core.date.DateUtil; 15 | import cn.hutool.core.net.NetUtil; 16 | import cn.hutool.core.util.CharsetUtil; 17 | import cn.hutool.core.util.StrUtil; 18 | import cn.hutool.core.util.URLUtil; 19 | import cn.hutool.log.Log; 20 | import cn.hutool.log.StaticLog; 21 | import io.netty.channel.ChannelHandlerContext; 22 | import io.netty.handler.codec.http.FullHttpRequest; 23 | import io.netty.handler.codec.http.HttpHeaderNames; 24 | import io.netty.handler.codec.http.HttpHeaderValues; 25 | import io.netty.handler.codec.http.HttpHeaders; 26 | import io.netty.handler.codec.http.HttpMethod; 27 | import io.netty.handler.codec.http.HttpRequest; 28 | import io.netty.handler.codec.http.HttpVersion; 29 | import io.netty.handler.codec.http.QueryStringDecoder; 30 | import io.netty.handler.codec.http.cookie.Cookie; 31 | import io.netty.handler.codec.http.cookie.ServerCookieDecoder; 32 | import io.netty.handler.codec.http.multipart.Attribute; 33 | import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; 34 | import io.netty.handler.codec.http.multipart.FileUpload; 35 | import io.netty.handler.codec.http.multipart.HttpDataFactory; 36 | import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; 37 | import io.netty.handler.codec.http.multipart.InterfaceHttpData; 38 | import io.netty.handler.codec.http.multipart.InterfaceHttpData.HttpDataType; 39 | 40 | /** 41 | * Http请求对象 42 | * 43 | * @author Looly 44 | * 45 | */ 46 | public class Request { 47 | private static final Log log = StaticLog.get(); 48 | 49 | public static final String METHOD_DELETE = HttpMethod.DELETE.name(); 50 | public static final String METHOD_HEAD = HttpMethod.HEAD.name(); 51 | public static final String METHOD_GET = HttpMethod.GET.name(); 52 | public static final String METHOD_OPTIONS = HttpMethod.OPTIONS.name(); 53 | public static final String METHOD_POST = HttpMethod.POST.name(); 54 | public static final String METHOD_PUT = HttpMethod.PUT.name(); 55 | public static final String METHOD_TRACE = HttpMethod.TRACE.name(); 56 | 57 | private static final HttpDataFactory HTTP_DATA_FACTORY = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); 58 | 59 | private FullHttpRequest nettyRequest; 60 | 61 | private String path; 62 | private String ip; 63 | private Map headers = new HashMap(); 64 | private Map params = new HashMap(); 65 | private Map cookies = new HashMap(); 66 | 67 | /** 68 | * 构造 69 | * 70 | * @param ctx ChannelHandlerContext 71 | * @param nettyRequest HttpRequest 72 | */ 73 | private Request(ChannelHandlerContext ctx, FullHttpRequest nettyRequest) { 74 | this.nettyRequest = nettyRequest; 75 | final String uri = nettyRequest.uri(); 76 | this.path = URLUtil.getPath(getUri()); 77 | 78 | this.putHeadersAndCookies(nettyRequest.headers()); 79 | 80 | // request URI parameters 81 | this.putParams(new QueryStringDecoder(uri)); 82 | if(nettyRequest.method() != HttpMethod.GET && !"application/octet-stream".equals(nettyRequest.headers().get("Content-Type"))){ 83 | HttpPostRequestDecoder decoder = null; 84 | try { 85 | decoder = new HttpPostRequestDecoder(HTTP_DATA_FACTORY, nettyRequest); 86 | this.putParams(decoder); 87 | } finally { 88 | if(null != decoder){ 89 | decoder.destroy(); 90 | decoder = null; 91 | } 92 | } 93 | } 94 | 95 | // IP 96 | this.putIp(ctx); 97 | } 98 | 99 | /** 100 | * @return Netty的HttpRequest 101 | */ 102 | public HttpRequest getNettyRequest() { 103 | return this.nettyRequest; 104 | } 105 | 106 | /** 107 | * 获得版本信息 108 | * 109 | * @return 版本 110 | */ 111 | public String getProtocolVersion() { 112 | return nettyRequest.protocolVersion().text(); 113 | } 114 | 115 | /** 116 | * 获得URI(带参数的路径) 117 | * 118 | * @return URI 119 | */ 120 | public String getUri() { 121 | return nettyRequest.uri(); 122 | } 123 | 124 | /** 125 | * @return 获得path(不带参数的路径) 126 | */ 127 | public String getPath() { 128 | return path; 129 | } 130 | 131 | /** 132 | * 获得Http方法 133 | * 134 | * @return Http method 135 | */ 136 | public String getMethod() { 137 | return nettyRequest.method().name(); 138 | } 139 | 140 | /** 141 | * 获得IP地址 142 | * 143 | * @return IP地址 144 | */ 145 | public String getIp() { 146 | return ip; 147 | } 148 | 149 | /** 150 | * 获得所有头信息 151 | * 152 | * @return 头信息Map 153 | */ 154 | public Map getHeaders() { 155 | return headers; 156 | } 157 | 158 | /** 159 | * 使用ISO8859_1字符集获得Header内容
160 | * 由于Header中很少有中文,故一般情况下无需转码 161 | * 162 | * @param headerKey 头信息的KEY 163 | * @return 值 164 | */ 165 | public String getHeader(String headerKey) { 166 | return headers.get(headerKey); 167 | } 168 | 169 | /** 170 | * @return 是否为普通表单(application/x-www-form-urlencoded) 171 | */ 172 | public boolean isXWwwFormUrlencoded() { 173 | return "application/x-www-form-urlencoded".equals(getHeader("Content-Type")); 174 | } 175 | 176 | /** 177 | * 获得指定的Cookie 178 | * 179 | * @param name cookie名 180 | * @return Cookie对象 181 | */ 182 | public Cookie getCookie(String name) { 183 | return cookies.get(name); 184 | } 185 | 186 | /** 187 | * @return 获得所有Cookie信息 188 | */ 189 | public Map getCookies() { 190 | return this.cookies; 191 | } 192 | 193 | /** 194 | * @return 客户浏览器是否为IE 195 | */ 196 | public boolean isIE() { 197 | String userAgent = getHeader("User-Agent"); 198 | if (StrUtil.isNotBlank(userAgent)) { 199 | userAgent = userAgent.toUpperCase(); 200 | if (userAgent.contains("MSIE") || userAgent.contains("TRIDENT")) { 201 | return true; 202 | } 203 | } 204 | return false; 205 | } 206 | 207 | /** 208 | * @param name 参数名 209 | * @return 获得请求参数 210 | */ 211 | public String getParam(String name) { 212 | final Object value = params.get(name); 213 | if(null == value){ 214 | return null; 215 | } 216 | 217 | if(value instanceof String){ 218 | return (String)value; 219 | } 220 | return value.toString(); 221 | } 222 | 223 | /** 224 | * @param name 参数名 225 | * @return 获得请求参数 226 | */ 227 | public Object getObjParam(String name) { 228 | return params.get(name); 229 | } 230 | 231 | /** 232 | * 获得GET请求参数
233 | * 会根据浏览器类型自动识别GET请求的编码方式从而解码
234 | * charsetOfServlet为null则默认的ISO_8859_1 235 | * 236 | * @param name 参数名 237 | * @param charset 字符集 238 | * @return 获得请求参数 239 | */ 240 | public String getParam(String name, Charset charset) { 241 | if (null == charset) { 242 | charset = Charset.forName(CharsetUtil.ISO_8859_1); 243 | } 244 | 245 | String destCharset = CharsetUtil.UTF_8; 246 | if (isIE()) { 247 | // IE浏览器GET请求使用GBK编码 248 | destCharset = CharsetUtil.GBK; 249 | } 250 | 251 | String value = getParam(name); 252 | if (METHOD_GET.equalsIgnoreCase(getMethod())) { 253 | value = CharsetUtil.convert(value, charset.toString(), destCharset); 254 | } 255 | return value; 256 | } 257 | 258 | /** 259 | * @param name 参数名 260 | * @param defaultValue 当客户端未传参的默认值 261 | * @return 获得请求参数 262 | */ 263 | public String getParam(String name, String defaultValue) { 264 | String param = getParam(name); 265 | return StrUtil.isBlank(param) ? defaultValue : param; 266 | } 267 | 268 | /** 269 | * @param name 参数名 270 | * @param defaultValue 当客户端未传参的默认值 271 | * @return 获得Integer类型请求参数 272 | */ 273 | public Integer getIntParam(String name, Integer defaultValue) { 274 | return Convert.toInt(getParam(name), defaultValue); 275 | } 276 | 277 | /** 278 | * @param name 参数名 279 | * @param defaultValue 当客户端未传参的默认值 280 | * @return 获得long类型请求参数 281 | */ 282 | public Long getLongParam(String name, Long defaultValue) { 283 | return Convert.toLong(getParam(name), defaultValue); 284 | } 285 | 286 | /** 287 | * @param name 参数名 288 | * @param defaultValue 当客户端未传参的默认值 289 | * @return 获得Double类型请求参数 290 | */ 291 | public Double getDoubleParam(String name, Double defaultValue) { 292 | return Convert.toDouble(getParam(name), defaultValue); 293 | } 294 | 295 | /** 296 | * @param name 参数名 297 | * @param defaultValue 当客户端未传参的默认值 298 | * @return 获得Float类型请求参数 299 | */ 300 | public Float getFloatParam(String name, Float defaultValue) { 301 | return Convert.toFloat(getParam(name), defaultValue); 302 | } 303 | 304 | /** 305 | * @param name 参数名 306 | * @param defaultValue 当客户端未传参的默认值 307 | * @return 获得Boolean类型请求参数 308 | */ 309 | public Boolean getBoolParam(String name, Boolean defaultValue) { 310 | return Convert.toBool(getParam(name), defaultValue); 311 | } 312 | 313 | /** 314 | * 格式:
315 | * 1、yyyy-MM-dd HH:mm:ss
316 | * 2、yyyy-MM-dd
317 | * 3、HH:mm:ss
318 | * 319 | * @param name 参数名 320 | * @param defaultValue 当客户端未传参的默认值 321 | * @return 获得Date类型请求参数,默认格式: 322 | */ 323 | public Date getDateParam(String name, Date defaultValue) { 324 | String param = getParam(name); 325 | return StrUtil.isBlank(param) ? defaultValue : DateUtil.parse(param); 326 | } 327 | 328 | /** 329 | * @param name 参数名 330 | * @param format 格式 331 | * @param defaultValue 当客户端未传参的默认值 332 | * @return 获得Date类型请求参数 333 | */ 334 | public Date getDateParam(String name, String format, Date defaultValue) { 335 | String param = getParam(name); 336 | return StrUtil.isBlank(param) ? defaultValue : DateUtil.parse(param, format); 337 | } 338 | 339 | /** 340 | * 获得请求参数
341 | * 列表类型值,常用于表单中的多选框 342 | * 343 | * @param name 参数名 344 | * @return 数组 345 | */ 346 | @SuppressWarnings("unchecked") 347 | public List getArrayParam(String name) { 348 | Object value = params.get(name); 349 | if(null == value){ 350 | return null; 351 | } 352 | 353 | if(value instanceof List){ 354 | return (List) value; 355 | }else if(value instanceof String){ 356 | return StrUtil.split((String)value, ','); 357 | }else{ 358 | throw new RuntimeException("Value is not a List type!"); 359 | } 360 | } 361 | 362 | /** 363 | * 获得所有请求参数 364 | * 365 | * @return Map 366 | */ 367 | public Map getParams() { 368 | return params; 369 | } 370 | 371 | /** 372 | * @return 是否为长连接 373 | */ 374 | public boolean isKeepAlive() { 375 | final String connectionHeader = getHeader(HttpHeaderNames.CONNECTION.toString()); 376 | // 无论任何版本Connection为close时都关闭连接 377 | if (HttpHeaderValues.CLOSE.toString().equalsIgnoreCase(connectionHeader)) { 378 | return false; 379 | } 380 | 381 | // HTTP/1.0只有Connection为Keep-Alive时才会保持连接 382 | if (HttpVersion.HTTP_1_0.text().equals(getProtocolVersion())) { 383 | if (false == HttpHeaderValues.KEEP_ALIVE.toString().equalsIgnoreCase(connectionHeader)) { 384 | return false; 385 | } 386 | } 387 | // HTTP/1.1默认打开Keep-Alive 388 | return true; 389 | } 390 | 391 | // --------------------------------------------------------- Protected method start 392 | /** 393 | * 填充参数(GET请求的参数) 394 | * 395 | * @param decoder QueryStringDecoder 396 | */ 397 | protected void putParams(QueryStringDecoder decoder) { 398 | if (null != decoder) { 399 | List valueList; 400 | for (Entry> entry : decoder.parameters().entrySet()) { 401 | valueList = entry.getValue(); 402 | if(null != valueList){ 403 | this.putParam(entry.getKey(), 1 == valueList.size() ? valueList.get(0) : valueList); 404 | } 405 | } 406 | } 407 | } 408 | 409 | /** 410 | * 填充参数(POST请求的参数) 411 | * 412 | * @param decoder QueryStringDecoder 413 | */ 414 | protected void putParams(HttpPostRequestDecoder decoder) { 415 | if (null == decoder) { 416 | return; 417 | } 418 | 419 | for (InterfaceHttpData data : decoder.getBodyHttpDatas()) { 420 | putParam(data); 421 | } 422 | } 423 | 424 | /** 425 | * 填充参数 426 | * 427 | * @param data InterfaceHttpData 428 | */ 429 | protected void putParam(InterfaceHttpData data) { 430 | final HttpDataType dataType = data.getHttpDataType(); 431 | if (dataType == HttpDataType.Attribute) { 432 | //普通参数 433 | Attribute attribute = (Attribute) data; 434 | try { 435 | this.putParam(attribute.getName(), attribute.getValue()); 436 | } catch (IOException e) { 437 | log.error(e); 438 | } 439 | }else if(dataType == HttpDataType.FileUpload){ 440 | //文件 441 | FileUpload fileUpload = (FileUpload) data; 442 | if(fileUpload.isCompleted()){ 443 | try { 444 | this.putParam(data.getName(), fileUpload.getFile()); 445 | } catch (IOException e) { 446 | log.error(e, "Get file param [{}] error!", data.getName()); 447 | } 448 | } 449 | } 450 | } 451 | 452 | /** 453 | * 填充参数 454 | * 455 | * @param key 参数名 456 | * @param value 参数值 457 | */ 458 | protected void putParam(String key, Object value) { 459 | this.params.put(key, value); 460 | } 461 | 462 | /** 463 | * 填充头部信息和Cookie信息 464 | * 465 | * @param headers HttpHeaders 466 | */ 467 | protected void putHeadersAndCookies(HttpHeaders headers) { 468 | for (Entry entry : headers) { 469 | this.headers.put(entry.getKey(), entry.getValue()); 470 | } 471 | 472 | // Cookie 473 | final String cookieString = this.headers.get(HttpHeaderNames.COOKIE.toString()); 474 | if (StrUtil.isNotBlank(cookieString)) { 475 | final Set cookies = ServerCookieDecoder.LAX.decode(cookieString); 476 | for (Cookie cookie : cookies) { 477 | this.cookies.put(cookie.name(), cookie); 478 | } 479 | } 480 | } 481 | 482 | /** 483 | * 设置客户端IP 484 | * 485 | * @param ctx ChannelHandlerContext 486 | */ 487 | protected void putIp(ChannelHandlerContext ctx) { 488 | String ip = getHeader("X-Forwarded-For"); 489 | if (StrUtil.isNotBlank(ip)) { 490 | ip = NetUtil.getMultistageReverseProxyIp(ip); 491 | } else { 492 | final InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress(); 493 | ip = insocket.getAddress().getHostAddress(); 494 | } 495 | this.ip = ip; 496 | } 497 | // --------------------------------------------------------- Protected method end 498 | 499 | @Override 500 | public String toString() { 501 | final StringBuilder sb = new StringBuilder(); 502 | sb.append("\r\nprotocolVersion: ").append(getProtocolVersion()).append("\r\n"); 503 | sb.append("uri: ").append(getUri()).append("\r\n"); 504 | sb.append("path: ").append(path).append("\r\n"); 505 | sb.append("method: ").append(getMethod()).append("\r\n"); 506 | sb.append("ip: ").append(ip).append("\r\n"); 507 | sb.append("headers:\r\n "); 508 | for (Entry entry : headers.entrySet()) { 509 | sb.append(" ").append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n"); 510 | } 511 | sb.append("params: \r\n"); 512 | for (Entry entry : params.entrySet()) { 513 | sb.append(" ").append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n"); 514 | } 515 | 516 | return sb.toString(); 517 | } 518 | 519 | /** 520 | * 构建Request对象 521 | * 522 | * @param ctx ChannelHandlerContext 523 | * @param nettyRequest Netty的HttpRequest 524 | * @return Request 525 | */ 526 | protected final static Request build(ChannelHandlerContext ctx, FullHttpRequest nettyRequest) { 527 | return new Request(ctx, nettyRequest); 528 | } 529 | } 530 | -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/handler/Response.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.handler; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | import java.nio.charset.Charset; 7 | import java.text.SimpleDateFormat; 8 | import java.util.Calendar; 9 | import java.util.GregorianCalendar; 10 | import java.util.HashSet; 11 | import java.util.Locale; 12 | import java.util.Map.Entry; 13 | import java.util.Set; 14 | import java.util.TimeZone; 15 | 16 | import com.xiaoleilu.loServer.ServerSetting; 17 | import com.xiaoleilu.loServer.listener.FileProgressiveFutureListener; 18 | 19 | import cn.hutool.core.date.DatePattern; 20 | import cn.hutool.core.date.DateUtil; 21 | import cn.hutool.core.util.CharsetUtil; 22 | import cn.hutool.core.util.StrUtil; 23 | import cn.hutool.http.HttpUtil; 24 | import cn.hutool.log.Log; 25 | import cn.hutool.log.StaticLog; 26 | import io.netty.buffer.ByteBuf; 27 | import io.netty.buffer.Unpooled; 28 | import io.netty.channel.ChannelFuture; 29 | import io.netty.channel.ChannelFutureListener; 30 | import io.netty.channel.ChannelHandlerContext; 31 | import io.netty.channel.DefaultFileRegion; 32 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 33 | import io.netty.handler.codec.http.DefaultHttpHeaders; 34 | import io.netty.handler.codec.http.DefaultHttpResponse; 35 | import io.netty.handler.codec.http.FullHttpResponse; 36 | import io.netty.handler.codec.http.HttpHeaderNames; 37 | import io.netty.handler.codec.http.HttpHeaderValues; 38 | import io.netty.handler.codec.http.HttpHeaders; 39 | import io.netty.handler.codec.http.HttpResponseStatus; 40 | import io.netty.handler.codec.http.HttpVersion; 41 | import io.netty.handler.codec.http.LastHttpContent; 42 | import io.netty.handler.codec.http.cookie.Cookie; 43 | import io.netty.handler.codec.http.cookie.DefaultCookie; 44 | import io.netty.handler.codec.http.cookie.ServerCookieEncoder; 45 | 46 | /** 47 | * 响应对象 48 | * 49 | * @author Looly 50 | * 51 | */ 52 | public class Response { 53 | private static final Log log = StaticLog.get(); 54 | 55 | /** 返回内容类型:普通文本 */ 56 | public final static String CONTENT_TYPE_TEXT = "text/plain"; 57 | /** 返回内容类型:HTML */ 58 | public final static String CONTENT_TYPE_HTML = "text/html"; 59 | /** 返回内容类型:XML */ 60 | public final static String CONTENT_TYPE_XML = "text/xml"; 61 | /** 返回内容类型:JAVASCRIPT */ 62 | public final static String CONTENT_TYPE_JAVASCRIPT = "application/javascript"; 63 | /** 返回内容类型:JSON */ 64 | public final static String CONTENT_TYPE_JSON = "application/json"; 65 | public final static String CONTENT_TYPE_JSON_IE = "text/json"; 66 | 67 | private ChannelHandlerContext ctx; 68 | private Request request; 69 | 70 | private HttpVersion httpVersion = HttpVersion.HTTP_1_1; 71 | private HttpResponseStatus status = HttpResponseStatus.OK; 72 | private String contentType = CONTENT_TYPE_HTML; 73 | private String charset = ServerSetting.getCharset(); 74 | private HttpHeaders headers = new DefaultHttpHeaders(); 75 | private Set cookies = new HashSet(); 76 | private Object content = Unpooled.EMPTY_BUFFER; 77 | // 发送完成标记 78 | private boolean isSent; 79 | 80 | public Response(ChannelHandlerContext ctx, Request request) { 81 | this.ctx = ctx; 82 | this.request = request; 83 | } 84 | 85 | /** 86 | * 设置响应的Http版本号 87 | * 88 | * @param httpVersion http版本号对象 89 | * @return 自己 90 | */ 91 | public Response setHttpVersion(HttpVersion httpVersion) { 92 | this.httpVersion = httpVersion; 93 | return this; 94 | } 95 | 96 | /** 97 | * 响应状态码
98 | * 使用io.netty.handler.codec.http.HttpResponseStatus对象 99 | * 100 | * @param status 状态码 101 | * @return 自己 102 | */ 103 | public Response setStatus(HttpResponseStatus status) { 104 | this.status = status; 105 | return this; 106 | } 107 | 108 | /** 109 | * 响应状态码 110 | * 111 | * @param status 状态码 112 | * @return 自己 113 | */ 114 | public Response setStatus(int status) { 115 | return setStatus(HttpResponseStatus.valueOf(status)); 116 | } 117 | 118 | /** 119 | * 设置Content-Type 120 | * 121 | * @param contentType Content-Type 122 | * @return 自己 123 | */ 124 | public Response setContentType(String contentType) { 125 | this.contentType = contentType; 126 | return this; 127 | } 128 | 129 | /** 130 | * 设置返回内容的字符集编码 131 | * 132 | * @param charset 编码 133 | * @return 自己 134 | */ 135 | public Response setCharset(String charset) { 136 | this.charset = charset; 137 | return this; 138 | } 139 | 140 | /** 141 | * 增加响应的Header
142 | * 重复的Header将被叠加 143 | * 144 | * @param name 名 145 | * @param value 值,可以是String,Date, int 146 | * @return 自己 147 | */ 148 | public Response addHeader(String name, Object value) { 149 | headers.add(name, value); 150 | return this; 151 | } 152 | 153 | /** 154 | * 设置响应的Header
155 | * 重复的Header将被替换 156 | * 157 | * @param name 名 158 | * @param value 值,可以是String,Date, int 159 | * @return 自己 160 | */ 161 | public Response setHeader(String name, Object value) { 162 | headers.set(name, value); 163 | return this; 164 | } 165 | 166 | /** 167 | * 设置响应体长度 168 | * 169 | * @param contentLength 响应体长度 170 | * @return 自己 171 | */ 172 | public Response setContentLength(long contentLength) { 173 | setHeader(HttpHeaderNames.CONTENT_LENGTH.toString(), contentLength); 174 | return this; 175 | } 176 | 177 | /** 178 | * 设置是否长连接 179 | * 180 | * @return 自己 181 | */ 182 | public Response setKeepAlive() { 183 | setHeader(HttpHeaderNames.CONNECTION.toString(), HttpHeaderValues.KEEP_ALIVE.toString()); 184 | return this; 185 | } 186 | 187 | // --------------------------------------------------------- Cookie start 188 | /** 189 | * 设定返回给客户端的Cookie 190 | * 191 | * @param cookie 192 | * @return 自己 193 | */ 194 | public Response addCookie(Cookie cookie) { 195 | cookies.add(cookie); 196 | return this; 197 | } 198 | 199 | /** 200 | * 设定返回给客户端的Cookie 201 | * 202 | * @param name Cookie名 203 | * @param value Cookie值 204 | * @return 自己 205 | */ 206 | public Response addCookie(String name, String value) { 207 | return addCookie(new DefaultCookie(name, value)); 208 | } 209 | 210 | /** 211 | * 设定返回给客户端的Cookie 212 | * 213 | * @param name cookie名 214 | * @param value cookie值 215 | * @param maxAgeInSeconds -1: 关闭浏览器清除Cookie. 0: 立即清除Cookie. n>0 : Cookie存在的秒数. 216 | * @param path Cookie的有效路径 217 | * @param domain the Cookie可见的域,依据 RFC 2109 标准 218 | * @return 自己 219 | */ 220 | public Response addCookie(String name, String value, int maxAgeInSeconds, String path, String domain) { 221 | Cookie cookie = new DefaultCookie(name, value); 222 | if (domain != null) { 223 | cookie.setDomain(domain); 224 | } 225 | cookie.setMaxAge(maxAgeInSeconds); 226 | cookie.setPath(path); 227 | return addCookie(cookie); 228 | } 229 | 230 | /** 231 | * 设定返回给客户端的Cookie
232 | * Path: "/"
233 | * No Domain 234 | * 235 | * @param name cookie名 236 | * @param value cookie值 237 | * @param maxAgeInSeconds -1: 关闭浏览器清除Cookie. 0: 立即清除Cookie. n>0 : Cookie存在的秒数. 238 | * @return 自己 239 | */ 240 | public Response addCookie(String name, String value, int maxAgeInSeconds) { 241 | return addCookie(name, value, maxAgeInSeconds, "/", null); 242 | } 243 | // --------------------------------------------------------- Cookie end 244 | 245 | /** 246 | * 设置响应HTML文本内容 247 | * 248 | * @param contentText 响应的文本 249 | * @return 自己 250 | */ 251 | public Response setContent(String contentText) { 252 | this.content = Unpooled.copiedBuffer(contentText, Charset.forName(charset)); 253 | return this; 254 | } 255 | 256 | /** 257 | * 设置响应文本内容 258 | * 259 | * @param contentText 响应的文本 260 | * @return 自己 261 | */ 262 | public Response setTextContent(String contentText) { 263 | setContentType(CONTENT_TYPE_TEXT); 264 | return setContent(contentText); 265 | } 266 | 267 | /** 268 | * 设置响应JSON文本内容 269 | * 270 | * @param contentText 响应的JSON文本 271 | * @return 自己 272 | */ 273 | public Response setJsonContent(String contentText) { 274 | setContentType(request.isIE() ? CONTENT_TYPE_JSON : CONTENT_TYPE_JSON); 275 | return setContent(contentText); 276 | } 277 | 278 | /** 279 | * 设置响应XML文本内容 280 | * 281 | * @param contentText 响应的XML文本 282 | * @return 自己 283 | */ 284 | public Response setXmlContent(String contentText) { 285 | setContentType(CONTENT_TYPE_XML); 286 | return setContent(contentText); 287 | } 288 | 289 | /** 290 | * 设置响应文本内容 291 | * 292 | * @param contentBytes 响应的字节 293 | * @return 自己 294 | */ 295 | public Response setContent(byte[] contentBytes) { 296 | return setContent(Unpooled.copiedBuffer(contentBytes)); 297 | } 298 | 299 | /** 300 | * 设置响应文本内容 301 | * 302 | * @param byteBuf 响应的字节 303 | * @return 自己 304 | */ 305 | public Response setContent(ByteBuf byteBuf) { 306 | this.content = byteBuf; 307 | return this; 308 | } 309 | 310 | /** 311 | * 设置响应到客户端的文件 312 | * 313 | * @param file 文件 314 | * @return 自己 315 | */ 316 | public Response setContent(File file) { 317 | this.content = file; 318 | return this; 319 | } 320 | 321 | /** 322 | * Sets the Date and Cache headers for the HTTP Response 323 | * 324 | * @param response HTTP response 325 | * @param fileToCache file to extract content type 326 | */ 327 | /** 328 | * 设置日期和过期时间 329 | * 330 | * @param lastModify 上一次修改时间 331 | * @param httpCacheSeconds 缓存时间,单位秒 332 | */ 333 | public void setDateAndCache(long lastModify, int httpCacheSeconds) { 334 | SimpleDateFormat formatter = new SimpleDateFormat(DatePattern.HTTP_DATETIME_PATTERN, Locale.US); 335 | formatter.setTimeZone(TimeZone.getTimeZone("GMT")); 336 | 337 | // Date header 338 | Calendar time = new GregorianCalendar(); 339 | setHeader(HttpHeaderNames.DATE.toString(), formatter.format(time.getTime())); 340 | 341 | // Add cache headers 342 | time.add(Calendar.SECOND, httpCacheSeconds); 343 | 344 | setHeader(HttpHeaderNames.EXPIRES.toString(), formatter.format(time.getTime())); 345 | setHeader(HttpHeaderNames.CACHE_CONTROL.toString(), "private, max-age=" + httpCacheSeconds); 346 | setHeader(HttpHeaderNames.LAST_MODIFIED.toString(), formatter.format(DateUtil.date(lastModify))); 347 | } 348 | 349 | // -------------------------------------------------------------------------------------- build HttpResponse start 350 | /** 351 | * 转换为Netty所用Response
352 | * 不包括content,一般用于返回文件类型的响应 353 | * 354 | * @return DefaultHttpResponse 355 | */ 356 | private DefaultHttpResponse toDefaultHttpResponse() { 357 | final DefaultHttpResponse defaultHttpResponse = new DefaultHttpResponse(httpVersion, status); 358 | 359 | fillHeadersAndCookies(defaultHttpResponse.headers()); 360 | 361 | return defaultHttpResponse; 362 | } 363 | 364 | /** 365 | * 转换为Netty所用Response
366 | * 用于返回一般类型响应(文本) 367 | * 368 | * @return FullHttpResponse 369 | */ 370 | private FullHttpResponse toFullHttpResponse() { 371 | final ByteBuf byteBuf = (ByteBuf) content; 372 | final FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(httpVersion, status, byteBuf); 373 | 374 | // headers 375 | final HttpHeaders httpHeaders = fullHttpResponse.headers(); 376 | fillHeadersAndCookies(httpHeaders); 377 | httpHeaders.set(HttpHeaderNames.CONTENT_LENGTH.toString(), byteBuf.readableBytes()); 378 | 379 | return fullHttpResponse; 380 | } 381 | 382 | /** 383 | * 填充头信息和Cookie信息 384 | * 385 | * @param httpHeaders Http头 386 | */ 387 | private void fillHeadersAndCookies(HttpHeaders httpHeaders) { 388 | httpHeaders.set(HttpHeaderNames.CONTENT_TYPE.toString(), StrUtil.format("{};charset={}", contentType, charset)); 389 | httpHeaders.set(HttpHeaderNames.CONTENT_ENCODING.toString(), charset); 390 | 391 | // Cookies 392 | for (Cookie cookie : cookies) { 393 | httpHeaders.add(HttpHeaderNames.SET_COOKIE.toString(), ServerCookieEncoder.LAX.encode(cookie)); 394 | } 395 | } 396 | // -------------------------------------------------------------------------------------- build HttpResponse end 397 | 398 | // -------------------------------------------------------------------------------------- send start 399 | /** 400 | * 发送响应到客户端
401 | * 402 | * @return ChannelFuture 403 | * @throws IOException 404 | */ 405 | public ChannelFuture send() { 406 | ChannelFuture channelFuture; 407 | if (content instanceof File) { 408 | // 文件 409 | File file = (File) content; 410 | try { 411 | channelFuture = sendFile(file); 412 | } catch (IOException e) { 413 | log.error(StrUtil.format("Send {} error!", file), e); 414 | channelFuture = sendError(HttpResponseStatus.FORBIDDEN, ""); 415 | } 416 | } else { 417 | // 普通文本 418 | channelFuture = sendFull(); 419 | } 420 | 421 | this.isSent = true; 422 | return channelFuture; 423 | } 424 | 425 | /** 426 | * @return 是否已经出发发送请求,内部使用
427 | */ 428 | protected boolean isSent() { 429 | return this.isSent; 430 | } 431 | 432 | /** 433 | * 发送响应到客户端 434 | * 435 | * @return ChannelFuture 436 | */ 437 | private ChannelFuture sendFull() { 438 | if (request != null && request.isKeepAlive()) { 439 | setKeepAlive(); 440 | return ctx.writeAndFlush(this.toFullHttpResponse()); 441 | } else { 442 | return sendAndCloseFull(); 443 | } 444 | } 445 | 446 | /** 447 | * 发送给到客户端并关闭ChannelHandlerContext 448 | * 449 | * @return ChannelFuture 450 | */ 451 | private ChannelFuture sendAndCloseFull() { 452 | return ctx.writeAndFlush(this.toFullHttpResponse()).addListener(ChannelFutureListener.CLOSE); 453 | } 454 | 455 | /** 456 | * 发送文件 457 | * 458 | * @param file 文件 459 | * @return ChannelFuture 460 | * @throws IOException 461 | */ 462 | private ChannelFuture sendFile(File file) throws IOException { 463 | final RandomAccessFile raf = new RandomAccessFile(file, "r"); 464 | 465 | // 内容长度 466 | long fileLength = raf.length(); 467 | this.setContentLength(fileLength); 468 | 469 | // 文件类型 470 | String contentType = HttpUtil.getMimeType(file.getName()); 471 | if (StrUtil.isBlank(contentType)) { 472 | // 无法识别默认使用数据流 473 | contentType = "application/octet-stream"; 474 | } 475 | this.setContentType(contentType); 476 | 477 | ctx.write(this.toDefaultHttpResponse()); 478 | ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise()).addListener(FileProgressiveFutureListener.build(raf)); 479 | 480 | return sendEmptyLast(); 481 | } 482 | 483 | /** 484 | * 发送结尾标记,表示发送结束 485 | * 486 | * @return ChannelFuture 487 | */ 488 | private ChannelFuture sendEmptyLast() { 489 | final ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); 490 | if (false == request.isKeepAlive()) { 491 | lastContentFuture.addListener(ChannelFutureListener.CLOSE); 492 | } 493 | 494 | return lastContentFuture; 495 | } 496 | // -------------------------------------------------------------------------------------- send end 497 | 498 | // ---------------------------------------------------------------------------- special response start 499 | 500 | /** 501 | * 302 重定向 502 | * 503 | * @param uri 重定向到的URI 504 | * @return ChannelFuture 505 | */ 506 | public ChannelFuture sendRedirect(String uri) { 507 | return this.setStatus(HttpResponseStatus.FOUND).setHeader(HttpHeaderNames.LOCATION.toString(), uri).send(); 508 | } 509 | 510 | /** 511 | * 304 文件未修改 512 | * 513 | * @return ChannelFuture 514 | */ 515 | public ChannelFuture sendNotModified() { 516 | return this.setStatus(HttpResponseStatus.NOT_MODIFIED).setHeader(HttpHeaderNames.DATE.toString(), DateUtil.formatHttpDate(DateUtil.date())).send(); 517 | } 518 | 519 | /** 520 | * 发送错误消息 521 | * 522 | * @param status 错误状态码 523 | * @param msg 消息内容 524 | * @return ChannelFuture 525 | */ 526 | public ChannelFuture sendError(HttpResponseStatus status, String msg) { 527 | if (ctx.channel().isActive()) { 528 | return this.setStatus(status).setContent(msg).send(); 529 | } 530 | return null; 531 | } 532 | 533 | /** 534 | * 发送404 Not Found 535 | * 536 | * @param msg 消息内容 537 | * @return ChannelFuture 538 | */ 539 | public ChannelFuture sendNotFound(String msg) { 540 | return sendError(HttpResponseStatus.NOT_FOUND, msg); 541 | } 542 | 543 | /** 544 | * 发送500 Internal Server Error 545 | * 546 | * @param msg 消息内容 547 | * @return ChannelFuture 548 | */ 549 | public ChannelFuture sendServerError(String msg) { 550 | return sendError(HttpResponseStatus.INTERNAL_SERVER_ERROR, msg); 551 | } 552 | 553 | // ---------------------------------------------------------------------------- special response end 554 | 555 | @Override 556 | public String toString() { 557 | final StringBuilder sb = new StringBuilder(); 558 | sb.append("headers:\r\n "); 559 | for (Entry entry : headers.entries()) { 560 | sb.append(" ").append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n"); 561 | } 562 | sb.append("content: ").append(StrUtil.str(content, CharsetUtil.UTF_8)); 563 | 564 | return sb.toString(); 565 | } 566 | 567 | // ---------------------------------------------------------------------------- static method start 568 | /** 569 | * 构建Response对象 570 | * 571 | * @param ctx ChannelHandlerContext 572 | * @param request 请求对象 573 | * @return Response对象 574 | */ 575 | protected static Response build(ChannelHandlerContext ctx, Request request) { 576 | return new Response(ctx, request); 577 | } 578 | 579 | /** 580 | * 构建Response对象,Request对象为空,将无法获得某些信息
581 | * 1. 无法使用长连接 582 | * 583 | * @param ctx ChannelHandlerContext 584 | * @return Response对象 585 | */ 586 | protected static Response build(ChannelHandlerContext ctx) { 587 | return new Response(ctx, null); 588 | } 589 | // ---------------------------------------------------------------------------- static method end 590 | } 591 | -------------------------------------------------------------------------------- /src/main/java/com/xiaoleilu/loServer/listener/FileProgressiveFutureListener.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.listener; 2 | 3 | import java.io.RandomAccessFile; 4 | 5 | import cn.hutool.core.io.IoUtil; 6 | import cn.hutool.log.Log; 7 | import cn.hutool.log.LogFactory; 8 | import io.netty.channel.ChannelProgressiveFuture; 9 | import io.netty.channel.ChannelProgressiveFutureListener; 10 | 11 | /** 12 | * 文件进度指示监听 13 | * 14 | * @author Looly 15 | * 16 | */ 17 | public class FileProgressiveFutureListener implements ChannelProgressiveFutureListener { 18 | private static final Log log = LogFactory.get(); 19 | 20 | private RandomAccessFile raf; 21 | 22 | public FileProgressiveFutureListener(RandomAccessFile raf) { 23 | this.raf = raf; 24 | } 25 | 26 | @Override 27 | public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) { 28 | log.debug("Transfer progress: {} / {}", progress, total); 29 | } 30 | 31 | @Override 32 | public void operationComplete(ChannelProgressiveFuture future) { 33 | IoUtil.close(raf); 34 | log.debug("Transfer complete."); 35 | } 36 | 37 | /** 38 | * 构建文件进度指示监听 39 | * 40 | * @param raf RandomAccessFile 41 | * @return 文件进度指示监听 42 | */ 43 | public static FileProgressiveFutureListener build(RandomAccessFile raf) { 44 | return new FileProgressiveFutureListener(raf); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/xiaoleilu/loServer/example/ExampleAction.java: -------------------------------------------------------------------------------- 1 | package com.xiaoleilu.loServer.example; 2 | 3 | import com.xiaoleilu.loServer.LoServer; 4 | import com.xiaoleilu.loServer.ServerSetting; 5 | import com.xiaoleilu.loServer.action.Action; 6 | import com.xiaoleilu.loServer.handler.Request; 7 | import com.xiaoleilu.loServer.handler.Response; 8 | 9 | /** 10 | * loServer样例程序
11 | * Action对象用于处理业务流程,类似于Servlet对象
12 | * 在启动服务器前必须将path和此Action加入到ServerSetting的ActionMap中
13 | * 使用ServerSetting.setPort方法设置监听端口,此处设置为8090(如果不设置则使用默认的8090端口) 14 | * 然后调用LoServer.start()启动服务
15 | * 在浏览器中访问http://localhost:8090/example?a=b既可在页面上显示response a: b 16 | * @author Looly 17 | * 18 | */ 19 | public class ExampleAction implements Action{ 20 | 21 | @Override 22 | public void doAction(Request request, Response response) { 23 | String a = request.getParam("a"); 24 | response.setContent("response a: " + a); 25 | throw new RuntimeException("Test"); 26 | } 27 | 28 | public static void main(String[] args) { 29 | ServerSetting.setAction("/example", ExampleAction.class); 30 | ServerSetting.setRoot("root"); 31 | ServerSetting.setPort(8090); 32 | LoServer.start(); 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ${format} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------