Netty傻瓜教程(三):拆包、粘包。用神器protobuf!

關於TCP/IP協議粘包的問題自行百度,我舉個例子,服務器發出的消息本來爲:「你好,我是服務端。」,客戶端接收的時候可能變成了:「你好,我是服」,這就是拆包了。html

因此服務端和客戶端數據交互要避免這些問題。幸虧,谷歌公司有個神器,叫作protobuf,能夠完美解決這個問題,此外還有交互數據小,代碼生成簡單,支持多種語言等特性。java

在Mac上安裝protobuf請參考:http://blog.csdn.net/dfqin/article/details/8198341數據庫

而後編寫一個後綴名爲proto的文件,定義消息格式,而後用proto命令生成java文件:bootstrap

Auth.proto文件以下api

option java_package = "com.hengzecn.protobuf";

package auth;

message AuthRequest{ // (1)
	required string user_id=1;
	required string password=2;
}

message AuthResponse{ //(2)
	required int32 result_code=1;
	required string result_message=2;
}

1. 包名自定義,也能夠不加,將生成的文件拖進相應的包中便可。服務器

MAC裏的編譯命令以下:併發

proto --proto_path=/Users/nantongribao/Desktop --java_out=/Users/nantongribao/Desktop /Users/nantongribao/Desktop/Auth.protosocket

最後生成Auth.java文件。ide

咱們將要實現,客戶端登陸驗證功能,客戶端向服務端發送用戶名和密碼,服務端驗證正確,向客戶端返回success信息,否錯返回錯誤信息。oop

服務器代碼:

package com.hengzecn.protobuf;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;

import java.util.logging.Level;
import java.util.logging.Logger;

public class AuthServer {
	private static Logger logger = Logger.getLogger(AuthServerInitHandler.class
			.getName());

	public void start(int port) throws Exception {
		EventLoopGroup bossGroup = new NioEventLoopGroup();// (1)
		EventLoopGroup workerGroup = new NioEventLoopGroup();// (2)
		try {
			ServerBootstrap b = new ServerBootstrap();// (3)
			b.group(bossGroup, workerGroup); // (4)
			b.channel(NioServerSocketChannel.class);
			b.childHandler(new ChannelInitializer<SocketChannel>() {
				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					//decoded
					ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
					ch.pipeline().addLast(new ProtobufDecoder(Auth.AuthRequest.getDefaultInstance()));
					//encoded
					ch.pipeline().addLast(new LengthFieldPrepender(4));
					ch.pipeline().addLast(new ProtobufEncoder());
					// register handler
					ch.pipeline().addLast(new AuthServerInitHandler());
				}
			});
			b.option(ChannelOption.SO_BACKLOG, 128);
			b.childOption(ChannelOption.SO_KEEPALIVE, true);
			//bind port, wait for success sync
			ChannelFuture f = b.bind(port).sync();
			//wait for channel close sync
			f.channel().closeFuture().sync();
		} finally {
			workerGroup.shutdownGracefully();
			bossGroup.shutdownGracefully();
		}
	}

	public static void main(String[] args) throws Exception {
		logger.log(Level.INFO, "AuthServer start...");
		new AuthServer().start(5555);
	}
}

1. LengthFieldBasedFrameDecoder定義了最大幀長度,以及指示包長度的頭部以及須要忽略掉的字節,詳見:http://docs.jboss.org/netty/3.1/api/org/jboss/netty/handler/codec/frame/LengthFieldBasedFrameDecoder.html

2. ProtobufDecoder引入定義的請求數據定義

3. LengthFieldPrepender在數據包中加入長度頭

下面是服務端的Handler:

package com.hengzecn.protobuf;

import java.util.logging.Level;
import java.util.logging.Logger;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class AuthServerInitHandler extends ChannelInboundHandlerAdapter {
	private Logger logger=Logger.getLogger(AuthServerInitHandler.class.getName());

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg)
			throws Exception {
		logger.log(Level.INFO, "AuthServerInitHandler channelRead");
		Auth.AuthRequest request=(Auth.AuthRequest)msg;
		System.out.println("request: userId="+request.getUserId()+", password="+request.getPassword());
		Auth.AuthResponse response=Auth.AuthResponse.newBuilder()
	                                                    .setResultCode(0)
							    .setResultMessage("success")
							    .build();
		ctx.writeAndFlush(response);
		//ctx.close();
	}

	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
		logger.log(Level.INFO, "AuthServerInitHandler channelReadComplete");
		ctx.flush();
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
			throws Exception {
		logger.log(Level.INFO, "AuthServerInitHandler exceptionCaught");
		cause.printStackTrace();
		ctx.close();
	}

}

1. Handler不判斷用戶名和密碼是否正確,只是簡單地回覆success,後續會加入數據庫驗證。

客戶端的Client文件和服務端差很少,詳情參看第二節中的介紹。

客戶端的Handler文件以下:

package com.hengzecn.protobuf;

import java.util.logging.Level;
import java.util.logging.Logger;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class AuthClientInitHandler extends ChannelInboundHandlerAdapter {
	private Logger logger=Logger.getLogger(AuthClientInitHandler.class.getName());

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		logger.log(Level.INFO, "AuthClientInitHandler exceptionCaught");
		Auth.AuthRequest request=Auth.AuthRequest.newBuilder()
							 .setUserId("010203")
							 .setPassword("abcde")
							 .build();
		ctx.writeAndFlush(request);
	}

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg)
			throws Exception {
		logger.log(Level.INFO, "AuthClientInitHandler channelRead");
		Auth.AuthResponse response=(Auth.AuthResponse)msg;
		System.out.println("response: code="+response.getResultCode()+", message="+response.getResultMessage());
		//ctx.close();
	}
}

1. 設置用戶名和密碼,併發送請求,這裏用到了channelActive。

2. 讀取服務器的回覆信息。

相關文章
相關標籤/搜索