关于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. 读取服务器的回复信息。