第 1 篇: Netty服务端和客户端互发消息

1. 服务端

1.1 新建Spring Boot项目

Group : com.hahashou.netty
Artifact : server
Type : Maven

1.2 pom.xml信息

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hahashou.netty</groupId>
    <artifactId>server</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>server</name>
    <description>Netty Server Project For Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.100.Final</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

1.3 修改application.properties为application.yml

server:
  port: 32000

logging:
  level:
    com.hahashou.netty: info

1.4 增加logback-spring.xml

<?xml version="1.0" encoding="utf-8" ?>
<!-- 日志级别(低->高) : TRACE < DEBUG < INFO < WARN < ERROR < FATAL -->
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 基本属性配置, 更换项目时修改此处 -->
    <property name="rootPath" value="server-logs"/>
    <property name="applicationName" value="server"/>
    <contextName>${applicationName}</contextName>
    <!-- 控制台日志: 彩色渲染输出 -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    <!-- Info日志 -->
    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${rootPath}/${applicationName}-info.log</file>
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50}-%msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <FileNamePattern>${rootPath}/${applicationName}-info.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
        </filter>
    </appender>
    <!-- Error日志 -->
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${rootPath}/${applicationName}-error.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <FileNamePattern>${rootPath}/${applicationName}-error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="INFO"/>
        <appender-ref ref="ERROR"/>
    </root>
    <!-- 异步打印 -->
    <appender name="FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="INFO"/>
        <appender-ref ref="ERROR"/>
    </appender>
</configuration>

1.5 新建config包, 放入4个类

package com.hahashou.netty.server.config;

import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.EventExecutorGroup;
import io.netty.util.concurrent.RejectedExecutionHandlers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: Netty线程组
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Configuration
public class EventLoopGroupConfig {

    private int bossNum = 1;

    private int workerNum = 4;

    private int businessNum = 1;

    private int maxPending = 100000;

    /**
     * TCP连接处理
     * @return
     */
    @Bean("bossGroup")
    public NioEventLoopGroup bossGroup() {
        return new NioEventLoopGroup(bossNum);
    }

    /**
     * Socket数据读写
     * @return
     */
    @Bean("workerGroup")
    public NioEventLoopGroup workerGroup() {
        return new NioEventLoopGroup(workerNum);
    }

    /**
     * Handler业务处理
     * @return
     */
    @Bean("businessGroup")
    public EventExecutorGroup businessGroup() {
        return new DefaultEventExecutorGroup(businessNum, new BusinessThreadFactory(),
                maxPending, RejectedExecutionHandlers.reject());
    }
    
    static class BusinessThreadFactory implements ThreadFactory {

        private final ThreadGroup group;

        private final AtomicInteger threadNumber = new AtomicInteger(1);

        private final String namePrefix;

        BusinessThreadFactory() {
            SecurityManager securityManager = System.getSecurityManager();
            group = (securityManager != null) ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup();
            namePrefix = "netty-server-";
        }

        @Override
        public Thread newThread(Runnable runnable) {
            Thread thread = new Thread(group, runnable, namePrefix + threadNumber.getAndIncrement(), 0);
            if (thread.isDaemon()) {
                thread.setDaemon(false);
            }
            if (thread.getPriority() != Thread.NORM_PRIORITY) {
                thread.setPriority(Thread.NORM_PRIORITY);
            }
            return thread;
        }
    }
}

package com.hahashou.netty.server.config;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @description:
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Component
@ChannelHandler.Sharable
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /** 因为只有一个客户端 */
    public static Channel CHANNEL;

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        CHANNEL = ctx.channel();
        String channelId = CHANNEL.id().asLongText();
        log.info("有客户端连接, channelId : " + channelId);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        CHANNEL = null;
        String channelId = ctx.channel().id().asLongText();
        log.info("有客户端断开连接, channelId : {}", channelId);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        log.info("客户端的消息: {}", msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        CHANNEL = null;
        String channelId = ctx.channel().id().asLongText();
        log.error("有客户端发生异常, channelId : {}", channelId);
    }
}

package com.hahashou.netty.server.config;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.concurrent.EventExecutorGroup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.net.InetAddress;
import java.nio.charset.Charset;

