TCP 서버 옵션 추가

This commit is contained in:
현성필
2025-02-02 23:01:40 +09:00
parent 40e6aac336
commit 54bf2a897b
5 changed files with 270 additions and 68 deletions
@@ -13,11 +13,21 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import MonacoEditor from '@/components/shared/MonacoEditor';
const defaultScript = `function processJson(message) {
//process message;
const defaultScript = `function processMessage(message, serviceId) {
// Process the message and serviceId
console.log("Received message:", message);
console.log("Service ID:", serviceId);
// Return response
return "response_message";
}`;
@@ -27,7 +37,11 @@ const TCPServerEditDialog = ({ open, onOpenChange, item }) => {
name: '',
port: 1024,
active: true,
script: defaultScript
script: defaultScript,
prefixSize: 4,
includePrefixLength: true,
serviceIdLength: 0,
serviceIdPosition: 'NONE'
});
// Initialize form when item changes
@@ -41,7 +55,11 @@ const TCPServerEditDialog = ({ open, onOpenChange, item }) => {
name: '',
port: 1024,
active: true,
script: defaultScript
script: defaultScript,
prefixSize: 4,
includePrefixLength: true,
serviceIdLength: 0,
serviceIdPosition: 'NONE'
});
}
}, [item]);
@@ -93,44 +111,108 @@ const TCPServerEditDialog = ({ open, onOpenChange, item }) => {
<ScrollArea className="max-h-[80vh]">
<form id="serverForm" onSubmit={handleSubmit} className="space-y-4 p-1">
{/* Server Name */}
<div className="space-y-2">
<Label htmlFor="name" className="must">
서버 이름
</Label>
<Input
id="name"
required
value={server.name}
onChange={(e) => handleChange('name', e.target.value)}
/>
</div>
{/* Basic Server Configuration */}
<Card>
<CardHeader>
<CardTitle>기본 설정</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Server Name */}
<div className="space-y-2">
<Label htmlFor="name" className="must">서버 이름</Label>
<Input
id="name"
required
value={server.name}
onChange={(e) => handleChange('name', e.target.value)}
/>
</div>
{/* Port Number */}
<div className="space-y-2">
<Label htmlFor="port" className="must">
포트
</Label>
<Input
id="port"
type="number"
required
min="1024"
max="65535"
value={server.port}
onChange={(e) => handleChange('port', parseInt(e.target.value))}
/>
</div>
{/* Port Number */}
<div className="space-y-2">
<Label htmlFor="port" className="must">포트</Label>
<Input
id="port"
type="number"
required
min="1024"
max="65535"
value={server.port}
onChange={(e) => handleChange('port', parseInt(e.target.value))}
/>
</div>
{/* Active Status */}
<div className="flex items-center space-x-2">
<Checkbox
id="active"
checked={server.active}
onCheckedChange={(checked) => handleChange('active', checked)}
/>
<Label htmlFor="active">Active</Label>
</div>
{/* Active Status */}
<div className="flex items-center space-x-2">
<Checkbox
id="active"
checked={server.active}
onCheckedChange={(checked) => handleChange('active', checked)}
/>
<Label htmlFor="active">Active</Label>
</div>
</CardContent>
</Card>
{/* Message Configuration */}
<Card>
<CardHeader>
<CardTitle>메시지 설정</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Prefix Size */}
<div className="space-y-2">
<Label htmlFor="prefixSize">Prefix Size</Label>
<Input
id="prefixSize"
type="number"
min="0"
value={server.prefixSize}
onChange={(e) => handleChange('prefixSize', parseInt(e.target.value))}
/>
</div>
{/* Include Prefix Length */}
<div className="flex items-center space-x-2">
<Checkbox
id="includePrefixLength"
checked={server.includePrefixLength}
onCheckedChange={(checked) => handleChange('includePrefixLength', checked)}
/>
<Label htmlFor="includePrefixLength">Include Prefix Length</Label>
</div>
{/* Service ID Length */}
<div className="space-y-2">
<Label htmlFor="serviceIdLength">Service ID Length</Label>
<Input
id="serviceIdLength"
type="number"
min="0"
value={server.serviceIdLength}
onChange={(e) => handleChange('serviceIdLength', parseInt(e.target.value))}
/>
</div>
{/* Service ID Position */}
<div className="space-y-2">
<Label htmlFor="serviceIdPosition">Service ID Position</Label>
<Select
value={server.serviceIdPosition}
onValueChange={(value) => handleChange('serviceIdPosition', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select position" />
</SelectTrigger>
<SelectContent>
<SelectItem value="NONE">None</SelectItem>
<SelectItem value="START">Start</SelectItem>
<SelectItem value="END">End</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Script Editor */}
<Card>
@@ -10,17 +10,17 @@ import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.DefaultThreadFactory;
import com.eactive.testmaster.socket.entity.NettyServerEntity;
public class NettyServer {
private final int port;
private final NettyServerEntity config;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private Channel serverChannel;
private volatile boolean isInitialized = false;
private final String script;
public NettyServer(int port, String script) {
this.port = port;
this.script = script;
public NettyServer(NettyServerEntity config) {
this.config = config;
}
public synchronized void start() throws InterruptedException {
@@ -29,7 +29,6 @@ public class NettyServer {
}
try {
// Use custom thread factories to avoid naming conflicts
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("netty-boss"));
workerGroup = new NioEventLoopGroup(
Runtime.getRuntime().availableProcessors() * 2,
@@ -45,20 +44,20 @@ public class NettyServer {
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new NettySocketHandler(script));
ch.pipeline().addLast(new NettySocketHandler(config.getScript(), config));
}
});
// Bind and wait for the server socket
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
ChannelFuture channelFuture = serverBootstrap.bind(config.getPort()).sync();
if (!channelFuture.isSuccess()) {
throw new RuntimeException("Failed to bind to port " + port);
throw new RuntimeException("Failed to bind to port " + config.getPort());
}
serverChannel = channelFuture.channel();
isInitialized = true;
System.out.println("Netty server started on port " + port);
System.out.println("Netty server started on port " + config.getPort());
} catch (Exception e) {
// Clean up resources if initialization fails
stop();
@@ -86,7 +85,11 @@ public class NettyServer {
}
}
isInitialized = false;
System.out.println("Netty server stopped on port " + port);
System.out.println("Netty server stopped on port " + config.getPort());
}
}
public NettyServerEntity getConfig() {
return config;
}
}
@@ -1,5 +1,6 @@
package com.eactive.testmaster.socket;
import com.eactive.testmaster.socket.entity.NettyServerEntity;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
@@ -15,8 +16,10 @@ public class NettySocketHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LoggerFactory.getLogger(NettySocketHandler.class);
private final ScriptEngine engine;
private final NettyServerEntity config;
public NettySocketHandler(String scriptContent) {
public NettySocketHandler(String scriptContent, NettyServerEntity config) {
this.config = config;
ScriptEngineManager manager = new ScriptEngineManager();
this.engine = manager.getEngineByName("nashorn");
try {
@@ -44,7 +47,6 @@ public class NettySocketHandler extends ChannelInboundHandlerAdapter {
logger.debug("Received new message from client: {}", ctx.channel().remoteAddress());
logger.debug("Total readable bytes: {}", in.readableBytes());
// Read the entire message as it comes
byte[] messageBytes = new byte[in.readableBytes()];
in.readBytes(messageBytes);
String message = new String(messageBytes);
@@ -53,15 +55,18 @@ public class NettySocketHandler extends ChannelInboundHandlerAdapter {
logger.debug("- Raw message: {}", message);
logger.debug("- Hex dump: {}", bytesToHex(messageBytes));
// Process the message, removing the length prefix
String messageContent = message.substring(4);
logger.debug("- Message content (without length): {}", messageContent);
// Extract message content based on configuration
String messageContent = extractMessageContent(message);
String serviceId = extractServiceId(message);
logger.debug("- Message content: {}", messageContent);
logger.debug("- Service ID: {}", serviceId);
String result;
try {
logger.debug("Executing script processor for message");
Invocable invocable = (Invocable) engine;
Object jsResult = invocable.invokeFunction("processMessage", messageContent);
Object jsResult = invocable.invokeFunction("processMessage", messageContent, serviceId);
result = String.valueOf(jsResult);
logger.debug("Script processing successful. Result: {}", result);
} catch (ScriptException | NoSuchMethodException e) {
@@ -70,20 +75,17 @@ public class NettySocketHandler extends ChannelInboundHandlerAdapter {
logger.error("Script execution failed. Using error message as result: {}", result);
}
// Format response with length prefix
byte[] responseBytes = result.getBytes();
String lengthPrefix = String.format("%04d", responseBytes.length + 4);
// Format response with configured prefix
byte[] responseBytes = formatResponse(result, serviceId);
logger.debug("Preparing response:");
logger.debug("- Response content: {}", result);
logger.debug("- Response length: {}", responseBytes.length);
logger.debug("- Length prefix: {}", lengthPrefix);
ByteBuf response = ctx.alloc().buffer(4 + responseBytes.length);
response.writeBytes(lengthPrefix.getBytes());
ByteBuf response = ctx.alloc().buffer(responseBytes.length);
response.writeBytes(responseBytes);
logger.debug("Sending response to client: {}", ctx.channel().remoteAddress());
logger.debug("- Total response size: {} bytes", 4 + responseBytes.length);
logger.debug("- Total response size: {} bytes", responseBytes.length);
ctx.writeAndFlush(response);
} catch (Exception e) {
@@ -95,9 +97,71 @@ public class NettySocketHandler extends ChannelInboundHandlerAdapter {
}
}
/**
* Utility method to convert byte array to hexadecimal string for logging
*/
private String extractMessageContent(String message) {
int prefixSize = config.getPrefixSize().intValue();
int serviceIdLength = config.getServiceIdLength().intValue();
String content = message.substring(prefixSize);
if (serviceIdLength > 0) {
if (config.getServiceIdPosition() == NettyServerEntity.ServiceIdPosition.START) {
content = content.substring(serviceIdLength);
} else if (config.getServiceIdPosition() == NettyServerEntity.ServiceIdPosition.END) {
content = content.substring(0, content.length() - serviceIdLength);
}
}
return content;
}
private String extractServiceId(String message) {
int prefixSize = config.getPrefixSize().intValue();
int serviceIdLength = config.getServiceIdLength().intValue();
if (serviceIdLength == 0 || config.getServiceIdPosition() == NettyServerEntity.ServiceIdPosition.NONE) {
return "";
}
if (config.getServiceIdPosition() == NettyServerEntity.ServiceIdPosition.START) {
return message.substring(prefixSize, prefixSize + serviceIdLength);
} else {
return message.substring(message.length() - serviceIdLength);
}
}
private byte[] formatResponse(String result, String serviceId) {
byte[] resultBytes = result.getBytes();
byte[] serviceIdBytes = serviceId.getBytes();
int contentLength = resultBytes.length +
(config.getServiceIdLength().intValue() > 0 ? serviceIdBytes.length : 0);
int totalLength = config.getIncludePrefixLength() ?
contentLength + config.getPrefixSize().intValue() :
contentLength;
String lengthPrefix = String.format("%0" + config.getPrefixSize() + "d", totalLength);
ByteBuf buffer = io.netty.buffer.Unpooled.buffer();
buffer.writeBytes(lengthPrefix.getBytes());
if (config.getServiceIdPosition() == NettyServerEntity.ServiceIdPosition.START) {
buffer.writeBytes(serviceIdBytes);
buffer.writeBytes(resultBytes);
} else {
buffer.writeBytes(resultBytes);
if (config.getServiceIdPosition() == NettyServerEntity.ServiceIdPosition.END) {
buffer.writeBytes(serviceIdBytes);
}
}
byte[] responseBytes = new byte[buffer.readableBytes()];
buffer.readBytes(responseBytes);
buffer.release();
return responseBytes;
}
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
@@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@@ -30,12 +32,31 @@ public class NettyServerEntity {
@Lob
private String script;
@Column(nullable = false, columnDefinition = "BIGINT DEFAULT 4")
private Long prefixSize = 4L;
@Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT TRUE")
private Boolean includePrefixLength = true;
@Column(nullable = false, columnDefinition = "BIGINT DEFAULT 0")
private Long serviceIdLength = 0L;
@Enumerated(EnumType.STRING)
@Column(nullable = false, columnDefinition = "VARCHAR(10) DEFAULT 'NONE'")
private ServiceIdPosition serviceIdPosition = ServiceIdPosition.NONE;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime createdAt = LocalDateTime.now();
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime lastModified = LocalDateTime.now();
public enum ServiceIdPosition {
NONE,
START,
END
}
// Standard getters and setters
public Long getId() {
return id;
@@ -97,4 +118,36 @@ public class NettyServerEntity {
public void setName(String name) {
this.name = name;
}
public Long getPrefixSize() {
return prefixSize;
}
public void setPrefixSize(Long prefixSize) {
this.prefixSize = prefixSize;
}
public Boolean getIncludePrefixLength() {
return includePrefixLength;
}
public void setIncludePrefixLength(Boolean includePrefixLength) {
this.includePrefixLength = includePrefixLength;
}
public Long getServiceIdLength() {
return serviceIdLength;
}
public void setServiceIdLength(Long serviceIdLength) {
this.serviceIdLength = serviceIdLength;
}
public ServiceIdPosition getServiceIdPosition() {
return serviceIdPosition;
}
public void setServiceIdPosition(ServiceIdPosition serviceIdPosition) {
this.serviceIdPosition = serviceIdPosition;
}
}
@@ -79,7 +79,7 @@ public class NettyServerService {
}
if (!activeServers.containsKey(entity.getPort())) {
NettyServer server = new NettyServer(entity.getPort(), entity.getScript());
NettyServer server = new NettyServer(entity);
server.start();
activeServers.put(entity.getPort(), server);
}