├── README.md
├── SUMMARY.md
├── _config.yml
├── assets
├── TestServer.jpg
├── TestServerAcceptRequest.jpg
├── UML.jpg
├── file-server.jpg
├── git-br-step2.jpg
├── git-br-step3.jpg
├── git-br-step4.jpg
├── git-br-step5.jpg
├── git-br-step6.jpg
├── git-br-step7.jpg
├── html.png
├── http-interface-diagrams.jpg
├── nio-echo-server.jpg
├── project-structure.jpg
├── test-echo.jpg
├── unit-test-never-stop.jpg
├── web-dirs.jpg
├── web.jpg
└── web_dir.png
├── chapter1.md
├── connectionconnectionreaderconnectionwriterjie-kou.md
├── connectorjie-kou.md
├── eventhandlerjie-kou-hefileeventhandler-shi-xian.md
├── eventlistenerjie-kou.md
├── http-getjing-tai-zi-yuan-qing-qiu-gong-neng-bian-xie.md
├── http-parser-flow-request-line-parse.md
├── http11xie-yi-jie-kou.md
├── jian-ting-duan-kou-jie-shou-qing-qiu.md
├── jie-shou-http-body.md
├── nioconnector.md
├── serverjie-kou.md
└── shi-xian-zui-ji-ben-de-servlet-zhi-chi.md
/README.md:
--------------------------------------------------------------------------------
1 | # 自己动手写servlet容器
2 |
3 | 一步一步从无到有写一个servlet容器。
4 |
5 | 一开始不会涉及复杂的部分,中间会进行多次重构,直到完成复杂的功能。
6 |
7 | # 目录
8 |
9 | * [简介](README.md)
10 | * [开发环境搭建](chapter1.md)
11 | * [Server接口](serverjie-kou.md)
12 | * [监听端口接收请求](jian-ting-duan-kou-jie-shou-qing-qiu.md)
13 | * [Connector接口](connectorjie-kou.md)
14 | * [EventListener接口](eventlistenerjie-kou.md)
15 | * [EventHandler接口和FileEventHandler实现](eventhandlerjie-kou-hefileeventhandler-shi-xian.md)
16 | * [NIOConnector](nioconnector.md)
17 | * [Connection接口](connectionconnectionreaderconnectionwriterjie-kou.md)
18 | * [HTTP1.1协议接口](http11xie-yi-jie-kou.md)
19 | * [HTTP请求parse流程、RequestLineParser、HttpQueryParameterParser](http-parser-flow-request-line-parse.md)
20 |
21 |
22 |
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | * [简介](README.md)
4 | * [开发环境搭建](chapter1.md)
5 | * [Server接口](serverjie-kou.md)
6 | * [监听端口接收请求](jian-ting-duan-kou-jie-shou-qing-qiu.md)
7 | * [Connector接口](connectorjie-kou.md)
8 | * [EventListener接口](eventlistenerjie-kou.md)
9 | * [EventHandler接口和FileEventHandler实现](eventhandlerjie-kou-hefileeventhandler-shi-xian.md)
10 | * [NIOConnector](nioconnector.md)
11 | * [Connection接口](connectionconnectionreaderconnectionwriterjie-kou.md)
12 | * [HTTP1.1协议接口](http11xie-yi-jie-kou.md)
13 | * [HTTP请求parse流程、RequestLineParser、HttpQueryParameterParser](http-parser-flow-request-line-parse.md)
14 | * [HTTP Get静态资源请求功能编写](http-getjing-tai-zi-yuan-qing-qiu-gong-neng-bian-xie.md)
15 | * [接收HTTP Body](jie-shou-http-body.md)
16 | * [实现最基本的Servlet支持](shi-xian-zui-ji-ben-de-servlet-zhi-chi.md)
17 |
18 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/assets/TestServer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/TestServer.jpg
--------------------------------------------------------------------------------
/assets/TestServerAcceptRequest.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/TestServerAcceptRequest.jpg
--------------------------------------------------------------------------------
/assets/UML.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/UML.jpg
--------------------------------------------------------------------------------
/assets/file-server.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/file-server.jpg
--------------------------------------------------------------------------------
/assets/git-br-step2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/git-br-step2.jpg
--------------------------------------------------------------------------------
/assets/git-br-step3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/git-br-step3.jpg
--------------------------------------------------------------------------------
/assets/git-br-step4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/git-br-step4.jpg
--------------------------------------------------------------------------------
/assets/git-br-step5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/git-br-step5.jpg
--------------------------------------------------------------------------------
/assets/git-br-step6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/git-br-step6.jpg
--------------------------------------------------------------------------------
/assets/git-br-step7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/git-br-step7.jpg
--------------------------------------------------------------------------------
/assets/html.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/html.png
--------------------------------------------------------------------------------
/assets/http-interface-diagrams.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/http-interface-diagrams.jpg
--------------------------------------------------------------------------------
/assets/nio-echo-server.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/nio-echo-server.jpg
--------------------------------------------------------------------------------
/assets/project-structure.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/project-structure.jpg
--------------------------------------------------------------------------------
/assets/test-echo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/test-echo.jpg
--------------------------------------------------------------------------------
/assets/unit-test-never-stop.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/unit-test-never-stop.jpg
--------------------------------------------------------------------------------
/assets/web-dirs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/web-dirs.jpg
--------------------------------------------------------------------------------
/assets/web.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/web.jpg
--------------------------------------------------------------------------------
/assets/web_dir.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pkpk1234/BeggarServletContainer-doc/a00c77c4328de1dd7945575f32d5b8f647a57047/assets/web_dir.png
--------------------------------------------------------------------------------
/chapter1.md:
--------------------------------------------------------------------------------
1 | # 开发环境搭建
2 |
3 | 简单起见,一开始的服务器只会是一个工程,构建会也只是一个jar包。
4 |
5 | 开发环境就用最流行的java8、maven3,IDE可以随自己喜好。
6 |
7 | 新建maven工程,如下:
8 |
9 | 
10 |
11 | POM文件如下:
12 |
13 | ```xml
14 |
15 |
18 | 4.0.0
19 |
20 | com.ljm
21 | begger-server
22 | 1.0-SNAPSHOT
23 |
24 |
25 | 1.8
26 | ${java.version}
27 | ${java.version}
28 | UTF-8
29 |
30 |
31 |
32 |
33 | junit
34 | junit
35 | 4.12
36 | test
37 |
38 |
39 |
40 |
41 | ```
42 |
43 | 到此为止,开发环境搭建完毕。下一步即可开始编写server代码。
44 |
45 |
--------------------------------------------------------------------------------
/connectionconnectionreaderconnectionwriterjie-kou.md:
--------------------------------------------------------------------------------
1 | # Connection接口
2 |
3 | 继续抽象的过程,无论Socket还是SocketChannle,其实都可以抽象为一个表示通信连接的Connection接口。每当Connector监听到端口有请求时,即建立了一个Connection。
4 |
5 | NIO的接口和BIO的接口差别实在太大了,没办法只能加了一个不伦不类的ChannelConnection接口,肯定有更好的方案,但是以我现在的水平暂时只能这样设计下了。等以后看了Netty或者Undertow的源码再重构吧。
6 |
7 | 重构后UML大致如下:Server包含了1个或者多个Connector,Connector包含一个EventListener,一个EventListener包含一个EventHandler。
8 |
9 | 每当Connector接受到请求时,就构造一个Connection,Connector将Connection传递给EventListener,EventListener再传递给EventHandler。EventHandler调用Connection获取请求数据,并写入响应数据。
10 |
11 | 之后如果需要加入Servlet的功能,则需要添加对于的EventHandler,再通过EventHandler将请求Dispatcher到相应的Servlet中,而服务器的其余部分基本不用修改。
12 |
13 | 面向对象的设计模式功力比较弱,先设计一个勉强能用的架构先。这样单线程Server的IO部分基本就搞好了。
14 |
15 | 完整代码:[https://github.com/pkpk1234/BeggarServletContainer/tree/step6](https://github.com/pkpk1234/BeggarServletContainer/tree/step6)
16 |
17 | 分支step6
18 |
19 | 
20 |
21 |
--------------------------------------------------------------------------------
/connectorjie-kou.md:
--------------------------------------------------------------------------------
1 | # Connector接口
2 |
3 | 上一步后,我们完成了一个可以接收Socket请求的服务器。这时大师又说话了,昨天周末看片去了,有个单元测试TestServer
4 |
5 | 没跑,你跑个看看,猜猜能跑过不。一跑果然不行啊,单元测试一直转圈,就不动。
6 |
7 | 
8 |
9 | 因为server.start\(\);会让当前线程无限循环,不断等待Socket请求,所以下面的单元测试方法根本不会走到断言那一步,也不会退出,所以大家都卡主了。
10 |
11 | ```java
12 | @Test
13 | public void testServerStart() throws IOException {
14 | server.start();
15 | assertTrue("服务器启动后,状态是STARTED", server.getStatus().equals(ServerStatus.STARTED));
16 | }
17 | ```
18 |
19 | 修改起来很简单,让server.start\(\);在单独的线程里面执行就好,然后再循环判断ServerStatus是否为STARTED,等待服务器启动。
20 |
21 | 如下:
22 |
23 | ```java
24 | @Test
25 | public void testServerStart() throws IOException {
26 | server.start();
27 | //如果server未启动,就sleep一下
28 | while (server.getStatus().equals(ServerStatus.STOPED)) {
29 | logger.info("等待server启动");
30 | try {
31 | Thread.sleep(500);
32 | } catch (InterruptedException e) {
33 | logger.error(e.getMessage(), e);
34 | }
35 | }
36 | assertTrue("服务器启动后,状态是STARTED", server.getStatus().equals(ServerStatus.STARTED));
37 | }
38 | ```
39 |
40 | 这时大师又说了,循环判断服务器是否启动的代码片段,和TestServerAcceptRequest里面有重复代码,启动Server的代码也是重复的,一看就是Ctrl+c Ctrl+v的,你就不会抽象出一个父类啊。再重构:
41 |
42 | ```java
43 | public abstract class TestServerBase {
44 | private static Logger logger = LoggerFactory.getLogger(TestServerBase.class);
45 |
46 | /**
47 | * 在单独的线程中启动Server,如果启动不成功,抛出异常
48 | *
49 | * @param server
50 | */
51 | protected void startServer(Server server) {
52 | //在另外一个线程中启动server
53 | new Thread(() -> {
54 | try {
55 | server.start();
56 | } catch (IOException e) {
57 | //转为RuntimeException抛出,避免异常丢失
58 | throw new RuntimeException(e);
59 | }
60 | }).start();
61 | }
62 |
63 | /**
64 | * 等待Server启动
65 | *
66 | * @param server
67 | */
68 | protected void waitServerStart(Server server) {
69 | //如果server未启动,就sleep一下
70 | while (server.getStatus().equals(ServerStatus.STOPED)) {
71 | logger.info("等待server启动");
72 | try {
73 | Thread.sleep(500);
74 | } catch (InterruptedException e) {
75 | logger.error(e.getMessage(), e);
76 | }
77 | }
78 | }
79 | }
80 | ```
81 |
82 | 和Server相关的单元测试都可以extends于TestServerBase。
83 |
84 | ```java
85 | public class TestServer extends TestServerBase {
86 | ... ...
87 | @Test
88 | public void testServerStart() {
89 | startServer(server);
90 | waitServerStart(server);
91 | assertTrue("服务器启动后,状态是STARTED", server.getStatus().equals(ServerStatus.STARTED));
92 | }
93 | ... ...
94 | }
95 | ```
96 |
97 | ```java
98 | public class TestServerAcceptRequest extends TestServerBase {
99 | ... ...
100 | @Test
101 | public void testServerAcceptRequest() {
102 | // 如果server没有启动,首先启动server
103 | if (server.getStatus().equals(ServerStatus.STOPED)) {
104 | startServer(server);
105 | waitServerStart(server);
106 | .... ...
107 | }
108 | ... ...
109 | }
110 | ```
111 |
112 | 再次执行单元测试,一切都OK。搞定单元测试后,大师又说了,看看你写的SimpleServer的start方法,
113 |
114 | SimpleServe当前就是用来监听并接收Socket请求的,start方法就应该如其名,只是启动监听,修改ServerStatus为STARTED,接受请求什么的和start方法有毛关系,弄出去。
115 |
116 | 按照大师说的重构一下,单独弄个accept方法,专门用于接受请求。
117 |
118 | ```java
119 | @Override
120 | public void start() throws IOException {
121 | //监听本地端口,如果监听不成功,抛出异常
122 | this.serverSocket = new ServerSocket(this.port);
123 | this.serverStatus = ServerStatus.STARTED;
124 | accept();
125 | return;
126 |
127 | }
128 |
129 | private void accept() {
130 | while (true) {
131 | Socket socket = null;
132 | try {
133 | socket = serverSocket.accept();
134 | logger.info("新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
135 | } catch (IOException e) {
136 | logger.error(e.getMessage(), e);
137 | } finally {
138 | IoUtils.closeQuietly(socket);
139 | }
140 | }
141 | }
142 | ```
143 |
144 | 这时大师又发话 了 ,我要用SSL,你直接new ServerSocket有啥用,重构去。
145 |
146 | 从start方法里面其实可以看到,Server启动接受\响应请求的组件后,组件的任何操作就和Server对象没一毛钱关系了,Server只是管理一下组件的生命周期而已。那么接受\响应请求的组件可以抽象出来,这样Server就不必和具体实现打交道了。
147 |
148 | 按照Tomcat和Jetty的惯例,接受\响应请求的组件叫Connector,生命周期也可以抽象成一个接口LifeCycle。根据这个思路去重构。
149 |
150 | ```java
151 | public interface LifeCycle {
152 |
153 | void start();
154 |
155 | void stop();
156 | }
157 | ```
158 |
159 | ```java
160 | public abstract class Connector implements LifeCycle {
161 | @Override
162 | public void start() {
163 | init();
164 | acceptConnect();
165 | }
166 |
167 | protected abstract void init() throws ConnectorException;
168 |
169 | protected abstract void acceptConnect() throws ConnectorException;
170 | }
171 | ```
172 |
173 | 将SimpleServer中和Socket相关的代码全部移动到SocketConnector里面
174 |
175 | ```java
176 | public class SocketConnector extends Connector {
177 | ... ...
178 | @Override
179 | protected void init() throws ConnectorException {
180 | //监听本地端口,如果监听不成功,抛出异常
181 | try {
182 | this.serverSocket = new ServerSocket(this.port);
183 | this.started = true;
184 | } catch (IOException e) {
185 | throw new ConnectorException(e);
186 | }
187 | }
188 | @Override
189 | protected void acceptConnect() throws ConnectorException {
190 | new Thread(() -> {
191 | while (true && started) {
192 | Socket socket = null;
193 | try {
194 | socket = serverSocket.accept();
195 | LOGGER.info("新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
196 | } catch (IOException e) {
197 | //单个Socket异常,不要影响整个Connector
198 | LOGGER.error(e.getMessage(), e);
199 | } finally {
200 | IoUtils.closeQuietly(socket);
201 | }
202 | }
203 | }).start();
204 | }
205 |
206 | @Override
207 | public void stop() {
208 | this.started = false;
209 | IoUtils.closeQuietly(this.serverSocket);
210 | }
211 | ... ...
212 |
213 | }
214 | ```
215 |
216 | SimpleServer重构为
217 |
218 | ```java
219 | public class SimpleServer implements Server {
220 | ... ...
221 | private SocketConnector socketConnector;
222 |
223 | ... ...
224 |
225 | @Override
226 | public void start() throws IOException {
227 | socketConnector.start();
228 | this.serverStatus = ServerStatus.STARTED;
229 | }
230 |
231 | @Override
232 | public void stop() {
233 | socketConnector.stop();
234 | this.serverStatus = ServerStatus.STOPED;
235 | logger.info("Server stop");
236 | }
237 | ... ...
238 | }
239 | ```
240 |
241 | 跑单元测试,全部OK,证明代码没问题。
242 |
243 | 大师瞄了一眼,说不 给你说了么,面向抽象编程啊,为毛还直接引用了SocketConnector,还有,我想要多个Connector,继续给我重构去。
244 |
245 | 重构思路简单,将SocketConnector替换为抽象类型Connector即可,但是怎么实例化呢,总有地方要处理这个抽象到具体的过程啊,这时又轮到Factory类干这个脏活了。
246 |
247 | 再次重构。
248 |
249 | 增加ConnectorFactory接口,及其实现SocketConnectorFactory
250 |
251 | ```java
252 | public class SocketConnectorFactory implements ConnectorFactory {
253 | private final SocketConnectorConfig socketConnectorConfig;
254 |
255 | public SocketConnectorFactory(SocketConnectorConfig socketConnectorConfig) {
256 | this.socketConnectorConfig = socketConnectorConfig;
257 | }
258 |
259 | @Override
260 | public Connector getConnector() {
261 | return new SocketConnector(this.socketConnectorConfig.getPort());
262 | }
263 | }
264 | ```
265 |
266 | SimpleServer也进行相应修改,不再实例化任何具体实现,只通过构造函数接收对应的抽象。
267 |
268 | ```java
269 | public class SimpleServer implements Server {
270 | private static Logger logger = LoggerFactory.getLogger(SimpleServer.class);
271 | private volatile ServerStatus serverStatus = ServerStatus.STOPED;
272 | private final int port;
273 | private final List connectorList;
274 |
275 | public SimpleServer(ServerConfig serverConfig, List connectorList) {
276 | this.port = serverConfig.getPort();
277 | this.connectorList = connectorList;
278 | }
279 |
280 | @Override
281 | public void start() {
282 | connectorList.stream().forEach(connector -> connector.start());
283 | this.serverStatus = ServerStatus.STARTED;
284 | }
285 |
286 | @Override
287 | public void stop() {
288 | connectorList.stream().forEach(connector -> connector.stop());
289 | this.serverStatus = ServerStatus.STOPED;
290 | logger.info("Server stop");
291 | }
292 | ... ...
293 |
294 | }
295 | ```
296 |
297 | ServerFactory也进行修改,将Server需要的依赖传递到Server的构造函数中。
298 |
299 | ```java
300 | public class ServerFactory {
301 | /**
302 | * 返回Server实例
303 | *
304 | * @return
305 | */
306 | public static Server getServer(ServerConfig serverConfig) {
307 | List connectorList = new ArrayList<>();
308 | ConnectorFactory connectorFactory =
309 | new SocketConnectorFactory(new SocketConnectorConfig(serverConfig.getPort()));
310 | connectorList.add(connectorFactory.getConnector());
311 | return new SimpleServer(serverConfig,connectorList);
312 | }
313 | }
314 | ```
315 |
316 | 这样我们就将对具体实现的依赖限制到了不多的几个Factory中,最核心的Server部分只操作了抽象。
317 |
318 | 执行所有单元测试,再次全部成功。
319 |
320 | 虽然目前为止,Server还是只能接收请求,但是代码结构还算OK,为下面编写请求处理做好了准备。
321 |
322 | 完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step3
323 |
324 | 分支step3
325 |
326 | 
327 |
328 |
329 |
330 |
--------------------------------------------------------------------------------
/eventhandlerjie-kou-hefileeventhandler-shi-xian.md:
--------------------------------------------------------------------------------
1 | # EventHandler接口和FileEventHandler实现
2 |
3 | 首先重构代码,让事件监听和事件处理分离开,各自责任更加独立。否则想将Echo功能替换为返回静态文件,又需要到处改代码。将责任分开后,只需要传入不同的事件处理器,即可实现不同效果。
4 |
5 | 增加EventHandler接口专门进行事件处理,SocketEventListener类中事件处理抽取到专门的EchoEventHandler实现中。
6 |
7 | 提前出AbstractEventListener类,规定了事件处理的模板
8 |
9 | ```java
10 | public abstract class AbstractEventListener implements EventListener {
11 | /**
12 | * 事件处理流程模板方法
13 | * @param event 事件对象
14 | * @throws EventException
15 | */
16 | @Override
17 | public void onEvent(T event) throws EventException {
18 | EventHandler eventHandler = getEventHandler(event);
19 | eventHandler.handle(event);
20 | }
21 |
22 | /**
23 | * 返回事件处理器
24 | * @param event
25 | * @return
26 | */
27 | protected abstract EventHandler getEventHandler(T event);
28 | }
29 | ```
30 |
31 | SocketEventListener重构为通过构造器传入事件处理器
32 |
33 | ```java
34 | public class SocketEventListener extends AbstractEventListener {
35 |
36 | private final EventHandler eventHandler;
37 |
38 | public SocketEventListener(EventHandler eventHandler) {
39 | this.eventHandler = eventHandler;
40 | }
41 |
42 | @Override
43 | protected EventHandler getEventHandler(Socket event) {
44 | return eventHandler;
45 | }
46 | }
47 | ```
48 |
49 | EchoEventHandler实现Echo
50 |
51 | ```java
52 | public class EchoEventHandler extends AbstractEventHandler {
53 |
54 | @Override
55 | protected void doHandle(Socket socket) {
56 | InputStream inputstream = null;
57 | OutputStream outputStream = null;
58 | try {
59 | inputstream = socket.getInputStream();
60 | outputStream = socket.getOutputStream();
61 | Scanner scanner = new Scanner(inputstream);
62 | PrintWriter printWriter = new PrintWriter(outputStream);
63 | printWriter.append("Server connected.Welcome to echo.\n");
64 | printWriter.flush();
65 | while (scanner.hasNextLine()) {
66 | String line = scanner.nextLine();
67 | if (line.equals("stop")) {
68 | printWriter.append("bye bye.\n");
69 | printWriter.flush();
70 | break;
71 | } else {
72 | printWriter.append(line);
73 | printWriter.append("\n");
74 | printWriter.flush();
75 | }
76 | }
77 | } catch (IOException e) {
78 | throw new HandlerException(e);
79 | } finally {
80 | IoUtils.closeQuietly(inputstream);
81 | IoUtils.closeQuietly(outputStream);
82 | }
83 | }
84 |
85 | }
86 | ```
87 |
88 | 再次将对具体实现的依赖限制到Factory中
89 |
90 | ```java
91 | public class ServerFactory {
92 | /**
93 | * 返回Server实例
94 | *
95 | * @return
96 | */
97 | public static Server getServer(ServerConfig serverConfig) {
98 | List connectorList = new ArrayList<>();
99 | //传入Echo事件处理器
100 | SocketEventListener socketEventListener = new SocketEventListener(new EchoEventHandler());
101 | ConnectorFactory connectorFactory =
102 | new SocketConnectorFactory(new SocketConnectorConfig(serverConfig.getPort()), socketEventListener);
103 | connectorList.add(connectorFactory.getConnector());
104 | return new SimpleServer(serverConfig, connectorList);
105 | }
106 | }
107 | ```
108 |
109 | 执行单元测试,一切正常。运行Server,用telnet进行echo,也是正常的。
110 |
111 | 现在添加返回静态文件功能。功能大致如下:
112 |
113 | 1. 服务器使用user.dir作为根目录。
114 | 2. 控制台输入文件路径,如果文件是目录,则打印目录中的文件列表;如果文件不是目录,且可读,则返回文件内容;如果不满足前面两种场景,返回文件找不到
115 |
116 | 新增FileEventHandler
117 |
118 | ```java
119 | public class FileEventHandler extends AbstractEventHandler {
120 |
121 | private final String docBase;
122 |
123 | public FileEventHandler(String docBase) {
124 | this.docBase = docBase;
125 | }
126 |
127 | @Override
128 | protected void doHandle(Socket socket) {
129 | getFile(socket);
130 | }
131 |
132 | /**
133 | * 返回文件
134 | *
135 | * @param socket
136 | */
137 | private void getFile(Socket socket) {
138 | InputStream inputstream = null;
139 | OutputStream outputStream = null;
140 | try {
141 | inputstream = socket.getInputStream();
142 | outputStream = socket.getOutputStream();
143 | Scanner scanner = new Scanner(inputstream, "UTF-8");
144 | PrintWriter printWriter = new PrintWriter(outputStream);
145 | printWriter.append("Server connected.Welcome to File Server.\n");
146 | printWriter.flush();
147 | while (scanner.hasNextLine()) {
148 | String line = scanner.nextLine();
149 | if (line.equals("stop")) {
150 | printWriter.append("bye bye.\n");
151 | printWriter.flush();
152 | break;
153 | } else {
154 | Path filePath = Paths.get(this.docBase, line);
155 | //如果是目录,就打印文件列表
156 | if (Files.isDirectory(filePath)) {
157 | printWriter.append("目录 ").append(filePath.toString())
158 | .append(" 下有文件:").append("\n");
159 | Files.list(filePath).forEach(fileName -> {
160 | printWriter.append(fileName.getFileName().toString())
161 | .append("\n").flush();
162 | });
163 | //如果文件可读,就打印文件内容
164 | } else if (Files.isReadable(filePath)) {
165 | printWriter.append("File ").append(filePath.toString())
166 | .append(" 的内容是:").append("\n").flush();
167 | Files.copy(filePath, outputStream);
168 | printWriter.append("\n");
169 | //其他情况返回文件找不到
170 | } else {
171 | printWriter.append("File ").append(filePath.toString())
172 | .append(" is not found.").append("\n").flush();
173 | }
174 |
175 | }
176 | }
177 | } catch (IOException e) {
178 | throw new HandlerException(e);
179 | } finally {
180 | IoUtils.closeQuietly(inputstream);
181 | IoUtils.closeQuietly(outputStream);
182 | }
183 | }
184 | }
185 | ```
186 |
187 | 修改ServerFactory,使用FileEventHandler
188 |
189 | ```java
190 | public class ServerFactory {
191 | /**
192 | * 返回Server实例
193 | *
194 | * @return
195 | */
196 | public static Server getServer(ServerConfig serverConfig) {
197 | List connectorList = new ArrayList<>();
198 | //使用FileEventHandler
199 | SocketEventListener socketEventListener =
200 | new SocketEventListener(new FileEventHandler(System.getProperty("user.dir")));
201 | ConnectorFactory connectorFactory =
202 | new SocketConnectorFactory(new SocketConnectorConfig(serverConfig.getPort()), socketEventListener);
203 | connectorList.add(connectorFactory.getConnector());
204 | return new SimpleServer(serverConfig, connectorList);
205 | }
206 | }
207 | ```
208 |
209 | 运行BootStrap启动Server进行验证:
210 |
211 | 
212 |
213 | 绿色框:输入回车,返回目录下文件列表。
214 |
215 | 黄色框:输入R EADME.MD,返回文件内容
216 |
217 | 蓝色框:输入不存在的文件,返回文件找不到。
218 |
219 | 完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step5
220 |
221 | 分支step5
222 |
223 | 
224 |
225 |
--------------------------------------------------------------------------------
/eventlistenerjie-kou.md:
--------------------------------------------------------------------------------
1 | # EventListener接口
2 |
3 | 让我们继续看SocketConnector中的acceptConnect方法:
4 |
5 | ```java
6 | @Override
7 | protected void acceptConnect() throws ConnectorException {
8 | new Thread(() -> {
9 | while (true && started) {
10 | Socket socket = null;
11 | try {
12 | socket = serverSocket.accept();
13 | LOGGER.info("新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
14 | } catch (IOException e) {
15 | //单个Socket异常,不要影响整个Connector
16 | LOGGER.error(e.getMessage(), e);
17 | } finally {
18 | IoUtils.closeQuietly(socket);
19 | }
20 | }
21 | }).start();
22 | }
23 | ```
24 |
25 | 注意socket = serverSocket.accept\(\),这里获取到socket之后只是打印日志,并没获取socket的输入输出进行操作。
26 |
27 | 操作socket的输入和输出是否应该在SocketConnector中?这时大师又说话了,Connector责任是啥,就是管理connect的啊,connect怎么使用,关它屁事。再看那个无限循环,像不像再等待事件来临啊,成功accept一个socket就是一个事件,对scoket的使用,其实就是事件响应嘛。
28 |
29 | OK,让我们按照这个思路来重构一下,目的就是加入事件机制,并将对具体实现的依赖控制在那几个工厂类里面去。
30 |
31 | 新增接口EventListener接口进行事件监听
32 |
33 | ```java
34 | public interface EventListener {
35 | /**
36 | * 事件发生时的回调方法
37 | * @param event 事件对象
38 | * @throws EventException 处理事件时异常都转换为该异常抛出
39 | */
40 | void onEvent(T event) throws EventException;
41 | }
42 | ```
43 |
44 | 为Socket事件实现一下,acceptConnect中打印日志的语句可以移动到这来
45 |
46 | ```java
47 | public class SocketEventListener implements EventListener {
48 | private static final Logger LOGGER = LoggerFactory.getLogger(SocketEventListener.class);
49 |
50 | @Override
51 | public void onEvent(Socket socket) throws EventException {
52 | LOGGER.info("新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
53 | }
54 | ```
55 |
56 | 重构Connector,添加事件机制,注意whenAccept方法调用了eventListener
57 |
58 | ```java
59 | public class SocketConnector extends Connector {
60 | ... ...
61 | private final EventListener eventListener;
62 |
63 | public SocketConnector(int port, EventListener eventListener) {
64 | this.port = port;
65 | this.eventListener = eventListener;
66 | }
67 |
68 | @Override
69 | protected void acceptConnect() throws ConnectorException {
70 | new Thread(() -> {
71 | while (true && started) {
72 | Socket socket = null;
73 | try {
74 | socket = serverSocket.accept();
75 | whenAccept(socket);
76 | } catch (Exception e) {
77 | //单个Socket异常,不要影响整个Connector
78 | LOGGER.error(e.getMessage(), e);
79 | } finally {
80 | IoUtils.closeQuietly(socket);
81 | }
82 | }
83 | }).start();
84 | }
85 |
86 | @Override
87 | protected void whenAccept(Socket socketConnect) throws ConnectorException {
88 | eventListener.onEvent(socketConnect);
89 | }
90 | ... ...
91 | }
92 | ```
93 |
94 | 重构ServerFactory,添加对具体实现的依赖
95 |
96 | ```java
97 | public class ServerFactory {
98 |
99 | public static Server getServer(ServerConfig serverConfig) {
100 | List connectorList = new ArrayList<>();
101 | SocketEventListener socketEventListener = new SocketEventListener();
102 | ConnectorFactory connectorFactory =
103 | new SocketConnectorFactory(new SocketConnectorConfig(serverConfig.getPort()), socketEventListener);
104 | connectorList.add(connectorFactory.getConnector());
105 | return new SimpleServer(serverConfig, connectorList);
106 | }
107 | }
108 | ```
109 |
110 | 再运行所有单元测试,一切都OK。
111 |
112 | 现在让我们来操作socket,实现一个echo功能的server吧。
113 |
114 | 直接添加到SocketEventListener中
115 |
116 | ```java
117 | public class SocketEventListener implements EventListener {
118 | private static final Logger LOGGER = LoggerFactory.getLogger(SocketEventListener.class);
119 |
120 | @Override
121 | public void onEvent(Socket socket) throws EventException {
122 | LOGGER.info("新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
123 | try {
124 | echo(socket);
125 | } catch (IOException e) {
126 | throw new EventException(e);
127 | }
128 | }
129 |
130 | private void echo(Socket socket) throws IOException {
131 | InputStream inputstream = null;
132 | OutputStream outputStream = null;
133 | try {
134 | inputstream = socket.getInputStream();
135 | outputStream = socket.getOutputStream();
136 | Scanner scanner = new Scanner(inputstream);
137 | PrintWriter printWriter = new PrintWriter(outputStream);
138 | printWriter.append("Server connected.Welcome to echo.\n");
139 | printWriter.flush();
140 | while (scanner.hasNextLine()) {
141 | String line = scanner.nextLine();
142 | if (line.equals("stop")) {
143 | printWriter.append("bye bye.\n");
144 | printWriter.flush();
145 | break;
146 | } else {
147 | printWriter.append(line);
148 | printWriter.append("\n");
149 | printWriter.flush();
150 | }
151 | }
152 | } finally {
153 | IoUtils.closeQuietly(inputstream);
154 | IoUtils.closeQuietly(outputStream);
155 | }
156 | }
157 | }
158 | ```
159 |
160 | 之前都是在单元测试里面启动Server的,这次需要启动Server后,用telnet去使用echo功能,所以再为Server编写一个启动类,在其main方法里面启动Server
161 |
162 | ```java
163 | public class BootStrap {
164 | public static void main(String[] args) throws IOException {
165 | ServerConfig serverConfig = new ServerConfig();
166 | Server server = ServerFactory.getServer(serverConfig);
167 | server.start();
168 | }
169 | }
170 | ```
171 |
172 | 服务器启动后,使用telnet进行验证,如下:
173 |
174 | 
175 |
176 | 到现在为止,我们的服务器终于有了实际功能,下一步终于可以去实现请求静态资源的功能了。
177 |
178 | 完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step4
179 |
180 | 分支step4
181 |
182 | 
183 |
184 |
--------------------------------------------------------------------------------
/http-getjing-tai-zi-yuan-qing-qiu-gong-neng-bian-xie.md:
--------------------------------------------------------------------------------
1 | # HTTP Get静态资源请求功能编写
2 |
3 | HTTP协议处理真是麻烦,早知道找个现成HTTP框架,只编写Servlet相关部分……
4 |
5 | ## 总体流程
6 |
7 | 先把HTTP处理流程定下来,根据前面定下来的框架,需要继承AbstractHttpEventHandler,在doHandle方法中编写接受请求内容、生成响应内容,并输出到客户端的代码,模板类:
8 |
9 | ```java
10 | public abstract class AbstractHttpEventHandler extends AbstractEventHandler {
11 | @Override
12 | protected void doHandle(Connection connection) {
13 | //1.从输入中构造出HTTP请求对象,Body的内容是延迟读取
14 | HttpRequestMessage requestMessage = doParserRequestMessage(connection);
15 | //2.构造HTTP响应对象
16 | HttpResponseMessage responseMessage = doGenerateResponseMessage(requestMessage);
17 | try {
18 | //3.输出响应到客户端
19 | doTransferToClient(responseMessage, connection);
20 | } catch (IOException e) {
21 | throw new HandlerException(e);
22 | } finally {
23 | //4.完成响应后,关闭Socket
24 | if (connection instanceof SocketConnection) {
25 | IOUtils.closeQuietly(((SocketConnection) connection).getSocket());
26 | }
27 | }
28 |
29 | }
30 |
31 | /**
32 | * 通过输入构造HttpRequestMessage
33 | *
34 | * @param connection
35 | * @return
36 | */
37 | protected abstract HttpRequestMessage doParserRequestMessage(Connection connection);
38 |
39 | /**
40 | * 根据HttpRequestMessage生成HttpResponseMessage
41 | *
42 | * @param httpRequestMessage
43 | * @return
44 | */
45 | protected abstract HttpResponseMessage doGenerateResponseMessage(
46 | HttpRequestMessage httpRequestMessage);
47 |
48 | /**
49 | * 写入HttpResponseMessage到客户端
50 | *
51 | * @param responseMessage
52 | * @param connection
53 | * @throws IOException
54 | */
55 | protected abstract void doTransferToClient(HttpResponseMessage responseMessage,
56 | Connection connection) throws IOException;
57 | }
58 | ```
59 |
60 | ## 构造出HTTP请求对象
61 |
62 | 还是先定下处理流程,首先构造RequestLine、然后构造QueryParameter和Headers,如果有Body,则构造Body。
63 |
64 | 整个过程,需要多个parser参与,并且有parser的输出是另外的parser的输入,所有这里通过ThreadLocal来保存这些在多个parser之间共享的变量,保存在HttpParserContext中。
65 |
66 | 这里通过copyRequestBytesBeforeBody方法确定是否具有body,同时将body之前字节都保存起来。
67 |
68 | ```java
69 | public abstract class AbstractHttpRequestMessageParser extends AbstractParser implements HttpRequestMessageParser {
70 | private static final Logger LOGGER = LoggerFactory.getLogger(AbstractHttpRequestMessageParser.class);
71 |
72 | /**
73 | * 定义parse流程
74 | *
75 | * @return
76 | */
77 | @Override
78 | public HttpRequestMessage parse(InputStream inputStream) throws IOException {
79 | //1.设置上下文:设置是否有body、body之前byte数组,以及body之前byte数组长度到上下文中
80 | getAndSetBytesBeforeBodyToContext(inputStream);
81 | //2.解析构造RequestLine
82 | RequestLine requestLine = parseRequestLine();
83 | //3.解析构造QueryParameters
84 | HttpQueryParameters httpQueryParameters = parseHttpQueryParameters();
85 | //4.解析构造HTTP请求头
86 | IMessageHeaders messageHeaders = parseRequestHeaders();
87 | //5.解析构造HTTP Body,如果有个的话
88 | Optional httpBody = parseRequestBody();
89 |
90 | HttpRequestMessage httpRequestMessage = new HttpRequestMessage(requestLine, messageHeaders, httpBody, httpQueryParameters);
91 | return httpRequestMessage;
92 | }
93 |
94 | /**
95 | * 读取请求发送的数据,并保存为byte数组设置到解析上下文中
96 | *
97 | * @param inputStream
98 | * @throws IOException
99 | */
100 | private void getAndSetBytesBeforeBodyToContext(InputStream inputStream) throws IOException {
101 | byte[] bytes = copyRequestBytesBeforeBody(inputStream);
102 | HttpParserContext.setHttpMessageBytes(bytes);
103 | HttpParserContext.setBytesLengthBeforeBody(bytes.length);
104 | }
105 |
106 | /**
107 | * 解析并构建RequestLine
108 | *
109 | * @return
110 | */
111 | protected abstract RequestLine parseRequestLine();
112 |
113 | /**
114 | * 解析并构建HTTP请求Headers集合
115 | *
116 | * @return
117 | */
118 | protected abstract IMessageHeaders parseRequestHeaders();
119 |
120 | /**
121 | * 解析并构建HTTP 请求Body
122 | *
123 | * @return
124 | */
125 | protected abstract Optional parseRequestBody();
126 |
127 | /**
128 | * 解析并构建QueryParameter集合
129 | *
130 | * @return
131 | */
132 | protected abstract HttpQueryParameters parseHttpQueryParameters();
133 |
134 | /**
135 | * 构造body(如果有)之前的字节数组
136 | * @param inputStream
137 | * @return
138 | * @throws IOException
139 | */
140 | private byte[] copyRequestBytesBeforeBody(InputStream inputStream) throws IOException {
141 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(inputStream.available());
142 | int i = -1;
143 | byte[] temp = new byte[3];
144 | while ((i = inputStream.read()) != -1) {
145 | byteArrayOutputStream.write(i);
146 | if ((char) i == '\r') {
147 | int len = inputStream.read(temp, 0, temp.length);
148 | byteArrayOutputStream.write(temp, 0, len);
149 | if ("\n\r\n".equals(new String(temp))) {
150 | break;
151 | }
152 | }
153 | }
154 | return byteArrayOutputStream.toByteArray();
155 | }
156 | }
157 | ```
158 |
159 | RequestLine解析时会设置Http请求方法和queryString到HttpParserContext中,作为QueryParameter解析的输入。
160 |
161 | ```java
162 | @Override
163 | protected RequestLine parseRequestLine() {
164 | RequestLine requestLine = this.httpRequestLineParser.parse();
165 | HttpParserContext.setHttpMethod(requestLine.getMethod());
166 | HttpParserContext.setRequestQueryString(requestLine.getRequestURI().getQuery());
167 | return requestLine;
168 | }
169 | ```
170 |
171 | QueryParameter的解析很简单,直接调用HttpQueryParameterParser即可。
172 |
173 | HttpHeader解析流程为从HttpParserContext中取出请求报文,去掉第一行,然后逐行处理,构造成key-value的形式保存起来。
174 |
175 | 同时,通过Content-Length头或者Transfer-Encoding判断请求是否包含Body。
176 |
177 | ```java
178 | public class DefaultHttpHeaderParser extends AbstractParser implements HttpHeaderParser {
179 | private static final String SPLITTER = ":";
180 | private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHttpHeaderParser.class);
181 |
182 | @Override
183 | public HttpMessageHeaders parse() {
184 | try {
185 | String httpText = getHttpTextFromContext();
186 | HttpMessageHeaders httpMessageHeaders = doParseHttpMessageHeaders(httpText);
187 | setHasBody(httpMessageHeaders);
188 | return httpMessageHeaders;
189 | } catch (UnsupportedEncodingException e) {
190 | throw new ParserException("Unsupported Encoding", e);
191 | }
192 | }
193 |
194 | /**
195 | * 从上下文获取bytes并转换为String
196 | *
197 | * @return
198 | * @throws UnsupportedEncodingException
199 | */
200 | private String getHttpTextFromContext() throws UnsupportedEncodingException {
201 | byte[] bytes = HttpParserContext.getHttpMessageBytes();
202 | return new String(bytes, "utf-8");
203 | }
204 |
205 | /**
206 | * 解析Body之前的文本构建HttpHeader,并保存到HttpMessageHeaders中
207 | *
208 | * @param httpText
209 | * @return
210 | */
211 | private HttpMessageHeaders doParseHttpMessageHeaders(String httpText) {
212 | HttpMessageHeaders httpMessageHeaders = new HttpMessageHeaders();
213 | String[] lines = httpText.split(CRLF);
214 | //跳过第一行
215 | for (int i = 1; i < lines.length; i++) {
216 | String keyValue = lines[i];
217 | if ("".equals(keyValue)) {
218 | break;
219 | }
220 | String[] temp = keyValue.split(SPLITTER);
221 | if (temp.length == 2) {
222 | httpMessageHeaders.addHeader(new HttpHeader(temp[0], temp[1].trim()));
223 | }
224 | }
225 | return httpMessageHeaders;
226 | }
227 |
228 | /**
229 | * 设置报文是否包含Body到上下文中
230 | */
231 | private void setHasBody(HttpMessageHeaders httpMessageHeaders) {
232 | if (httpMessageHeaders.hasHeader("Content-Length")
233 | || (httpMessageHeaders.getFirstHeader("Transfer-Encoding") != null
234 | && "chunked".equals(httpMessageHeaders.getFirstHeader("Transfer-Encoding").getValue())))
235 | {
236 | HttpParserContext.setHasBody(true);
237 | }
238 | }
239 | }
240 | ```
241 |
242 | 如果请求包含了Body,就从InputStream中读取Content-Length长度的内容作为Body内容。Transfer-Encoding的暂时没处理,后续再加。
243 |
244 | ```java
245 | public class DefaultHttpBodyParser implements HttpBodyParser {
246 | @Override
247 | public HttpBody parse() {
248 | int contentLength = HttpParserContext.getBodyInfo().getContentLength();
249 | InputStream inputStream = HttpParserContext.getInputStream();
250 | try {
251 | byte[] body = IOUtils.readFully(inputStream, contentLength);
252 | String contentType = HttpParserContext.getContentType();
253 | String encoding = getEncoding(contentType);
254 | HttpBody httpBody =
255 | new HttpBody(contentType, encoding, body);
256 | return httpBody;
257 | } catch (IOException e) {
258 | throw new ParserException(e);
259 | }
260 | }
261 |
262 | /**
263 | * 获取encoding
264 | * 例如:Content-type: application/json; charset=utf-8
265 | *
266 | * @param contentType
267 | * @return
268 | */
269 | private String getEncoding(String contentType) {
270 | String encoding = "utf-8";
271 | if (StringUtils.isNotBlank(contentType) && contentType.contains(";")) {
272 | encoding = contentType.split(";")[1].trim().replace("charset=", "");
273 | }
274 | return encoding;
275 | }
276 | }
277 | ```
278 |
279 | 到此为止HTTP请求对象构造完毕。
280 |
281 | ## 编写静态资源Handler
282 |
283 | 在AbstractHttpEventHandler的基础上,添加静态资源返回功能。
284 |
285 | 总体流程为:从HTTP请求对象中获取到请求路径,在docBase目录下查找对应路径的资源并返回。
286 |
287 | 需要注意的是不同类型文件的Content-Type响应头需要设置正确,否则文件不会正确显示。
288 |
289 | ```java
290 | public class HttpStaticResourceEventHandler extends AbstractHttpEventHandler {
291 |
292 | private final String docBase;
293 | private final AbstractHttpRequestMessageParser httpRequestMessageParser;
294 |
295 | public HttpStaticResourceEventHandler(String docBase,
296 | AbstractHttpRequestMessageParser httpRequestMessageParser) {
297 | this.docBase = docBase;
298 | this.httpRequestMessageParser = httpRequestMessageParser;
299 | }
300 |
301 | @Override
302 | protected HttpRequestMessage doParserRequestMessage(Connection connection) {
303 | try {
304 | HttpRequestMessage httpRequestMessage = httpRequestMessageParser
305 | .parse(connection.getInputStream());
306 | return httpRequestMessage;
307 | } catch (IOException e) {
308 | throw new HandlerException(e);
309 | }
310 | }
311 |
312 | @Override
313 | protected HttpResponseMessage doGenerateResponseMessage(
314 | HttpRequestMessage httpRequestMessage) {
315 | String path = httpRequestMessage.getRequestLine().getRequestURI().getPath();
316 | Path filePath = Paths.get(docBase, path);
317 | //目录、无法读取的文件都返回404
318 | if (Files.isDirectory(filePath) || !Files.isReadable(filePath)) {
319 | return HttpResponseConstants.HTTP_404;
320 | } else {
321 | ResponseLine ok = ResponseLineConstants.RES_200;
322 | HttpMessageHeaders headers = HttpMessageHeaders.newBuilder()
323 | .addHeader("status", "200").build();
324 | HttpBody httpBody = null;
325 | try {
326 | setContentType(filePath, headers);
327 | httpBody = new HttpBody(new FileInputStream(filePath.toFile()));
328 | } catch (FileNotFoundException e) {
329 | return HttpResponseConstants.HTTP_404;
330 | } catch (IOException e) {
331 | throw new HandlerException(e);
332 | }
333 | HttpResponseMessage httpResponseMessage = new HttpResponseMessage(ok, headers,
334 | Optional.ofNullable(httpBody));
335 | return httpResponseMessage;
336 | }
337 |
338 | }
339 |
340 | /**
341 | * 根据文件后缀设置文件Content-Type
342 | *
343 | * @param filePath
344 | * @param headers
345 | * @throws IOException
346 | */
347 | private void setContentType(Path filePath, HttpMessageHeaders headers) throws IOException {
348 | //使用Files.probeContentType在mac上总是返回null
349 | //String contentType = Files.probeContentType(filePath);
350 | String contentType = MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(filePath.toString());
351 | headers.addHeader(new HttpHeader("Content-Type", contentType));
352 | if (contentType.indexOf("text") == -1) {
353 | headers.addHeader(new HttpHeader("Content-Length",
354 | String.valueOf(filePath.toFile().length())));
355 | }
356 | }
357 |
358 | @Override
359 | protected void doTransferToClient(HttpResponseMessage responseMessage,
360 | Connection connection) throws IOException {
361 | HttpResponseMessageWriter httpResponseMessageWriter = new HttpResponseMessageWriter();
362 | httpResponseMessageWriter.write(responseMessage, connection);
363 | }
364 |
365 | }
366 | ```
367 |
368 | ## 启动服务器测试
369 |
370 | 修改BootStrap,添加对应功能
371 |
372 | ```java
373 | EventListener socketEventListener3 =
374 | new ConnectionEventListener(
375 | new HttpStaticResourceEventHandler(System.getProperty("user.dir"),
376 | new DefaultHttpRequestMessageParser(new DefaultHttpRequestLineParser(),
377 | new DefaultHttpQueryParameterParser(),
378 | new DefaultHttpHeaderParser(),
379 | new DefaultHttpBodyParser())));
380 | SocketConnector connector3 =
381 | SocketConnectorFactory.build(18083, socketEventListener3);
382 | ServerConfig serverConfig = ServerConfig.builder()
383 | .addConnector(connector3)
384 | .build();
385 | Server server = ServerFactory.getServer(serverConfig);
386 | server.start();
387 | ```
388 |
389 | 新建web目录,添加html、图片和js
390 |
391 | 
392 |
393 | index.html
394 |
395 | ```html
396 |
397 |
398 |
399 |
400 | Hello Beggar Servlet Container
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 | ```
410 |
411 | index.js
412 |
413 | ```js
414 | var date = new Date();
415 | var content = "now is " + date;
416 | var div = document.querySelector("#content");
417 | div.innerHTML = content;
418 | ```
419 |
420 | main.css
421 |
422 | ```js
423 | body {
424 | background-color: antiquewhite;
425 | }
426 |
427 | #content {
428 | background-color: cornflowerblue;
429 | }
430 | ```
431 |
432 | 本地测试:
433 |
434 | 用浏览器访问[http://localhost:18083/web/index.html](http://localhost:18083/web/index.html)
435 |
436 | 显示如下
437 |
438 | 
439 |
440 | 完整代码:[https://github.com/pkpk1234/BeggarServletContainer/tree/step9](https://github.com/pkpk1234/BeggarServletContainer/tree/step9)
441 |
442 | //TODO: 整理写死的字符串
443 |
444 |
--------------------------------------------------------------------------------
/http-parser-flow-request-line-parse.md:
--------------------------------------------------------------------------------
1 | # HTTP请求parse流程、RequestLineParser、HttpQueryParameterParser
2 |
3 | 根据标准, HTTP请求消息格式如下:
4 |
5 | ```
6 | Request-Line
7 | *(( general-header
8 | | request-header
9 | | entity-header ) CRLF)
10 | CRLF
11 | [ message-body ]
12 | ```
13 |
14 | 所有如果要将HTTP请求从输入流中parse为Java对象,需要完成3个步骤:
15 |
16 | 1. 解析Request-Line
17 | 2. 解析headers
18 | 3. 解析body
19 |
20 | 让我们首先分析最简单的解析Request-Line。
21 |
22 | ## 解析Request-Line
23 |
24 | 根据标准Request-Line的格式如下:
25 |
26 | ```
27 | Method SP Request-URI SP HTTP-Version CRLF
28 | ```
29 |
30 | 其中Request-URI是客户端使用URLEncode之后的URI,即如果元素URI中包含非ASCII字符,客户端必须将其编码为 %编码值 的形式。
31 |
32 | 分为4段:
33 |
34 | 1. 第一段是HTTP方法名,即GET、POST、PUT等。
35 | 2. 第二段是请求路径,如 /index.html
36 | 3. 第三段是HTTP协议版本,一般是HTTP/1.1。
37 | 4. 最后是一个CRLF回车换行。
38 |
39 | 除了CRLF,段与段之间用空格分隔。综上所述,解析步骤为:
40 |
41 | 1. 去掉行尾的CRLF,并trim两端的空格。
42 | 2. 使用空格将字符串分为大小为3的数组a。
43 | 3. method是a\[0\],Request-URI是a\[1\],HTTP-Version是a\[2\]。
44 |
45 | 代码如下:
46 |
47 | ```java
48 | public class DefaultRequestLineParser implements RequestLineParser {
49 | private static final String SPLITTER = "\\s+";
50 | private static final String CRLF = "\r\n";
51 |
52 | @Override
53 | public RequestLine parse(String startLine) {
54 | //去掉末尾的CRLF和空格,转化为Method SP Request-URI SP HTTP-Version
55 | String str = startLine.replaceAll(CRLF, "").trim();
56 | String[] parts = str.split(SPLITTER);
57 | //数组格式{Method,Request-URI,HTTP-Version}
58 | if (parts.length == 3) {
59 | String method = parts[0];
60 | URI uri = URI.create(parts[1]);
61 | String httpVersion = parts[2];
62 | return new RequestLine(method, uri, httpVersion);
63 | }
64 | throw new ParserException("startline format illegal");
65 | }
66 | }
67 | ```
68 |
69 | ## 构造QueryParameters
70 |
71 | 当提交的请求包含查询信息,如 /person?age=20&gender=male,Request-URI将包含这些信息。我们可以将查询信息构造为对象,并保存到HttpMessage接口中。
72 |
73 | 解析步骤也很简单:
74 |
75 | 1. 调用Request-Line构造而来的URI的getQuery\(\)方法,获取到query字符串。
76 | 2. 使用&将query字符串拆分为key-value字符串组成的数组。
77 | 3. 遍历这个数组,将key-value字符串转换为HttpQueryParameter对象。
78 |
79 | ```java
80 | public class DefaultHttpQueryParameterParser implements HttpQueryParameterParser {
81 | private final HttpQueryParameters httpQueryParameters;
82 | private static final String SPLITTER = "&";
83 | private static final String KV_SPLITTER = "=";
84 |
85 | public DefaultHttpQueryParameterParser() {
86 | this.httpQueryParameters = new HttpQueryParameters();
87 | }
88 |
89 | @Override
90 | public HttpQueryParameters parse(String queryString) {
91 | if (queryString.contains(SPLITTER)) {
92 | String[] keyValues = queryString.split(SPLITTER);
93 | for (String keyValue : keyValues) {
94 | if (keyValue.contains(KV_SPLITTER)) {
95 | String[] temp = keyValue.split(KV_SPLITTER);
96 | if (temp.length == 2) {
97 | this.httpQueryParameters
98 | .addQueryParameter(new HttpQueryParameter(temp[0], temp[1]));
99 | }
100 |
101 | }
102 | }
103 | }
104 | return this.httpQueryParameters;
105 | }
106 | }
107 | ```
108 |
109 | ```java
110 | public class HttpQueryParameters {
111 |
112 | private final LinkedListMultimap prametersMultiMap;
113 |
114 | public HttpQueryParameters() {
115 | this.prametersMultiMap = LinkedListMultimap.create();
116 | }
117 |
118 |
119 | public List getQueryParameter(String name) {
120 | return this.prametersMultiMap.get(name);
121 | }
122 |
123 | public HttpQueryParameter getFirstQueryParameter(String name) {
124 | if (this.prametersMultiMap.containsKey(name)) {
125 | return this.prametersMultiMap.get(name).get(0);
126 | }
127 | return null;
128 | }
129 |
130 | public List getQueryParameters() {
131 | return this.prametersMultiMap.values();
132 | }
133 |
134 | public void addQueryParameter(HttpQueryParameter httpQueryParameter) {
135 | this.prametersMultiMap.put(httpQueryParameter.getName(), httpQueryParameter);
136 | }
137 |
138 | public void removeQueryParameter(HttpQueryParameter httpQueryParameter) {
139 | this.prametersMultiMap.remove(httpQueryParameter.getName(), httpQueryParameter);
140 | }
141 |
142 | public void removeQueryParameter(String httpQueryParameter) {
143 | this.prametersMultiMap.removeAll(httpQueryParameter);
144 | }
145 |
146 | public boolean hasRequestParameter(String httpQueryParameter) {
147 | return this.prametersMultiMap.containsKey(httpQueryParameter);
148 | }
149 |
150 | public Set getQueryParameterNames() {
151 | return this.prametersMultiMap.keySet();
152 | }
153 | }
154 | ```
155 |
156 | HttpQueryParameters中使用guava的LinkedListMultimap保存HttpQueryParameters,以支持同名参数的场景,并保证遍历时访问HttpQueryParameters的顺序,和添加时一致。
157 |
158 | 单元测试:
159 |
160 | 测试Request Line的解析
161 |
162 | ```java
163 | public class TestDefaultRequestLineParser {
164 | private static final Logger
165 | LOGGER = LoggerFactory.getLogger(TestDefaultRequestLineParser.class);
166 |
167 | @Test
168 | public void test() {
169 | DefaultRequestLineParser defaultRequestLineParser
170 | = new DefaultRequestLineParser();
171 | RequestLine result = defaultRequestLineParser.parse("GET /hello.txt HTTP/1.1\r\n");
172 | String method = result.getMethod();
173 | assertEquals("GET", method);
174 | final URI requestURI = result.getRequestURI();
175 | assertEquals(URI.create("/hello.txt"), requestURI);
176 | LOGGER.info(requestURI.getQuery());
177 | LOGGER.info(requestURI.getFragment());
178 | assertEquals("HTTP/1.1", result.getHttpVersion());
179 | }
180 |
181 | @Test
182 | public void testQuery() {
183 | DefaultRequestLineParser defaultRequestLineParser
184 | = new DefaultRequestLineParser();
185 | RequestLine result = defaultRequestLineParser.parse("GET /test?a=123&a1=1&b=456 HTTP/1.1\r\n");
186 | String method = result.getMethod();
187 | assertEquals("GET", method);
188 | final URI requestURI = result.getRequestURI();
189 | assertEquals(URI.create("/test?a=123&a1=1&b=456"), requestURI);
190 | LOGGER.info(requestURI.getQuery());
191 | assertEquals("HTTP/1.1", result.getHttpVersion());
192 | }
193 | }
194 | ```
195 |
196 | 测试Query字符串解析
197 |
198 | ```java
199 | public class TestDefaultHttpQueryParameterParser {
200 |
201 | @Test
202 | public void test() {
203 | String queryStr = "a=123&a1=1&b=456&a=321";
204 | DefaultHttpQueryParameterParser httpRequestParameterParser
205 | = new DefaultHttpQueryParameterParser();
206 | HttpQueryParameters result = httpRequestParameterParser.parse(queryStr);
207 | List parameters = result.getQueryParameter("a");
208 | assertNotNull(parameters);
209 | assertEquals(2, parameters.size());
210 | assertEquals("123", parameters.get(0).getValue());
211 | assertEquals("321", parameters.get(1).getValue());
212 | }
213 | }
214 | ```
215 |
216 | 完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step8/
217 |
218 | 分支step8
219 |
220 |
--------------------------------------------------------------------------------
/http11xie-yi-jie-kou.md:
--------------------------------------------------------------------------------
1 | # HTTP1.1协议实体接口
2 |
3 | 好不容易完成了一个相对容易扩展的IO层,终于可以开始进入HTTP部分的编写了。早知道就直接找Netty之类的IO框架了,当然那样就没啥意思,也无法找到自己技术上缺陷并改进了。
4 |
5 | 我们先把HTTP1协议实体接口,用Java对象把HTTP1协议中需要的实体表示出来。然后再编写HTTP Parser相关功能,将IO中获取到的信息转换为上一步中定义的实体对象。这里先将HTTP1协议接口定下来。
6 |
7 | 根据[HTTP1.1协议标准](https://tools.ietf.org/html/rfc2616),HTTP协议实体如下:
8 |
9 | ```
10 | generic-message = start-line
11 | *(message-header CRLF)
12 | CRLF
13 | [ message-body ]
14 | ```
15 |
16 | 分为四个部分:
17 |
18 | 1. 开始行
19 | 2. 消息头
20 | 3. 空行
21 | 4. 消息体
22 |
23 | 开始行有分为请求开始行和响应开始行两类:SP代表空白字符
24 |
25 | ```
26 | Request-Line = Method SP Request-URI SP HTTP-Version CRLF
27 | ```
28 |
29 | ```
30 | Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
31 | ```
32 |
33 | 转换为Java后,结构如下
34 |
35 | StartLine用于表示开始行,RequestLine和ResponseLine都实现该接口,同时添加了返回特定各自信息的方法。
36 |
37 | HttBody用表示Body内容,使用泛型指定Body内容的格式,StringContentHttBody表示Body的内容是字符串,ByteContentHttBody表示Body内容是二进制,并与InputStream进行返回。
38 |
39 | IMessageHeaders用户持有所有的HttpHeader。HttpMessageHeaders内部使用Guava的ArrayListMultimap<String, HttpHeader>保持HttpHeader,以实现对同名多个消息头的支持。
40 |
41 | HttpMessage接口则同时使用 StartLine、IMessageHeaders、HttpBody三个接口,用于表示HTTP Message。
42 |
43 | 完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step7
44 |
45 | 分支step7
46 |
47 | 
48 |
49 |
--------------------------------------------------------------------------------
/jian-ting-duan-kou-jie-shou-qing-qiu.md:
--------------------------------------------------------------------------------
1 | # 监听端口接收请求
2 |
3 | 上一步中我们已经定义好了Server接口,并进行了多次重构,但是实际上那个Server是没啥毛用的东西。现在要为其添加真正有用的功能。大师说了,饭要一口一口吃,衣服要一件一件脱,那么首先来定个小目标——启动ServerSocket监听请求,不要什么多线程不要什么NIO,先完成最简单的功能。下面还是一步一步来写代码并进行重构优化代码结构。
4 |
5 | 关于Socket和ServerSocket怎么用,网上很多文章写得比我好,大家自己找找就好。
6 |
7 | 代码写起来很简单:(下面的代码片段有很多问题哦,大神们请不要急着喷,看完再抽)
8 |
9 | ```java
10 | public class SimpleServer implements Server {
11 |
12 | ... ...
13 | @Override
14 | public void start() {
15 | Socket socket = null;
16 | try {
17 | this.serverSocket = new ServerSocket(this.port);
18 | this.serverStatus = ServerStatus.STARTED;
19 | System.out.println("Server start");
20 | while (true) {
21 | socket = serverSocket.accept();// 从连接队列中取出一个连接,如果没有则等待
22 | System.out.println(
23 | "新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
24 | }
25 | }
26 | catch (IOException e) {
27 | e.printStackTrace();
28 | }
29 | finally {
30 | if (socket != null) {
31 | try {
32 | socket.close();
33 | }
34 | catch (IOException e) {
35 | e.printStackTrace();
36 | }
37 | }
38 | }
39 |
40 | }
41 |
42 | @Override
43 | public void stop() {
44 | try {
45 | if (this.serverSocket != null) {
46 | this.serverSocket.close();
47 | }
48 | }
49 | catch (IOException e) {
50 | e.printStackTrace();
51 | }
52 | this.serverStatus = ServerStatus.STOPED;
53 | System.out.println("Server stop");
54 | }
55 |
56 | ... ...
57 | }
58 | ```
59 |
60 | 添加单元测试:
61 |
62 | ```java
63 | public class TestServerAcceptRequest {
64 | private static Server server;
65 | // 设置超时时间为500毫秒
66 | private static final int TIMEOUT = 500;
67 |
68 | @BeforeClass
69 | public static void init() {
70 | ServerConfig serverConfig = new ServerConfig();
71 | server = ServerFactory.getServer(serverConfig);
72 | }
73 |
74 | @Test
75 | public void testServerAcceptRequest() {
76 | // 如果server没有启动,首先启动server
77 | if (server.getStatus().equals(ServerStatus.STOPED)) {
78 | //在另外一个线程中启动server
79 | new Thread(() -> {
80 | server.start();
81 | }).run();
82 | //如果server未启动,就sleep一下
83 | while (server.getStatus().equals(ServerStatus.STOPED)) {
84 | System.out.println("等待server启动");
85 | try {
86 | Thread.sleep(500);
87 | }
88 | catch (InterruptedException e) {
89 | e.printStackTrace();
90 | }
91 | }
92 | Socket socket = new Socket();
93 | SocketAddress endpoint = new InetSocketAddress("localhost",
94 | ServerConfig.DEFAULT_PORT);
95 | try {
96 | // 试图发送请求到服务器,超时时间为TIMEOUT
97 | socket.connect(endpoint, TIMEOUT);
98 | assertTrue("服务器启动后,能接受请求", socket.isConnected());
99 | }
100 | catch (IOException e) {
101 | e.printStackTrace();
102 | }
103 | finally {
104 | try {
105 | socket.close();
106 | }
107 | catch (IOException e) {
108 | e.printStackTrace();
109 | }
110 | }
111 | }
112 | }
113 |
114 | @AfterClass
115 | public static void destroy() {
116 | server.stop();
117 | }
118 | }
119 | ```
120 |
121 | 运行单元测试,我檫,怎么偶尔一直输出“等待server启动",用大师的话说就算”只看见轮子转,不见车跑“。原因其实很简单,因为多线程咯,测试线程一直无法获取到另外一个线程中更新的值。大师又说了,早看不惯满天的System.out.println和到处重复的
122 |
123 | ```java
124 | try {
125 | socket.close();
126 | } catch (IOException e) {
127 | e.printStackTrace();
128 | }
129 | ```
130 |
131 | 了。
132 |
133 | 大师还说了,代码太垃圾了,问题很多:如果Server.start\(\)时端口被占用、权限不足,start方法根本没有抛出异常嘛,调用者难道像SB一样一直等下去,还有,Socket如果异常了,while\(true\)就退出了,难道一个Socket异常,整个服务器就都挂了,这代码就是一坨屎嘛,滚去重构。
134 |
135 | 首先为ServerStatus属性添加volatile,保证其可见性。
136 |
137 | 代码片段:
138 |
139 | ```java
140 | public class SimpleServer implements Server {
141 | private volatile ServerStatus serverStatus = ServerStatus.STOPED;
142 | ... ...
143 | }
144 | ```
145 |
146 | 然后引入sl4j+log4j2,替换掉漫天的System.out.println。
147 |
148 | 然后编写closeQuietly方法,专门处理socket的关闭。
149 |
150 | ```java
151 | public class IoUtils {
152 |
153 | private static Logger logger = LoggerFactory.getLogger(IoUtils.class);
154 |
155 | /**
156 | * 安静地关闭,不抛出异常
157 | * @param closeable
158 | */
159 | public static void closeQuietly(Closeable closeable) {
160 | if(closeable != null) {
161 | try {
162 | closeable.close();
163 | } catch (IOException e) {
164 | logger.error(e.getMessage(),e);
165 | }
166 | }
167 | }
168 | }
169 | ```
170 |
171 | 最后start方法异常时,需要让调用者得到通知,并且一个Socket异常,不影响整个服务器。
172 |
173 | 重构后再跑单元测试:一切OK
174 |
175 | 
176 |
177 | 到目前为止,一个单线程的可以接收请求的Server就完成了。
178 |
179 | 完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step2
180 |
181 | 分支step2
182 |
183 | 
184 |
185 |
--------------------------------------------------------------------------------
/jie-shou-http-body.md:
--------------------------------------------------------------------------------
1 | # 接收HTTP Body
2 |
3 | 解析请求中的Body需要注意Transfer-Encoding头。
4 |
5 | 当Transfer-Encoding的值为chunked时,不应该使用Content-Length去读取Body。
6 |
7 | chunked的Body格式为:
8 |
9 | > 数据以一系列分块的形式进行发送。
10 | >
11 | > [`Content-Length`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Length)
12 | >
13 | > 首部在这种情况下不被发送。。在每一个分块的开头需要添加当前分块的长度,以十六进制的形式表示,后面紧跟着 '
14 | >
15 | > `\r\n`
16 | >
17 | > ' ,之后是分块本身,后面也是'
18 | >
19 | > `\r\n`
20 | >
21 | > ' 。终止块是一个常规的分块,不同之处在于其长度为0。终止块后面是一个挂载(trailer),由一系列(或者为空)的实体消息首部构成
22 |
23 | [https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Transfer-Encoding](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Transfer-Encoding)
24 |
25 | 所以chunked的Body使用 0\r\n\r\n 作为Body的结束标志。
26 |
27 | 由此可以构造延迟读取Body内容的流:
28 |
29 | ```java
30 | public class HttpBodyInputStream extends InputStream {
31 | //chunked body使用 0\r\n\r\n结尾
32 | private static final char[] TAILER_CHARS = {'0', '\r', '\n', '\r', '\n'};
33 | private final InputStream inputStream;
34 | private long contentLength;
35 | private boolean isChuncked;
36 | private long readed = 0;
37 | private boolean isFinished = false;
38 | private int idx = 0;
39 |
40 | public HttpBodyInputStream(InputStream inputStream, boolean ifChuncked) {
41 | this.inputStream = inputStream;
42 | this.isChuncked = ifChuncked;
43 | }
44 |
45 | @Override
46 | public int read() throws IOException {
47 | if (isChuncked) {
48 | return readChunked();
49 | } else {
50 | return readByte();
51 | }
52 | }
53 |
54 | /**
55 | * 读取chunked body
56 | *
57 | * @return
58 | * @throws IOException
59 | */
60 | private int readChunked() throws IOException {
61 | if (isFinished) {
62 | return -1;
63 | }
64 | int i = this.inputStream.read();
65 | if (i == -1) {
66 | return i;
67 | } else {
68 | if (idx == TAILER_CHARS.length - 1) {
69 | isFinished = true;
70 | } else if (TAILER_CHARS[idx++] != (char) i) {
71 | idx = 0;
72 | }
73 | }
74 | return i;
75 | }
76 |
77 | /**
78 | * 读取非chunked body
79 | *
80 | * @return
81 | * @throws IOException
82 | */
83 | private int readByte() throws IOException {
84 | readed++;
85 | if (readed > contentLength) {
86 | return -1;
87 | } else {
88 | return this.inputStream.read();
89 | }
90 | }
91 |
92 | public long getContentLength() {
93 | return contentLength;
94 | }
95 |
96 | public void setContentLength(long contentLength) {
97 | this.contentLength = contentLength;
98 | }
99 | }
100 | ```
101 |
102 | 此处BodyParser并不处理压缩和编码,只是直接返回raw body,使用处再根据实际情况进行处理。
103 |
104 | 单元测试:
105 |
106 | ```java
107 | public class TestHttpBodyInputStream {
108 | private static final Logger LOGGER =
109 | LoggerFactory.getLogger(TestHttpBodyInputStream.class);
110 | /*
111 | 7
112 | Mozilla
113 | 9
114 | Developer
115 | 7
116 | Network
117 | 0
118 |
119 | */
120 | private static final String TEST_OTHER_MESSAGE = "testtest";
121 | private static final String CHUNKED_BODY = "7\r\n" +
122 | "Mozilla\r\n" +
123 | "9\r\n" +
124 | "Developer\r\n" +
125 | "7\r\n" +
126 | "Network\r\n" +
127 | "0\r\n" +
128 | "\r\n";
129 |
130 | private static final String BODY = CHUNKED_BODY + TEST_OTHER_MESSAGE;
131 |
132 | private static final String NORMAL_BODY = "Mozilla Developer Network";
133 |
134 | @Test
135 | public void testReadChunkedBody() throws IOException {
136 | InputStream in = new ByteArrayInputStream(BODY.getBytes());
137 | HttpBodyInputStream httpBodyInputStream = new HttpBodyInputStream(in, true);
138 | ByteOutputStream out = new ByteOutputStream();
139 | IOUtils.copy(httpBodyInputStream, out);
140 | LOGGER.info(out.toString());
141 | assertEquals(CHUNKED_BODY, out.toString());
142 | }
143 |
144 | @Test
145 | public void testReadBody() throws IOException {
146 | InputStream in = new ByteArrayInputStream(NORMAL_BODY.getBytes());
147 | HttpBodyInputStream httpBodyInputStream = new HttpBodyInputStream(in, false);
148 | httpBodyInputStream.setContentLength(25);
149 | ByteOutputStream out = new ByteOutputStream();
150 | IOUtils.copy(httpBodyInputStream, out);
151 | LOGGER.info(out.toString());
152 | assertEquals(NORMAL_BODY, out.toString());
153 | }
154 | }
155 | ```
156 |
157 | 完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step10
158 |
159 |
--------------------------------------------------------------------------------
/nioconnector.md:
--------------------------------------------------------------------------------
1 | # NIOConnector
2 |
3 | 现在为Server添加NIOConnector,添加之前可以发现我们的代码其实是有问题的。比如现在的代码是无法让服务器支持同时监听多个端口和IP的,如同时监听 127.0.0.1:18080和0.0.0.0:18443现在是无法做到的。因为当期的端口号是Server的属性,并且只有一个,但是端口其实应该是Connector的属性,因为Connector专门负责了Server的IO。
4 |
5 | 重构一下,将端口号从Server中去掉,取而代之的是Connector列表;将当期的Connector抽象类重命名为AbstractConnector,再新建接口Connector,添加getPort和getHost两个方法,让Connector支持将监听绑定到不同IP的功能。
6 |
7 | 去掉getPort方法
8 |
9 | ```java
10 | public interface Server {
11 | /**
12 | * 启动服务器
13 | */
14 | void start() throws IOException;
15 |
16 | /**
17 | * 关闭服务器
18 | */
19 | void stop();
20 |
21 | /**
22 | * 获取服务器启停状态
23 | * @return
24 | */
25 | ServerStatus getStatus();
26 |
27 | /**
28 | * 获取服务器管理的Connector列表
29 | * @return
30 | */
31 | List getConnectorList();
32 | }
33 | ```
34 |
35 | 去掉port属性和方法
36 |
37 | ```java
38 | public class SimpleServer implements Server {
39 | private static Logger logger = LoggerFactory.getLogger(SimpleServer.class);
40 | private volatile ServerStatus serverStatus = ServerStatus.STOPED;
41 | private final List connectorList;
42 |
43 | public SimpleServer(List connectorList) {
44 | this.connectorList = connectorList;
45 | }
46 | ... ...
47 | }
48 | ```
49 |
50 | 添加HOST属性绑定IP,添加backLog属性设置ServerSocket的TCP属性SO\_BACKLOG。修改init方法,支持ServerSocket绑定IP。
51 |
52 | ```java
53 | public class SocketConnector extends AbstractConnector {
54 | private static final Logger LOGGER = LoggerFactory.getLogger(SocketConnector.class);
55 | private static final String LOCALHOST = "localhost";
56 | private static final int DEFAULT_BACKLOG = 50;
57 | private final int port;
58 | private final String host;
59 | private final int backLog;
60 | private ServerSocket serverSocket;
61 | private volatile boolean started = false;
62 | private final EventListener eventListener;
63 |
64 | public SocketConnector(int port, EventListener eventListener) {
65 | this(port, LOCALHOST, DEFAULT_BACKLOG, eventListener);
66 | }
67 |
68 | public SocketConnector(int port, String host, int backLog, EventListener eventListener) {
69 | this.port = port;
70 | this.host = StringUtils.isBlank(host) ? LOCALHOST : host;
71 | this.backLog = backLog;
72 | this.eventListener = eventListener;
73 | }
74 |
75 |
76 | @Override
77 | protected void init() throws ConnectorException {
78 |
79 | //监听本地端口,如果监听不成功,抛出异常
80 | try {
81 | InetAddress inetAddress = InetAddress.getByName(this.host);
82 | this.serverSocket = new ServerSocket(this.port, backLog, inetAddress);
83 | this.started = true;
84 | } catch (IOException e) {
85 | throw new ConnectorException(e);
86 | }
87 | }
88 | ```
89 |
90 | 执行单元测试,一切OK。现在可以开始添加NIO了。
91 |
92 | 根据前面一步一步搭建的架构,需要添加支持NIO的EventListener和EventHandler两个实现即可。
93 |
94 | NIOEventListener中莫名其妙出现了SelectionKey,表面这个类和SelectionKey是强耦合的,说明Event这块的架构设计是很烂的,势必又要重构,今天先不改了,完成功能先。
95 |
96 | ```java
97 | public class NIOEventListener extends AbstractEventListener {
98 | private final EventHandler eventHandler;
99 |
100 | public NIOEventListener(EventHandler eventHandler) {
101 | this.eventHandler = eventHandler;
102 | }
103 |
104 | @Override
105 | protected EventHandler getEventHandler(SelectionKey event) {
106 | return this.eventHandler;
107 | }
108 | }
109 | ```
110 |
111 | 同意的道理,NIOEchoEventHandler也不应该和SelectionKey强耦合,echo功能简单,如果是返回文件内容的功能,那样的话,大段大段的文件读写代码是完全无法复用的。
112 |
113 | ```java
114 | public class NIOEchoEventHandler extends AbstractEventHandler {
115 | @Override
116 | protected void doHandle(SelectionKey key) {
117 | try {
118 | if (key.isReadable()) {
119 | SocketChannel client = (SocketChannel) key.channel();
120 | ByteBuffer output = (ByteBuffer) key.attachment();
121 | client.read(output);
122 | } else if (key.isWritable()) {
123 | SocketChannel client = (SocketChannel) key.channel();
124 | ByteBuffer output = (ByteBuffer) key.attachment();
125 | output.flip();
126 | client.write(output);
127 | output.compact();
128 | }
129 | } catch (IOException e) {
130 | throw new HandlerException(e);
131 | }
132 | }
133 | }
134 | ```
135 |
136 | 修改ServerFactory,添加NIO功能,这里的代码也是有很大设计缺陷的,ServerFactory只应该根据传入的config信息构造Server,而不是每次都去改工厂。
137 |
138 | ```java
139 | public class ServerFactory {
140 | /**
141 | * 返回Server实例
142 | *
143 | * @return
144 | */
145 | public static Server getServer(ServerConfig serverConfig) {
146 | List connectorList = new ArrayList<>();
147 | SocketEventListener socketEventListener =
148 | new SocketEventListener(new FileEventHandler(System.getProperty("user.dir")));
149 | ConnectorFactory connectorFactory =
150 | new SocketConnectorFactory(new SocketConnectorConfig(serverConfig.getPort()), socketEventListener);
151 | //NIO
152 | NIOEventListener nioEventListener = new NIOEventListener(new NIOEchoEventHandler());
153 | //监听18081端口
154 | SocketChannelConnector socketChannelConnector = new SocketChannelConnector(18081,nioEventListener);
155 |
156 | connectorList.add(connectorFactory.getConnector());
157 | connectorList.add(socketChannelConnector);
158 | return new SimpleServer(connectorList);
159 | }
160 | }
161 | ```
162 |
163 | 运行BootStrap,启动Server,telnet访问18081端口,功能是勉强实现了,但是架构设计是有重大缺陷的,进一步添加功能之前,需要重构好架构才行。
164 |
165 | 
166 |
167 | 完整代码:[https://github.com/pkpk1234/BeggarServletContainer/tree/step6](https://github.com/pkpk1234/BeggarServletContainer/tree/step6)
168 |
169 | 分支step6
170 |
171 | 
172 |
173 |
--------------------------------------------------------------------------------
/serverjie-kou.md:
--------------------------------------------------------------------------------
1 | # Server接口编写
2 |
3 | 开发环境搭建好了,可以开始写代码了。
4 |
5 | 但是应该怎么写呢,完全没头绪。还是从核心基本功能入手,Servlet容器,说白了就是一HTTP服务器嘛,能支持Servlet。
6 |
7 | 别人要用你的服务器,总是需要启动的,用完了,是需要关闭的。
8 |
9 | 那么先定义一个Server,用来表示服务器,提供启动和停止功能。
10 |
11 | 大师们总说面向接口编程,那么先定义一个Server接口:
12 |
13 | ```java
14 | public interface Server {
15 | /**
16 | * 启动服务器
17 | */
18 | void start();
19 |
20 | /**
21 | * 关闭服务器
22 | */
23 | void stop();
24 | }
25 | ```
26 |
27 | 大师们还说,要多测试,所以再添加一个单元测试类。但是现在只有接口,没实现,没法测,没关系,先写个输出字符串到标准输出的实现再说。
28 |
29 | ```java
30 | public class SimpleServer implements Server {
31 | @Override
32 | public void start() {
33 | System.out.println("Server start");
34 | }
35 |
36 | @Override
37 | public void stop() {
38 | System.out.println("Server stop");
39 | }
40 | }
41 | ```
42 |
43 | 有了这个实现,就可以写出单元测试了。
44 |
45 | ```java
46 | public class TestServer {
47 | private static final Server SERVER = new SimpleServer();
48 |
49 | @BeforeClass
50 |
51 | @Test
52 | public void testServerStart() {
53 | SERVER.start();
54 | }
55 |
56 | @Test
57 | public void testServerStop() {
58 | SERVER.stop();
59 | }
60 | }
61 | ```
62 |
63 | 先不管这个SimpleServer啥用没有,看看上面的单元测试,里面出现了具体实现SimpleServer,大师很不高兴,如果编写了ComplicatedServer,这里代码岂不是要改。重构一下,添加一个工厂类。
64 |
65 | ```java
66 | public class ServerFactory {
67 | /**
68 | * 返回Server实例
69 | * @return
70 | */
71 | public static Server getServer() {
72 | return new SimpleServer();
73 | }
74 | }
75 | ```
76 |
77 | 单元测试重构后如下:这样就将接口和具体实现隔离开了,代码更加灵活。
78 |
79 | ```java
80 | public class TestServer {
81 | private static final Server SERVER = ServerFactory.getServer();
82 |
83 | @BeforeClass
84 |
85 | @Test
86 | public void testServerStart() {
87 | SERVER.start();
88 | }
89 |
90 | @Test
91 | public void testServerStop() {
92 | SERVER.stop();
93 | }
94 | }
95 | ```
96 |
97 | 再看单元测试,没法写assert断言啊,难道要用客户端请求下才知道Server的启停状态?Server要自己提供状态查询接口。
98 |
99 | 重构Server,添加getStatus接口,返回Server状态,状态应是枚举,暂定STARTED、STOPED两种,只有调用了start方法后,状态才会变为STARTED。
100 |
101 | Server重构后如下:
102 |
103 | ```java
104 | public class SimpleServer implements Server {
105 |
106 | private ServerStatus serverStatus = ServerStatus.STOPED;
107 |
108 | @Override
109 | public void start() {
110 | this.serverStatus = ServerStatus.STARTED;
111 | System.out.println("Server start");
112 | }
113 |
114 | @Override
115 | public void stop() {
116 | this.serverStatus = ServerStatus.STOPED;
117 | System.out.println("Server stop");
118 | }
119 |
120 | @Override
121 | public ServerStatus getStatus() {
122 | return serverStatus;
123 | }
124 |
125 | }
126 | ```
127 |
128 | 再为单元测试添加断言:
129 |
130 | ```java
131 | public class TestServer {
132 | private static final Server SERVER = ServerFactory.getServer();
133 |
134 | @Test
135 | public void testServerStart() {
136 | SERVER.start();
137 | assertTrue("服务器启动后,状态是STARTED",SERVER.getStatus().equals(ServerStatus.STARTED));
138 | }
139 |
140 | @Test
141 | public void testServerStop() {
142 | SERVER.stop();
143 | assertTrue("服务器关闭后,状态是STOPED",SERVER.getStatus().equals(ServerStatus.STOPED));
144 | }
145 | }
146 | ```
147 |
148 | 再继续看Server接口,要接受客户端的请求,需要监听本地端口,端口应该作为构造参数传入,并且Server应该具有默认的端口。再继续重构。
149 |
150 | ```java
151 | public class SimpleServer implements Server {
152 |
153 | private ServerStatus serverStatus = ServerStatus.STOPED;
154 | public final int DEFAULT_PORT = 18080;
155 | private final int PORT;
156 |
157 | public SimpleServer(int PORT) {
158 | this.PORT = PORT;
159 | }
160 |
161 | public SimpleServer() {
162 | this.PORT = DEFAULT_PORT;
163 | }
164 |
165 | @Override
166 | public void start() {
167 | this.serverStatus = ServerStatus.STARTED;
168 | System.out.println("Server start");
169 | }
170 |
171 | @Override
172 | public void stop() {
173 | this.serverStatus = ServerStatus.STOPED;
174 | System.out.println("Server stop");
175 | }
176 |
177 | @Override
178 | public ServerStatus getStatus() {
179 | return serverStatus;
180 | }
181 |
182 | public int getPORT() {
183 | return PORT;
184 | }
185 | }
186 | ```
187 |
188 | 问题又来了,ServerFactory没法传端口,最简单的方法是修改ServerFactory.getServer\(\)方法,增加一个端口参数。但是以后要为Server指定管理端口怎么办,又加参数?大师说NO,用配置类,为配置类加属性就行了。
189 |
190 | ```java
191 | public class ServerConfig {
192 |
193 | public static final int DEFAULT_PORT = 18080;
194 | private final int port;
195 |
196 | public ServerConfig(int PORT) {
197 | this.port = PORT;
198 | }
199 |
200 | public ServerConfig() {
201 | this.port = DEFAULT_PORT;
202 | }
203 |
204 | public int getPort() {
205 | return port;
206 | }
207 | }
208 | ```
209 |
210 | Server重构,修改构造函数
211 |
212 | ```java
213 | public class SimpleServer implements Server {
214 |
215 | private ServerStatus serverStatus = ServerStatus.STOPED;
216 | private final int port;
217 |
218 | public SimpleServer(ServerConfig serverConfig) {
219 | this.port = serverConfig.getPort();
220 | }
221 |
222 | @Override
223 | public void start() {
224 | this.serverStatus = ServerStatus.STARTED;
225 | System.out.println("Server start");
226 | }
227 |
228 | @Override
229 | public void stop() {
230 | this.serverStatus = ServerStatus.STOPED;
231 | System.out.println("Server stop");
232 | }
233 |
234 | @Override
235 | public ServerStatus getStatus() {
236 | return serverStatus;
237 | }
238 |
239 | @Override
240 | public int getPort() {
241 | return port;
242 | }
243 | }
244 | ```
245 |
246 | ServerFactory重构
247 |
248 | ```java
249 | public class ServerFactory {
250 | /**
251 | * 返回Server实例
252 | * @return
253 | */
254 | public static Server getServer(ServerConfig serverConfig) {
255 | return new SimpleServer(serverConfig);
256 | }
257 | }
258 | ```
259 |
260 | 单元测试重构
261 |
262 | ```java
263 | public class TestServer {
264 | private static Server server;
265 |
266 | @BeforeClass
267 | public static void init() {
268 | ServerConfig serverConfig = new ServerConfig();
269 | server = ServerFactory.getServer(serverConfig);
270 | }
271 |
272 | @Test
273 | public void testServerStart() {
274 | server.start();
275 | assertTrue("服务器启动后,状态是STARTED", server.getStatus().equals(ServerStatus.STARTED));
276 | }
277 |
278 | @Test
279 | public void testServerStop() {
280 | server.stop();
281 | assertTrue("服务器关闭后,状态是STOPED", server.getStatus().equals(ServerStatus.STOPED));
282 | }
283 |
284 | @Test
285 | public void testServerPort() {
286 | int port = server.getPort();
287 | assertTrue("默认端口号", ServerConfig.DEFAULT_PORT == port);
288 | }
289 | }
290 | ```
291 |
292 | 跑下测试:
293 |
294 | 
295 |
296 | OK,经过多轮重构,Server接口编写暂时完成。下一步开始实现真正有用的功能。
297 |
298 | 完整代码:https://github.com/pkpk1234/BeggarServletContainer
299 |
300 |
--------------------------------------------------------------------------------
/shi-xian-zui-ji-ben-de-servlet-zhi-chi.md:
--------------------------------------------------------------------------------
1 | # 实现最基本的Servlet支持
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------