/**
 * @description: Netty-TCP服务
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Component
@Slf4j
public class NettyServer implements ApplicationListener<ApplicationStartedEvent> {

	/** 查看当前主机可用端口范围 cat /proc/sys/net/ipv4/ip_local_port_range
	 * 端口分为知名端口(0~1023)、注册端口(1024~49511)、动态/私有端口(49152~65535) */
	public static int PORT = 35000;

    @Resource
    private NioEventLoopGroup bossGroup;

    @Resource
    private NioEventLoopGroup workerGroup;

    @Resource
    private EventExecutorGroup businessGroup;

	@Resource
	private NettyServerHandler nettyServerHandler;

    @Override
	public void onApplicationEvent(ApplicationStartedEvent event) {
		createServer(bossGroup, workerGroup, businessGroup, nettyServerHandler, PORT);
    }

    public void createServer(NioEventLoopGroup bossGroup, NioEventLoopGroup workerGroup, EventExecutorGroup businessGroup,
                             NettyServerHandler nettyServerHandler, int port) {
		try {
			String hostAddress = InetAddress.getLocalHost().getHostAddress();
			log.info("本机IP: {}", hostAddress);
			ServerBootstrap serverBootstrap = new ServerBootstrap();
			ChannelFuture channelFuture = serverBootstrap.group(bossGroup, workerGroup)
					.channel(NioServerSocketChannel.class)
					.childHandler(new ChannelInitializer<SocketChannel>() {
						@Override
						public void initChannel(SocketChannel ch) {
							ChannelPipeline pipeline = ch.pipeline();
							pipeline.addLast(new StringDecoder(Charset.forName("UTF-8")));
							pipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
							pipeline.addLast(businessGroup, nettyServerHandler);
						}
					})
					// 服务端可连接队列数,对应TCP/IP协议listen函数中backlog参数
					.option(ChannelOption.SO_BACKLOG, 1024)
					.childOption(ChannelOption.TCP_NODELAY, true)
					.childOption(ChannelOption.SO_KEEPALIVE, true)
					.bind(hostAddress, port)
					.sync();
			if (channelFuture.isSuccess()) {
				log.info("TCP服务启动成功, port={}", port);
			}
			channelFuture.channel().closeFuture().sync();
		} catch (Exception exception) {
			log.error("TCP服务启动失败: {}", exception.getMessage());
		}
	}

    @PreDestroy
    public void destroy() {
    	bossGroup.shutdownGracefully().syncUninterruptibly();
		workerGroup.shutdownGracefully().syncUninterruptibly();
        log.info("TCP服务关闭成功");
    }
}

package com.hahashou.netty.server.config;

import lombok.Data;

/**
 * @description:
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Data
public class Message {

    private String message;
}


1.6 新建controller包, 放入1个类

package com.hahashou.netty.server.controller;

import com.hahashou.netty.server.config.Message;
import com.hahashou.netty.server.config.NettyServerHandler;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @description:
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@RestController
@RequestMapping("/server")
@Slf4j
public class ServerController {

    @PostMapping("/send")
    public String send(@RequestBody Message dto) {
        Channel channel = NettyServerHandler.CHANNEL;
        if (channel == null) {
            return "无客户端连接";
        }
        channel.writeAndFlush(Unpooled.copiedBuffer(dto.getMessage(), CharsetUtil.UTF_8));
        return "success";
    }
}

1.7 启动服务端, 测试

服务端启动后测试

2. 客户端

2.1 新建Spring Boot项目

Group : com.hahashou.netty
Artifact : client
Type : Maven

2.2 pom.xml信息

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hahashou.netty</groupId>
    <artifactId>client</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>client</name>
    <description>Netty Client Project For Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.100.Final</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2.3 修改application.properties为application.yml

server:
  port: 32001

logging:
  level:
    com.hahashou.netty: info

2.4 增加logback-spring.xml

<?xml version="1.0" encoding="utf-8" ?>
<!-- 日志级别(低->高) : TRACE < DEBUG < INFO < WARN < ERROR < FATAL -->
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 基本属性配置, 更换项目时修改此处 -->
    <property name="rootPath" value="client-logs"/>
    <property name="applicationName" value="client"/>
    <contextName>${applicationName}</contextName>
    <!-- 控制台日志: 彩色渲染输出 -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    <!-- Info日志 -->
    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${rootPath}/${applicationName}-info.log</file>
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50}-%msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <FileNamePattern>${rootPath}/${applicationName}-info.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
        </filter>
    </appender>
    <!-- Error日志 -->
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${rootPath}/${applicationName}-error.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <FileNamePattern>${rootPath}/${applicationName}-error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="INFO"/>
        <appender-ref ref="ERROR"/>
    </root>
    <!-- 异步打印 -->
    <appender name="FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="INFO"/>
        <appender-ref ref="ERROR"/>
    </appender>
</configuration>

2.5 新建config包, 放入4个类

package com.hahashou.netty.client.config;

import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.EventExecutorGroup;
import io.netty.util.concurrent.RejectedExecutionHandlers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: Netty线程组
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Configuration
public class EventLoopGroupConfig {

    private int workerNum = 4;

    private int businessNum = 1;

    private int maxPending = 100000;

    @Bean("workerGroup")
    public NioEventLoopGroup workerGroup() {
        return new NioEventLoopGroup(workerNum);
    }

    /**
     * Handler业务处理
     * @return
     */
    @Bean("businessGroup")
    public EventExecutorGroup businessGroup() {
        return new DefaultEventExecutorGroup(businessNum, new BusinessThreadFactory(), maxPending, RejectedExecutionHandlers.reject());
    }
    
    static class BusinessThreadFactory implements ThreadFactory {

        private final ThreadGroup group;

        private final AtomicInteger threadNumber = new AtomicInteger(1);

        private final String namePrefix;

        BusinessThreadFactory() {
            SecurityManager securityManager = System.getSecurityManager();
            group = securityManager != null ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup();
            namePrefix = "client-thread-";
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
            if (thread.isDaemon()) {
                thread.setDaemon(false);
            }
            if (thread.getPriority() != Thread.NORM_PRIORITY) {
                thread.setPriority(Thread.NORM_PRIORITY);
            }
            return thread;
        }
    }
}

