在游戏中会经常遇到这一种情况,世界/阵营频道聊天,或者某个玩家获得了特别NB的物品,或者某个活动已经开始,都需要通知某个组或者所有的客户端,这些就需要网络编程中的组播/多播。其实就是类似与设计模式中的发布-订阅模式。Netty框架中的ChannelGroup接口提供了一些方法来帮我们实现需求。老样子,我们直接使用简单的代码例子说明。为了简便,我们实现最简单世界聊天,原理就是一个玩家将聊天信息发送给服务器,服务器将消息转发给所有链接的客户端。
消息的传输继续使用protobuf协议,Netty框架使用4.0版本。关于Netty和protobuf的使用前面的文章有详细介绍。
首先定义pb文件,ChatRequest.proto:
package message;
option java_package = "message.request";
option java_outer_classname = "ChatRequest";
message Command {
optional int32 chatType = 1;
optional string content = 2;
}
ChatResponse.proto和ChatRequest.proto的定义是一样的。ChatResponse.proto:
package message;
option java_package = "message.response";
option java_outer_classname = "ChatResponse";
message ChatMessage {
optional int32 chatType = 1;
optional string content = 2;
}
将java文件生成到项目TestNettyTcpMulticast中的src目中下。
server端:NettyTcpMulticastServer.java
package server;
import message.request.ChatRequest;
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;
public class NettyTcpMulticastServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2);
try {
ServerBootstrap bs = new ServerBootstrap();
bs.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ProtobufDecoder protobufDecoder = new ProtobufDecoder(ChatRequest.Command.getDefaultInstance());
//Decoder
ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(2 * 1024 * 1024, 0, 4, 0, 4));
ch.pipeline().addLast("protobufDecoder", protobufDecoder);
//Encoder
ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(4));
ch.pipeline().addLast("protobufEncoder", new ProtobufEncoder());
//handler
ch.pipeline().addLast(new SocketHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
//Bind and start to accept incoming connections
ChannelFuture cf = bs.bind(8080).sync();
cf.channel().closeFuture().sync();
} finally {
workGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
server逻辑处理,SockerHandler.java
package server;
import message.request.ChatRequest;
import message.response.ChatResponse;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
public class SocketHandler extends ChannelInboundHandlerAdapter {
//channels保存所有链接进来的channel
private static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
//将链接的channel加到channels中
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
channels.add(ctx.channel());
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ChatRequest.Command command = (ChatRequest.Command)msg;
ChatResponse.ChatMessage.Builder build = ChatResponse.ChatMessage.newBuilder();
build.setChatType(command.getChatType())
.setContent(command.getContent() + ",已经经过服务器转发");
ChatResponse.ChatMessage chatMessage = build.build();
//将消息发送给所有链接的客户端
channels.writeAndFlush(chatMessage);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
这里我们弄两个客户端,一个发聊天内容,经过服务器后将内容转给所有连接的客户端。这是NettyTcpMulitcastSendReceiveClient.java
package client;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import message.request.ChatRequest;
import message.response.ChatResponse;
public class NettyTcpMulitcastSendReceiveClient {
public static void main(String[] args) throws IOException {
Socket socket = null;
DataOutputStream dataOut = null;
DataInputStream dataIn = null;
try {
socket = new Socket("localhost", 8080);
dataOut = new DataOutputStream(socket.getOutputStream());
dataIn = new DataInputStream(socket.getInputStream());
ChatRequest.Command.Builder requestBuiler = ChatRequest.Command.newBuilder();
requestBuiler.setChatType(0)
.setContent("我是发送方");
ChatRequest.Command Command = requestBuiler.build();
byte[] dataOutBytes = Command.toByteArray();
//先发送header
dataOut.writeInt(dataOutBytes.length);
//再发送body
dataOut.write(dataOutBytes);
dataOut.flush();
int bodyLength = dataIn.readInt();
byte[] bodyBytes = new byte[bodyLength];
dataIn.read(bodyBytes);
//将字节流转成ChatResponse.ChatMessage类型
ChatResponse.ChatMessage chatResponse = ChatResponse.ChatMessage.parseFrom(bodyBytes);
System.out.println("chatType: " + chatResponse.getChatType());
System.out.println("chatContent: " + chatResponse.getContent());
} finally {
// 关闭连接
dataIn.close();
dataOut.close();
socket.close();
}
}
}
package client;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
import message.response.ChatResponse;
public class NettyTcpMulitcastReceiveClient {
public static void main(String[] args) throws IOException {
Socket socket = null;
DataInputStream dataIn = null;
try {
socket = new Socket("localhost", 8080);
dataIn = new DataInputStream(socket.getInputStream());
int bodyLength = dataIn.readInt();
byte[] bodyBytes = new byte[bodyLength];
dataIn.read(bodyBytes);
//将字节流转成ChatResponse.ChatMessage类型
ChatResponse.ChatMessage chatResponse = ChatResponse.ChatMessage.parseFrom(bodyBytes);
int chatType = chatResponse.getChatType();
String chatContent = chatResponse.getContent();
System.out.println("chatType: " + chatType);
System.out.println("chatContent: " + chatContent);
} finally {
// 关闭连接
dataIn.close();
socket.close();
}
}
}
例子是相当简单的。可以测试一下程序,首先启动服务端程序,然后先启动NettyTcpMulitcastReceiveClient.java他会阻塞在socket的read处,再启动NettyTcpMulitcastSendReceiveClient发送聊天内容。就可以看到两个客户程序都接收到了聊天信息。