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

相关文章
相关标签/搜索