package com.hahashou.netty.client.config;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @description: 
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Component
@ChannelHandler.Sharable
@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    public static Channel SERVER;

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        SERVER = ctx.channel();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        SERVER = null;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        log.info("服务端的消息: {}", msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        SERVER = null;
    }
}

package com.hahashou.netty.client.config;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.concurrent.EventExecutorGroup;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.net.InetAddress;
import java.nio.charset.Charset;

/**
 * @description: Netty-TCP服务
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Component
@Slf4j
public class NettyClient implements ApplicationListener<ApplicationStartedEvent> {

	public static int PORT = 35000;

    @Resource
    private NioEventLoopGroup workerGroup;

	@Resource
	private EventExecutorGroup businessGroup;

	@Resource
	private NettyClientHandler nettyClientHandler;

	public static Channel CHANNEL;

    @SneakyThrows
	@Override
	public void onApplicationEvent(ApplicationStartedEvent event) {
		String host = InetAddress.getLocalHost().getHostAddress();
		log.info("本机IP: {}", host);
		createClient(workerGroup, businessGroup, nettyClientHandler, host, PORT);
    }

    public void createClient(NioEventLoopGroup workerGroup, EventExecutorGroup businessGroup,
							 NettyClientHandler nettyClientHandler, String host, int port) {
		Bootstrap bootstrap = new Bootstrap();
		bootstrap.group(workerGroup)
				.channel(NioSocketChannel.class)
				.handler(new ChannelInitializer<NioSocketChannel>() {
					@Override
					protected void initChannel(NioSocketChannel ch) throws Exception {
						ChannelPipeline pipeline = ch.pipeline();
						pipeline.addLast(new StringDecoder(Charset.forName("UTF-8")));
						pipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
						pipeline.addLast(businessGroup, nettyClientHandler);
					}});
		try {
			CHANNEL = bootstrap.connect(host, port).sync().channel();
		} catch (InterruptedException exception) {
			log.error("客户端中断异常 : {}", exception.getMessage());
		}
	}

    @PreDestroy
    public void destroy() {
		workerGroup.shutdownGracefully().syncUninterruptibly();
        log.info("客户端关闭成功");
    }
}

package com.hahashou.netty.client.config;

import lombok.Data;

/**
 * @description:
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Data
public class Message {

    private String message;
}

2.6 新建controller包, 放入1个类

package com.hahashou.netty.client.controller;

import com.hahashou.netty.client.config.Message;
import com.hahashou.netty.client.config.NettyClientHandler;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @description:
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@RestController
@RequestMapping("/client")
@Slf4j
public class ClientController {

    @PostMapping("/send")
    public String send(@RequestBody Message dto) {
        Channel channel = NettyClientHandler.CHANNEL;
        if (channel == null) {
            return "服务端已下线";
        }
        channel.writeAndFlush(Unpooled.copiedBuffer(dto.getMessage(), CharsetUtil.UTF_8));
        return "success";
    }
}

2.7 启动客户端, 测试

客户端启动后测试
服务端接收信息后打印

2.8 服务端向客户端发消息

服务端向客户端发消息
客户端接收到信息打印

2.9 服务端停止, 客户端向服务端发消息

服务端停止

  • 8
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哈哈兽0026

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值