TCP建立连接/断开连接的知识工作中真的用不到吗?

​关于TCP连接的建立和释放过程的问题,已经快变成现在互联网公司面试必问的题目了。不过很多时候,大部分人都觉得这只是书本上的知识。实际工作中怎么可能用到呢?网络连接协议这么底层的东西,写代码的时候会用不就好了?

然后我今天还真的遇到了一个问题,下面就开始了!

首先,公司使用了consul作为服务注册组件。众所周知,consul的默认端口是8500。

由于我在应用中编写了很多的测试案例,有部分测试案例是和consul的实例化过程密切相关的。

consul环境公司有提供测试环境,但考虑到本地测试的顺畅性,我一般不太愿意在本地测试的时候就使用共用环境,能自己搭建的一般都考虑自己搭建。

所以consul我在本地也搭建了一个单节点的集群,那访问地址自然就是http://localhost:8500了。

考虑到有时候我运行全量单元测试案例的时候,不一定会启动本地的consul服务,但我又不想在没有启动consul服务的情况下,导致运行全量测试案例报错。

所以,我就写了一个用于监听本地某个tcp端口是否处于监听状态的工具类,代码如下:

public class ServiceStatusUtils {
    /**
     * 用于监测本地端口的可用性,这样可以方便在本地未启动相关服务时,关闭相关的测试案例;
     * 比如本地的consul(8500),本地的MySQL(3306)
     * @param port
     * @return
     */
    public static boolean isLocalPortActive(int port) {
        return isConnectionActive("127.0.0.1", port);
    }

    /**
     * 用于监测远端服务的可用性,这样可以根据远端服务的状态构建不同的测试案例集合
     * @param ipAddress
     * @param port
     * @return
     */
    public static boolean isConnectionActive(String ipAddress, int port) {
        try (Socket socket = new Socket()) {
            SocketAddress sa = new InetSocketAddress(ipAddress, port);
            socket.connect(sa);
            return true;
        } catch (IOException e) {
            return false;
        }
    }
}

有了这个工具类,再结合Spring Boot启动过程中支持的基于条件判断加载实例对象的功能,我便可以编写基于本地consul环境是否启用作为判断条件,执行不同测试逻辑分支的测试类了。

如下:

