關於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. 讀取服務器的回覆信息。