2. 监听端口接收请求

上一步中我们已经定义好了Server接口,并进行了多次重构,但是实际上那个Server是没啥毛用的东西。

现在要为其添加真正有用的功能。

大师说了,饭要一口一口吃,衣服要一件一件脱,那么首先来定个小目标——启动ServerSocket监听请求,不要什么多线程不要什么NIO,先完成最简单的功能。

下面还是一步一步来写代码并进行重构优化代码结构。

关于Socket和ServerSocket怎么用,网上很多文章写得比我好,大家自己找找就好。

代码写起来很简单:(下面的代码片段有很多问题哦,大神们请不要急着喷,看完再抽)

public class SimpleServer implements Server {
... ...
@Override
public void start() {
Socket socket = null;
try {
this.serverSocket = new ServerSocket(this.port);
this.serverStatus = ServerStatus.STARTED;
System.out.println("Server start");
while (true) {
socket = serverSocket.accept();// 从连接队列中取出一个连接,如果没有则等待
System.out.println(
"新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
}
}
catch (IOException e) {
e.printStackTrace();
}
finally {
if (socket != null) {
try {
socket.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
} } @Override
public void stop() {
try {
if (this.serverSocket != null) {
this.serverSocket.close();
}
}
catch (IOException e) {
e.printStackTrace();
}
this.serverStatus = ServerStatus.STOPED;
System.out.println("Server stop");
} ... ...
}

添加单元测试:

public class TestServerAcceptRequest {
private static Server server;
// 设置超时时间为500毫秒
private static final int TIMEOUT = 500; @BeforeClass
public static void init() {
ServerConfig serverConfig = new ServerConfig();
server = ServerFactory.getServer(serverConfig);
} @Test
public void testServerAcceptRequest() {
// 如果server没有启动,首先启动server
if (server.getStatus().equals(ServerStatus.STOPED)) {
//在另外一个线程中启动server
new Thread(() -> {
server.start();
}).run();
//如果server未启动,就sleep一下
while (server.getStatus().equals(ServerStatus.STOPED)) {
System.out.println("等待server启动");
try {
Thread.sleep(500);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
Socket socket = new Socket();
SocketAddress endpoint = new InetSocketAddress("localhost",
ServerConfig.DEFAULT_PORT);
try {
// 试图发送请求到服务器,超时时间为TIMEOUT
socket.connect(endpoint, TIMEOUT);
assertTrue("服务器启动后,能接受请求", socket.isConnected());
}
catch (IOException e) {
e.printStackTrace();
}
finally {
try {
socket.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
} @AfterClass
public static void destroy() {
server.stop();
}
}

运行单元测试,我檫,怎么偶尔一直输出“等待server启动",用大师的话说就算”只看见轮子转,不见车跑“。原因其实很简单,因为多线程咯,测试线程一直无法获取到另外一个线程中更新的值。大师又说了,早看不惯满天的System.out.println和到处重复的

try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}

了。

大师还说了,代码太垃圾了,问题很多:如果Server.start()时端口被占用、权限不足,start方法根本没有抛出异常嘛,调用者难道像SB一样一直等下去,还有,Socket如果异常了,while(true)就退出了,难道一个Socket异常,整个服务器就都挂了,这代码就是一坨屎嘛,滚去重构。

首先为ServerStatus属性添加volatile,保证其可见性。

public class SimpleServer implements Server {
private volatile ServerStatus serverStatus = ServerStatus.STOPED;
... ...
}

然后引入sl4j+log4j2,替换掉漫天的System.out.println。

然后编写closeQuietly方法,专门处理socket的关闭。

public class IoUtils {

    private static Logger logger = LoggerFactory.getLogger(IoUtils.class);

    /**
* 安静地关闭,不抛出异常
* @param closeable
*/
public static void closeQuietly(Closeable closeable) {
if(closeable != null) {
try {
closeable.close();
} catch (IOException e) {
logger.error(e.getMessage(),e);
}
}
}
}

最后start方法异常时,需要让调用者得到通知,并且一个Socket异常,不影响整个服务器。

重构后再跑单元测试:一切OK。

到目前为止,一个单线程的可以接收请求的Server就完成了。

3. Connector接口

上一步后,我们完成了一个可以接收Socket请求的服务器。这时大师又说话了,昨天周末看片去了,有个单元测试TestServer

没跑,你跑个看看,猜猜能跑过不。一跑果然不行啊,单元测试一直转圈,就不动。

因为server.start();会让当前线程无限循环,不断等待Socket请求,所以下面的单元测试方法根本不会走到断言那一步,也不会退出,所以大家都卡住了。

@Test
public void testServerStart() throws IOException {
server.start();
assertTrue("服务器启动后,状态是STARTED", server.getStatus().equals(ServerStatus.STARTED));
}

修改起来很简单,让server.start();在单独的线程里面执行就好,然后再循环判断ServerStatus是否为STARTED,等待服务器启动。

如下:

@Test
public void testServerStart() throws IOException {
server.start();
//如果server未启动,就sleep一下
while (server.getStatus().equals(ServerStatus.STOPED)) {
logger.info("等待server启动");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
}
assertTrue("服务器启动后,状态是STARTED", server.getStatus().equals(ServerStatus.STARTED));
}

这时大师又说了,循环判断服务器是否启动的代码片段,和TestServerAcceptRequest里面有重复代码,启动Server的代码也是重复的,一看就是Ctrl+c Ctrl+v的,你就不会抽象出一个父类啊。再重构:

public abstract class TestServerBase {
private static Logger logger = LoggerFactory.getLogger(TestServerBase.class);
/**
* 在单独的线程中启动Server,如果启动不成功,抛出异常
*
* @param server
*/
protected void startServer(Server server) {
//在另外一个线程中启动server
new Thread(() -> {
try {
server.start();
} catch (IOException e) {
//转为RuntimeException抛出,避免异常丢失
throw new RuntimeException(e);
}
}).start();
}
/**
* 等待Server启动
*
* @param server
*/
protected void waitServerStart(Server server) {
//如果server未启动,就sleep一下
while (server.getStatus().equals(ServerStatus.STOPED)) {
logger.info("等待server启动");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
}
}
}

和Server相关的单元测试都可以extends于TestServerBase。

public class TestServer extends TestServerBase {
... ...
@Test
public void testServerStart() {
startServer(server);
waitServerStart(server);
assertTrue("服务器启动后,状态是STARTED", server.getStatus().equals(ServerStatus.STARTED));
}
... ...
}
public class TestServerAcceptRequest extends TestServerBase {
... ...
@Test
public void testServerAcceptRequest() {
// 如果server没有启动,首先启动server
if (server.getStatus().equals(ServerStatus.STOPED)) {
startServer(server);
waitServerStart(server);
.... ...
}
... ...
}

再次执行单元测试,一切都OK。搞定单元测试后,大师又说了,看看你写的SimpleServer的start方法,

SimpleServe当前就是用来监听并接收Socket请求的,start方法就应该如其名,只是启动监听,修改ServerStatus为STARTED,接受请求什么的和start方法有毛关系,弄出去。

按照大师说的重构一下,单独弄个accept方法,专门用于接受请求。

    @Override
public void start() throws IOException {
//监听本地端口,如果监听不成功,抛出异常
this.serverSocket = new ServerSocket(this.port);
this.serverStatus = ServerStatus.STARTED;
accept();
return;
}
private void accept() {
while (true) {
Socket socket = null;
try {
socket = serverSocket.accept();
logger.info("新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
} catch (IOException e) {
logger.error(e.getMessage(), e);
} finally {
IoUtils.closeQuietly(socket);
}
}
}

这时大师又发话了 ,我要用SSL,你直接new ServerSocket有啥用,重构去。

从start方法里面其实可以看到,Server启动接受\响应请求的组件后,组件的任何操作就和Server对象没一毛钱关系了,Server只是管理一下组件的生命周期而已。那么接受\响应请求的组件可以抽象出来,这样Server就不必和具体实现打交道了。

按照Tomcat和Jetty的惯例,接受\响应请求的组件叫Connector,生命周期也可以抽象成一个接口LifeCycle。根据这个思路去重构。

public interface LifeCycle {
void start();
void stop();
}
public abstract class Connector implements LifeCycle {
@Override
public void start() {
init();
acceptConnect();
}
protected abstract void init() throws ConnectorException;
protected abstract void acceptConnect() throws ConnectorException;
}

将SimpleServer中和Socket相关的代码全部移动到SocketConnector里面

public class SocketConnector extends Connector {
... ...
@Override
protected void init() throws ConnectorException {
//监听本地端口,如果监听不成功,抛出异常
try {
this.serverSocket = new ServerSocket(this.port);
this.started = true;
} catch (IOException e) {
throw new ConnectorException(e);
}
}
@Override
protected void acceptConnect() throws ConnectorException {
new Thread(() -> {
while (true && started) {
Socket socket = null;
try {
socket = serverSocket.accept();
LOGGER.info("新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
} catch (IOException e) {
//单个Socket异常,不要影响整个Connector
LOGGER.error(e.getMessage(), e);
} finally {
IoUtils.closeQuietly(socket);
}
}
}).start();
}
@Override
public void stop() {
this.started = false;
IoUtils.closeQuietly(this.serverSocket);
}
... ...
}

SimpleServer重构为

public class SimpleServer implements Server {
... ...
private SocketConnector socketConnector;
... ...
@Override
public void start() throws IOException {
socketConnector.start();
this.serverStatus = ServerStatus.STARTED;
}
@Override
public void stop() {
socketConnector.stop();
this.serverStatus = ServerStatus.STOPED;
logger.info("Server stop");
}
... ...
}

跑单元测试,全部OK,证明代码没问题。

大师瞄了一眼,说不 给你说了么,面向抽象编程啊,为毛还直接引用了SocketConnector,还有,我想要多个Connector,继续给我重构去。

重构思路简单,将SocketConnector替换为抽象类型Connector即可,但是怎么实例化呢,总有地方要处理这个抽象到具体的过程啊,这时又轮到Factory类干这个脏活了。

再次重构。

增加ConnectorFactory接口,及其实现SocketConnectorFactory

public class SocketConnectorFactory implements ConnectorFactory {
private final SocketConnectorConfig socketConnectorConfig;
public SocketConnectorFactory(SocketConnectorConfig socketConnectorConfig) {
this.socketConnectorConfig = socketConnectorConfig;
}
@Override
public Connector getConnector() {
return new SocketConnector(this.socketConnectorConfig.getPort());
}
}

SimpleServer也进行相应修改,不再实例化任何具体实现,只通过构造函数接收对应的抽象。

public class SimpleServer implements Server {
private static Logger logger = LoggerFactory.getLogger(SimpleServer.class);
private volatile ServerStatus serverStatus = ServerStatus.STOPED;
private final int port;
private final List<Connector> connectorList;
public SimpleServer(ServerConfig serverConfig, List<Connector> connectorList) {
this.port = serverConfig.getPort();
this.connectorList = connectorList;
}
@Override
public void start() {
connectorList.stream().forEach(connector -> connector.start());
this.serverStatus = ServerStatus.STARTED;
}
@Override
public void stop() {
connectorList.stream().forEach(connector -> connector.stop());
this.serverStatus = ServerStatus.STOPED;
logger.info("Server stop");
}
... ...
}

ServerFactory也进行修改,将Server需要的依赖传递到Server的构造函数中。

public class ServerFactory {
/**
* 返回Server实例
*
* @return
*/
public static Server getServer(ServerConfig serverConfig) {
List<Connector> connectorList = new ArrayList<>();
ConnectorFactory connectorFactory =
new SocketConnectorFactory(new SocketConnectorConfig(serverConfig.getPort()));
connectorList.add(connectorFactory.getConnector());
return new SimpleServer(serverConfig,connectorList);
}
}

这样我们就将对具体实现的依赖限制到了不多的几个Factory中,最核心的Server部分只操作了抽象。

执行所有单元测试,再次全部成功。

虽然目前为止,Server还是只能接收请求,但是代码结构还算OK,为下面编写请求处理做好了准备。

完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step3

乞丐版servlet容器第2篇的更多相关文章

  1. 乞丐版servlet容器第1篇

    本系列参照pkpk1234大神的BeggarServletContainer,具体请访问:https://github.com/pkpk1234/BeggarServletContainer. 一步一 ...

  2. 乞丐版servlet容器第4篇

    6. NIOConnector 现在为Server添加NIOConnector,添加之前可以发现我们的代码其实是有问题的.比如现在的代码是无法让服务器支持同时监听多个端口和IP的,如同时监听 127. ...

  3. 乞丐版servlet容器第3篇

    4 EventListener接口 让我们继续看SocketConnector中的acceptConnect方法: @Override protected void acceptConnect() t ...

  4. 对Servlet容器的补充和一个问题的请教

    [0]README 0.1)本文是对 一个servlet容器  的补充: 0.2)发这个博文的最终目的是为了请教各位前辈,帮我解决一个问题,问题描述在文末, 谢谢: [1]Servlet容器 1.1) ...

  5. 【串线篇】spring boot使用外置的Servlet容器

    嵌入式Servlet容器:应用打成可执行的jar 优点:简单.便携: 缺点:默认不支持JSP.优化定制比较复杂 (使用定制器[ServerProperties/自定义EmbeddedServletCo ...

  6. 【串线篇】spring boot嵌入式Servlet容器自动配置原理

    EmbeddedServletContainerAutoConfiguration:嵌入式的Servlet容器自动配置? @AutoConfigureOrder(Ordered.HIGHEST_PREC ...

  7. 【串线篇】spring boot嵌入式Servlet容器启动原理;

    什么时候创建嵌入式的Servlet容器工厂?什么时候获取嵌入式的Servlet容器并启动Tomcat: 获取嵌入式的Servlet容器工厂: 1).SpringBoot应用启动运行run方法 2).r ...

  8. 【串线篇】spring boot配置嵌入式servlet容器

    SpringBoot默认使用Tomcat作为嵌入式的Servlet容器 问题? 一.如何定制和修改Servlet容器的相关配置 1.方法1修改和server有关的配置(ServerProperties ...

  9. 深入剖析tomcat之一个简单的servlet容器

    上一篇,我们讲解了如果开发一个简单的Http服务器,这一篇,我们扩展一下,让我们的服务器具备servlet的解析功能. 简单介绍下Servlet接口 如果我们想要自定义一个Servlet,那么我们必须 ...

随机推荐

  1. Spring @Trasactionl 失效, JDK,CGLIB动态代理

    @Transaction:  http://blog.csdn.net/bao19901210/article/details/41724355 Spring上下文:  http://blog.csd ...

  2. vue基础——Class与Style绑定

    Class与Style绑定 操作元素的class列表和内联样式是数据绑定的一个常见的需求. 因为它们都是属性,所以我们可以用v-bind来处理它们:只需要通过表达式计算出字符串结果即可.不过,字符串拼 ...

  3. sqlserver字符串拆分函数

    CREATE FUNCTION f_splitSTR(@s varchar(8000), --待分拆的字符串@split varchar(10) --数据分隔符)RETURNS @re TABLE(c ...

  4. 协变(covariance),逆变(contravariance)与不变(invariance)

    协变,逆变与不变 能在使用父类型的场景中改用子类型的被称为协变. 能在使用子类型的场景中改用父类型的被称为逆变. 不能做到以上两点的被称为不变. 以上的场景通常包括数组,继承和泛型. 协变逆变与泛型( ...

  5. Delphi 动态数组合并

    TIntArray = array of Integer; function MergeArray(const ArrayA, ArrayB: TIntArray): TIntArray; var i ...

  6. Javaweb连接数据库

    在JSP中使用JDBC驱动连接mysql数据库. 1: 下载mysql的Java连接程序 2: 解压目录下的mysql-connector-java-5.0.24-bin.jar文件就是连接MySql ...

  7. Python内置类型性能分析

    Python内置类型性能分析 timeit模块 timeit模块可以用来测试一小段Python代码的执行速度. class timeit.Timer(stmt='pass', setup='pass' ...

  8. 在Ubuntu上安装微信

    1) 从https://github.com/geeeeeeeeek/electronic-wechat/releases地址中下载linux-x64.tar.gz文件到/opt/wechat文件夹 ...

  9. hdoj1043 Eight(逆向BFS+打表+康拓展开)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1043 思路: 由于自己对康拓展开用的太少,看到这个题没想到康拓展开,最开始打算直接转换为数字,但太占内 ...

  10. Water Tree(树链剖分+dfs时间戳)

    Water Tree http://codeforces.com/problemset/problem/343/D time limit per test 4 seconds memory limit ...