netty服務端實戰(一)


  先分享一下自己的經歷。

  去年7月進入新公司沒多久,部門領導就給我分配了一個任務:給公司的一個戶外設備寫一個采集數據程序,將數據入庫,然后做一個web端。因為領導是做.NET的,當時在來之前有和領導溝通過,領導的意思是希望來一個會網絡編程和多線程,部門急需一個可以來做采集程序的java,我當時有點心虛,是這樣回復領導:自己也只是有1年多java的工作,不會網絡編程,自己搭一個簡單的web項目架構還是可以應付的,我只能盡自己的最大的可能做好這件事。

  后來進入公司后,做采集程序是一頭霧水,自己之前只是做web的,對web項目的架構以及業務還是比較熟悉,但是關於和硬件之間的通訊還真的是從來都沒有接觸過。It is the first step that is troublesome。問過一些朋友,也百度過看過一些技術博客,有用socket 連接,多線程后加while死循環來接受做采集,后來慢慢發現用netty這種NIO網絡應用框架比較合適。

  接受就是熟悉一下具體的業務,戶外設備會主動連接服務器IP,定時向服務器的指定端口發送報文。所以當前的采集程序結合netty的情況就是需要寫一個netty的server端,來監聽服務器本機的該指定端口,接受戶外設備發送過來的報文即可,而不需要再寫一個netty的client端。

  首先說明一下,本項目應用的框架:springboot+netty+rabbitMq+mybatis+lombok+logback,數據庫選用的是MYSQL。

  開發環境:idea2018+jdk1.8+mysql5.6.35+maven3.5.3

  接下來就是搭建項目:

  1.idea快速創建springboot項目,在pom.xml文件中配置依賴包

<dependencies>
        <!--web模塊starter依賴-->
        <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>

        <!--  netty依賴 springboot2.0自動導入版本-->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
        </dependency>
        <!--rabbitmq依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--mybatis依賴-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--mysql依賴-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--pagehelper 分頁插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>
        <!--lombok 插件-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.6</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <!--編寫更少量的代碼:使用apache commons工具類庫:
        https://www.cnblogs.com/ITtangtang/p/3966955.html-->
        <!--apache.commons.lang3-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <!--你可以把這個工具看成是java.util的擴展-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>${commons-collections4.version}</version>
        </dependency>
        <!--apache.codec:編碼方法的工具類包
        https://blog.csdn.net/u012881904/article/details/52767853-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
    </dependencies>

2.自定義netty的server端BootNettyServer類

/**
* netty的server類
*/
@Component
@Slf4j
public class BootNettyServer {
@Autowired
BootNettyInitializer bootNettyInitializer;

public void run(int port) throws Exception {
/**
* 配置服務端的NIO線程組
* NioEventLoopGroup 是用來處理I/O操作的Reactor線程組
* bossGroup:用來接收進來的連接,workerGroup:用來處理已經被接收的連接,進行socketChannel的網絡讀寫,
* bossGroup接收到連接后就會把連接信息注冊到workerGroup
* workerGroup的EventLoopGroup默認的線程數是CPU核數的二倍
*/
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();//ServerBootstrap 是一個啟動NIO服務的輔助啟動類
serverBootstrap
.group(bossGroup, workerGroup)//設置group,將bossGroup, workerGroup線程組傳遞到ServerBootstrap
.channel(NioServerSocketChannel.class)//ServerSocketChannel是以NIO的selector為基礎進行實現的,用來接收新的連接,這里告訴Channel通過NioServerSocketChannel獲取新的連接
/**
* option是設置 bossGroup,childOption是設置workerGroup
* netty 默認數據包傳輸大小為1024字節, 設置它可以自動調整下一次緩沖區建立時分配的空間大小,避免內存的浪費 最小 初始化 最大 (根據生產環境實際情況來定)
* 使用對象池,重用緩沖區
*/
.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64, 10496, 1048576))
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64, 10496, 1048576))
.childHandler(bootNettyInitializer);//設置 I/O處理類,主要用於網絡I/O事件,記錄日志,編碼、解碼消息
System.out.println("正在監聽8234端口中....");
ChannelFuture f = serverBootstrap.bind(port).sync();//綁定端口,同步等待成功
f.channel().closeFuture().sync();//等待服務器監聽端口關閉
} catch (InterruptedException e) {

} finally {
/**
* 退出,釋放線程池資源
*/
bossGroup.shutdownGracefully().sync();
workerGroup.shutdownGracefully().sync();
}
}
}

3.自定義初始化類BootNettyInitializer類

  繼承ChannelInitializer,實現initChannel方法,初始化信道時可以配置心跳、編碼器、解碼器、以及業務處理ChannelHandler類。

/**
 * Channel初始化器
 * @param <SocketChannel>
 */
@Component
public class BootNettyInitializer<SocketChannel> extends ChannelInitializer<Channel> {

