一直都是做web应用后端的,好奇游戏服务器后端是怎样搭建的,看了下网上的资料,将springboot和netty4.1.36.Final结合起来搭建了一个简单的游戏服务器,下面是目录结构
package com.butterfly.game.netty.message;
/**
* Created by on 2019/10/14.
* 自定义协议头
* @author
*/
public class Header {
//标识
private byte tag;
//编码
private byte encode;
//加密
private byte encrypt;
//其他字段1
private byte extend1;
//其他字段2
private byte extend2;
//会话id
private String sessionid;
//包长度
private int length;
//指令
private int command;
public Header(){
}
public Header(byte tag, byte encode, byte encrypt, byte extend1, byte extend2, String sessionid, int length, int command) {
this.tag = tag;
this.encode = encode;
this.encrypt = encrypt;
this.extend1 = extend1;
this.extend2 = extend2;
this.sessionid = sessionid;
this.length = length;
this.command = command;
}
public byte getTag() {
return tag;
}
public void setTag(byte tag) {
this.tag = tag;
}
public byte getEncode() {
return encode;
}
public void setEncode(byte encode) {
this.encode = encode;
}
public byte getEncrypt() {
return encrypt;
}
public void setEncrypt(byte encrypt) {
this.encrypt = encrypt;
}
public byte getExtend1() {
return extend1;
}
public void setExtend1(byte extend1) {
this.extend1 = extend1;
}
public byte getExtend2() {
return extend2;
}
public void setExtend2(byte extend2) {
this.extend2 = extend2;
}
public String getSessionid() {
return sessionid;
}
public void setSessionid(String sessionid) {
this.sessionid = sessionid;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getCommand() {
return command;
}
public void setCommand(int command) {
this.command = command;
}
}
package com.butterfly.game.netty.message;
import com.butterfly.game.netty.decoder.MessageDecoder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* Created by on 2019/10/14.
* 包体
*
* @author
*/
public class Message {
private Header header;
private String data;
public Header getHeader() {
return header;
}
public void setHeader(Header header) {
this.header = header;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public Message(Header header) {
this.header = header;
}
public Message(Header header, String data) {
this.header = header;
this.data = data;
}
public byte[] toByte() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(MessageDecoder.PACKAGE_TAG);
out.write(header.getEncode());
out.write(header.getEncrypt());
out.write(header.getExtend1());
out.write(header.getExtend2());
byte[] bb = new byte[32];
byte[] bb2 = header.getSessionid().getBytes();
for (int i = 0; i < bb2.length;i++){
bb[i] = bb2[i];
}
try {
out.write(bb);
byte[] bbb = data.getBytes("UTF-8");
out.write(intToBytes2(bbb.length));
out.write(intToBytes2(header.getCommand()));
out.write(bbb);
out.write('\n');
} catch (IOException e) {
e.printStackTrace();
}
return out.toByteArray();
}
private static byte[] intToBytes2(int value) {
byte[] src = new byte[4];
src[0] = (byte) ((value >> 24) & 0xFF);
src[1] = (byte) ((value >> 16) & 0xFF);
src[2] = (byte) ((value >> 8) & 0xFF);
src[3] = (byte) (value & 0xFF);
return src;
}
public static void main(String[] args) {
ByteBuf heapBuffer = Unpooled.buffer(8);
System.out.println(heapBuffer);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
out.write(intToBytes2(1));
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = out.toByteArray();
heapBuffer.writeBytes(data);
System.out.println(heapBuffer);
int a = heapBuffer.readInt();
System.out.println(a);
}
}
package com.butterfly.game.netty.decoder;
import com.butterfly.game.netty.message.Header;
import com.butterfly.game.netty.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import java.util.List;
/**
* Created by on 2019/10/14.
*
* @author
*/
public class MessageDecoder extends ByteToMessageDecoder {
public static final int HEAD_LENGTH = 45;
public static final byte PACKAGE_TAG = 0x01;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
in.markReaderIndex();
if (in.readableBytes() < HEAD_LENGTH) {
throw new CorruptedFrameException("包长度问题");
}
byte tag = in.readByte();
if (tag != PACKAGE_TAG) {
throw new CorruptedFrameException("标识问题");
}
byte encode = in.readByte();
byte encrypt = in.readByte();
byte extend1 = in.readByte();
byte extend2 = in.readByte();
byte sessionByte[] = new byte[32];
in.readBytes(sessionByte);
String sessionid = new String(sessionByte, "UTF-8");
int length = in.readInt();
int command = in.readInt();
Header header = new Header(tag, encode, encrypt, extend1, extend2, sessionid, length, command);
byte[] data = new byte[length];
in.readBytes(data);
Message message = new Message(header, new String(data, "UTF-8"));
out.add(message);
}
}
package com.butterfly.game.netty.encoder;
import com.butterfly.game.netty.decoder.MessageDecoder;
import com.butterfly.game.netty.message.Header;
import com.butterfly.game.netty.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
/**
* Created by on 2019/10/14.
*
* @author
*/
public class MessageEncoder extends MessageToByteEncoder<Message>{
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
Header header = msg.getHeader();
out.writeByte(MessageDecoder.PACKAGE_TAG);
out.writeByte(header.getEncode());
out.writeByte(header.getEncrypt());
out.writeByte(header.getExtend1());
out.writeByte(header.getExtend2());
out.writeBytes(header.getSessionid().getBytes());
out.writeInt(header.getLength());
out.writeInt(header.getCommand());
out.writeBytes(msg.getData().getBytes("UTF-8"));
}
}
package com.butterfly.game.netty.server;
import com.butterfly.game.netty.decoder.MessageDecoder;
import com.butterfly.game.netty.encoder.MessageEncoder;
import com.butterfly.game.netty.handler.ServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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.LineBasedFrameDecoder;
import org.springframework.stereotype.Component;
/**
* Created by on 2019/10/14.
*
* @author
*/
@Component
public class TimeServer {
private int port = 88888;
public void run() throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGrgoup = new NioEventLoopGroup();
ByteBuf heapBuffer = Unpooled.buffer(8);
heapBuffer.writeBytes("\r".getBytes());
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGrgoup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("encoder", new MessageEncoder())
.addLast("decoder", new MessageDecoder())
//防止TCP粘包/拆包
.addFirst(new LineBasedFrameDecoder(65535))
//ServerHandler实现了业务逻辑
.addLast(new ServerHandler());
}
})
//服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝
.option(ChannelOption.SO_BACKLOG, 1024)
//Socket参数,连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性。
// 可以将此功能视为TCP的心跳机制,需要注意的是:默认的心跳间隔是7200s即2小时。Netty默认关闭该功能。
.childOption(ChannelOption.SO_KEEPALIVE, true);
//绑定服务器,等待绑定完成,调用sync()的原因是当前线程阻塞
ChannelFuture f = b.bind(port).sync();
//关闭channel和块,直到它被关闭
f.channel().closeFuture().sync();
} finally {
//关闭EventLoopGroup,释放所有资源(包括所有创建的线程)
workerGrgoup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
public void start(int port) throws InterruptedException {
this.port = port;
this.run();
}
}
ActionMapUtil.java
package com.butterfly.game.netty.invote;
import com.butterfly.game.netty.message.Message;
import io.netty.channel.ChannelHandlerContext;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* Created by on 2019/10/14.
* 请求分发器
* @author
*/
public class ActionMapUtil {
private static Map<Integer, Action> map = new HashMap<>();
public static Object invote(Integer key, Object... args) throws Exception {
Action action = map.get(key);
if (action != null) {
Method method = action.getMethod();
try {
return method.invoke(action.getObject(), args);
} catch (Exception e) {
throw e;
}
}
return null;
}
public static void put(Integer key, Action action) {
map.put(key, action);
}
}
Action.java
package com.butterfly.game.netty.invote;
import java.io.ObjectOutput;
import java.lang.reflect.Method;
/**
* Created by on 2019/10/14.
*
* @author
*/
public class Action {
private Method method;
private Object object;
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
}
请求分发器ServerHandler
package com.butterfly.game.netty.handler;
import com.butterfly.game.netty.invote.ActionMapUtil;
import com.butterfly.game.netty.message.Header;
import com.butterfly.game.netty.message.Message;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* Created by on 2019/10/14.
* 处理并分发
*
* @author
*/
public class ServerHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
Message m = (Message) msg;
Header header = m.getHeader();
/* 请求分发*/
ActionMapUtil.invote(header.getCommand(), ctx, m);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
super.exceptionCaught(ctx, cause);
}
}
ActionBeanPostProcessor 项目初始化时将所有请求方法放到ActionMapUtil里
package com.butterfly.game.netty.core;
import com.butterfly.game.netty.invote.Action;
import com.butterfly.game.netty.invote.ActionMapUtil;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Created by on 2019/10/14.
*
* @author
*/
@Component
public class ActionBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Method[] methods = bean.getClass().getMethods();
for (Method method : methods) {
ActionMap actionMap = method.getAnnotation(ActionMap.class);
if (actionMap != null) {
Action action = new Action();
action.setMethod(method);
action.setObject(bean);
ActionMapUtil.put(actionMap.key(), action);
}
}
return bean;
}
}
自定义注解ActionMap,类似于mvc的@RequestMapping
package com.butterfly.game.netty.core;
import java.lang.annotation.*;
/**
* Created by on 2019/10/14.
* 类似于mvc的@RequestMapping
* @author
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface ActionMap {
int key();
}
自定义注解NettyController,类似于mvc的@Controller注解
package com.butterfly.game.netty.core;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
/**
* Created by on 2019/10/14.
* 类似于mvc的Controller注解
* @author
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Component
public @interface NettyController {
}
package com.butterfly.game.netty.controller;
import com.butterfly.game.netty.core.ActionMap;
import com.butterfly.game.netty.core.NettyController;
import com.butterfly.game.netty.message.Message;
import io.netty.channel.ChannelHandlerContext;
/**
* Created by on 2019/10/14.
*
* @author
*/
@NettyController
public class UserAction {
@ActionMap(key = 1)
public String login(ChannelHandlerContext ct, Message message){
System.out.println(message.getData());
return null;
}
}
package com.butterfly.game;
import com.butterfly.game.netty.server.TimeServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class GameApplication {
public static void main(String[] args) throws InterruptedException {
ConfigurableApplicationContext context = SpringApplication.run(GameApplication.class, args);
TimeServer server = context.getBean(TimeServer.class);
server.start(8888);
}
}
package com.butterfly.game.netty;
import com.butterfly.game.netty.message.Header;
import com.butterfly.game.netty.message.Message;
import com.sun.org.apache.xpath.internal.SourceTree;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* Created by on 2019/10/14.
*
* @author
*/
public class TestClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",8888);
try {
OutputStream out = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
while(true){
String send = scanner.nextLine();
System.out.println("客户端"+send);
byte[] by = send.getBytes("UTF-8");
Header header = new Header((byte)1,(byte)1,(byte)1,(byte)1,(byte)1,"6ebf17wee14361fbxf5dwd741587fhk2",by.length,1);
Message message = new Message(header,send);
out.write(message.toByte());
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket.close();
}
}
}