TCP / IP服务器的春季启动处理

时间:2020-07-15 10:47:41

标签: java spring-boot sockets tcp ethernet

必须实现服务器以通过以太网连接处理以下协议:

Establishing a connection
The client connects to the configured server via TCP / IP.
After the connection has been established, the client initially sends a heartbeat message to the
Server:

{
  "MessageID": "Heartbeat"
}

Response:
{
  "ResponseCode": "Ok"
}

Communication process
To maintain the connection, the client sends every 10 seconds when inactive
Heartbeat message.
Server and client must close the connection if they are not receiving a message for longer than 20 seconds.
An answer must be given within 5 seconds to request. 
If no response is received, the connection must also be closed.
The protocol does not contain numbering or any other form of identification.
Communication partner when sending the responses makes sure that they are in the same sequence.

Message structure:
The messages are embedded in an STX-ETX frame.
STX (0x02) message ETX (0x03)
An `escaping` of STX and ETX within the message is not necessary since it is in JSON format

Escape sequence are following:

JSON.stringify({“ a”:“ \ x02 \ x03 \ x10”})→“ {” a \“:” \ u0002 \ u0003 \ u0010 \“}”

不仅应使用心跳消息。典型的消息应该像这样:

{
  "MessageID": "CheckAccess"
  "Parameters": {
    "MediaType": "type",
    "MediaData": "data"
  }
} 

以及适当的答复:

{
  "ResponseCode":   "some-code",
  "DisplayMessage": "some-message",
  "SessionID":      "some-id"
}

它应该是多客户端服务器。而且协议没有任何标识。
但是,我们必须至少识别客户端的发送地址。

找不到有关如何将此类服务器添加到Spring Boot应用程序并在启动时启用并处理其输入和输出逻辑的解决方案。
任何建议都将受到高度赞赏。


解决方案

为TCP服务器配置以下内容:

@Slf4j
@Component
@RequiredArgsConstructor
public class TCPServer {
    private final InetSocketAddress hostAddress;
    private final ServerBootstrap serverBootstrap;

    private Channel serverChannel;

    @PostConstruct
    public void start() {
        try {
            ChannelFuture serverChannelFuture = serverBootstrap.bind(hostAddress).sync();
            log.info("Server is STARTED : port {}", hostAddress.getPort());

            serverChannel = serverChannelFuture.channel().closeFuture().sync().channel();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    @PreDestroy
    public void stop() {
        if (serverChannel != null) {
            serverChannel.close();
            serverChannel.parent().close();
        }
    }
}

@PostConstruct在应用程序启动期间启动服务器。

它的配置:

@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(NettyProperties.class)
public class NettyConfiguration {

    private final LoggingHandler loggingHandler = new LoggingHandler(LogLevel.DEBUG);
    private final NettyProperties nettyProperties;

    @Bean(name = "serverBootstrap")
    public ServerBootstrap bootstrap(SimpleChannelInitializer initializer) {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup(), workerGroup())
                .channel(NioServerSocketChannel.class)
                .handler(loggingHandler)
                .childHandler(initializer);
        bootstrap.option(ChannelOption.SO_BACKLOG, nettyProperties.getBacklog());
        bootstrap.childOption(ChannelOption.SO_KEEPALIVE, nettyProperties.isKeepAlive());
        return bootstrap;
    }

    @Bean(destroyMethod = "shutdownGracefully")
    public NioEventLoopGroup bossGroup() {
        return new NioEventLoopGroup(nettyProperties.getBossCount());
    }

    @Bean(destroyMethod = "shutdownGracefully")
    public NioEventLoopGroup workerGroup() {
        return new NioEventLoopGroup(nettyProperties.getWorkerCount());
    }

    @Bean
    @SneakyThrows
    public InetSocketAddress tcpSocketAddress() {
        return new InetSocketAddress(nettyProperties.getTcpPort());
    }
}

初始化逻辑:

@Component
@RequiredArgsConstructor
public class SimpleChannelInitializer extends ChannelInitializer<SocketChannel> {

    private final StringEncoder stringEncoder = new StringEncoder();
    private final StringDecoder stringDecoder = new StringDecoder();

    private final QrReaderProcessingHandler readerServerHandler;
    private final NettyProperties nettyProperties;