/**
 * 如下测试案例只有在本地consul启动后才会执行,否则会直接返回true,本地consul启动端口需要是默认的8500
 * @author jingxuan
 * @date 2020/12/17 7:31 下午
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ThriftServiceConfigurationTest.MockInsightServiceConfiguration.class)
@TestPropertySource(properties = {"thrift.service.port=9888", "thrift.service.workerThreads=100", "thrift.service.name=insight" })
@ImportAutoConfiguration(ConfigurationPropertiesAutoConfiguration.class)
@Slf4j
public class ThriftServiceConfigurationTest {
    @Configuration
    @Conditional(LocalConsulIsActive.class)
    @Import(ThriftServiceConfiguration.class)
    static class MockInsightServiceConfiguration {
        @Bean
        InsightService.Iface insightService() {
            return new InsightService.Iface() {
                @Override
                public void ping() throws TException {
                }

                @Override
                public GetUserPropertyResponse getUserProperty(Context ctx, GetUserPropertyRequest req) throws TException {
                    return null;
                }
                @Override
                public UpdateUserPropertyResponse updateUserProperty(Context ctx, UpdateUserPropertyRequest req) throws TException {
                    return null;
                }
                @Override
                public SetUserPropertyResponse setUserProperty(Context ctx, SetUserPropertyRequest req) throws TException {
                    return null;
                }
                @Override
                public GetUserActivityResponse getUserActivity(Context ctx, GetUserActivityRequest req) throws TException {
                    return null;
                }
                @Override
                public UpdateUserActivityResponse updateUserActivity(Context ctx, UpdateUserActivityRequest req) throws TException {
                    return null;
                }
                @Override
                public SetUserActivityResponse setUserActivity(Context ctx, SetUserActivityRequest req) throws TException {
                    return null;
                }
            };
        }

        @Bean
        ThriftServerAddressRegisterConsul thriftServerAddressRegisterConsul() {
            return new ThriftServerAddressRegisterConsul();
        }

    }

    static class LocalConsulIsActive extends SpringBootCondition {
        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
            if (ServiceStatusUtils.isLocalPortActive(8500)) {
                return ConditionOutcome.match();
            }
            log.info("本地consul未启动,相关测试案例将跳过;请在正式提交代码前务必开启本地consul测试功能正确性!");
            return ConditionOutcome.noMatch("Local consul is not started.");
        }
    }

    @Autowired(required = false)
    protected MockInsightServiceConfiguration mockConfig;

    @Autowired(required = false)
    protected ThriftServiceConfiguration configuration;

    @Autowired(required = false)
    protected NettyServerProxyFactory proxyFactory;

    @Test
    public void shouldLoadConfigurationBean() {
        if (mockConfig == null) return ;
        assertThat(configuration, is(notNullValue()));
        assertThat(configuration.getPort(), is(9888));
        assertThat(configuration.getName(), is("insight"));
        assertThat(configuration.getWorkerThreads(), is(100));
    }

    @Test
    public void shouldLoadPropertyFromPropertiesFile() {
        if (mockConfig == null) return ;
        assertThat(configuration.getPort(), is(9888));
        assertThat(configuration.getWorkerThreads(), is(100));
    }

    @Test
    public void shouldLoadNettyServerProxyFactory() {
        if (mockConfig == null) return ;
        assertThat(proxyFactory, is(notNullValue()));
    }
}

主要的封装逻辑,主要就是基于@Conditional(LocalConsulIsActive.class)来做的。

这套框架一直运行的好好的,无论我本地有没有启动consul服务,我的测试案例都是可以正常全量通过。

然后昨天,这套逻辑突然不好用了!执行测试案例的时候,结果变成了这样:

使用terminal确认了下本地的8500端口,发现返回是这样的:

(base) ➜  ~ telnet 127.0.0.1 8500
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Connection closed by foreign host.

看起来TCP连接是在连接上了之后,立刻就被服务端断开了。

抓包确认下:

一共八个TCP包,三个建立连接的:SYN -> SYN ACK -> ACK;一个更新数据框口大小的:TCP Window Update;四个断开连接的:FIN ACK -> ACK -> FIN ACK -> ACK。

可以很明显地发现,断开连接是由8500端口发起,也就是由服务器端主动断开连接了。

再看下本地网络监听情况:

a(base) ➜  ~ lsof -nP -iTCP:8500 | grep LISTEN

com.docke   758 jingxuan  100u  IPv6 0x3c2d2ad55e1c0a53      0t0  TCP *:8500 (LISTEN)

啊哈!原来是docker干的。因为我最近将consul从本地裸装服务迁移到docker镜像中了,镜像的启动命令如下:

docker run --name devtools -d \
-p 3306:3306 -p 2181:2181 -p 9092:9092 \
-p 8500:8500 -p 8600:8600/udp -p 8021:8021 \
-p 9000:9000 -p 8088:8088 -p 9870:9870 -p 4040:4040 \
-v /Users/jingxuan/docker/consul/:/var/consul/ \
-v /Users/jingxuan/docker/kafka-logs/:/tmp/kafka-logs/ \
-v /Users/jingxuan/docker/hadoop/data/:/tmp/hadoop/data/ \
jingxuan/devtools:mysql-remote-connect

看起来docker会在镜像启动后,对所有-p参数配置的端口都直接启用tcp监听。然后,8500端口的consul我在docker容器中并没有启动服务。所以,连接一旦建立,容器内部发现处理不了这类请求,立马就请求客户端断开连接。

为了让我的测试案例可以重新正常跑起来,我写了一段测试代码,确认是否可以通过调整部分逻辑,支持这种奇怪的场景:

    public static void main(String[] args) throws IOException {
        InetSocketAddress sockAdr = new InetSocketAddress("localhost", 8500);
        Socket socket = new Socket();
        socket.connect(sockAdr);
        socket.isInputShutdown();
        System.out.println(socket);
    }

System.out.println(socket);增加断点,查看socket中是否有api可以让我知道服务端主动断开连接了,结果让人失望:

从上面的结果来看,socket对象正常的不能再正常。难道是使用java的socket连接时,服务端没有发送断链请求吗?

抓个包看看:

嗯,服务端发送断链请求了,Java端表示不管,我就是不回复断开链接。厉害了,Java老哥!

最后的结论是,这种情况下,光靠TCP/IP四层协议判断consul服务是否生效已经不可能了(至少在Java语言中是不可能的),只能使用HTTP层来做条件判断了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

镜悬xhs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值