SpringBoot整合Netty並使用Protobuf進行資料傳輸(附工程)

虛無境發表於2019-03-04

前言

本篇文章主要介紹的是SpringBoot整合Netty以及使用Protobuf進行資料傳輸的相關內容。Protobuf會簡單的介紹下用法,至於Netty在之前的文章中已經簡單的介紹過了,這裡就不再過多細說了。

Protobuf

介紹

protocolbuffer(以下簡稱PB)是google 的一種資料交換的格式,它獨立於語言,獨立於平臺。google 提供了多種語言的實現:java、c#、c++、go 和python,每一種實現都包含了相應語言的編譯器以及庫檔案。由於它是一種二進位制的格式,比使用 xml進行資料交換快許多。可以把它用於分散式應用之間的資料通訊或者異構環境下的資料交換。作為一種效率和相容性都很優秀的二進位制資料傳輸格式,可以用於諸如網路傳輸、配置檔案、資料儲存等諸多領域。

官方地址: https://github.com/google/protobuf

使用

這裡的使用就只介紹Java相關的使用。
首先我們需要建立一個proto檔案,在該檔案定義我們需要傳輸的檔案。
例如我們需要定義一個使用者的資訊,包含的欄位主要有編號、名稱、年齡。
那麼該protobuf檔案的格式如下:
:這裡使用的是proto3,相關的註釋我已寫了,這裡便不再過多講述了。需要注意一點的是proto檔案和生成的Java檔名稱不能一致!

syntax = "proto3";
// 生成的包名
option java_package="com.pancm.protobuf";
//生成的java名
option java_outer_classname = "UserInfo";

message UserMsg {  
      
     // ID  
     int32 id = 1;  
      
    // 姓名  
     string name = 2;  
      
    // 年齡  
      int32 age = 3;  
	  
	 // 狀態 
     int32 state = 4;  
} 
複製程式碼

建立好該檔案之後,我們把該檔案和protoc.exe(生成Java檔案的軟體)放到E盤目錄下的protobuf資料夾下,然後再到該目錄的dos介面下輸入:protoc.exe --java_out=檔案絕對路徑名稱
例如:

protoc.exe --java_out=E:protobuf User.proto
複製程式碼

輸入完之後,回車即可在同級目錄看到已經生成好的Java檔案,然後將該檔案放到專案中該檔案指定的路徑下即可。

注:生成protobuf的檔案軟體和測試的protobuf檔案我也整合到該專案中了,可以直接獲取的。

Java檔案生成好之後,我們再來看怎麼使用。
這裡我就直接貼程式碼了,並且將註釋寫在程式碼中,應該更容易理解些吧。。。
程式碼示例:

	 // 按照定義的資料結構,建立一個物件  
    	UserInfo.UserMsg.Builder userInfo = UserInfo.UserMsg.newBuilder();  
    	userInfo.setId(1);
    	userInfo.setName("xuwujing");
    	userInfo.setAge(18);
    	UserInfo.UserMsg userMsg = userInfo.build();  
        // 將資料寫到輸出流 
        ByteArrayOutputStream output = new ByteArrayOutputStream();  
        userMsg.writeTo(output);  
        // 將資料序列化後傳送 
        byte[] byteArray = output.toByteArray();  
        // 接收到流並讀取
        ByteArrayInputStream input = new ByteArrayInputStream(byteArray);  
        // 反序列化  
        UserInfo.UserMsg userInfo2 = UserInfo.UserMsg.parseFrom(input);  
        System.out.println("id:" + userInfo2.getId());  
        System.out.println("name:" + userInfo2.getName());  
        System.out.println("age:" + userInfo2.getAge());  
複製程式碼

注:這裡說明一點,因為protobuf是通過二進位制進行傳輸,所以需要注意下相應的編碼。還有使用protobuf也需要注意一下一次傳輸的最大位元組長度。

輸出結果:

id:1
name:xuwujing
age:18
複製程式碼

SpringBoot整合Netty

說明:如果想直接獲取工程那麼可以直接跳到底部,通過連結下載工程程式碼。

開發準備

環境要求
JDK::1.8
Netty::4.0或以上(不包括5)
Protobuf:3.0或以上

如果對Netty不熟的話,可以看看我之前寫的一些文章。大神請無視~。~
地址:https://blog.csdn.net/column/details/17640.html

首先還是Maven的相關依賴:

<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
		<netty.version>4.1.22.Final</netty.version>
		<protobuf.version>3.5.1</protobuf.version>
		<springboot>1.5.9.RELEASE</springboot>
		<fastjson>1.2.41</fastjson>
		<maven.compiler.source>1.8</maven.compiler.source>
   		<maven.compiler.target>1.8</maven.compiler.target>
	</properties>


	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
			<version>${springboot}</version>
		</dependency>


		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<version>${springboot}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<version>${springboot}</version>
			<optional>true</optional>
		</dependency>

		
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>${netty.version}</version>
		</dependency>

		<dependency>
			<groupId>com.google.protobuf</groupId>
			<artifactId>protobuf-java</artifactId>
			<version>${protobuf.version}</version>
		</dependency>

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>${fastjson}</version>
		</dependency>

		
	<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
			<scope>test</scope>
		</dependency> 
</dependencies>
複製程式碼

新增了相應的maven依賴之後,配置檔案這塊暫時沒有什麼可以新增的,因為暫時就一個監聽的埠而已。

程式碼編寫

程式碼模組主要分為服務端和客戶端。
主要實現的業務邏輯:
服務端啟動成功之後,客戶端也啟動成功,這時服務端會傳送一條protobuf格式的資訊給客戶端,然後客戶端給予相應的應答。客戶端與服務端連線成功之後,客戶端每個一段時間會傳送心跳指令給服務端,告訴服務端該客戶端還存過中,如果客戶端沒有在指定的時間傳送資訊,服務端會關閉與該客戶端的連線。當客戶端無法連線到服務端之後,會每隔一段時間去嘗試重連,只到重連成功!

服務端

首先是編寫服務端的啟動類,相應的註釋在程式碼中寫得很詳細了,這裡也不再過多講述了。不過需要注意的是,在之前的我寫的Netty文章中,是通過main方法直接啟動服務端,因此是直接new一個物件的。而在和SpringBoot整合之後,我們需要將Netty交給springBoot去管理,所以這裡就用了相應的註解。
程式碼如下:

@Service("nettyServer")
public class NettyServer {
	private static final int port = 9876; // 設定服務端埠
	private static EventLoopGroup boss = new NioEventLoopGroup(); // 通過nio方式來接收連線和處理連線
	private static EventLoopGroup work = new NioEventLoopGroup(); // 通過nio方式來接收連線和處理連線
	private static ServerBootstrap b = new ServerBootstrap();
	
	@Autowired
	private NettyServerFilter nettyServerFilter;
	
	
	public void run() {
		try {
			b.group(boss, work);
			b.channel(NioServerSocketChannel.class);
			b.childHandler(nettyServerFilter); // 設定過濾器
			// 伺服器繫結埠監聽
			ChannelFuture f = b.bind(port).sync();
			System.out.println("服務端啟動成功,埠是:" + port);
			// 監聽伺服器關閉監聽
			f.channel().closeFuture().sync();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			// 關閉EventLoopGroup,釋放掉所有資源包括建立的執行緒
			work.shutdownGracefully();
			boss.shutdownGracefully();
		}
	}
}
複製程式碼

服務端主類編寫完畢之後,我們再來設定下相應的過濾條件。
這裡需要繼承Netty中ChannelInitializer類,然後重寫initChannel該方法,進行新增相應的設定,如心跳超時設定,傳輸協議設定,以及相應的業務實現類。
程式碼如下:

	@Component
	 public class NettyServerFilter extends ChannelInitializer<SocketChannel> {
	
	@Autowired
	private NettyServerHandler nettyServerHandler;
	
     @Override
     protected void initChannel(SocketChannel ch) throws Exception {
         ChannelPipeline ph = ch.pipeline();
      
         //入參說明: 讀超時時間、寫超時時間、所有型別的超時時間、時間格式
         ph.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
         // 解碼和編碼,應和客戶端一致
         //傳輸的協議 Protobuf
         ph.addLast(new ProtobufVarint32FrameDecoder());
         ph.addLast(new ProtobufDecoder(UserMsg.getDefaultInstance()));
         ph.addLast(new ProtobufVarint32LengthFieldPrepender());
         ph.addLast(new ProtobufEncoder());
         
         //業務邏輯實現類
         ph.addLast("nettyServerHandler", nettyServerHandler);
       }
     }
複製程式碼

服務相關的設定的程式碼寫完之後,我們再來編寫主要的業務程式碼。
使用Netty編寫業務層的程式碼,我們需要繼承ChannelInboundHandlerAdapterSimpleChannelInboundHandler類,在這裡順便說下它們兩的區別吧。
繼承SimpleChannelInboundHandler類之後,會在接收到資料後會自動release掉資料佔用的Bytebuffer資源。並且繼承該類需要指定資料格式。
而繼承ChannelInboundHandlerAdapter則不會自動釋放,需要手動呼叫ReferenceCountUtil.release()等方法進行釋放。繼承該類不需要指定資料格式。
所以在這裡,個人推薦服務端繼承
ChannelInboundHandlerAdapter
,手動進行釋放,防止資料未處理完就自動釋放了。而且服務端可能有多個客戶端進行連線,並且每一個客戶端請求的資料格式都不一致,這時便可以進行相應的處理。
客戶端根據情況可以繼承SimpleChannelInboundHandler類。好處是直接指定好傳輸的資料格式,就不需要再進行格式的轉換了。

程式碼如下:

@Service("nettyServerHandler")
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

	/** 空閒次數 */
	private int idle_count = 1;
	/** 傳送次數 */
	private int count = 1;


	/**
	 * 建立連線時,傳送一條訊息
	 */
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		System.out.println("連線的客戶端地址:" + ctx.channel().remoteAddress());
		UserInfo.UserMsg userMsg = UserInfo.UserMsg.newBuilder().setId(1).setAge(18).setName("xuwujing").setState(0)
				.build();
		ctx.writeAndFlush(userMsg);
		super.channelActive(ctx);
	}

	/**
	 * 超時處理 如果5秒沒有接受客戶端的心跳,就觸發; 如果超過兩次,則直接關閉;
	 */
	@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception {
		if (obj instanceof IdleStateEvent) {
			IdleStateEvent event = (IdleStateEvent) obj;
			if (IdleState.READER_IDLE.equals(event.state())) { // 如果讀通道處於空閒狀態,說明沒有接收到心跳命令
				System.out.println("已經5秒沒有接收到客戶端的資訊了");
				if (idle_count > 1) {
					System.out.println("關閉這個不活躍的channel");
					ctx.channel().close();
				}
				idle_count++;
			}
		} else {
			super.userEventTriggered(ctx, obj);
		}
	}

	/**
	 * 業務邏輯處理
	 */
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		System.out.println("第" + count + "次" + ",服務端接受的訊息:" + msg);
		try {
			// 如果是protobuf型別的資料
		  if (msg instanceof UserMsg) {
				UserInfo.UserMsg userState = (UserInfo.UserMsg) msg;
				if (userState.getState() == 1) {
					System.out.println("客戶端業務處理成功!");
				} else if(userState.getState() == 2){
					System.out.println("接受到客戶端傳送的心跳!");
				}else{
					System.out.println("未知命令!");
				}
			} else {
				System.out.println("未知資料!" + msg);
				return;
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			ReferenceCountUtil.release(msg);
		}
		count++;
	}

	/**
	 * 異常處理
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		ctx.close();
	}
}
複製程式碼

還有個服務端的啟動類,之前是通過main方法直接啟動, 不過這裡改成了通過springBoot進行啟動,差別不大。
程式碼如下:

@SpringBootApplication
public class NettyServerApp {

	public static void main(String[] args) {
		// 啟動嵌入式的 Tomcat 並初始化 Spring 環境及其各 Spring 元件
		ApplicationContext context = SpringApplication.run(NettyServerApp.class, args);
		NettyServer nettyServer = context.getBean(NettyServer.class);
		nettyServer.run();
	}

}
複製程式碼

到這裡服務端相應的程式碼就編寫完畢了。

客戶端

客戶端這邊的程式碼和服務端的很多地方都類似,我就不再過多細說了,主要將一些不同的程式碼拿出來簡單的講述下。
首先是客戶端的主類,基本和服務端的差不多,也就是多了監聽的埠和一個監聽器(用來監聽是否和服務端斷開連線,用於重連)。
主要實現的程式碼邏輯如下:

	public void doConnect(Bootstrap bootstrap, EventLoopGroup eventLoopGroup) {
		ChannelFuture f = null;
		try {
			if (bootstrap != null) {
				bootstrap.group(eventLoopGroup);
				bootstrap.channel(NioSocketChannel.class);
				bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
				bootstrap.handler(nettyClientFilter);
				bootstrap.remoteAddress(host, port);
				f = bootstrap.connect().addListener((ChannelFuture futureListener) -> {
					final EventLoop eventLoop = futureListener.channel().eventLoop();
					if (!futureListener.isSuccess()) {
						System.out.println("與服務端斷開連線!在10s之後準備嘗試重連!");
						eventLoop.schedule(() -> doConnect(new Bootstrap(), eventLoop), 10, TimeUnit.SECONDS);
					}
				});
				if(initFalg){
					System.out.println("Netty客戶端啟動成功!");
					initFalg=false;
				}
				// 阻塞
				f.channel().closeFuture().sync();
			}
		} catch (Exception e) {
			System.out.println("客戶端連線失敗!"+e.getMessage());
		}
	}
複製程式碼

注:監聽器這塊的實現用的是JDK1.8的寫法。

客戶端過濾其這塊基本和服務端一直。不過需要注意的是,傳輸協議、編碼和解碼應該一致,還有心跳的讀寫時間應該小於服務端所設定的時間。
改動的程式碼如下:

	ChannelPipeline ph = ch.pipeline();
        /*
         * 解碼和編碼,應和服務端一致
         * */
        //入參說明: 讀超時時間、寫超時時間、所有型別的超時時間、時間格式
        ph.addLast(new IdleStateHandler(0, 4, 0, TimeUnit.SECONDS)); 
