黑匣子
满天星

基于Netty的RPC架构学习笔记(十):自定义数据包协议

数据包简介

粘包、分包现象

假如客户端需要给服务端发送数据
give me a coffee give me a tea
可能会出现粘包或者分包的现象

  • 粘包现象
    give me a coffeegive me a tea
  • 分包现象
    give me
    a coffeegive me a tea

    粘包和分包出现的原因是:没有一个稳定数据结构,解决方法比如

  • 分割符
    give me a coffee|give me a tea|
    give me a coffee|
    give me a tea|
    • 长度 + 数据
      16give me a coffee13give me a tea
      16give me a coffee
      13give me a tea

      数据包格式

      +———-——+———–——+———-——+———-——+———–——+
      | 包头 | 模块号 | 命令号 | 长度 | 数据 |
      +———-——+———–——+———-——+———-——+———–——+
      包头4字节 (一些不常用的东西)
      模块号2字节short(Player 1号)
      命令号2字节short(Player要做的事情,比如获取玩家数据 1号)
      长度4字节(描述数据部分字节长度)
      Player 1
      1 获取玩家数据
      2 注册用户
      3 购买金币

举个🌰

需要的jar:netty-3.10.5.final.jar

Common项目

新建项目Common==》新建包model、 module、codc、constant、serial==>model包下新建Request.java

Request.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.cn.model;
/**
* 请求对象
*
*/
public class Request {

/**
* 请求模块
*/
private short module;

/**
* 命令号
*/
private short cmd;

/**
* 数据部分
*/
private byte[] data;

public short getModule() {
return module;
}

public void setModule(short module) {
this.module = module;
}

public short getCmd() {
return cmd;
}

public void setCmd(short cmd) {
this.cmd = cmd;
}

public byte[] getData() {
return data;
}

public void setData(byte[] data) {
this.data = data;
}


public int getDataLength(){
if(data == null){
return 0;
}
return data.length;
}
}

constant包下新建

ConstantValue.java

1
2
3
4
5
6
7
8
9
10
package com.cn.constant;

public interface ConstantValue {

/**
* 包头,包头是一个常量
*/
public static final int FLAG = -32523523;

}

codc包下新建RequestDecoder.java和RequestEncoder.java

RequestEncoder.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.cn.codc;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.oneone.OneToOneEncoder;

import com.cn.constant.ConstantValue;
import com.cn.model.Request;

/**
* 请求编码器
* <pre>
* 数据包格式
* +——----——+——-----——+——----——+——----——+——-----——+
* | 包头 | 模块号 | 命令号 | 长度 | 数据 |
* +——----——+——-----——+——----——+——----——+——-----——+
* </pre>
* 包头4字节
* 模块号2字节short
* 命令号2字节short
* 长度4字节(描述数据部分字节长度)
*
*
*
*/
public class RequestEncoder extends OneToOneEncoder{

@Override
protected Object encode(ChannelHandlerContext context, Channel channel, Object rs) throws Exception {
Request request = (Request)(rs);

ChannelBuffer buffer = ChannelBuffers.dynamicBuffer();
//包头
buffer.writeInt(ConstantValue.FLAG);
//module
buffer.writeShort(request.getModule());
//cmd
buffer.writeShort(request.getCmd());
//长度
buffer.writeInt(request.getDataLength());
//data
if(request.getData() != null){
buffer.writeBytes(request.getData());
}

return buffer;
}

}

RequestDecoder.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package com.cn.codc;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;

import com.cn.constant.ConstantValue;
import com.cn.model.Request;

