Netty是一个功能强大的通信框架,以前也用过与他类似的MINA,MINA虽然功能很多,但在处理TCP的拆包粘包问题时,需要自己编写编码器(对数据进行过滤),而Netty则提供了一些方法来解决,在本次Demo中,作者也使用了其中的一种,至于其他的,可以上网上搜索,也可以查看官方文档。本文主要是以搭建项目为主。话不多说,直接上代码。
首先我们先准备一个没有整合Netty的项目,该项目由Springboot、Mybatis以及MySQL构成。相信各位对于这种代码都已经看吐了,但作者还是要上传一下,因为作者会在当中穿插着使用Netty。
第一步,我们需要引入相关的jar包,作者使用Maven来管理这些依赖:
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>1.5.15.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mcd</groupId>
<artifactId>netty</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>netty</name>
<description>Demo 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.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- Netty -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha1</version>
</dependency>
<!-- MySql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 打包时忽略测试文件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
mvn clean package,也可以在IDEA中使用Maven工具进行打包,效果相同。
application.yml文件:
application.yml:
# 配置Tomcat的启动端口、项目名
server:
port: 18080
context-path: /netty
# 配置日志文件
logging.config:
classpath:/logback.xml
# 文件上传下载大小、速度配置
spring:
http:
multipart:
max-file-size: 1000Mb
max-request-size: 1000Mb
# 数据库连接配置
jdbc:
driver: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mcd_netty?useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123
# mybatis配置
# mybatis全局配置文件
mybatis_config_file: mybatis-config.xml
# mybatis映射文件路径
mapper_path: /mapper/**.xml
# mybatis实体类包路径配置
entity_package: com.mcd.netty.entity
logback.xml以及Mybatis的全局配置文件mybatis-config.xml,在这里也一并放出,也可以参考网上的:
logback.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 定义参数常量 -->
<!-- TRACE<DEBUG<INFO<WARN<ERROR -->
<!-- logger.trace("msg") logger.debug... -->
<property name="log.level" value="debug"/>
<property name="log.maxHistory" value="30"/>
<property name="log.filePath" value="F:/ideaProject/saleProject/netty/log"/>
<property name="log.pattern"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 控制台设置 -->
<appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- DEBUG -->
<appender name="debugAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 文件路径 -->
<file>${log.filePath}/debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 文件名称 -->
<fileNamePattern>${log.filePath}/debug/debug.%d{yyyy-MM-dd}.log.gz
</fileNamePattern>
<!-- 文件最大保存历史数量 -->
<maxHistory>${log.maxHistory}</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- INFO -->
<appender name="infoAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 文件路径 -->
<file>${log.filePath}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 文件名称 -->
<fileNamePattern>${log.filePath}/info/info.%d{yyyy-MM-dd}.log.gz
</fileNamePattern>
<!-- 文件最大保存历史数量 -->
<maxHistory>${log.maxHistory}</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- ERROR -->
<appender name="errorAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 文件路径 -->
<file>${log.filePath}/erorr.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 文件名称 -->
<fileNamePattern>${log.filePath}/error/error.%d{yyyy-MM-dd}.log.gz
</fileNamePattern>
<!-- 文件最大保存历史数量 -->
<maxHistory>${log.maxHistory}</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<logger name="com.mcd.netty" level="${log.level}" additivity="true">
<appender-ref ref="debugAppender"/>
<appender-ref ref="infoAppender"/>
<appender-ref ref="errorAppender"/>
</logger>
<root level="info">
<appender-ref ref="consoleAppender"/>
</root>
</configuration>
mybatis-config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置全局属性 -->
<settings>
<!-- 使用jdbc的getGeneratedKeys获取数据库自增主键值 -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 使用列标签替换列别名 默认:true -->
<setting name="useColumnLabel" value="true"/>
<!-- 开始驼峰命名转换:Table{create_time} -> Entity{createTime} -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
配置文件已经全部写完了,接下来开始加载一些配置,作者在这里把配置分为了跨域、Dao和事务,在基础包路径下创建config包,其中包含web、service、dao三层;web中完成跨域的配置,service完成事务的配置,dao中完成数据库相关配置。文件如下:
CorsConfig.java:
package com.mcd.netty.config.web;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* @Description: 配置对外接口访问设置
* @Author: amao
* @CreateDate: 2018/12/24 15:57
*/
//表明这是一个配置类
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*"); // 1
corsConfiguration.addAllowedHeader("*"); // 2
corsConfiguration.addAllowedMethod("*"); // 3
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig()); // 4
return new CorsFilter(source);
}
}
TransactionManagementConfiguration.java:
package com.mcd.netty.config.service;//package com.mcd.netty.config.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;
import javax.sql.DataSource;
/**
* @Description: 事务配置类
* @Author: amao
* @CreateDate: 2018/10/18 16:43
*/
@Configuration
@EnableTransactionManagement
public class TransactionManagementConfiguration implements TransactionManagementConfigurer {
@Autowired
private DataSource dataSource;
@Override
public PlatformTransactionManager annotationDrivenTransactionManager() {
return new DataSourceTransactionManager(dataSource);
}
}
DataSourceConfiguration.java:
package com.mcd.netty.config.dao;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.beans.PropertyVetoException;
/**
* @Description: 数据库连接配置
* @Author: amao
* @CreateDate: 2019/1/15 10:48
*/
//表明这是一个配置类
@Configuration
//配置mybatis mapper的扫描路径
@MapperScan("com.mcd.netty.dao")
public class DataSourceConfiguration {
//引用yml文件驱动类型
@Value("${jdbc.driver}")
private String jdbcDriver;
//引用yml文件数据库路径
@Value("${jdbc.url}")
private String jdbcUrl;
//引用yml文件用户名
@Value("${jdbc.username}")
private String jdbcUsername;
//引用yml文件密码
@Value("${jdbc.password}")
private String jdbcPassword;
@Bean(name = "dataSource")
public ComboPooledDataSource createDataSource() throws PropertyVetoException {
//创建dataSouce
ComboPooledDataSource dataSource = new ComboPooledDataSource();
//设置驱动类型
dataSource.setDriverClass(jdbcDriver);
//设置数据库路径
dataSource.setJdbcUrl(jdbcUrl);
//设置用户名
dataSource.setUser(jdbcUsername);
//设置密码
dataSource.setPassword(jdbcPassword);
// 关闭连接后不自动commit
dataSource.setAutoCommitOnClose(false);
return dataSource;
}
}
SessionFactoryConfiguration.java:
package com.mcd.netty.config.dao;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
import java.io.IOException;
/**
* @Description: 数据库会话配置
* @Author: amao
* @CreateDate: 2019/1/15 11:04
*/
//表明这是一个配置类
@Configuration
public class SessionFactoryConfiguration {
//读取mybatis全局配置文件
@Value("${mybatis_config_file}")
private String mybatisConfigFilePath;
//读取数据库设置
@Qualifier("dataSource")
@Autowired
private DataSource dataSource;
//读取实体类包路径
@Value("${entity_package}")
private String entityPackage;
//读取映射文件的路径
@Value("${mapper_path}")
private String mapperPath;
//使用spring工厂类创建会话
@Bean(name = "sqlSessionFactory")
public SqlSessionFactoryBean createSqlSessionFactoryBean() throws IOException {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource(mybatisConfigFilePath));
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
String packageSearchPath = PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + mapperPath;
sqlSessionFactoryBean.setMapperLocations(resolver.getResources(packageSearchPath));
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage(entityPackage);
return sqlSessionFactoryBean;
}
}
当这些配置完成后,就可以启动项目了,但是控制台会打印一条警告信息,提示你mapper文件夹下没有xml文件,这时候,我们需要完成一些基础的CRUD,这里就不过多解释了,直接上代码,不懂的同学可以先去学习后在看本文:
controller.NettyController.java:
package com.mcd.netty.controller;
import com.mcd.netty.service.NettyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/netty")
public class NettyController {
private static final Logger log = LoggerFactory.getLogger(NettyController.class);
@Autowired
private NettyService nettyService;
@RequestMapping(value = "/getMessage")
public Map<String,Object> getMessage(){
log.info("test");
Map<String,Object> modelMap = new HashMap<>();
modelMap.put("message", nettyService.getAllFacility());
return modelMap;
}
}
service.NettyService.java:
package com.mcd.netty.service;
import com.mcd.netty.entity.NettyMsg;
import java.util.List;
public interface NettyService {
boolean saveMessage(NettyMsg nettyMsg);
List<NettyMsg> getAllFacility();
}
service.impl.NettyServiceImpl.java:
package com.mcd.netty.service.impl;
import com.mcd.netty.dao.NettyDao;
import com.mcd.netty.entity.NettyMsg;
import com.mcd.netty.service.NettyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
@Service
public class NettyServiceImpl implements NettyService {
private static final Logger log = LoggerFactory.getLogger(NettyServiceImpl.class);
@Autowired
private NettyDao nettyDao;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveMessage(NettyMsg nettyMsg) {
try {
nettyMsg.setSaveTime(new Date());
// 1.判断是否有数据
if (nettyDao.getMessageByFacilityId(nettyMsg.getFacilityId()) == null) {
if (nettyDao.saveMessage(nettyMsg) != 1) {
throw new RuntimeException("存储失败");
}
} else if (nettyDao.updateMessage(nettyMsg) != 1) {
throw new RuntimeException("更新失败");
}
return true;
} catch (RuntimeException e) {
log.error(e.getMessage());
throw new RuntimeException("数据存储失败");
}
}
@Override
public List<NettyMsg> getAllFacility() {
return nettyDao.getAllFacility();
}
}
dao.NettyDao.java:
package com.mcd.netty.dao;
import com.mcd.netty.entity.NettyMsg;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface NettyDao {
NettyMsg getMessageByFacilityId(@Param("facilityId") String facilityId);
int saveMessage(NettyMsg nettyMsg);
int updateMessage(NettyMsg nettyMsg);
List<NettyMsg> getAllFacility();
}
mapper/NettyDao.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mcd.netty.dao.NettyDao">
<!-- Java实体类与数据库表字段绑定 -->
<resultMap id="msgMap" type="com.mcd.netty.entity.NettyMsg">
<!-- 设备编号 -->
<result property="facilityId" column="FACILITY_ID"/>
<!-- 设备名称 -->
<result property="facilityName" column="FACILITY_NAME"/>
<!-- 经度 -->
<result property="longitude" column="LONGITUDE"/>
<!-- 纬度 -->
<result property="latitude" column="LATITUDE"/>
<!-- 高程 -->
<result property="height" column="HEIGHT"/>
<!-- RTK状态 -->
<result property="RTKStatus" column="RTK_STATUS"/>
<!-- 卫星数量 -->
<result property="satelliteCount" column="SATELLITE_COUNT"/>
<!-- 入库时间 -->
<result property="saveTime" column="SAVE_TIME"/>
</resultMap>
<select id="getMessageByFacilityId" resultMap="msgMap">
SELECT * FROM t_netty_msg WHERE FACILITY_ID = #{facilityId}
</select>
<insert id="saveMessage" parameterType="com.mcd.netty.entity.NettyMsg">
INSERT INTO t_netty_msg
(FACILITY_ID, FACILITY_NAME, LONGITUDE, LATITUDE,
HEIGHT, RTK_STATUS, SATELLITE_COUNT, SAVE_TIME)
VALUES
(#{facilityId},#{facilityName},#{longitude},#{latitude},
#{height},#{RTKStatus},#{satelliteCount},#{saveTime})
</insert>
<update id="updateMessage" parameterType="com.mcd.netty.entity.NettyMsg">
UPDATE t_netty_msg
<set>
<if test="facilityName != null">FACILITY_NAME = #{facilityName},</if>
<if test="longitude != null">LONGITUDE = #{longitude},</if>
<if test="latitude != null">LATITUDE = #{latitude},</if>
<if test="height != null">HEIGHT = #{height},</if>
<if test="RTKStatus != null">RTK_STATUS = #{RTKStatus},</if>
<if test="satelliteCount != null">SATELLITE_COUNT = #{satelliteCount},</if>
<if test="saveTime != null">SAVE_TIME = #{saveTime},</if>
</set>
WHERE FACILITY_ID = #{facilityId}
</update>
<select id="getAllFacility" resultMap="msgMap">
SELECT * FROM t_netty_msg
</select>
</mapper>
entity.NettyMsg.java:
package com.mcd.netty.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
public class NettyMsg {
// 设备编号 使用UUID
private String facilityId;
// 设备名称
private String facilityName;
// 经度
private double longitude;
// 纬度
private double latitude;
// 高程
private double height;
// RTK状态 例如4,5
private int RTKStatus;
// 卫星数量
private int satelliteCount;
// 入库时间
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date saveTime;
public String getFacilityId() {
return facilityId;
}
public void setFacilityId(String facilityId) {
this.facilityId = facilityId;
}
public String getFacilityName() {
return facilityName;
}
public void setFacilityName(String facilityName) {
this.facilityName = facilityName;
}
public double getLongitude() {
return longitude;
}
public void setLongitude(double longitude) {
this.longitude = longitude;
}
public double getLatitude() {
return latitude;
}
public void setLatitude(double latitude) {
this.latitude = latitude;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public int getRTKStatus() {
return RTKStatus;
}
public void setRTKStatus(int RTKStatus) {
this.RTKStatus = RTKStatus;
}
public int getSatelliteCount() {
return satelliteCount;
}
public void setSatelliteCount(int satelliteCount) {
this.satelliteCount = satelliteCount;
}
public Date getSaveTime() {
return saveTime;
}
public void setSaveTime(Date saveTime) {
this.saveTime = saveTime;
}
@Override
public String toString() {
return "Message{" +
"facilityId='" + facilityId + '\'' +
", facilityName='" + facilityName + '\'' +
", longitude=" + longitude +
", latitude=" + latitude +
", height=" + height +
", RTKStatus=" + RTKStatus +
", satelliteCount=" + satelliteCount +
", saveTime=" + saveTime +
'}';
}
}
这里使用IDEA快捷键生成的,也可以引入lombok来减少getter/setter方法等的编写。到此,基本的流程已经结束了,现在可以通过各个Client(例如Browser,Mobile Phone等等)访问这个Web接口了。
接下来,进入本文的重头戏 => Netty,在看本文前,需要了解Netty的用法,本Demo中Netty用spring来管理,直接上代码:
DiscardServer.java:
package com.mcd.netty.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DiscardServer {
private static final Logger log = LoggerFactory.getLogger(DiscardServer.class);
@Autowired
private ChildChannelHandler childChannelHandler;
public void run(int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
System.out.println("准备运行端口:" + port);
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_REUSEADDR, true) //允许重复使用端口
.childHandler(childChannelHandler);
//绑定端口,同步等待成功
ChannelFuture f = bootstrap.bind(port).sync();
//等待服务监听端口关闭
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error(e.getMessage());
} finally {
//退出,释放线程资源
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
该Java文件中包含了Netty的整体流程,需要自己动手更改的是childHandler方法中的处理器,我们这边给他自动注入对象,接下来就是ChildChannelHandler.java:
package com.mcd.netty.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Autowired
private DiscardServerHandler discardServerHandler;
public void initChannel(SocketChannel socketChannel) throws Exception {
// 定义分隔符为$$(字符串末尾分割)
ByteBuf delimiter = Unpooled.copiedBuffer("$$".getBytes());
// 添加分隔符解码器,通过分隔符来解决拆包粘包的问题
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(2048, delimiter));
// 自定义解码器,用来获取数据并做持久化处理
socketChannel.pipeline().addLast(discardServerHandler);
}
}
这里面我定义了两个编码器,一个来解决TCP的拆包粘包问题,第二个用来把数据转换成对应的实体类,并做持久化处理。 DelimiterBasedFrameDecoder这个解码器是由Netty提供的,根据报文结尾的分隔符来分割数据,从而解决粘包的问题,其他Netty提供的解码器在这就不做讲解了,用法类似。
接下里就是自定义解码器 DiscardServerHandler.java:
package com.mcd.netty.netty;
import com.mcd.netty.entity.NettyMsg;
import com.mcd.netty.service.NettyService;
import com.mcd.netty.util.NettyUtils;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@ChannelHandler.Sharable
public class DiscardServerHandler extends ChannelHandlerAdapter {
private static final Logger log = LoggerFactory.getLogger(DiscardServerHandler.class);
@Autowired
private NettyService nettyService;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
// 1. 数据根据分隔符拆分
String[] dataArray = NettyUtils.splitMsg(msg, ",");
if (dataArray == null) {
NettyUtils.write(ctx, NettyUtils.WRITE_FAIL_MSG);
}
for (String s : dataArray) {
System.out.println(s);
}
// 2. 绑定对象
NettyMsg nettyMsg = NettyUtils.bindObject(NettyMsg.class,dataArray);
// 3. 存储数据,存储成功返回客户端消息
if(!nettyService.saveMessage(nettyMsg)){
NettyUtils.write(ctx, NettyUtils.WRITE_SUCCESS_MSG);
}
NettyUtils.write(ctx, NettyUtils.WRITE_FAIL_MSG);
} catch (RuntimeException e) {
log.error(e.getMessage());
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 出现异常就关闭
log.error(cause.getMessage());
ctx.close();
}
}
里面一些比较相似的部分我进行了封装,做成了工具类,参考如下,由于作者基础较差,反射应用的不是很熟练,各位可以自己重写该绑定对象方法。
NettyUtils.java:
package com.mcd.netty.util;
import com.mcd.netty.entity.NettyMsg;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class NettyUtils {
private static final Logger log = LoggerFactory.getLogger(NettyUtils.class);
public static final String WRITE_SUCCESS_MSG = "received success";
public static final String WRITE_FAIL_MSG = "received fail";
public static String[] splitMsg(Object msg, String regex) {
String message = ((ByteBuf) msg).toString(CharsetUtil.UTF_8);
if ("".equals(message) || message == null) {
return null;
}
return message.split(regex);
}
public static void write(ChannelHandlerContext ctx, String msg) {
ctx.write(
ctx.alloc().buffer(4 * msg.length())
.writeBytes(msg.getBytes())
);
ctx.flush();
}
public static NettyMsg bindObject(Class nettyMsgName, String[] dataArray) {
try {
Class clazz = Class.forName(nettyMsgName.getName());
NettyMsg nettyMsg = (NettyMsg) clazz.newInstance();
nettyMsg.setFacilityId(dataArray[0]);
nettyMsg.setFacilityName(dataArray[1]);
nettyMsg.setLongitude(Double.parseDouble(dataArray[2]));
nettyMsg.setLatitude(Double.parseDouble(dataArray[3]));
nettyMsg.setHeight(Double.parseDouble(dataArray[4]));
nettyMsg.setRTKStatus(Integer.parseInt(dataArray[5]));
nettyMsg.setSatelliteCount(Integer.parseInt(dataArray[6]));
return nettyMsg;
} catch (ClassNotFoundException e) {
log.error(e.getMessage());
} catch (IllegalAccessException e) {
log.error(e.getMessage());
} catch (InstantiationException e) {
log.error(e.getMessage());
}
return null;
}
}
接下来是启动类,在这里面同时启动Tomcat服务和Netty的TCP服务,代码如下:
NettyApplication.java:
package com.mcd.netty;
import com.mcd.netty.netty.DiscardServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class NettyApplication implements CommandLineRunner {
@Autowired
private DiscardServer discardServer;
public static void main(String[] args) {
SpringApplication.run(NettyApplication.class, args);
}
@Override
public void run(String... strings) throws Exception {
discardServer.run(28080);
}
}
到此,就可以启动项目了(不了解CommandLineRunner的同学可在网上搜索资料),在本地启动一个网络调试助手,发送数据,再启动一个Chrome,调用接口,可以看到数据实时在更新。到此,基础功能就实现了。