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:
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
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.