Netty-6-案例-客户端登录

上文中,说了客户端服务端的通信协议以及编解码,本文延续上文,实现一个客户端的登录

具体流程就是客户端去发送登录的数据包,然后服务端解码,验证是否可以登录,然后返回给客户端是否登录成功

我们之前有写过一个客户端服务端通信的案例,就是给客户端和服务端启动的时候都添加一个逻辑处理器,写数据就重写channelActive()方法,读数据就重写channelRead()方法,具体的可以回顾之前的文章(Netty-3-客户端服务端通信案例)

准备工作

首先,这里改造了一下上文中的PacketCodeC类, 就是用来编解码的类,改成了单例模式的,然后把 ByteBuf 分配器抽取出一个参数,第一个实参是 ctx.alloc() 获取的就是与当前连接相关的 ByteBuf 分配器,建议这样来使用,具体修改后的代码如下:

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
public class PacketCodeC {

private static final int MAGIC_NUMBER = 0x12345678;

private final Map<Byte, Class<? extends Packet>> packetTypeMap;
private final Map<Byte, Serializer> serializerMap;

public static final PacketCodeC INSTANCE = new PacketCodeC();

private PacketCodeC() {
packetTypeMap = new HashMap<>();
packetTypeMap.put(LOGIN_REQUEST, LoginRequestPacket.class);

serializerMap = new HashMap<>();
Serializer serializer = new JSONSerializer();
serializerMap.put(serializer.getSerializerAlgorithm(), serializer);
}

/**
* 编码
* @param packet
* @return ByteBuf
*/
public ByteBuf encode(ByteBufAllocator byteBufAllocator, Packet packet) {
// 创建ByteBuf对象
ByteBuf byteBuf = byteBufAllocator.ioBuffer();

// 序列化java对象
byte[] bytes = Serializer.DEFAULT.serialize(packet);

// 实际编码过程
byteBuf.writeInt(MAGIC_NUMBER);
byteBuf.writeByte(packet.getVersion());
byteBuf.writeByte(Serializer.DEFAULT.getSerializerAlgorithm());
byteBuf.writeByte(packet.getCommand());
byteBuf.writeInt(bytes.length);
byteBuf.writeBytes(bytes);

return byteBuf;
}

/**
* 解码
* @param byteBuf
* @return Packet
*/
public Packet decode(ByteBuf byteBuf) {
// 跳过魔数
byteBuf.skipBytes(4);

// 跳过版本号
byteBuf.skipBytes(1);

// 序列化算法标识
byte serializeAlgorithm = byteBuf.readByte();

// 指令
byte command = byteBuf.readByte();

// 数据包长度
int length = byteBuf.readInt();

byte[] bytes = new byte[length];
byteBuf.readBytes(bytes);

// 具体数据内容
Class<? extends Packet> requestType = getRequestType(command);
Serializer serializer = getSerializer(serializeAlgorithm);

if (requestType != null && serializer != null) {
return serializer.deserialize(requestType, bytes);
}

return null;
}

private Serializer getSerializer(byte serializeAlgorithm) {
return serializerMap.get(serializeAlgorithm);
}

private Class<? extends Packet> getRequestType(byte command) {

return packetTypeMap.get(command);
}
}

客户端发送登录请求

新建一个客户端的逻辑处理器,用来发送登录数据包.代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j
public class ClientHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {

log.info("{},客户端开始登录...", new Date());

// 创建登录的对象
LoginRequestPacket loginRequestPacket = new LoginRequestPacket();

loginRequestPacket.setUsername("zhangsan");
loginRequestPacket.setPassword("123456");
loginRequestPacket.setUserId(1);

// 编码
ByteBuf byteBuf = PacketCodeC.INSTANCE.encode(ctx.alloc(), loginRequestPacket);

// 写到服务端
ctx.channel().writeAndFlush(byteBuf);

}

}

