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