TCP 서버 모드 추가

This commit is contained in:
현성필
2025-01-21 13:19:21 +09:00
parent e23684d833
commit cb9201ddd2
106 changed files with 17710 additions and 0 deletions
@@ -0,0 +1,88 @@
package com.eactive.testmaster.socket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.DefaultThreadFactory;
public class NettyServer {
private final int port;
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 synchronized void start() throws InterruptedException {
if (isInitialized) {
return;
}
try {
// Use custom thread factories to avoid naming conflicts
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("netty-boss"));
workerGroup = new NioEventLoopGroup(
Runtime.getRuntime().availableProcessors() * 2,
new DefaultThreadFactory("netty-worker")
);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new NettySocketHandler(script));
}
});
// Bind and wait for the server socket
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
if (!channelFuture.isSuccess()) {
throw new RuntimeException("Failed to bind to port " + port);
}
serverChannel = channelFuture.channel();
isInitialized = true;
System.out.println("Netty server started on port " + port);
} catch (Exception e) {
// Clean up resources if initialization fails
stop();
throw new RuntimeException("Failed to start Netty server", e);
}
}
public synchronized void stop() throws InterruptedException {
try {
if (serverChannel != null) {
serverChannel.close().sync();
serverChannel = null;
}
} finally {
// Always attempt to shut down the event loop groups
try {
if (workerGroup != null) {
workerGroup.shutdownGracefully().sync();
workerGroup = null;
}
} finally {
if (bossGroup != null) {
bossGroup.shutdownGracefully().sync();
bossGroup = null;
}
}
isInitialized = false;
System.out.println("Netty server stopped on port " + port);
}
}
}
@@ -0,0 +1,114 @@
package com.eactive.testmaster.socket;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class NettySocketHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LoggerFactory.getLogger(NettySocketHandler.class);
private final ScriptEngine engine;
public NettySocketHandler(String scriptContent) {
ScriptEngineManager manager = new ScriptEngineManager();
this.engine = manager.getEngineByName("nashorn");
try {
engine.eval(scriptContent);
} catch (ScriptException e) {
logger.error("Failed to initialize script", e);
throw new RuntimeException("Script initialization failed", e);
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
logger.info("Client connected: {}", ctx.channel().remoteAddress());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
logger.info("Client disconnected: {}", ctx.channel().remoteAddress());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
try {
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);
logger.debug("Message processing details:");
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);
String result;
try {
logger.debug("Executing script processor for message");
Invocable invocable = (Invocable) engine;
Object jsResult = invocable.invokeFunction("processMessage", messageContent);
result = String.valueOf(jsResult);
logger.debug("Script processing successful. Result: {}", result);
} catch (ScriptException | NoSuchMethodException e) {
logger.error("Script execution error", e);
result = "Error: " + e.getMessage();
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);
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());
response.writeBytes(responseBytes);
logger.debug("Sending response to client: {}", ctx.channel().remoteAddress());
logger.debug("- Total response size: {} bytes", 4 + responseBytes.length);
ctx.writeAndFlush(response);
} catch (Exception e) {
logger.error("Error processing message from client: {}", ctx.channel().remoteAddress(), e);
logger.error("Error occurred at reader index: {}", in.readerIndex());
} finally {
logger.debug("Releasing ByteBuf resources");
ReferenceCountUtil.release(msg);
}
}
/**
* Utility method to convert byte array to hexadecimal string for logging
*/
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString().trim();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.error("Channel error:", cause);
ctx.close();
}
}
@@ -0,0 +1,143 @@
package com.eactive.testmaster.socket.controller;
import com.eactive.testmaster.socket.entity.NettyServerEntity;
import com.eactive.testmaster.socket.service.NettyServerService;
import java.util.List;
import javax.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequestMapping("/mgmt/socket")
public class NettyServerController {
private static final String NETTY_SERVER = "nettyServer";
private static final String NETTY_SERVER_LIST_VIEW = "page/socket/serverList";
private static final String NETTY_SERVER_EDIT = "page/socket/serverEdit";
private static final String REDIRECT_TO_LIST = "redirect:/mgmt/socket/list_view.do";
private final NettyServerService nettyServerService;
public NettyServerController(NettyServerService nettyServerService) {
this.nettyServerService = nettyServerService;
}
@GetMapping("/list_view.do")
@Secured("ROLE_MANAGE_NETTY")
public ModelAndView listView(@ModelAttribute("serverSearch") NettyServerSearch serverSearch,
Pageable pageable) {
ModelAndView mav = new ModelAndView(NETTY_SERVER_LIST_VIEW);
Page<NettyServerEntity> page = nettyServerService.findAll(serverSearch.buildSpecification(), pageable);
mav.addObject("page", page);
mav.addObject("pageable", pageable);
mav.addObject("modelSearch", serverSearch);
return mav;
}
@GetMapping("/create_view.do")
@Secured("ROLE_MANAGE_NETTY")
public ModelAndView createView() {
ModelAndView mav = new ModelAndView(NETTY_SERVER_EDIT);
mav.addObject(NETTY_SERVER, new NettyServerEntity());
return mav;
}
@GetMapping("/update_view.do")
@Secured("ROLE_MANAGE_NETTY")
public ModelAndView updateView(@RequestParam("id") Long id) {
ModelAndView mav = new ModelAndView(NETTY_SERVER_EDIT);
NettyServerEntity server = nettyServerService.findById(id);
mav.addObject(NETTY_SERVER, server);
return mav;
}
@PostMapping("/register.do")
@Secured("ROLE_MANAGE_NETTY")
public String register(@Valid @ModelAttribute(NETTY_SERVER) NettyServerEntity server,
BindingResult bindingResult,
ModelMap model) {
if (bindingResult.hasErrors()) {
model.addAttribute(NETTY_SERVER, server);
return NETTY_SERVER_EDIT;
}
try {
nettyServerService.createServer(server);
return REDIRECT_TO_LIST;
} catch (Exception e) {
bindingResult.rejectValue("port", "error.port.inuse",
"Port is already in use or invalid");
model.addAttribute(NETTY_SERVER, server);
return NETTY_SERVER_EDIT;
}
}
@PostMapping("/update.do")
@Secured("ROLE_MANAGE_NETTY")
public String update(@RequestParam("id") Long id,
@Valid @ModelAttribute(NETTY_SERVER) NettyServerEntity server,
BindingResult bindingResult,
ModelMap model) {
if (bindingResult.hasErrors()) {
model.addAttribute(NETTY_SERVER, server);
return NETTY_SERVER_EDIT;
}
try {
nettyServerService.updateServer(id, server);
return REDIRECT_TO_LIST;
} catch (Exception e) {
bindingResult.rejectValue("port", "error.update.failed",
"Failed to update server");
model.addAttribute(NETTY_SERVER, server);
return NETTY_SERVER_EDIT;
}
}
@PostMapping("/delete.do")
@Secured("ROLE_MANAGE_NETTY")
public String delete(@RequestParam("checkedIdForDel") List<String> ids) {
for (String id : ids) {
try {
nettyServerService.deleteServer(Long.parseLong(id));
} catch (Exception e) {
// Log error but continue with other deletions
}
}
return REDIRECT_TO_LIST;
}
@PostMapping("/servers/{id}/start")
@Secured("ROLE_MANAGE_NETTY")
public ResponseEntity<Void> startServer(@PathVariable Long id) {
try {
nettyServerService.startServer(id);
return ResponseEntity.ok().build();
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/servers/{id}/stop")
@Secured("ROLE_MANAGE_NETTY")
public ResponseEntity<Void> stopServer(@PathVariable Long id) {
try {
nettyServerService.stopServer(id);
return ResponseEntity.ok().build();
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
}
@@ -0,0 +1,39 @@
package com.eactive.testmaster.socket.controller;
import com.eactive.testmaster.common.search.BaseSearch;
import com.eactive.testmaster.common.search.ColumnSearchModel;
import com.eactive.testmaster.common.search.SearchCondition;
import com.eactive.testmaster.common.search.SearchModel;
import com.eactive.testmaster.socket.entity.NettyServerEntity;
import java.util.ArrayList;
import java.util.List;
public class NettyServerSearch implements BaseSearch<NettyServerEntity> {
private String name;
private Long port;
@Override
public List<SearchModel> buildSearchCondition() {
List<SearchModel> models = new ArrayList<>();
models.add(new ColumnSearchModel("name", SearchCondition.LIKE, name));
models.add(new ColumnSearchModel("port", SearchCondition.EQUAL, port));
return models;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getPort() {
return port;
}
public void setPort(Long port) {
this.port = port;
}
}
@@ -0,0 +1,101 @@
package com.eactive.testmaster.socket.entity;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import org.hibernate.annotations.Type;
@Entity
@Table(name = "netty_servers")
public class NettyServerEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(unique = true, nullable = false)
private Integer port;
@Column(nullable = false)
private Boolean active = true;
@Lob
private String script;
@Type(type = "org.hibernate.type.LocalDateTimeType")
private LocalDateTime createdAt = LocalDateTime.now();
@Type(type = "org.hibernate.type.LocalDateTimeType")
private LocalDateTime lastModified = LocalDateTime.now();
// Standard getters and setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
this.active = active;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getLastModified() {
return lastModified;
}
public void setLastModified(LocalDateTime lastModified) {
this.lastModified = lastModified;
}
@PreUpdate
protected void onUpdate() {
lastModified = LocalDateTime.now();
}
public String getScript() {
return script;
}
public void setScript(String script) {
this.script = script;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -0,0 +1,18 @@
package com.eactive.testmaster.socket.repository;
import com.eactive.testmaster.socket.entity.NettyServerEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface NettyServerRepository extends JpaRepository<NettyServerEntity, Long>, JpaSpecificationExecutor<NettyServerEntity> {
Optional<NettyServerEntity> findByPort(Integer port);
List<NettyServerEntity> findByActive(Boolean active);
}
@@ -0,0 +1,153 @@
package com.eactive.testmaster.socket.service;
import com.eactive.testmaster.socket.NettyServer;
import com.eactive.testmaster.socket.entity.NettyServerEntity;
import com.eactive.testmaster.socket.repository.NettyServerRepository;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.persistence.EntityNotFoundException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class NettyServerService {
private final NettyServerRepository repository;
private final Map<Integer, NettyServer> activeServers = new HashMap<>();
public NettyServerService(NettyServerRepository repository) {
this.repository = repository;
}
@Transactional(readOnly = true)
public Page<NettyServerEntity> findAll(Specification<NettyServerEntity> spec, Pageable pageable) {
return repository.findAll(spec, pageable);
}
@Transactional(readOnly = true)
public NettyServerEntity findById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Server not found with id: " + id));
}
public NettyServerEntity createServer(NettyServerEntity server) {
validatePort(server.getPort());
server.setActive(true);
return repository.save(server);
}
public NettyServerEntity updateServer(Long id, NettyServerEntity updatedServer) {
NettyServerEntity existingServer = findById(id);
// If port is being changed, validate the new port
if (!existingServer.getPort().equals(updatedServer.getPort())) {
validatePort(updatedServer.getPort());
// Stop the server if it's running on the old port
try {
stopServerIfRunning(existingServer.getPort());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Failed to stop server during port update", e);
}
}
// Update all fields
existingServer.setPort(updatedServer.getPort());
existingServer.setActive(updatedServer.getActive());
existingServer.setName(updatedServer.getName());
existingServer.setScript(updatedServer.getScript()); // Add this line to update the script
return repository.save(existingServer);
}
public void deleteServer(Long id) throws InterruptedException {
NettyServerEntity server = findById(id);
stopServerIfRunning(server.getPort());
repository.deleteById(id);
}
public void startServer(Long id) throws InterruptedException {
NettyServerEntity entity = findById(id);
if (!entity.getActive()) {
throw new IllegalStateException("Cannot start inactive server: " + id);
}
if (!activeServers.containsKey(entity.getPort())) {
NettyServer server = new NettyServer(entity.getPort(), entity.getScript());
server.start();
activeServers.put(entity.getPort(), server);
}
}
@Transactional
public void stopServer(Long id) throws InterruptedException {
NettyServerEntity entity = findById(id);
stopServerIfRunning(entity.getPort());
}
private void stopServerIfRunning(Integer port) throws InterruptedException {
NettyServer server = activeServers.remove(port);
if (server != null) {
server.stop();
}
}
// @Transactional
// public void deactivateServer(Long id) throws InterruptedException {
// NettyServerEntity entity = findById(id);
// stopServerIfRunning(entity.getPort());
// entity.setActive(false);
// repository.save(entity);
// }
// @Transactional(readOnly = true)
// public List<NettyServerEntity> getAllServers() {
// return repository.findAll();
// }
// @Transactional(readOnly = true)
// public List<NettyServerEntity> getActiveServers() {
// return repository.findByActive(true);
// }
// @Transactional(readOnly = true)
// public boolean isServerRunning(Integer port) {
// return activeServers.containsKey(port);
// }
// @Transactional(readOnly = true)
// public Map<Integer, Boolean> getServerStatuses(List<Integer> ports) {
// Map<Integer, Boolean> statuses = new HashMap<>();
// for (Integer port : ports) {
// statuses.put(port, activeServers.containsKey(port));
// }
// return statuses;
// }
private void validatePort(Integer port) {
if (port == null) {
throw new IllegalArgumentException("Port cannot be null");
}
if (port < 1024 || port > 65535) {
throw new IllegalArgumentException("Port must be between 1024 and 65535");
}
if (port >= 5701 && port <= 5801) {
throw new IllegalArgumentException(
"Port " + port + " conflicts with Hazelcast port range (5701-5801)"
);
}
Optional<NettyServerEntity> existingServer = repository.findByPort(port);
if (existingServer.isPresent()) {
throw new IllegalArgumentException("Port " + port + " is already in use");
}
}
}