/**
* 请求解码器
* <pre>
* 数据包格式
* +——----——+——-----——+——----——+——----——+——-----——+
* | 包头 | 模块号 | 命令号 | 长度 | 数据 |
* +——----——+——-----——+——----——+——----——+——-----——+
* </pre>
* 包头4字节
* 模块号2字节short
* 命令号2字节short
* 长度4字节(描述数据部分字节长度)
*
*
*
*/
//FrameDecoder可以协助我们解决粘包分包的问题
public class RequestDecoder extends FrameDecoder{

/**
* 数据包基本长度
*/
public static int BASE_LENTH = 4 + 2 + 2 + 4;

@Override
protected Object decode(ChannelHandlerContext arg0, Channel arg1, ChannelBuffer buffer) throws Exception {

//可读长度必须大于基本长度
if(buffer.readableBytes() >= BASE_LENTH){
//防止socket字节流攻击
if(buffer.readableBytes() > 2048){
buffer.skipBytes(buffer.readableBytes());
}

//记录包头开始的index
int beginReader;

while(true){
beginReader = buffer.readerIndex();
buffer.markReaderIndex();
if(buffer.readInt() == ConstantValue.FLAG){
break;
}

//未读到包头,略过一个字节
buffer.resetReaderIndex();
buffer.readByte();

//长度又变得不满足
if(buffer.readableBytes() < BASE_LENTH){
return null;
}
}

//模块号
short module = buffer.readShort();
//命令号
short cmd = buffer.readShort();
//长度
int length = buffer.readInt();

//判断请求数据包数据是否到齐
if(buffer.readableBytes() < length){
//还原读指针到最开始的地方
buffer.readerIndex(beginReader);
return null;
}

//读取data数据
byte[] data = new byte[length];
buffer.readBytes(data);

Request request = new Request();
request.setModule(module);
request.setCmd(cmd);
request.setData(data);

//继续往下传递
return request;

}
//数据包不完整,需要等待后面的包来,(buffer.readableBytes() < BASE_LENTH
return null;
}

}

ChannelBuffer writeindex和readindex
wirteindex初始值是0,当写一个int,wirteindex就是4
readindex同上
但是注意readindex是不能超过writeindex的

modle包下新建

Response.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.cn.model;
/**

- 返回对象

-
*
*/
public class Response {
/**

- 请求模块
*/
private short module;

/**

- 命令号
*/
private short cmd;

/**

- 状态码
*/
private int stateCode;

/**

- 数据部分
*/
private byte[] data;

public short getModule() {
return module;
}

public void setModule(short module) {
this.module = module;
}

public short getCmd() {
return cmd;
}

public void setCmd(short cmd) {
this.cmd = cmd;
}

public int getStateCode() {
return stateCode;
}

public void setStateCode(int stateCode) {
this.stateCode = stateCode;
}

public byte[] getData() {
return data;
}

public void setData(byte[] data) {
this.data = data;
}

public int getDataLength(){
if(data == null){
return 0;
}
return data.length;
}
}

model下新建

StateCode.java,response的状态码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.cn.model;

public interface StateCode {

/**
* 成功
*/
public static int SUCCESS = 0;

/**
* 失败
*/
public static int FAIL = 1;

}

codc包下新建ResponseEncoder.java和ResponseDecoder.java

ResponseEncoder.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.cn.codc;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.oneone.OneToOneEncoder;
import com.cn.constant.ConstantValue;
import com.cn.model.Response;

/**
* 请求编码器
* <pre>
* 数据包格式
* +——----——+——-----——+——----——+——----——+——-----——+——-----——+
* | 包头 | 模块号 | 命令号 | 状态码 | 长度 | 数据 |
* +——----——+——-----——+——----——+——----——+——-----——+——-----——+
* </pre>
* 包头4字节
* 模块号2字节short
* 命令号2字节short
* 长度4字节(描述数据部分字节长度)
*
*
*/
public class ResponseEncoder extends OneToOneEncoder{

@Override
protected Object encode(ChannelHandlerContext context, Channel channel, Object rs) throws Exception {
Response response = (Response)(rs);

ChannelBuffer buffer = ChannelBuffers.dynamicBuffer();
//包头
buffer.writeInt(ConstantValue.FLAG);
//module
buffer.writeShort(response.getModule());
//cmd
buffer.writeShort(response.getCmd());
//状态码
buffer.writeInt(response.getStateCode());
//长度
buffer.writeInt(response.getDataLength());
//data
if(response.getData() != null){
buffer.writeBytes(response.getData());
}

return buffer;
}

}

ResponseDecoder.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.cn.codc;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
import com.cn.constant.ConstantValue;
import com.cn.model.Response;

