Building a Real‑Time Chat System with Netty, SpringBoot, and JavaFX

This article walks through the design and implementation of a three‑tier instant‑messaging platform—including a Netty‑based chat server, a JavaFX client, and a SpringBoot web admin console—covering architecture, protocol design, database schema, key Netty handlers, threading, and authentication details.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Building a Real‑Time Chat System with Netty, SpringBoot, and JavaFX

Overview

The project implements a complete instant‑messaging system composed of three modules: a Netty‑based chat server, a JavaFX desktop client, and a SpringBoot web management console for user administration.

Technical Stack

SpringBoot for all modules (server, client, web console)

Netty for TCP long‑connection, full‑duplex communication

JavaFX for the desktop UI

SpringMVC, JPA and Apache Shiro for the web console

Maven for build management

Database Design

A single sys_user table stores user credentials, status, and admin flag. It is used for web login, server‑side authentication, and loading the client’s friend list.

CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(64) DEFAULT NULL COMMENT '用户名:登陆账号',
  `pass_word` varchar(128) DEFAULT NULL COMMENT '密码',
  `name` varchar(16) DEFAULT NULL COMMENT '昵称',
  `sex` char(1) DEFAULT NULL COMMENT '性别:1-男,2女',
  `status` bit(1) DEFAULT NULL COMMENT '用户状态:1-有效,0-无效',
  `online` bit(1) DEFAULT NULL COMMENT '在线状态:1-在线,0-离线',
  `salt` varchar(128) DEFAULT NULL COMMENT '密码盐值',
  `admin` bit(1) DEFAULT NULL COMMENT '是否管理员(只有管理员才能登录Web端):1-是,0-否',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

Communication Protocol

Each TCP packet consists of an 8‑byte length header, a 2‑byte message‑type field, and a JSON payload. The length header solves TCP sticky‑packet problems; the type field routes the payload to the appropriate handler.

Netty Handlers (Server & Client)

IdleStateHandler – detects read/write idle events to trigger heartbeats.

HeartBeatHandler – processes heartbeat messages and closes idle connections.

StringLengthFieldDecoder – custom LengthFieldBasedFrameDecoder that reads the 8‑byte length field and extracts a complete frame.

StringDecoder / StringEncoder – convert between byte streams and UTF‑8 strings.

JsonDecoder / JsonEncoder – transform JSON strings to Java beans and vice‑versa using a custom MessageEnDeCoder.

BussMessageHandler – the main inbound handler that forwards messages to a thread‑pool executor.

Server Bootstrap

public void start() {
    EventLoopGroup boss = new NioEventLoopGroup();
    EventLoopGroup worker = new NioEventLoopGroup();
    ServerBootstrap sb = new ServerBootstrap();
    sb.group(boss, worker)
      .channel(NioServerSocketChannel.class)
      .option(ChannelOption.SO_BACKLOG, 1024)
      .handler(new LoggingHandler(LogLevel.INFO))
      .childHandler(new ChannelInitializer<SocketChannel>() {
          @Override
          protected void initChannel(SocketChannel ch) throws Exception {
              ch.pipeline().addLast(new IdleStateHandler(25, 20, 0, TimeUnit.SECONDS));
              ch.pipeline().addLast(new StringLengthFieldDecoder());
              ch.pipeline().addLast(new StringDecoder(Charset.forName("UTF-8")));
              ch.pipeline().addLast(new JsonDecoder());
              ch.pipeline().addLast(new HeartBeatHandler());
              ch.pipeline().addLast(new JsonEncoder());
              ch.pipeline().addLast(bussMessageHandler);
          }
      });
    try {
        ChannelFuture f = sb.bind(port).sync();
        f.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        logger.error("Server start failed: {}", ExceptionUtils.getStackTrace(e));
    } finally {
        worker.shutdownGracefully();
        boss.shutdownGracefully();
    }
}

Client Bootstrap

public void login(String userName, String password) throws Exception {
    Bootstrap cb = new Bootstrap();
    EventLoopGroup group = new NioEventLoopGroup();
    cb.group(group)
      .channel(NioSocketChannel.class)
      .option(ChannelOption.TCP_NODELAY, true)
      .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
      .handler(new ChannelInitializer<SocketChannel>() {
          @Override
          protected void initChannel(SocketChannel ch) throws Exception {
              ch.pipeline().addLast(new IdleStateHandler(20, 15, 0, TimeUnit.SECONDS));
              ch.pipeline().addLast(new StringLengthFieldDecoder());
              ch.pipeline().addLast(new StringDecoder(Charset.forName("UTF-8")));
              ch.pipeline().addLast(new JsonDecoder());
              ch.pipeline().addLast(new JsonEncoder());
              ch.pipeline().addLast(bussMessageHandler);
              ch.pipeline().addLast(new HeartBeatHandler());
          }
      });
    ChannelFuture f = cb.connect(server, port).sync();
    if (f.isSuccess()) {
        channel = (SocketChannel) f.channel();
        LoginRequest req = new LoginRequest();
        req.setTime(new Date());
        req.setUserName(userName);
        req.setPassword(password);
        req.setMessageType(MessageEnDeCoder.LoginRequest);
        channel.writeAndFlush(req).addListener(new GenericFutureListener<Future<? super Void>>() {
            @Override
            public void operationComplete(Future<? super Void> future) throws Exception {
                if (future.isSuccess()) {
                    logger.info("Login request sent successfully");
                } else {
                    logger.error("Login request failed: {}", ExceptionUtils.getStackTrace(future.cause()));
                }
            }
        });
    } else {
        group.shutdownGracefully();
        throw new RuntimeException("Network error");
    }
}

Thread‑Pool Executor