    @Autowired
    BootNettyHandler bootNettyHandler;
    @Autowired
    MyDecoder myDecoder;
    @Override
    protected void initChannel(Channel ch) throws Exception {
        // ChannelOutboundHandler,依照逆序執行
        ch.pipeline().addLast("decoder", myDecoder);
      
        ch.pipeline().addLast(bootNettyHandler);//添加業務處理handler
        System.out.println("信道初始化中....");
    }
}

4.自定義業務處理類BootNettyHandler

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;/**
 * I/O數據讀寫處理類
 */
@Component
@ChannelHandler.Sharable
@Slf4j
public class BootNettyHandler extends ChannelInboundHandlerAdapter {
    //  將當前客戶端連接存入map,實現長連接,控制設備下發指令
    public  static Map<String, Channel> ctxMap = new LinkedHashMap<String, Channel>();
    /**
     * 從客戶端收到新的數據時,這個方法會在收到消息時被調用
     *
     * @param ctx
     * @param msg
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        SocketChannel channel = (SocketChannel) ctx.channel();//將字符串轉成每兩個字符加空格形式的字符串 7D 7D 21 3C C5 3B 0D 0D 7D 7D 25 3C
        String regex = "(.{2})";
        String input = msg.toString().replaceAll(regex, "$1 ");
     //將報文信息記錄到日志中 log.info(channel.remoteAddress().getHostString()
+ ": " + input); System.out.println("服務端接受信息為: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息:" + input); //具體的處理業務邏輯,解析報文幀結構,將數據入庫 ...... } /** * 從客戶端收到新的數據、讀取完成時調用 * @param ctx */ @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } /** * 當出現 Throwable 對象才會被調用,即當 Netty 由於 IO 錯誤或者處理器在處理事件時拋出的異常時 * @param ctx * @param cause */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { System.out.println("程序異常,斷開客戶端連接"); //獲取客戶端的請求地址 取到的值為客戶端的 ip+端口號 InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress(); String clientIp = insocket.getAddress().getHostAddress();//設備IP地址(個人將設備的IP地址當作 map 的key,channel對象做value) if(ctxMap.get(clientIp)!=null){//如果不為空就剔除 ctxMap.remove(clientIp, ctx.channel()); }else{//否則就將當前的設備ip+端口存進map 當做下發設備的標識的key } cause.printStackTrace(); ctx.close();//拋出異常,斷開與客戶端的連接 log.info(clientIp+"連接斷開"); } /** * 客戶端與服務端第一次建立連接時執行 * @param ctx * @throws Exception */ @Override public void channelActive(ChannelHandlerContext ctx) { InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress(); String clientIp = insocket.getAddress().getHostAddress(); if(ctxMap.get(clientIp)!=null){//如果不為空就不存 }else{//否則就將當前的設備ip+端口存進map 當做下發設備的標識的key ctxMap.put(clientIp, ctx.channel()); } log.info(clientIp+"連接"); } /** * 客戶端與服務端斷連時執行 * @param ctx * @throws Exception */ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress(); String clientIp = insocket.getAddress().getHostAddress(); if(ctxMap.get(clientIp)!=null){//如果不為空就剔除 ctxMap.remove(clientIp, ctx.channel()); }else{//否則就將當前的設備ip+端口存進map 當做下發設備的標識的key } ctx.close(); //斷開連接時,必須關閉,否則造成資源浪費,並發量很大情況下可能造成宕機 log.info(clientIp+"斷開"); } /** * 服務端當read超時, 會調用這個方法 * @param ctx * @param evt * @throws Exception */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception, IOException { if (evt instanceof IdleStateEvent) { IdleStateEvent event = (IdleStateEvent) evt; String eventType = null; switch (event.state()) { case READER_IDLE: eventType = "讀空閑"; break; case WRITER_IDLE: eventType = "些空閑"; break; case ALL_IDLE: eventType = "讀寫空閑"; break; default: } String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); log.info(date + " " + ctx.channel().remoteAddress() + " " + eventType); } } }

回到開始位置,自定義netty的server端BootNettyServer類該如何啟動?

  我的方案是在application啟動類中來啟動。

application類實現CommandLineRunner,來啟動nettyserver服務

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.rtstjkx.jkxlistener.repository")
@SpringBootApplication
public class KjxApplication implements CommandLineRunner {

    @Value("${netty.port}")
    private Integer  port;
    @Autowired
    BootNettyServer bootNettyServer;
    public static void main(String[] args) throws Exception {
        SpringApplication.run(KjxApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        /**
         * 啟動netty服務端服務
         */
        bootNettyServer.run(port);
    }
}

至此,采集程序完成。

  此程序僅個人學習理解后完成,如有不足的地方,還請進一步多多給出指導,thank you !!!

代碼git:git@github.com:white66/jkxLinstener.git

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM