SpringBoot整合Netty并使用Protobuf進(jìn)行數(shù)據(jù)傳輸 (附工程)

前言
本篇文章主要介紹的是SpringBoot整合Netty以及使用Protobuf進(jìn)行數(shù)據(jù)傳輸?shù)南嚓P(guān)內(nèi)容。Protobuf會(huì)簡(jiǎn)單的介紹下用法,至于Netty在之前的文章中已經(jīng)簡(jiǎn)單的介紹過(guò)了,這里就不再過(guò)多細(xì)說(shuō)了。
Protobuf
介紹
protocolbuffer(以下簡(jiǎn)稱(chēng)PB)是google 的一種數(shù)據(jù)交換的格式,它獨(dú)立于語(yǔ)言,獨(dú)立于平臺(tái)。google 提供了多種語(yǔ)言的實(shí)現(xiàn):java、c#、c++、go 和python,每一種實(shí)現(xiàn)都包含了相應(yīng)語(yǔ)言的編譯器以及庫(kù)文件。由于它是一種二進(jìn)制的格式,比使用 xml進(jìn)行數(shù)據(jù)交換快許多。可以把它用于分布式應(yīng)用之間的數(shù)據(jù)通信或者異構(gòu)環(huán)境下的數(shù)據(jù)交換。作為一種效率和兼容性都很優(yōu)秀的二進(jìn)制數(shù)據(jù)傳輸格式,可以用于諸如網(wǎng)絡(luò)傳輸、配置文件、數(shù)據(jù)存儲(chǔ)等諸多領(lǐng)域。
官方地址:?https://github.com/google/protobuf
使用
這里的使用就只介紹Java相關(guān)的使用。
首先我們需要建立一個(gè)proto文件,在該文件定義我們需要傳輸?shù)奈募?br>例如我們需要定義一個(gè)用戶的信息,包含的字段主要有編號(hào)、名稱(chēng)、年齡。
那么該protobuf文件的格式如下:
注:這里使用的是proto3,相關(guān)的注釋我已寫(xiě)了,這里便不再過(guò)多講述了。需要注意一點(diǎn)的是proto文件和生成的Java文件名稱(chēng)不能一致!
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;
// 狀態(tài)
int32 state = 4;
}
創(chuàng)建好該文件之后,我們把該文件和protoc.exe(生成Java文件的軟件)放到E盤(pán)目錄下的protobuf文件夾下,然后再到該目錄的dos界面下輸入:protoc.exe --java_out=文件絕對(duì)路徑名稱(chēng)。
例如:
protoc.exe --java_out=E:\protobuf User.proto
輸入完之后,回車(chē)即可在同級(jí)目錄看到已經(jīng)生成好的Java文件,然后將該文件放到項(xiàng)目中該文件指定的路徑下即可。
注:生成protobuf的文件軟件和測(cè)試的protobuf文件我也整合到該項(xiàng)目中了,可以直接獲取的。
Java文件生成好之后,我們?cè)賮?lái)看怎么使用。
這里我就直接貼代碼了,并且將注釋寫(xiě)在代碼中,應(yīng)該更容易理解些吧。。。
代碼示例:
// 按照定義的數(shù)據(jù)結(jié)構(gòu),創(chuàng)建一個(gè)對(duì)象
UserInfo.UserMsg.Builder userInfo = UserInfo.UserMsg.newBuilder();
userInfo.setId(1);
userInfo.setName("xuwujing");
userInfo.setAge(18);
UserInfo.UserMsg userMsg = userInfo.build();
// 將數(shù)據(jù)寫(xiě)到輸出流
ByteArrayOutputStream output = new ByteArrayOutputStream();
userMsg.writeTo(output);
// 將數(shù)據(jù)序列化后發(fā)送
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());
注:這里說(shuō)明一點(diǎn),因?yàn)?strong>protobuf是通過(guò)二進(jìn)制進(jìn)行傳輸,所以需要注意下相應(yīng)的編碼。還有使用protobuf也需要注意一下一次傳輸?shù)淖畲笞止?jié)長(zhǎng)度。
輸出結(jié)果:
id:1
name:xuwujing
age:18
SpringBoot整合Netty
說(shuō)明:如果想直接獲取工程那么可以直接跳到底部,通過(guò)鏈接下載工程代碼。
開(kāi)發(fā)準(zhǔn)備
環(huán)境要求
JDK::1.8
Netty::4.0或以上(不包括5)
Protobuf:3.0或以上
如果對(duì)Netty不熟的話,可以看看我之前寫(xiě)的一些文章。大神請(qǐng)無(wú)視~。~
地址:https://blog.csdn.net/column/details/17640.html
首先還是Maven的相關(guān)依賴(lài):
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<java.version>1.8java.version>
<netty.version>4.1.22.Finalnetty.version>
<protobuf.version>3.5.1protobuf.version>
<springboot>1.5.9.RELEASEspringboot>
<fastjson>1.2.41fastjson>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
<version>${springboot}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<version>${springboot}version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<version>${springboot}version>
<optional>trueoptional>
dependency>
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
<version>${netty.version}version>
dependency>
<dependency>
<groupId>com.google.protobufgroupId>
<artifactId>protobuf-javaartifactId>
<version>${protobuf.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>${fastjson}version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
dependencies>
添加了相應(yīng)的maven依賴(lài)之后,配置文件這塊暫時(shí)沒(méi)有什么可以添加的,因?yàn)闀簳r(shí)就一個(gè)監(jiān)聽(tīng)的端口而已。
代碼編寫(xiě)
代碼模塊主要分為服務(wù)端和客戶端。
主要實(shí)現(xiàn)的業(yè)務(wù)邏輯:
服務(wù)端啟動(dòng)成功之后,客戶端也啟動(dòng)成功,這時(shí)服務(wù)端會(huì)發(fā)送一條protobuf格式的信息給客戶端,然后客戶端給予相應(yīng)的應(yīng)答??蛻舳伺c服務(wù)端連接成功之后,客戶端每個(gè)一段時(shí)間會(huì)發(fā)送心跳指令給服務(wù)端,告訴服務(wù)端該客戶端還存過(guò)中,如果客戶端沒(méi)有在指定的時(shí)間發(fā)送信息,服務(wù)端會(huì)關(guān)閉與該客戶端的連接。當(dāng)客戶端無(wú)法連接到服務(wù)端之后,會(huì)每隔一段時(shí)間去嘗試重連,只到重連成功!
服務(wù)端
首先是編寫(xiě)服務(wù)端的啟動(dòng)類(lèi),相應(yīng)的注釋在代碼中寫(xiě)得很詳細(xì)了,這里也不再過(guò)多講述了。不過(guò)需要注意的是,在之前的我寫(xiě)的Netty文章中,是通過(guò)main方法直接啟動(dòng)服務(wù)端,因此是直接new一個(gè)對(duì)象的。而在和SpringBoot整合之后,我們需要將Netty交給springBoot去管理,所以這里就用了相應(yīng)的注解。
代碼如下:
@Service("nettyServer")
public class NettyServer {
private static final int port = 9876; // 設(shè)置服務(wù)端端口
private static EventLoopGroup boss = new NioEventLoopGroup(); // 通過(guò)nio方式來(lái)接收連接和處理連接
private static EventLoopGroup work = new NioEventLoopGroup(); // 通過(guò)nio方式來(lái)接收連接和處理連接
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); // 設(shè)置過(guò)濾器
// 服務(wù)器綁定端口監(jiān)聽(tīng)
ChannelFuture f = b.bind(port).sync();
System.out.println("服務(wù)端啟動(dòng)成功,端口是:" + port);
// 監(jiān)聽(tīng)服務(wù)器關(guān)閉監(jiān)聽(tīng)
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 關(guān)閉EventLoopGroup,釋放掉所有資源包括創(chuàng)建的線程
work.shutdownGracefully();
boss.shutdownGracefully();
}
}
}
服務(wù)端主類(lèi)編寫(xiě)完畢之后,我們?cè)賮?lái)設(shè)置下相應(yīng)的過(guò)濾條件。
這里需要繼承Netty中ChannelInitializer類(lèi),然后重寫(xiě)initChannel該方法,進(jìn)行添加相應(yīng)的設(shè)置,如心跳超時(shí)設(shè)置,傳輸協(xié)議設(shè)置,以及相應(yīng)的業(yè)務(wù)實(shí)現(xiàn)類(lèi)。
代碼如下:
@Component
public class NettyServerFilter extends ChannelInitializer {
@Autowired
private NettyServerHandler nettyServerHandler;
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline ph = ch.pipeline();
//入?yún)⒄f(shuō)明: 讀超時(shí)時(shí)間、寫(xiě)超時(shí)時(shí)間、所有類(lèi)型的超時(shí)時(shí)間、時(shí)間格式
ph.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
// 解碼和編碼,應(yīng)和客戶端一致
//傳輸?shù)膮f(xié)議 Protobuf
ph.addLast(new ProtobufVarint32FrameDecoder());
ph.addLast(new ProtobufDecoder(UserMsg.getDefaultInstance()));
ph.addLast(new ProtobufVarint32LengthFieldPrepender());
ph.addLast(new ProtobufEncoder());
//業(yè)務(wù)邏輯實(shí)現(xiàn)類(lèi)
ph.addLast("nettyServerHandler", nettyServerHandler);
}
}
服務(wù)相關(guān)的設(shè)置的代碼寫(xiě)完之后,我們?cè)賮?lái)編寫(xiě)主要的業(yè)務(wù)代碼。
使用Netty編寫(xiě)業(yè)務(wù)層的代碼,我們需要繼承ChannelInboundHandlerAdapter?或SimpleChannelInboundHandler類(lèi),在這里順便說(shuō)下它們兩的區(qū)別吧。
繼承SimpleChannelInboundHandler類(lèi)之后,會(huì)在接收到數(shù)據(jù)后會(huì)自動(dòng)release掉數(shù)據(jù)占用的Bytebuffer資源。并且繼承該類(lèi)需要指定數(shù)據(jù)格式。
而繼承ChannelInboundHandlerAdapter則不會(huì)自動(dòng)釋放,需要手動(dòng)調(diào)用ReferenceCountUtil.release()等方法進(jìn)行釋放。繼承該類(lèi)不需要指定數(shù)據(jù)格式。
所以在這里,個(gè)人推薦服務(wù)端繼承ChannelInboundHandlerAdapter,手動(dòng)進(jìn)行釋放,防止數(shù)據(jù)未處理完就自動(dòng)釋放了。而且服務(wù)端可能有多個(gè)客戶端進(jìn)行連接,并且每一個(gè)客戶端請(qǐng)求的數(shù)據(jù)格式都不一致,這時(shí)便可以進(jìn)行相應(yīng)的處理。
客戶端根據(jù)情況可以繼承SimpleChannelInboundHandler類(lèi)。好處是直接指定好傳輸?shù)臄?shù)據(jù)格式,就不需要再進(jìn)行格式的轉(zhuǎn)換了。
代碼如下:
@Service("nettyServerHandler")
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/** 空閑次數(shù) */
private int idle_count = 1;
/** 發(fā)送次數(shù) */
private int count = 1;
/**
* 建立連接時(shí),發(fā)送一條消息
*/
@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);
}
/**
* 超時(shí)處理 如果5秒沒(méi)有接受客戶端的心跳,就觸發(fā); 如果超過(guò)兩次,則直接關(guān)閉;
*/
@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())) { // 如果讀通道處于空閑狀態(tài),說(shuō)明沒(méi)有接收到心跳命令
System.out.println("已經(jīng)5秒沒(méi)有接收到客戶端的信息了");
if (idle_count > 1) {
System.out.println("關(guān)閉這個(gè)不活躍的channel");
ctx.channel().close();
}
idle_count++;
}
} else {
super.userEventTriggered(ctx, obj);
}
}
/**
* 業(yè)務(wù)邏輯處理
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("第" + count + "次" + ",服務(wù)端接受的消息:" + msg);
try {
// 如果是protobuf類(lèi)型的數(shù)據(jù)
if (msg instanceof UserMsg) {
UserInfo.UserMsg userState = (UserInfo.UserMsg) msg;
if (userState.getState() == 1) {
System.out.println("客戶端業(yè)務(wù)處理成功!");
} else if(userState.getState() == 2){
System.out.println("接受到客戶端發(fā)送的心跳!");
}else{
System.out.println("未知命令!");
}
} else {
System.out.println("未知數(shù)據(jù)!" + 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();
}
}
還有個(gè)服務(wù)端的啟動(dòng)類(lèi),之前是通過(guò)main方法直接啟動(dòng), 不過(guò)這里改成了通過(guò)springBoot進(jìn)行啟動(dòng),差別不大。
代碼如下:
@SpringBootApplication
public class NettyServerApp {
public static void main(String[] args) {
// 啟動(dòng)嵌入式的 Tomcat 并初始化 Spring 環(huán)境及其各 Spring 組件
ApplicationContext context = SpringApplication.run(NettyServerApp.class, args);
NettyServer nettyServer = context.getBean(NettyServer.class);
nettyServer.run();
}
}
到這里服務(wù)端相應(yīng)的代碼就編寫(xiě)完畢了。
客戶端
客戶端這邊的代碼和服務(wù)端的很多地方都類(lèi)似,我就不再過(guò)多細(xì)說(shuō)了,主要將一些不同的代碼拿出來(lái)簡(jiǎn)單的講述下。
首先是客戶端的主類(lèi),基本和服務(wù)端的差不多,也就是多了監(jiān)聽(tīng)的端口和一個(gè)監(jiān)聽(tīng)器(用來(lái)監(jiān)聽(tīng)是否和服務(wù)端斷開(kāi)連接,用于重連)。
主要實(shí)現(xiàn)的代碼邏輯如下:
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("與服務(wù)端斷開(kāi)連接!在10s之后準(zhǔn)備嘗試重連!");
eventLoop.schedule(() -> doConnect(new Bootstrap(), eventLoop), 10, TimeUnit.SECONDS);
}
});
if(initFalg){
System.out.println("Netty客戶端啟動(dòng)成功!");
initFalg=false;
}
// 阻塞
f.channel().closeFuture().sync();
}
} catch (Exception e) {
System.out.println("客戶端連接失敗!"+e.getMessage());
}
}
注:監(jiān)聽(tīng)器這塊的實(shí)現(xiàn)用的是JDK1.8的寫(xiě)法。
客戶端過(guò)濾其這塊基本和服務(wù)端一直。不過(guò)需要注意的是,傳輸協(xié)議、編碼和解碼應(yīng)該一致,還有心跳的讀寫(xiě)時(shí)間應(yīng)該小于服務(wù)端所設(shè)置的時(shí)間。
改動(dòng)的代碼如下:
ChannelPipeline ph = ch.pipeline();
/*
* 解碼和編碼,應(yīng)和服務(wù)端一致
* */
//入?yún)⒄f(shuō)明: 讀超時(shí)時(shí)間、寫(xiě)超時(shí)時(shí)間、所有類(lèi)型的超時(shí)時(shí)間、時(shí)間格式
ph.addLast(new IdleStateHandler(0, 4, 0, TimeUnit.SECONDS));
客戶端的業(yè)務(wù)代碼邏輯。
主要實(shí)現(xiàn)的幾點(diǎn)邏輯是心跳按時(shí)發(fā)送以及解析服務(wù)發(fā)送的protobuf格式的數(shù)據(jù)。
這里比服務(wù)端多個(gè)個(gè)注解, 該注解Sharable主要是為了多個(gè)handler可以被多個(gè)channel安全地共享,也就是保證線程安全。
廢話就不多說(shuō)了,代碼如下:
@Service("nettyClientHandler")
@ChannelHandler.Sharable
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
@Autowired
private NettyClient nettyClient;
/** 循環(huán)次數(shù) */
private int fcount = 1;
/**
* 建立連接時(shí)
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("建立連接時(shí):" + new Date());
ctx.fireChannelActive();
}
/**
* 關(guān)閉連接時(shí)
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("關(guān)閉連接時(shí):" + new Date());
final EventLoop eventLoop = ctx.channel().eventLoop();
nettyClient.doConnect(new Bootstrap(), eventLoop);
super.channelInactive(ctx);
}
/**
* 心跳請(qǐng)求處理 每4秒發(fā)送一次心跳請(qǐng)求;
*
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception {
System.out.println("循環(huán)請(qǐng)求的時(shí)間:" + new Date() + ",次數(shù)" + fcount);
if (obj instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) obj;
if (IdleState.WRITER_IDLE.equals(event.state())) { // 如果寫(xiě)通道處于空閑狀態(tài),就發(fā)送心跳命令
UserMsg.Builder userState = UserMsg.newBuilder().setState(2);
ctx.channel().writeAndFlush(userState);
fcount++;
}
}
}
/**
* 業(yè)務(wù)邏輯處理
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 如果不是protobuf類(lèi)型的數(shù)據(jù)
if (!(msg instanceof UserMsg)) {
System.out.println("未知數(shù)據(jù)!" + msg);
return;
}
try {
// 得到protobuf的數(shù)據(jù)
UserInfo.UserMsg userMsg = (UserInfo.UserMsg) msg;
// 進(jìn)行相應(yīng)的業(yè)務(wù)處理。。。
// 這里就從簡(jiǎn)了,只是打印而已
System.out.println(
"客戶端接受到的用戶信息。編號(hào):" + userMsg.getId() + ",姓名:" + userMsg.getName() + ",年齡:" + userMsg.getAge());
// 這里返回一個(gè)已經(jīng)接受到數(shù)據(jù)的狀態(tài)
UserMsg.Builder userState = UserMsg.newBuilder().setState(1);
ctx.writeAndFlush(userState);
System.out.println("成功發(fā)送給服務(wù)端!");
} catch (Exception e) {
e.printStackTrace();
} finally {
ReferenceCountUtil.release(msg);
}
}
}
那么到這里客戶端的代碼也編寫(xiě)完畢了。
功能測(cè)試
首先啟動(dòng)服務(wù)端,然后再啟動(dòng)客戶端。
我們來(lái)看看結(jié)果是否如上述所說(shuō)。
服務(wù)端輸出結(jié)果:
服務(wù)端啟動(dòng)成功,端口是:9876
連接的客戶端地址:/127.0.0.1:53319
第1次,服務(wù)端接受的消息:state: 1
客戶端業(yè)務(wù)處理成功!
第2次,服務(wù)端接受的消息:state: 2
接受到客戶端發(fā)送的心跳!
第3次,服務(wù)端接受的消息:state: 2
接受到客戶端發(fā)送的心跳!
第4次,服務(wù)端接受的消息:state: 2
接受到客戶端發(fā)送的心跳!
客戶端輸入結(jié)果:
Netty客戶端啟動(dòng)成功!
建立連接時(shí):Mon Jul 16 23:31:58 CST 2018
客戶端接受到的用戶信息。編號(hào):1,姓名:xuwujing,年齡:18
成功發(fā)送給服務(wù)端!
循環(huán)請(qǐng)求的時(shí)間:Mon Jul 16 23:32:02 CST 2018,次數(shù)1
循環(huán)請(qǐng)求的時(shí)間:Mon Jul 16 23:32:06 CST 2018,次數(shù)2
循環(huán)請(qǐng)求的時(shí)間:Mon Jul 16 23:32:10 CST 2018,次數(shù)3
循環(huán)請(qǐng)求的時(shí)間:Mon Jul 16 23:32:14 CST 2018,次數(shù)4
通過(guò)打印信息可以看出如上述所說(shuō)。
接下來(lái)我們?cè)賮?lái)看看客戶端是否能夠?qū)崿F(xiàn)重連。
先啟動(dòng)客戶端,再啟動(dòng)服務(wù)端。
客戶端輸入結(jié)果:
Netty客戶端啟動(dòng)成功!
與服務(wù)端斷開(kāi)連接!在10s之后準(zhǔn)備嘗試重連!
客戶端連接失敗!AbstractChannel$CloseFuture@1fbaa3ac(incomplete)
建立連接時(shí):Mon Jul 16 23:41:33 CST 2018
客戶端接受到的用戶信息。編號(hào):1,姓名:xuwujing,年齡:18
成功發(fā)送給服務(wù)端!
循環(huán)請(qǐng)求的時(shí)間:Mon Jul 16 23:41:38 CST 2018,次數(shù)1
循環(huán)請(qǐng)求的時(shí)間:Mon Jul 16 23:41:42 CST 2018,次數(shù)2
循環(huán)請(qǐng)求的時(shí)間:Mon Jul 16 23:41:46 CST 2018,次數(shù)3
服務(wù)端輸出結(jié)果:
服務(wù)端啟動(dòng)成功,端口是:9876
連接的客戶端地址:/127.0.0.1:53492
第1次,服務(wù)端接受的消息:state: 1
客戶端業(yè)務(wù)處理成功!
第2次,服務(wù)端接受的消息:state: 2
接受到客戶端發(fā)送的心跳!
第3次,服務(wù)端接受的消息:state: 2
接受到客戶端發(fā)送的心跳!
第4次,服務(wù)端接受的消息:state: 2
結(jié)果也如上述所說(shuō)!
其它
關(guān)于SpringBoot整合Netty使用Protobuf進(jìn)行數(shù)據(jù)傳輸?shù)竭@里就結(jié)束了。
SpringBoot整合Netty使用Protobuf進(jìn)行數(shù)據(jù)傳輸?shù)捻?xiàng)目工程地址:
https://github.com/xuwujing/springBoot-study/tree/master/springboot-netty-protobuf
對(duì)了,也有不使用springBoot整合的Netty項(xiàng)目工程地址:
https://github.com/xuwujing/Netty-study/tree/master/Netty-protobuf
- 推薦閱讀 -
《架構(gòu)師離職后,成為自由開(kāi)發(fā)者的第 100 天》
下方二維碼關(guān)注我

互聯(lián)網(wǎng)草根,堅(jiān)持分享技術(shù)、創(chuàng)業(yè)、產(chǎn)品等心得和總結(jié)~

點(diǎn)擊“閱讀原文”,領(lǐng)取 2020 年最新免費(fèi)技術(shù)資料大全
