关于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层来做条件判断了。