简体   繁体   中英

spring boot handling for TCP/IP server

Have to implement a server for handling the following protocol through Ethernet connection:

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 \\ "}"

Not only heartbeat messages should be used. A typical message should be like:

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

And the appropriate response:

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

It should be a multi-client server. And protocol doesn't have any identification.
However, we have to identify the client at least the IP address from which it was sent.

Could not find some solution on how to add such server to Spring Boot application and enable on startup & handle input and output logic for it.
Any suggestions are highly appreciated.


Solution

Configured following for TCP server:

@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 launches server during startup of an application.

Configuration for it as well:

@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());
    }
}

Initialization logic:

@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);
    }
}

Properties configuration:

@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;
}

and a snippet from application.yml :

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

And handler is quite trivial.

Checked locally by running at the console:

telnet localhost 9090

It works fine there. I hope it will be fine for access from clients.

Since the protocol is NOT based on top of HTTP (unlike WebSocket which piggyback on HTTP in the first place), your only option is to use TCP server yourself and wire it up within spring context to gain full advantage of spring along with.

Netty is best known for low-level TCP/IP communication and it's easy to wrap up Netty server within spring app.

In fact, spring boot provides Netty HTTP server out of the box but this is not what you need .

TCP communication server with Netty And SpringBoot project is a simple and effective example of what you need.

Take a look at TCPServer from this project which uses Netty's ServerBootstrap for starting custom TCP server.

Once you have the server, you can wire up either Netty codecs OR Jackson OR any other message converter as you seem fit for your application domain data marshalling/unmarshalling .

[Update - July 17, 2020]
Against the updated understanding of the question (both HTTP and TCP requests are terminating on the same endpoint), following is the updated solution proposal

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

Following changes/additions are required for this solution to work:

  1. Add Netty based listener in your existing spring boot app OR create a separate spring boot app for TCP server. Say this endpoint is listening for TCP traffic on port 9090
  2. Add HAProxy as the terminating endpoint for ingress traffic
  3. Configure HAProxy so that it sends all HTTP traffic to your existing spring boot HTTP endpoint (mentioned as be_http) on port 8080
  4. Configure HAProxy so that all non HTTP traffic is sent to the new TCP spring boot endpoint (mentioned as be_tcp) on port 9090.

Following HAProxy configuration will suffice. These are excerpt which are relevant for this problem, please add other HAProxy directives as applicable for normal HAProxy setup:

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

Following HAProxy documentation links are particularly useful

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

Personally I'll play around and validate tcp-request inspect-delay and adjust it as per actual needs since this has potential for adding delay in request in worst case scenario where connection has been made but no contents are available yet to evaluate if request is HTTP or not.

Addressing the need for we have to identify the client at least the IP address from which it was sent , you have an option to use Proxy Protocol while sending it back to backend. I have updated the sample config above to include proxy protocol in be_tcp (added send_proxy). I have also removed send_proxy from be_http since it's not needed for spring boot, instead you'll likely rely upon regular X-Forwarded-For header for be_http backend instead.

Within be_tcp backend, you can use Netty's HAProxyMessage to get the actual source IP address using sourceAddress() API. All in all, this is a workable solution. I myself have used HAProxy with proxy protocol (at both ends, frontend and backend) and it's much more stable for the job.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM