最近使用netty搭建了一個服務端和一個客戶端網絡通信的demo,記錄一下,不多說,先上項目結構圖
當時maven有點問題,所以直接引入的jar包,反正也只有一個。(ClientHandler和ServerHandler類分別用HeartBeatClientHandler和HeartBeatServerHandler代替)
搭建服務端之前還有一些事情要做,對,就是自定義協議,還有編碼解碼
這部分是參考了網上的一些資料
A 首先是協議部分
1 新建LuckHeader.java
package luck;
public class LuckHeader {
// 協議版本
private int version;
// 消息內容長度
private int contentLength;
// 服務名稱
private String sessionId;
public LuckHeader(int version, int contentLength, String sessionId) {
this.version = version;
this.contentLength = contentLength;
this.sessionId = sessionId;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
public int getContentLength() {
return contentLength;
}
public void setContentLength(int contentLength) {
this.contentLength = contentLength;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
}
這個屬於協議頭部分,下面是消息內容部分
2 新建LuckMessage.java
package luck;
public class LuckMessage {
private LuckHeader luckHeader;
private String content;
public LuckMessage(LuckHeader luckHeader, String content) {
this.luckHeader = luckHeader;
this.content = content;
}
public LuckHeader getLuckHeader() {
return luckHeader;
}
public void setLuckHeader(LuckHeader luckHeader) {
this.luckHeader = luckHeader;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return String.format("[version=%d,contentLength=%d,sessionId=%s,content=%s]",
luckHeader.getVersion(),
luckHeader.getContentLength(),
luckHeader.getSessionId(),
content);
}
}
到這裏一個簡單的luck協議的消息的模板就做好了.
B 接下來是編碼解碼部分
1 新建LuckDecoder.java
package encode;
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import luck.LuckHeader;
import luck.LuckMessage;
public class LuckDecoder extends ByteToMessageDecoder{
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//獲取協議版本
int version = in.readInt();
//獲取消息長度
int contentLength = in.readInt();
//獲取sessionid
byte[] sessionByte = new byte[36];
in.readBytes(sessionByte);
String sessionid = new String(sessionByte);
//組裝協議頭
LuckHeader hearder = new LuckHeader(version, contentLength, sessionid);
//讀取消息內容
byte[] content = in.readBytes(in.readableBytes()).array();
LuckMessage message = new LuckMessage(hearder, new String(content));
out.add(message);
}
}
2 新建LuckEncoder.java
package encode;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import luck.LuckHeader;
import luck.LuckMessage;
public class LuckEncoder extends MessageToByteEncoder<LuckMessage>{
@Override
protected void encode(ChannelHandlerContext ctx, LuckMessage message, ByteBuf in) throws Exception {
//將message轉換成二進制數據
LuckHeader header = message.getLuckHeader();
//寫入的順序就是協議的順序
//標題頭信息
in.writeInt(header.getVersion());
in.writeInt(message.getContent().length());
in.writeBytes(header.getSessionId().getBytes());
//主題信息
in.writeBytes(message.getContent().getBytes());
}
}
注意一下 ,這2個類的編碼解碼的順序要一致纔可以,不然會報錯
好,準備工作做完了,接下來重頭戲就是搭建服務端了
新建Server.java
package server;
import java.util.concurrent.TimeUnit;
import encode.LuckDecoder;
import encode.LuckEncoder;
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.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
/**
* Netty通信的步驟:
①創建兩個NIO線程組,一個專門用於網絡事件處理(接受客戶端的連接),另一個則進行網絡通信的讀寫。
②創建一個ServerBootstrap對象,配置Netty的一系列參數,例如接受傳出數據的緩存大小等。
③創建一個用於實際處理數據的類ChannelInitializer,進行初始化的準備工作,比如設置接受傳出數據的字符集、格式以及實際處理數據的接口。
④綁定端口,執行同步阻塞方法等待服務器端啓動即可。
* @author Administrator
*
*/
public class Server {
private int port;
public Server(int port){
this.port = port;
}
public void run(){
EventLoopGroup bossGroup = new NioEventLoopGroup(); //用於處理服務器端接受客戶端
EventLoopGroup workerGroup = new NioEventLoopGroup(); //進行網絡通信(讀寫)
try{
ServerBootstrap bootstrap = new ServerBootstrap();//輔助工具類,用於服務器通道的一系列配置
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() { //配置具體的數據處理方式
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new IdleStateHandler(5, 0, 0,TimeUnit.SECONDS));
pipeline.addLast(new LuckEncoder());
pipeline.addLast(new LuckDecoder());
//處理事件的類
//pipeline.addLast(new ServerHandler());
pipeline.addLast(new HeartBeatServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)//設置tcp緩衝區(// 保持連接數 )
.option(ChannelOption.SO_SNDBUF, 32*1024)//設置發送數據緩衝大小
.option(ChannelOption.SO_RCVBUF, 32*1024)//設置接受數據緩衝大小
.childOption(ChannelOption.SO_KEEPALIVE , true)//保持連接
.option(ChannelOption.TCP_NODELAY, true);//有數據立即發送
ChannelFuture future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();
}catch(Exception e){
e.printStackTrace();
}finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
new Server(8077).run();
}
}
服務端有2個線程池,一個處理客戶端的連接,一個處理io任務,ServerBootstrap對象用於啓動NIO服務端的輔助啓動類(目的是降低服務端的開發複雜度),IdleStateHandler類是服務端監聽客戶端心跳需要配置的handler,處理事件的類由HeartBeatServerHandler處理,然後由bootstrap綁定監聽一個端口,監聽客戶端的連接
實現斷線的類HeartBeatServerHandler.java,瞧好咯
package server;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.ReferenceCountUtil;
import luck.LuckMessage;
public class HeartBeatServerHandler extends SimpleChannelInboundHandler<LuckMessage>{
//連接失敗次數
private int connectTime = 0;
//定義最大未連接次數
private static final int MAX_UN_CON_TIME = 3;
@Override
protected void messageReceived(ChannelHandlerContext ctx, LuckMessage msg) throws Exception {
try{
LuckMessage lmsg = (LuckMessage) msg;
if (lmsg.getContent().equals("h")) {
//心跳消息
//吧連接失敗次數清0
System.out.println("Server接收到心跳消息 ,失敗次數清0:" + msg.toString());
connectTime = 0 ;
} else {
System.out.println("Server接收到普通消息 :" + msg.toString());
}
}catch(Exception e){
e.printStackTrace();
}finally {
ReferenceCountUtil.release(msg);
}
}
public void userEventTriggered(ChannelHandlerContext ctx, Object evt){
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
//讀超時
System.out.println("服務端讀超時=====連接失敗次數" + connectTime);
if (connectTime >= MAX_UN_CON_TIME) {
System.out.println("服務端關閉channel");
ctx.channel().close();
connectTime = 0;
} else {
connectTime ++;
}
}else if (event.state() == IdleState.WRITER_IDLE) {
/*寫超時*/
System.out.println("服務端寫超時");
} else if (event.state() == IdleState.ALL_IDLE) {
/*總超時*/
System.out.println("服務端全部超時");
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// cause.printStackTrace();
System.out.println("服務端發生錯誤:" + cause.getMessage());
ctx.channel().close();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("有客戶端連接");
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 關閉,等待重連
ctx.close();
System.out.println("===服務端===(客戶端失效)");
}
}
messageReceived方法是接受到消息的時候調用的,這個裏面我搞了一些騷操作,可以看可不看,主要是接受到的消息是否爲心跳消息,是的話,把連接失敗次數connectTime清0;其他方法 裏面都有註釋。
重點是userEventTriggered方法,IdleState定義了幾個事件
讀事件 READER_IDLE
寫事件 WRITER_IDLE
全部事件 ALL_IDLE
這個很好理解 就是字面意思,這裏用讀事件舉得例子,也就是服務端沒有收到客戶端的消息(讀到客戶端的消息)的時候就觸發該事件,當超過三次沒有收到客戶端的消息的時候 就斷開與改客戶端的連接ctx.channel().close();一個連接相當於一個channel。
服務端配置好了 接下來配置客戶端
新建Client.java
package client;
import java.net.InetSocketAddress;
import java.util.Random;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import encode.LuckDecoder;
import encode.LuckEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import luck.LuckHeader;
import luck.LuckMessage;
public class Client {
private static Channel channel;
private static Bootstrap bootstrap;
private static ChannelFutureListener channelFutureListener = null;
public static void main(String[] args) throws Exception {
new Client();
}
public Client() throws Exception{
System.out.println("構造方法");
init();
sendData();
}
static Scanner in = new Scanner(System.in);
public static void sendData() throws Exception {
System.out.println("senddata");
//組裝協議信息
int version = 1;
String sessionId = UUID.randomUUID().toString();
String content = "";
LuckHeader header = new LuckHeader(version, content.length(), sessionId);
LuckMessage message = null;
do {
content = in.nextLine();
message = new LuckMessage(header, content);
channel.writeAndFlush(message);
} while (!content.equals("q"));
}
public void init() throws InterruptedException{
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
bootstrap = new Bootstrap();
bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new IdleStateHandler(0, 0, 5, TimeUnit.SECONDS));
pipeline.addLast("encoder",new LuckEncoder());
pipeline.addLast("decoder",new LuckDecoder());
// pipeline.addLast(new ClientHandler());
pipeline.addLast(new HeartBeatClientHanlder());
}
});
//設置tcp協議的屬性
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.option(ChannelOption.TCP_NODELAY, true);
bootstrap.option(ChannelOption.SO_TIMEOUT, 5000);
connect();
}catch(Exception e){
e.printStackTrace();
}finally {
// workerGroup.shutdownGracefully();
}
}
/**
* 重新連接服務器
* @throws InterruptedException
*/
public static void connect(){
if (channel != null && channel.isActive()) {
return;
}
System.out.println("連接中");
ChannelFuture channelFuture = null;
try {
channelFuture = bootstrap.connect("192.168.1.12", 8077).sync();
channelFuture.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture futureListener) throws Exception {
if (futureListener.isSuccess()) {
channel = futureListener.channel();
System.out.println("連接成功");
} else {
System.out.println("連接失敗");
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
與服務端不同的是,客戶端只有一個NioEventLoopGroup,其他配置都是大同小異,
說明一下 static Scanner in = new Scanner(System.in);這句話定義爲類的靜態變量是爲了保證,如果在服務器切斷了改客戶端的連接,並且重連了以後,再進入到sendData方法中只有一個Scanner ;如果定義在sendData方法裏面,重連的時候就要發送2次消息才能讓服務端接收到,而且是作爲一條消息發過去的 ,2條消息中間還會有亂碼。重連幾次就需要幾條消息一起 才能發過去
ok,現在都寫好了 運行試一下
先啓動服務端Server.java然後啓動客戶端Client.java
然後客戶端不發送消息知道服務端斷開連接
此時客戶端的channelInactive方法會調用connect方法,重新連接
到這裏斷線重連的簡單例子就寫好了 歡迎各位大牛指點