    @Override
    protected void initChannel(SocketChannel socketChannel) {
        ChannelPipeline pipeline = socketChannel.pipeline();

        pipeline.addLast(new DelimiterBasedFrameDecoder(1024 * 1024, Delimiters.lineDelimiter()));
        pipeline.addLast(new ReadTimeoutHandler(nettyProperties.getClientTimeout()));
        pipeline.addLast(stringDecoder);
        pipeline.addLast(stringEncoder);
        pipeline.addLast(readerServerHandler);
    }
}

属性配置:

@Getter
@Setter
@ConfigurationProperties(prefix = "netty")
public class NettyProperties {
    @NotNull
    @Size(min = 1000, max = 65535)
    private int tcpPort;

    @Min(1)
    @NotNull
    private int bossCount;

    @Min(2)
    @NotNull
    private int workerCount;

    @NotNull
    private boolean keepAlive;

    @NotNull
    private int backlog;

    @NotNull
    private int clientTimeout;
}

application.yml的摘录:

netty:
  tcp-port: 9090
  boss-count: 1
  worker-count: 14
  keep-alive: true
  backlog: 128
  client-timeout: 20

处理程序很简单。

通过在控制台上运行进行本地检查:

telnet本地主机9090

在那很好用。我希望可以从客户端访问它。

1 个答案:

答案 0 :(得分:3)

由于该协议不是基于HTTP的(与WebSocket首先依赖于HTTP的WebSocket不同),您唯一的选择是 自己使用TCP服务器 在spring环境中将其连接起来,以充分利用spring的优势。

Netty 以低级TCP / IP通信而闻名,并且很容易在Spring应用程序中封装Netty服务器。

实际上,spring boot提供了Netty HTTP server,但 这不是您所需要的

TCP communication server with Netty And SpringBoot项目是您需要的简单有效的示例。

使用该项目使用Netty的ServerBootstrap启动自定义TCP服务器,来看看TCPServer

拥有服务器后,您可以连接 Netty编解码器 Jackson 或任何其他消息似乎适合您的应用程序域数据 编组/解组 的计算机转换器。

[更新-2020年7月17日]
针对问题的最新理解(HTTP和TCP请求都终止于同一端点),以下是更新的解决方案建议

                ----> HTTP Server (be_http)
               |
----> HAProxy -
               |
                ----> TCP Server (be_tcp)
                

此解决方案必须进行以下更改/添加:

  1. 在现有的Spring Boot应用程序中添加基于Netty的侦听器,或为TCP服务器创建单独的Spring Boot应用程序。假设此端点正在监听端口9090上的TCP流量
  2. 将HAProxy添加为入口流量的终结点
  3. 配置HAProxy,以便将所有HTTP通信发送到端口8080上的现有Spring Boot HTTP终结点(称为be_http)
  4. 配置HAProxy,以便将所有非HTTP流量发送到端口9090上的新TCP Spring Boot端点(称为be_tcp)。

遵循HAProxy配置就足够了。这些是与该问题相关的摘录,请添加其他适用于常规HAProxy设置的HAProxy指令:

           
listen 443
  mode tcp
  bind :443 name tcpsvr
  /* add other regular directives */
  tcp-request inspect-delay 1s
  tcp-request content accept if HTTP
  tcp-request content accept if !HTTP
  use-server be_http if HTTP
  use-server be_tcp if !HTTP
  /* backend server definition */
  server be_http 127.0.0.1:8080
  server be_tcp 127.0.0.1:9090 send-proxy

以下HAProxy文档链接特别有用

  1. Fetching samples from buffer contents - Layer 6
  2. Pre-defined ACLs
  3. tcp-request inspect-delay
  4. tcp-request content

我个人将亲自试用并验证 tcp-request inspect-delay 并根据实际需要进行调整,因为在最坏的情况下,这可能会增加请求的延迟已建立连接但尚无内容可用于评估请求是否为HTTP的情况。

要满足 的需要,我们必须至少识别出客户端的发送源IP地址 ,您可以选择在使用Proxy Protocol的同时将其发送回后端。我已经更新了上面的示例配置,在be_tcp中添加了代理协议(添加了send_proxy)。我还从be_http中删除了send_proxy,因为在春季启动时不需要它,相反,您可能会依赖于常规的X-Forwarded-For标头作为be_http后端。

在be_tcp后端中,您可以使用HAProxyMessage API使用Netty的sourceAddress()来获取实际的源IP地址。总而言之,这是一个可行的解决方案。我本人已将HAProxy与代理协议一起使用(在两端,前端和后端),并且工作稳定得多。

相关问题