複製程式碼

客戶端的業務程式碼邏輯。
主要實現的幾點邏輯是心跳按時傳送以及解析服務傳送的protobuf格式的資料。
這裡比服務端多個個註解, 該註解Sharable主要是為了多個handler可以被多個channel安全地共享,也就是保證執行緒安全。
廢話就不多說了,程式碼如下:

	@Service("nettyClientHandler")
	@ChannelHandler.Sharable
	public class NettyClientHandler extends ChannelInboundHandlerAdapter {
	@Autowired
	private NettyClient nettyClient;
	
	/** 迴圈次數 */
	private int fcount = 1;
	
	/**
	 * 建立連線時
	 */
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		System.out.println("建立連線時:" + new Date());
		ctx.fireChannelActive();
	}

	/**
	 * 關閉連線時
	 */
	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		System.out.println("關閉連線時:" + new Date());
		final EventLoop eventLoop = ctx.channel().eventLoop();
		nettyClient.doConnect(new Bootstrap(), eventLoop);
		super.channelInactive(ctx);
	}

	/**
	 * 心跳請求處理 每4秒傳送一次心跳請求;
	 * 
	 */
	@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception {
		System.out.println("迴圈請求的時間:" + new Date() + ",次數" + fcount);
		if (obj instanceof IdleStateEvent) {
			IdleStateEvent event = (IdleStateEvent) obj;
			if (IdleState.WRITER_IDLE.equals(event.state())) { // 如果寫通道處於空閒狀態,就傳送心跳命令
				UserMsg.Builder userState = UserMsg.newBuilder().setState(2);
				ctx.channel().writeAndFlush(userState);
				fcount++;
			}
		}
	}

	/**
	 * 業務邏輯處理
	 */
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// 如果不是protobuf型別的資料
		if (!(msg instanceof UserMsg)) {
			System.out.println("未知資料!" + msg);
			return;
		}
		try {

			// 得到protobuf的資料
			UserInfo.UserMsg userMsg = (UserInfo.UserMsg) msg;
			// 進行相應的業務處理。。。
			// 這裡就從簡了,只是列印而已
			System.out.println(
					"客戶端接受到的使用者資訊。編號:" + userMsg.getId() + ",姓名:" + userMsg.getName() + ",年齡:" + userMsg.getAge());

			// 這裡返回一個已經接受到資料的狀態
			UserMsg.Builder userState = UserMsg.newBuilder().setState(1);
			ctx.writeAndFlush(userState);
			System.out.println("成功傳送給服務端!");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			ReferenceCountUtil.release(msg);
		}
	 }
	}