/**
* response解码器
* <pre>
* 数据包格式
* +——----——+——-----——+——----——+——----——+——-----——+——-----——+
* | 包头 | 模块号 | 命令号 | 状态码 | 长度 | 数据 |
* +——----——+——-----——+——----——+——----——+——-----——+——-----——+
* </pre>
* 包头4字节
* 模块号2字节short
* 命令号2字节short
* 长度4字节(描述数据部分字节长度)
*
-
*
*/
public class ResponseDecoder extends FrameDecoder{

/**
* 数据包基本长度
*/
public static int BASE_LENTH = 4 + 2 + 2 + 4;

@Override
protected Object decode(ChannelHandlerContext arg0, Channel arg1, ChannelBuffer buffer) throws Exception {

//可读长度必须大于基本长度
if(buffer.readableBytes() >= BASE_LENTH){

//记录包头开始的index
int beginReader = buffer.readerIndex();

while(true){
if(buffer.readInt() == ConstantValue.FLAG){
break;
}
}

//模块号
short module = buffer.readShort();
//命令号
short cmd = buffer.readShort();
//状态码
int stateCode = buffer.readInt();
//长度
int length = buffer.readInt();

if(buffer.readableBytes() < length){
//还原读指针
buffer.readerIndex(beginReader);
return null;
}

byte[] data = new byte[length];
buffer.readBytes(data);

Response response = new Response();
response.setModule(module);
response.setCmd(cmd);
response.setStateCode(stateCode);
response.setData(data);

//继续往下传递
return response;

}
//数据包不完整,需要等待后面的包来
return null;
}

}

注意: 上面的代码在遇到socket字节流攻击的时候会有异常,包括为什么需要包头,会在下面一节中的最后进行讲解

新建serial包==》将上节最后的两个序列化的工具类粘贴过来(BufferFactory.java和Serializer.java)
model包下新建fuben包==》新建和request包和response包==》request包下新建FightRequest.java==》response包下新建FightResponse.java
这里的副本简单理解为打游戏刷的副本这个对象

FightRequest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.cn.module.fuben.request;

import com.cn.serial.Serializer;

public class FightRequest extends Serializer{

/**
* 副本id
*/
private int fubenId;

/**
* 次数
*/
private int count;

public int getFubenId() {
return fubenId;
}

public void setFubenId(int fubenId) {
this.fubenId = fubenId;
}

public int getCount() {
return count;
}

public void setCount(int count) {
this.count = count;
}

@Override
protected void read() {
this.fubenId = readInt();
this.count = readInt();
}

@Override
protected void write() {
writeInt(fubenId);
writeInt(count);
}



}

Client项目

新建Client项目,将之前client代码拷贝过来

Client.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.client;

import java.net.InetSocketAddress;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import com.cn.codc.RequestEncoder;
import com.cn.codc.ResponseDecoder;
import com.cn.model.Request;
import com.cn.module.fuben.request.FightRequest;
/**
* netty客户端入门
*
*
*/
public class Client {

public static void main(String[] args) throws InterruptedException {

//服务类
ClientBootstrap bootstrap = new ClientBootstrap();

//线程池
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();

//socket工厂
bootstrap.setFactory(new NioClientSocketChannelFactory(boss, worker));

//管道工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {

@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
//修改为刚刚写的ResponseEncoder和RequestEndoder
pipeline.addLast("decoder", new ResponseDecoder());
pipeline.addLast("encoder", new RequestEncoder());
pipeline.addLast("hiHandler", new HiHandler());
return pipeline;
}
});

//连接服务端
ChannelFuture connect = bootstrap.connect(new InetSocketAddress("127.0.0.1", 10101));
Channel channel = connect.sync().getChannel();

System.out.println("client start");

Scanner scanner = new Scanner(System.in);
while(true){
System.out.println("请输入");
int fubenId = Integer.parseInt(scanner.nextLine());
int count = Integer.parseInt(scanner.nextLine());

FightRequest fightRequest = new FightRequest();
fightRequest.setFubenId(fubenId);
fightRequest.setCount(count);

Request request = new Request();
request.setModule((short) 1);
request.setCmd((short) 1);
request.setData(fightRequest.getBytes());
//发送请求
channel.write(request);
}
}

}

主要的变化是

1
2
3
4
5
6
//修改为刚刚写的ResponseEncoder和RequestEndoder
//response解码
pipeline.addLast("decoder", new ResponseDecoder());
//requset编码
pipeline.addLast("encoder", new RequestEncoder());
pipeline.addLast("hiHandler", new HiHandler());

和最后变成发送requst

HiHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package com.client;

import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

