给大家推荐一个网站:http://blog.csdn.net/youxijishu/article/details/44938751
通信协议分析
一,Socket传输方式
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
由于TCP无消息保护边界, 需要在消息接收端处理消息边界问题。也就是为什么我们以前使用UDP没有此问题。反而使用TCP后,出现少包的现象。
二,粘包分析
上面说了原理,但可能有人使用TCP通信会出现多包/少包,而一些人不会。那么我们具体分析一下,少包,多包的情况。
正常情况,发送及时每消息发送,接收也不繁忙,及时处理掉消息。像UDP一样.
发送粘包,多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包. 这种情况和客户端处理繁忙,接收缓存区积压,用户一次从接收缓存区多个数据包的接收端处理一样。
发送粘包或接收缓存区积压,但用户缓冲区大于接收缓存区数据包总大小。此时需要考虑处理一次处理多数据包的情况,但每个数据包都是完整的。
发送粘包或接收缓存区积压, 用户缓存区不是数据包大小的整数倍。此时需要考虑处理一次处理多数据包的情况,同时也需要考虑数据包不完整。
为了解决以上引起接收的包不完整的情况,我们必须知道每个包的大小。这样才能从数据流中取出完整的数据。实现思路如下:
协议分成三个部分:
(1)一个包的总数据流长度,即一次传输的字节数(占一个int位,4个字节)
(2)数据体的长度,即用于逻辑计算的数据的长度。(占一个int位,4个字节)
(3)数据体字节。
一个完整的协议格式如下:
总长度_数据体长度_数据体。(总长度包括它自己本身占的字节数)
三,Java代码部分实现
(1)接收到第一个数据流之后,选判断这个数据流的长度是否大于4个字节,如果小于4个字节,则保存此次接收到的数据,下次再接收到数据时,把新接收的数据字节添加到上次接收的字节后面。
(2)如果第一个数据流大于4个字节,读取这4个字节,得到整个数据包的长度n,判断剩下的字节是否大于等于(n-4)个字节。如果是,则从剩下的字节中取出(n - 4) 个字节,这就是数据包的内容。如果还有剩下的字节,则执行(1)。
netty实现:netty对这些过程进行了封装,使用起来非常方便:
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
public class MessageDecoder extends LengthFieldBasedFrameDecoder{
public MessageDecoder(int maxFrameLength, int lengthFieldOffset,
int lengthFieldLength) {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength);
// TODO Auto-generated constructor stub
}
protected Object decode(ChannelHandlerContext ctx,ByteBuf in)throws Exception{
System.out.println("可读字节:" + in.readableBytes());
ByteBuf frame = (ByteBuf) super.decode(ctx, in);
if(frame == null){
return null;
}
int len = frame.readInt();
len = frame.readInt();
byte[] bys = new byte[len];
frame.readBytes(bys);
String result = new String(bys,"utf8");
System.out.println("Game接收到的json:" +result);
//此处返回的信息就是在public void channelRead(ChannelHandlerContext ctx, Object msg)使用的msg
return result;
}
}
把这个类添加到netty的处理链中就可以了:
public void start() {
this.initServerInfo();
int PORT = ConfigManager.getLocalConfig().getPort();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);
b.channel(NioServerSocketChannel.class);
b.option(ChannelOption.SO_BACKLOG, 2048);
b.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 设置读取超时时间,30s
pipeline.addLast(new ReadTimeoutHandler(TIME_OUT));
// 设置写超时时间:30s
pipeline.addLast(new WriteTimeoutHandler(TIME_OUT));
// 设置空闲时间:60s
pipeline.addLast(new IdleStateHandler(TIME_OUT, TIME_OUT,
TIME_OUT));
// pipeline.addLast(new LineBasedFrameDecoder(2048));
// pipeline.addLast(new StringDecoder(Charset.forName("utf8")));
pipeline.addLast(new MessageDecoder(1024 * 1024, 4, 4));
pipeline.addLast(new MessageEncoder());
pipeline.addLast(new SystemHandle());
pipeline.addLast(new MainProcessHandle());
}
});
try {
ChannelFuture f = b.bind(PORT).sync();
System.out.println("====服务器启动成功====");
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
CommonLog.ERR("服务器初始化错误" + e.getMessage() + "\n" + e.getCause());
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}