├── 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 | ![](/assets/project-structure.jpg) 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大致如下:![](/assets/UML.jpg)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 | ![](/assets/git-br-step6.jpg) 20 | 21 | -------------------------------------------------------------------------------- /connectorjie-kou.md: -------------------------------------------------------------------------------- 1 | # Connector接口 2 | 3 | 上一步后,我们完成了一个可以接收Socket请求的服务器。这时大师又说话了,昨天周末看片去了,有个单元测试TestServer 4 | 5 | 没跑,你跑个看看,猜猜能跑过不。一跑果然不行啊,单元测试一直转圈,就不动。 6 | 7 | ![](/assets/unit-test-never-stop.jpg) 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 | ![](/assets/git-br-step3.jpg) 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 | ![](/assets/file-server.jpg) 212 | 213 | 绿色框:输入回车,返回目录下文件列表。 214 | 215 | 黄色框:输入README.MD,返回文件内容 216 | 217 | 蓝色框:输入不存在的文件,返回文件找不到。 218 | 219 | 完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step5 220 | 221 | 分支step5 222 | 223 | ![](/assets/git-br-step5.jpg) 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 | ![](/assets/test-echo.jpg) 175 | 176 | 到现在为止,我们的服务器终于有了实际功能,下一步终于可以去实现请求静态资源的功能了。 177 | 178 | 完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step4 179 | 180 | 分支step4 181 | 182 | ![](/assets/git-br-step4.jpg) 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 | ![](/assets/web-dirs.jpg) 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 | ![](/assets/web.jpg) 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后,结构如下![](/assets/http-interface-diagrams.jpg) 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 | ![](/assets/git-br-step7.jpg) 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 | ![](/assets/TestServerAcceptRequest.jpg) 176 | 177 | 到目前为止,一个单线程的可以接收请求的Server就完成了。 178 | 179 | 完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step2 180 | 181 | 分支step2 182 | 183 | ![](/assets/git-br-step2.jpg) 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 | ![](/assets/nio-echo-server.jpg) 166 | 167 | 完整代码:[https://github.com/pkpk1234/BeggarServletContainer/tree/step6](https://github.com/pkpk1234/BeggarServletContainer/tree/step6) 168 | 169 | 分支step6 170 | 171 | ![](/assets/git-br-step6.jpg) 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 | ![](/assets/TestServer.jpg) 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 | --------------------------------------------------------------------------------