複製程式碼

那麼到這裡客戶端的程式碼也編寫完畢了。

功能測試

首先啟動服務端,然後再啟動客戶端。
我們來看看結果是否如上述所說。

服務端輸出結果:

服務端啟動成功,埠是:9876
連線的客戶端地址:/127.0.0.1:53319
第1次,服務端接受的訊息:state: 1

客戶端業務處理成功!
第2次,服務端接受的訊息:state: 2

接受到客戶端傳送的心跳!
第3次,服務端接受的訊息:state: 2

接受到客戶端傳送的心跳!
第4次,服務端接受的訊息:state: 2

接受到客戶端傳送的心跳!
複製程式碼

客戶端輸入結果:

Netty客戶端啟動成功!
建立連線時:Mon Jul 16 23:31:58 CST 2018
客戶端接受到的使用者資訊。編號:1,姓名:xuwujing,年齡:18
成功傳送給服務端!
迴圈請求的時間:Mon Jul 16 23:32:02 CST 2018,次數1
迴圈請求的時間:Mon Jul 16 23:32:06 CST 2018,次數2
迴圈請求的時間:Mon Jul 16 23:32:10 CST 2018,次數3
迴圈請求的時間:Mon Jul 16 23:32:14 CST 2018,次數4
複製程式碼

通過列印資訊可以看出如上述所說。

接下來我們再來看看客戶端是否能夠實現重連。
先啟動客戶端,再啟動服務端。

客戶端輸入結果:

Netty客戶端啟動成功!
與服務端斷開連線!在10s之後準備嘗試重連!
客戶端連線失敗!AbstractChannel$CloseFuture@1fbaa3ac(incomplete)
建立連線時:Mon Jul 16 23:41:33 CST 2018
客戶端接受到的使用者資訊。編號:1,姓名:xuwujing,年齡:18
成功傳送給服務端!
迴圈請求的時間:Mon Jul 16 23:41:38 CST 2018,次數1
迴圈請求的時間:Mon Jul 16 23:41:42 CST 2018,次數2
迴圈請求的時間:Mon Jul 16 23:41:46 CST 2018,次數3
複製程式碼

服務端輸出結果:

服務端啟動成功,埠是:9876
連線的客戶端地址:/127.0.0.1:53492
第1次,服務端接受的訊息:state: 1

客戶端業務處理成功!
第2次,服務端接受的訊息:state: 2

接受到客戶端傳送的心跳!
第3次,服務端接受的訊息:state: 2

接受到客戶端傳送的心跳!
第4次,服務端接受的訊息:state: 2
複製程式碼

結果也如上述所說!

其它

關於SpringBoot整合Netty使用Protobuf進行資料傳輸到這裡就結束了。
SpringBoot整合Netty使用Protobuf進行資料傳輸的專案工程地址:
https://github.com/xuwujing/springBoot-study/tree/master/springboot-netty-protobuf

對了,也有不使用springBoot整合的Netty專案工程地址:
https://github.com/xuwujing/Netty-study/tree/master/Netty-protobuf

原創不易,如果感覺不錯,希望給個推薦!您的支援是我寫作的最大動力!
版權宣告:
作者:虛無境
部落格園出處:http://www.cnblogs.com/xuwujing
CSDN出處:http://blog.csdn.net/qazwsxpcm    
個人部落格出處:http://www.panchengming.com

相關文章