TCP/IP 服务器的 Spring Boot 处理

naz*_*art 7 java sockets tcp ethernet spring-boot

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

\n
Establishing a connection\nThe client connects to the configured server via TCP / IP.\nAfter the connection has been established, the client initially sends a heartbeat message to the\nServer:\n\n{\n  "MessageID": "Heartbeat"\n}\n\nResponse:\n{\n  "ResponseCode": "Ok"\n}\n\nCommunication process\nTo maintain the connection, the client sends every 10 seconds when inactive\nHeartbeat message.\nServer and client must close the connection if they are not receiving a message for longer than 20 seconds.\nAn answer must be given within 5 seconds to request. \nIf no response is received, the connection must also be closed.\nThe protocol does not contain numbering or any other form of identification.\nCommunication partner when sending the responses makes sure that they are in the same sequence.\n\nMessage structure:\nThe messages are embedded in an STX-ETX frame.\nSTX (0x02) message ETX (0x03)\nAn `escaping` of STX and ETX within the message is not necessary since it is in JSON format\n\nEscape sequence are following:\n
Run Code Online (Sandbox Code Playgroud)\n
\n

JSON.stringify({"a":"\\x02\\x03\\x10"})\xe2\x86\x92"{"a\\":"\\u0002\\u0003\\u0010\\"} ”

\n
\n

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

\n
{\n  "MessageID": "CheckAccess"\n  "Parameters": {\n    "MediaType": "type",\n    "MediaData": "data"\n  }\n} \n
Run Code Online (Sandbox Code Playgroud)\n

以及适当的回应:

\n
{\n  "ResponseCode":   "some-code",\n  "DisplayMessage": "some-message",\n  "SessionID":      "some-id"\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n
\n

解决方案

\n

TCP服务器配置如下:

\n
@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class TCPServer {\n    private final InetSocketAddress hostAddress;\n    private final ServerBootstrap serverBootstrap;\n\n    private Channel serverChannel;\n\n    @PostConstruct\n    public void start() {\n        try {\n            ChannelFuture serverChannelFuture = serverBootstrap.bind(hostAddress).sync();\n            log.info("Server is STARTED : port {}", hostAddress.getPort());\n\n            serverChannel = serverChannelFuture.channel().closeFuture().sync().channel();\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    @PreDestroy\n    public void stop() {\n        if (serverChannel != null) {\n            serverChannel.close();\n            serverChannel.parent().close();\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

它的配置也是如此:

\n
@Configuration\n@RequiredArgsConstructor\n@EnableConfigurationProperties(NettyProperties.class)\npublic class NettyConfiguration {\n\n    private final LoggingHandler loggingHandler = new LoggingHandler(LogLevel.DEBUG);\n    private final NettyProperties nettyProperties;\n\n    @Bean(name = "serverBootstrap")\n    public ServerBootstrap bootstrap(SimpleChannelInitializer initializer) {\n        ServerBootstrap bootstrap = new ServerBootstrap();\n        bootstrap.group(bossGroup(), workerGroup())\n                .channel(NioServerSocketChannel.class)\n                .handler(loggingHandler)\n                .childHandler(initializer);\n        bootstrap.option(ChannelOption.SO_BACKLOG, nettyProperties.getBacklog());\n        bootstrap.childOption(ChannelOption.SO_KEEPALIVE, nettyProperties.isKeepAlive());\n        return bootstrap;\n    }\n\n    @Bean(destroyMethod = "shutdownGracefully")\n    public NioEventLoopGroup bossGroup() {\n        return new NioEventLoopGroup(nettyProperties.getBossCount());\n    }\n\n    @Bean(destroyMethod = "shutdownGracefully")\n    public NioEventLoopGroup workerGroup() {\n        return new NioEventLoopGroup(nettyProperties.getWorkerCount());\n    }\n\n    @Bean\n    @SneakyThrows\n    public InetSocketAddress tcpSocketAddress() {\n        return new InetSocketAddress(nettyProperties.getTcpPort());\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

初始化逻辑:

\n
@Component\n@RequiredArgsConstructor\npublic class SimpleChannelInitializer extends ChannelInitializer<SocketChannel> {\n\n    private final StringEncoder stringEncoder = new StringEncoder();\n    private final StringDecoder stringDecoder = new StringDecoder();\n\n    private final QrReaderProcessingHandler readerServerHandler;\n    private final NettyProperties nettyProperties;\n\n    @Override\n    protected void initChannel(SocketChannel socketChannel) {\n        ChannelPipeline pipeline = socketChannel.pipeline();\n\n        pipeline.addLast(new DelimiterBasedFrameDecoder(1024 * 1024, Delimiters.lineDelimiter()));\n        pipeline.addLast(new ReadTimeoutHandler(nettyProperties.getClientTimeout()));\n        pipeline.addLast(stringDecoder);\n        pipeline.addLast(stringEncoder);\n        pipeline.addLast(readerServerHandler);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

属性配置:

\n
@Getter\n@Setter\n@ConfigurationProperties(prefix = "netty")\npublic class NettyProperties {\n    @NotNull\n    @Size(min = 1000, max = 65535)\n    private int tcpPort;\n\n    @Min(1)\n    @NotNull\n    private int bossCount;\n\n    @Min(2)\n    @NotNull\n    private int workerCount;\n\n    @NotNull\n    private boolean keepAlive;\n\n    @NotNull\n    private int backlog;\n\n    @NotNull\n    private int clientTimeout;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

和一个片段application.yml

\n
netty:\n  tcp-port: 9090\n  boss-count: 1\n  worker-count: 14\n  keep-alive: true\n  backlog: 128\n  client-timeout: 20\n
Run Code Online (Sandbox Code Playgroud)\n

处理程序非常简单。

\n

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

\n
\n

远程登录本地主机 9090

\n
\n

那里工作得很好。我希望客户的访问能够顺利。

\n

Avn*_*ish 6

由于该协议不是基于 HTTP 的(与首先搭载在 HTTP 上的 WebSocket 不同),因此您唯一的选择是自己使用 TCP 服务器并将其连接到 Spring 上下文中,以充分利用 Spring 的优势。

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

事实上,Spring Boot 提供了开箱即用的Netty HTTP 服务器,但这不是您所需要的

使用 Netty 和 SpringBoot项目进行 TCP 通信服务器是您所需要的一个简单而有效的示例。

看一下这个项目中的TCPServer,它使用 Netty 的 ServerBootstrap 来启动自定义 TCP 服务器。

拥有服务器后,您可以连接Netty 编解码器Jackson或任何其他消息转换器,只要您认为适合您的应用程序域数据编组/解组

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

                ----> HTTP 服务器 (be_http)
               |
----> HAProxy -
               |
                ----> TCP 服务器 (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 指令:

           
听 443
  TCP模式
  绑定:443 名称 tcpsvr
  /* 添加其他常规指令 */
  tcp-请求检查-延迟 1s
  tcp-请求内容如果是 HTTP 则接受
  tcp-请求内容接受 if !HTTP
  如果是 HTTP,则使用服务器 be_http
  如果 !HTTP 使用服务器 be_tcp
  /* 后端服务器定义 */
  服务器 be_http 127.0.0.1:8080
  服务器 be_tcp 127.0.0.1:9090 发送代理

以下 HAProxy 文档链接特别有用

  1. 从缓冲区内容中获取样本 - 第 6 层
  2. 预定义 ACL
  3. TCP 请求检查延迟
  4. TCP请求内容

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

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

在 be_tcp 后端中,您可以使用 Netty 的HAProxyMessage使用sourceAddress() API获取实际的源 IP 地址。总而言之,这是一个可行的解决方案。我自己已经使用了 HAProxy 和代理协议(在两端,前端和后端),它对于工作来说更加稳定。