Business logic is executed in a dedicated ThreadPoolExecutor with configurable core size, max size, keep‑alive time and queue capacity.

public class TaskDispatcher {
    private ThreadPoolExecutor threadPool;
    public TaskDispatcher() {
        int core = 15;
        int max = 50;
        int keepAlive = 30;
        int queueCap = 1024;
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(queueCap);
        threadPool = new ThreadPoolExecutor(core, max, keepAlive, TimeUnit.SECONDS, queue);
    }
    public void submit(Channel channel, Message msg) {
        ExecutorBase exec = null;
        String type = msg.getMessageType();
        if (MessageEnDeCoder.LoginRequest.equals(type)) {
            exec = new LoginExecutor(channel, msg);
        } else if (MessageEnDeCoder.SendMsgRequest.equalsIgnoreCase(type)) {
            exec = new SendMsgExecutor(channel, msg);
        }
        if (exec != null) {
            threadPool.submit(exec);
        }
    }
}

Login Executor

public class LoginExecutor extends ExecutorBase {
    @Override
    public void run() {
        LoginRequest req = (LoginRequest) message;
        boolean ok = userService.checkLogin(req.getUserName(), req.getPassword());
        LoginResponse resp = new LoginResponse();
        resp.setUserName(req.getUserName());
        resp.setMessageType(MessageEnDeCoder.LoginResponse);
        resp.setTime(new Date());
        resp.setResultCode(ok ? "0000" : "9999");
        resp.setResultMessage(ok ? "Login successful" : "Login failed");
        if (ok) {
            userService.updateOnlineStatus(req.getUserName(), true);
            SessionManager.addSession(req.getUserName(), channel);
        }
        channel.writeAndFlush(resp).addListener(f -> {
            if (!ok) {
                channel.disconnect();
            }
        });
    }
}

Send‑Message Executor

public class SendMsgExecutor extends ExecutorBase {
    @Override
    public void run() {
        SendMsgRequest req = (SendMsgRequest) message;
        String target = req.getRecvUserName();
        Channel targetCh = SessionManager.getSession(target);
        SendMsgResponse resp = new SendMsgResponse();
        resp.setMessageType(MessageEnDeCoder.SendMsgResponse);
        resp.setTime(new Date());
        if (targetCh != null) {
            targetCh.writeAndFlush(req).addListener(f -> {
                if (f.isSuccess()) {
                    resp.setResultCode("0000");
                    resp.setResultMessage("Message sent to " + target);
                } else {
                    resp.setResultCode("9999");
                    resp.setResultMessage("Failed to forward to " + target);
                }
                channel.writeAndFlush(resp);
            });
        } else {
            resp.setResultCode("9999");
            resp.setResultMessage("User " + target + " is offline");
            channel.writeAndFlush(resp);
        }
    }
}

Session Management

public class SessionManager {
    private static final ConcurrentHashMap<String, Channel> sessions = new ConcurrentHashMap<>();
    public static void addSession(String user, Channel ch) { sessions.put(user, ch); }
    public static String removeSession(String user) { sessions.remove(user); return user; }
    public static String removeSession(Channel ch) {
        for (Map.Entry<String, Channel> e : sessions.entrySet()) {
            if (e.getValue().id().asLongText().equals(ch.id().asLongText())) {
                sessions.remove(e.getKey());
                return e.getKey();
            }
        }
        return null;
    }
    public static Channel getSession(String user) { return sessions.get(user); }
}

Web Management Console

The console is a SpringBoot MVC application secured with Apache Shiro. Authentication is performed by a custom UserDbRealm that validates credentials against the sys_user table.

public class UserDbRealm extends AuthorizingRealm {
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken up = (UsernamePasswordToken) token;
        String username = up.getUsername();
        String password = up.getPassword() == null ? "" : new String(up.getPassword());
        // TODO: query DB and verify credentials
        ShiroUser user = new ShiroUser();
        return new SimpleAuthenticationInfo(user, password, getName());
    }
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
    chain.addPathDefinition("/css/**", "anon");
    chain.addPathDefinition("/img/**", "anon");
    chain.addPathDefinition("/js/**", "anon");
    chain.addPathDefinition("/login", "anon");
    chain.addPathDefinition("/**", "authc");
    return chain;
}

A generic query service builds JPA Specification objects from request parameters, supporting sorting, pagination and common operators (=, !=, like, >, <, in, etc.).

public <T> Page<T> query(JpaSpecificationExecutor<T> dao, Map<String, Object> filters,
                     String sort, String order, int pageIndex, int pageSize) {
    Pageable pageable;
    if (StringUtils.isEmpty(sort)) {
        pageable = PageRequest.of(pageIndex - 1, pageSize);
    } else {
        Sort.Direction dir = "desc".equalsIgnoreCase(order) ? Sort.Direction.DESC : Sort.Direction.ASC;
        pageable = PageRequest.of(pageIndex - 1, pageSize, Sort.by(dir, sort));
    }
    Specification<T> spec = (root, query, cb) -> {
        List<Predicate> preds = new ArrayList<>();
        // Build predicates from filters (omitted for brevity)
        return preds.isEmpty() ? cb.conjunction() : cb.and(preds.toArray(new Predicate[0]));
    };
    return dao.findAll(spec, pageable);
}

Key Takeaways

Netty provides high‑performance, asynchronous TCP communication with a clear handler pipeline.

Fixed‑length header solves sticky‑packet issues; a 2‑byte type field enables simple routing.

Business logic is decoupled from I/O via a thread‑pool executor.

SessionManager maintains a map of online users for fast message forwarding.

SpringBoot + Shiro offers a lightweight, secure web admin console.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

NettyDatabase designSpringBootthread poolInstant MessagingTCP ProtocolJavaFXShiro Authentication
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.