必须实现服务器以通过以太网连接处理以下协议:
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
在那很好用。我希望可以从客户端访问它。
答案 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)
此解决方案必须进行以下更改/添加:
遵循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文档链接特别有用
我个人将亲自试用并验证 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与代理协议一起使用(在两端,前端和后端),并且工作稳定得多。