网络包设计 (2+4+n)
[包头 2 Bytes] : 除了包头2个字节外包体的大小
[包体 4 Bytes] :包的协议ID
[包体 ] : protobuf 字节数据
小编使用的是gradle项目进行搭建游戏引擎,开发工具:eclipse
gradle项目引用的依赖:
allprojects {
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'eclipse'
apply plugin: 'findbugs'
compileJava.options.encoding = 'UTF-8'
sourceCompatibility = 1.8
targetCompatibility = 1.8
ext.projectName = "$name"
repositories {
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
jcenter()
}
dependencies {
compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.5.1'
compile group: 'io.netty', name: 'netty-all', version: '4.1.6.Final'
compile group: 'org.springframework', name: 'spring-context', version: '5.0.7.RELEASE'
compile group: 'org.springframework', name: 'spring-core', version: '5.0.7.RELEASE'
compile group: 'org.springframework', name: 'spring-beans', version: '5.0.7.RELEASE'
compile group: 'org.springframework', name: 'spring-context-support', version: '5.0.7.RELEASE'
compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.4.15'
compile group: 'com.google.guava', name: 'guava', version: '25.1-jre'
compile group: 'com.alibaba', name: 'fastjson', version: '1.2.58'
compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.7'
compile group: 'commons-lang', name: 'commons-lang', version: '2.6'
compile(
fileTree(dir: '../libs', include: '*.jar'),
)
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.0'
}
// findbugs
tasks.withType(FindBugs) {
ignoreFailures = true
reports {
xml.enabled = false
html.enabled = true
}
reportLevel = "high"
}
}
subprojects {
sourceSets.main.java.srcDirs = ["src", "conf"]
task initPath {
sourceSets.main.java.srcDirs.each { it.mkdirs() }
}
//清除上次的编译过的文件
task clearPj(type: Delete) {
delete 'build', 'target'
}
task copyJar(type: Copy) {
from configurations.runtime
into('build/libs/lib')
}
//把JAR复制到目标目录
task release(type: Copy, dependsOn: [build, copyJar]) {
from 'conf'
into('build/libs/conf') // 目标位置
}
task export(type: Copy) {
from 'build/libs'
into '../export/' + projectName
}
task zip(type: Zip) {
from 'build/libs'
}
task copyzip(type: Copy) {
from 'build/distributions'
into '../export/zip/'
}
}
main的启动方法
小编使用的是springboot的加载方式进行初始化相关的配置
/**
* socket 启动类
*
* @bk https://home.cnblogs.com/u/huanuan/
* @简书 https://www.jianshu.com/u/d29cc7d7ca49
* @Author 六月星辰
* @Date 2020年1月10日
*/
@Slf4j
public class ScoketApp {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BeanConfig.class);
context.start();
SocketServer socket = context.getBean(SocketServer.class);
new Thread(socket).start();
}
}
/**
* 服务配置
*
* @bk https://home.cnblogs.com/u/huanuan/
* @简书 https://www.jianshu.com/u/d29cc7d7ca49
* @Author 六月星辰
* @Date 2020年1月10日
*/
@Configuration
@PropertySource("classpath:/scoket.properties")
@ImportResource("classpath:/quartz.xml")
public class BeanConfig {
@Autowired
Environment env;
@Autowired
void init() {
// 系统配置 设置
String ip = env.getProperty("ip");
int port = env.getProperty("port", int.class);
System.err.println("ip = " + ip + "port = " + port);
}
@Bean
SocketServer socketServer() {
return new SocketServer(scoketInitializer(), env.getProperty("port", int.class));
}
@Bean
SocketInitializer scoketInitializer() {
return new SocketInitializer();
}
}
scoket.properties的配置
#scoket的ip
ip=127.0.0.1
#socket的端口
port=8888
SocketInitializer 初始化器
/**
* socket 初始器
*
* @bk https://home.cnblogs.com/u/huanuan/
* @简书 https://www.jianshu.com/u/d29cc7d7ca49
* @Author 六月星辰
* @Date 2020年1月11日
*/
@AllArgsConstructor
public class SocketInitializer extends ChannelInitializer < SocketChannel > {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// TODO Auto-generated method stub
ChannelPipeline pipeline = ch.pipeline();
System.out.println("报告");
System.out.println("信息:有一客户端链接到本服务端");
System.out.println("IP:" + ch.localAddress().getHostName());
System.out.println("Port:" + ch.localAddress().getPort());
System.out.println("报告完毕");
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast("decoder",new Decoder());//解码器
pipeline.addLast("encoder", new Encoder());//编码器
// pipeline.addLast(new ByteArrayEncoder());
pipeline.addLast(new ServerHandler()); // 客户端触发操作
}
}
解码器 (TCP的包处理)
/**
* 解码器
*
* @bk https://home.cnblogs.com/u/huanuan/
* @简书 https://www.jianshu.com/u/d29cc7d7ca49
* @Author 六月星辰
* @Date 2020年1月10日
*/
@Slf4j
public class Decoder extends MessageToMessageDecoder<ByteBuf> {
/**
* [包头 2 Bytes] : 除了包头2个字节外包体的大小</br>
* [包体 4 Bytes] :包的协议ID </br>
* [包体 ] : protobuf 字节数据</br>
* 1.先读取2个字节并解析出剩下数据流的长度dataLength</br>
* 2.如果剩下的数据流不满足dataLength的长度则继续等待</br>
* 3.如果剩下的数据流满足dataLength的长度则放行
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
// out.add(Unpooled.wrappedBuffer(msg));//不进行任何的解析直接放行
// 可读长度
int readableBytes = msg.readableBytes();
if (readableBytes < 4) {
System.err.println("接受到信息! 可读长度小于4! 不进行解码!");
return;
}
// 标记读取位置
msg.markReaderIndex();
int dataLength = msg.readShort();// 读取两个节 查看剩下的数据流长度
if (msg.readableBytes() < dataLength) {
log.info("剩下可读长度小于 dataLength! 剩下可读长度 = {} ", msg.readableBytes());
// 移除读取标准位置
msg.resetReaderIndex();
return;
}
ByteBuf buf = Unpooled.buffer(dataLength + 2);
buf.writeShort(dataLength);
buf.writeBytes(msg);
// 放行到hander层
out.add(Unpooled.wrappedBuffer(buf));
}
}
解码器
/**
* 编码器
*
* @bk https://home.cnblogs.com/u/huanuan/
* @简书 https://www.jianshu.com/u/d29cc7d7ca49
* @Author 六月星辰
* @Date 2020年1月10日
*/
public class Encoder extends MessageToMessageEncoder<byte[]> {
@Override
protected void encode(ChannelHandlerContext ctx, byte[] msg, List<Object> out) throws Exception {
// TODO Auto-generated method stub
out.add(Unpooled.wrappedBuffer(msg));
}
}
服务器Handler
/**
* 服务器Handler
*
* @bk https://home.cnblogs.com/u/huanuan/
* @简书 https://www.jianshu.com/u/d29cc7d7ca49
* @Author 六月星辰
* @Date 2020年1月10日
*/
public class ServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
// TODO Auto-generated method stub
short readShort = msg.readShort();//2个字节
int cmd = msg.readInt();//4个字节
int dataLeng= readShort -4;//protobuf的长度
byte[] result1 = new byte[dataLeng];
msg.readBytes(result1);
c2s_login_user parseFrom = GameProto.c2s_login_user.parseFrom(result1);
System.err.println("收到客户端的protobuf信息 = "+parseFrom.toString());
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
super.channelActive(ctx);
System.err.println("客户端链接上!");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// TODO Auto-generated method stub
super.exceptionCaught(ctx, cause);
}
}
模拟客户端
/**
* 模拟客户端发送
*
* @bk https://home.cnblogs.com/u/huanuan/
* @简书 https://www.jianshu.com/u/d29cc7d7ca49
* @Author 六月星辰
* @Date 2020年1月11日
*/
public class MyClientTest {
public static void main(String[] args) {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class).handler(new MyClientInitializer());
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
客户端初始化器
/**
* 客户端初始化器
*
* @bk https://home.cnblogs.com/u/huanuan/
* @简书 https://www.jianshu.com/u/d29cc7d7ca49
* @Author 六月星辰
* @Date 2020年1月11日
*/
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// TODO Auto-generated method stub
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new ByteArrayEncoder());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new MyClientHandler());
}
}
客户端处理器
/**
* 客户端处理器
*
* @bk https://home.cnblogs.com/u/huanuan/
* @简书 https://www.jianshu.com/u/d29cc7d7ca49
* @Author 六月星辰
* @Date 2020年1月11日
*/
public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//与服务端链接成功 并发送一个登录的请求
//组建protobuf的字节数据
Builder builder = c2s_login_user.newBuilder();
builder.setAccount("123456789");
builder.setPassword("123456789");
byte[] byteArray = builder.build().toByteArray();
//发送的长度 = 2 +4 +n
int length = 6 + byteArray.length;
int dataLength = 4 + byteArray.length;
// 4个字节长度的命令号
int cmd = 1001;// 命令号
//获取发送长度的字节ByteBuf流
ByteBuf buf = Unpooled.buffer(length);
buf.writeShort(dataLength);//[包头 2 Bytes] : 除了包头2个字节外包体的大小
buf.writeInt(cmd);// [包体 4 Bytes] :包的协议ID
buf.writeBytes(byteArray);// [包体 ] : protobuf 字节数据
byte[] newdata = new byte[buf.readableBytes()];
buf.readBytes(newdata);
//发送数据
ctx.writeAndFlush(Unpooled.copiedBuffer(newdata)); // 必须有flush
}
/**
* 处理服务端发送过来的消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
// TODO Auto-generated method stub
System.out.println("读取服务端通道信息..");
}
}
先启动服务端在启动客户端,效果如下
image.png
image.png
小编项目源码下载地址:
https://github.com/zhuhuanuan/nettySocket