述
上文中,说了客户端服务端的通信协议以及编解码,本文延续上文,实现一个客户端的登录
具体流程就是客户端去发送登录的数据包,然后服务端解码,验证是否可以登录,然后返回给客户端是否登录成功
我们之前有写过一个客户端服务端通信的案例,就是给客户端和服务端启动的时候都添加一个逻辑处理器,写数据就重写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
85public 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会回调ClientHandler
的 channelActive()
方法,然后,我们就在这个方法里面去构建数据包,然后转码,通过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
27public 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);
这里就是客户端拿到服务端发过来的数据,然后解码打印,就完成了
最后依次启动服务端和客户端就可以看到效果了,客户端控制台输出如下:
总结
具体流程图如下:
图片来源,侵删
完整代码已上传到github, 传送门