使用netty進行客戶端網絡編程及實現斷線重連功能

最近使用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方法,重新連接

到這裏斷線重連的簡單例子就寫好了  歡迎各位大牛指點

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章