import com.cn.model.Response;
import com.cn.model.StateCode;
import com.cn.module.fuben.request.FightRequest;
import com.cn.module.fuben.response.FightResponse;
/**
* 消息接受处理类
*
*
*/
public class HiHandler extends SimpleChannelHandler {

/**
* 接收消息
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
//修改为response对象
Response message = (Response)e.getMessage();

if(message.getModule() == 1){

if(message.getCmd() == 1){
//从server端获取数据并输出
FightResponse fightResponse = new FightResponse();
fightResponse.readFromBytes(message.getData());

System.out.println("gold:" + fightResponse.getGold());

}else if(message.getCmd() == 2){

}

}else if (message.getModule() == 1){


}
}

/**
* 捕获异常
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("exceptionCaught");
super.exceptionCaught(ctx, e);
}

/**
* 新连接
*/
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelConnected");
super.channelConnected(ctx, e);
}

/**
* 必须是链接已经建立,关闭通道的时候才会触发
*/
@Override
public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelDisconnected");
super.channelDisconnected(ctx, e);
}

/**
* channel关闭的时候触发
*/
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelClosed");
super.channelClosed(ctx, e);
}
}

将原来收到和回写的对象转化成response对象

Server项目

新建Server项目,将之前server代码拷贝过来

Server.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.server;

import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;

import com.cn.codc.RequestDecoder;
import com.cn.codc.ResponseEncoder;
/**
* netty服务端入门
*
*/
public class Server {

public static void main(String[] args) {

//服务类
ServerBootstrap bootstrap = new ServerBootstrap();

//boss线程监听端口,worker线程负责数据读写
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();

//设置niosocket工厂
bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));

//设置管道的工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {

@Override
public ChannelPipeline getPipeline() throws Exception {

ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new RequestDecoder());
pipeline.addLast("encoder", new ResponseEncoder());
pipeline.addLast("helloHandler", new HelloHandler());
return pipeline;
}
});

bootstrap.bind(new InetSocketAddress(10101));

System.out.println("start!!!");

}

}

主要变化

1
2
3
4
5
6
7
ChannelPipeline pipeline = Channels.pipeline();
//对request解码
pipeline.addLast("decoder", new RequestDecoder());
//response编码
pipeline.addLast("encoder", new ResponseEncoder());
pipeline.addLast("helloHandler", new HelloHandler());
return pipeline;

HiHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package com.server;

import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

import com.cn.model.Request;
import com.cn.model.Response;
import com.cn.model.StateCode;
import com.cn.module.fuben.request.FightRequest;
import com.cn.module.fuben.response.FightResponse;
/**
* 消息接受处理类

*
*/
public class HelloHandler extends SimpleChannelHandler {

/**
* 接收消息
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {

Request message = (Request)e.getMessage();

if(message.getModule() == 1){

if(message.getCmd() == 1){

FightRequest fightRequest = new FightRequest();
fightRequest.readFromBytes(message.getData());
//需要打的副本id是xxx打了xxx次
System.out.println("fubenId:" +fightRequest.getFubenId() + " " + "count:" + fightRequest.getCount());

//回写数据
FightResponse fightResponse = new FightResponse();
fightResponse.setGold(9999);

Response response = new Response();
response.setModule((short) 1);
response.setCmd((short) 1);
response.setStateCode(StateCode.SUCCESS);
response.setData(fightResponse.getBytes());
ctx.getChannel().write(response);
}else if(message.getCmd() == 2){

}

}else if (message.getModule() == 1){


}
}

/**
* 捕获异常
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("exceptionCaught");
super.exceptionCaught(ctx, e);
}

/**
* 新连接
*/
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelConnected");
super.channelConnected(ctx, e);
}

/**
* 必须是链接已经建立,关闭通道的时候才会触发
*/
@Override
public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelDisconnected");
super.channelDisconnected(ctx, e);
}

/**
* channel关闭的时候触发
*/
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("channelClosed");
super.channelClosed(ctx, e);
}
}

将收到的数据强转为requset

结果:

运行:
client端

1
2
3
4
请输入:
101
10
//要打101副本,打十次

server端

1
fubenID:101 count:10

client端

1
gold:999

弊端:

每次都要做判断

1
2
3
4
if(message.getModule() == 1){

if(message.getCmd() == 1){
}

-------------The End-------------

本文标题:基于Netty的RPC架构学习笔记(十):自定义数据包协议

文章作者:Leesin.Dong

发布时间:2019年03月10日 - 10:03

最后更新:2019年03月10日 - 23:03

原始链接:http://mmmmmm.me/2019-03-10-10.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

客官客官,不可以,你要对我负责~~~