├── .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 extends Filter> 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 extends Action> 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 extends Action> 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 reportHTTP 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 |
--------------------------------------------------------------------------------