这里就是先是继承ChannelInboundHandlerAdapter类,然后重写了channelActive()方法,当客户端连接到服务端之后,Netty会回调ClientHandlerchannelActive()方法,然后,我们就在这个方法里面去构建数据包,然后转码,通过writeAndFlush()方法将数据写到服务端

最后就是在客户端启动的时候把这个逻辑处理器加进去就行了,在NettyCilent.java中添加逻辑处理器,部分代码如下:

1
2
3
4
5
6
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ClientHandler());
}
});

到这儿客户端发送数据的操作就完了,接下来是服务端接收数据然后解码,验证是否可以登录

服务端接收数据并验证

服务端也是新建一个逻辑处理器,用来接收客户端发来的数据,解码验证,具体代码如下:

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
public class ServerHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

// 收到消息后解码
ByteBuf byteBuf = (ByteBuf) msg;

Packet packet = PacketCodeC.INSTANCE.decode(byteBuf);

// 判断是不是登录请求数据包
if (packet instanceof LoginRequestPacket) {
LoginRequestPacket loginRequestPacket = (LoginRequestPacket) packet;

// 校验用户名密码
if (valid(loginRequestPacket)) {
// 登录成功
} else {
// 登录失败
}
}
}

private boolean valid(LoginRequestPacket loginRequestPacket) {
return true;
}
}

这里也是继承ChannelInboundHandlerAdapter,然后重写了channelRead()方法,netty收到数据之后,会回调这个方法,然后我们解码,校验数据(这里所有情况都返回true了)

然后服务端校验完成之后,要把登录结果返回给客户端

服务端返回校验结果

这里就又需要一个服务端响应的数据包了,,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Data
public class LoginResponsePacket extends Packet {

private Boolean success;

private String reason;

@Override
public Byte getCommand() {
return Command.LOGIN_RESPONSE;
}
}

然后Command中定义登录响应的命令是2:

1
Byte LOGIN_RESPONSE = 2;

然后就是在ServerHandler中,服务端验证完成之后,新建响应的数据包,返回给客户端,修改后的代码如下:

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
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

// 收到消息后解码
ByteBuf byteBuf = (ByteBuf) msg;

Packet packet = PacketCodeC.INSTANCE.decode(byteBuf);

// 判断是不是登录请求数据包
if (packet instanceof LoginRequestPacket) {
LoginRequestPacket loginRequestPacket = (LoginRequestPacket) packet;

LoginResponsePacket loginResponsePacket = new LoginResponsePacket();
loginRequestPacket.setVersion(loginRequestPacket.getVersion());

// 校验用户名密码
if (valid(loginRequestPacket)) {
// 登录成功
loginResponsePacket.setSuccess(true);
} else {
// 登录失败
loginResponsePacket.setSuccess(false);
loginResponsePacket.setReason("账号密码错误");
}

// 编码返回给客户端
ByteBuf responseBuffer = PacketCodeC.INSTANCE.encode(ctx.alloc(), loginResponsePacket);
ctx.channel().writeAndFlush(responseBuffer);

}
}

客户端接收返回结果

客户端接收数据处理也是需要重写channelRead()方法,代码如下(也是在ClientHandler类里):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;

// 收到服务端发来的消息后,解码
Packet packet = PacketCodeC.INSTANCE.decode(byteBuf);

// 判断是不是登录请求数据包
if (packet instanceof LoginResponsePacket) {
LoginResponsePacket loginResponsePacket = (LoginResponsePacket) packet;

if (loginResponsePacket.getSuccess()) {
log.info("登录成功....");
} else {
log.info("登录失败,原因:{}", loginResponsePacket.getReason());
}

}
}

这里还需要注意一点,在PacketCodeC类里面,需要在packetTypeMap这个map里面配置上我们刚刚的响应数据包.

1
packetTypeMap.put(LOGIN_RESPONSE, LoginResponsePacket.class);

这里就是客户端拿到服务端发过来的数据,然后解码打印,就完成了

最后依次启动服务端和客户端就可以看到效果了,客户端控制台输出如下:
image

总结

具体流程图如下:
image
图片来源,侵删

完整代码已上传到github, 传送门