網(wǎng)絡包設計 (2+4+n)
[包頭 2 Bytes] : 除了包頭2個字節(jié)外包體的大小
[包體 4 Bytes] :包的協(xié)議ID
[包體 ] : protobuf 字節(jié)數(shù)據(jù)
小編使用的是gradle項目進行搭建游戲引擎掰吕,開發(fā)工具: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的加載方式進行初始化相關(guān)的配置
/**
* socket 啟動類
*
* @bk https://home.cnblogs.com/u/huanuan/
* @簡書 http://www.reibang.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/
* @簡書 http://www.reibang.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() {
// 系統(tǒng)配置 設置
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/
* @簡書 http://www.reibang.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()); // 客戶端觸發(fā)操作
}
}
解碼器 (TCP的包處理)
/**
* 解碼器
*
* @bk https://home.cnblogs.com/u/huanuan/
* @簡書 http://www.reibang.com/u/d29cc7d7ca49
* @Author 六月星辰
* @Date 2020年1月10日
*/
@Slf4j
public class Decoder extends MessageToMessageDecoder<ByteBuf> {
/**
* [包頭 2 Bytes] : 除了包頭2個字節(jié)外包體的大小</br>
* [包體 4 Bytes] :包的協(xié)議ID </br>
* [包體 ] : protobuf 字節(jié)數(shù)據(jù)</br>
* 1.先讀取2個字節(jié)并解析出剩下數(shù)據(jù)流的長度dataLength</br>
* 2.如果剩下的數(shù)據(jù)流不滿足dataLength的長度則繼續(xù)等待</br>
* 3.如果剩下的數(shù)據(jù)流滿足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();// 讀取兩個節(jié) 查看剩下的數(shù)據(jù)流長度
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/
* @簡書 http://www.reibang.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/
* @簡書 http://www.reibang.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個字節(jié)
int cmd = msg.readInt();//4個字節(jié)
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);
}
}
模擬客戶端
/**
* 模擬客戶端發(fā)送
*
* @bk https://home.cnblogs.com/u/huanuan/
* @簡書 http://www.reibang.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/
* @簡書 http://www.reibang.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/
* @簡書 http://www.reibang.com/u/d29cc7d7ca49
* @Author 六月星辰
* @Date 2020年1月11日
*/
public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//與服務端鏈接成功 并發(fā)送一個登錄的請求
//組建protobuf的字節(jié)數(shù)據(jù)
Builder builder = c2s_login_user.newBuilder();
builder.setAccount("123456789");
builder.setPassword("123456789");
byte[] byteArray = builder.build().toByteArray();
//發(fā)送的長度 = 2 +4 +n
int length = 6 + byteArray.length;
int dataLength = 4 + byteArray.length;
// 4個字節(jié)長度的命令號
int cmd = 1001;// 命令號
//獲取發(fā)送長度的字節(jié)ByteBuf流
ByteBuf buf = Unpooled.buffer(length);
buf.writeShort(dataLength);//[包頭 2 Bytes] : 除了包頭2個字節(jié)外包體的大小
buf.writeInt(cmd);// [包體 4 Bytes] :包的協(xié)議ID
buf.writeBytes(byteArray);// [包體 ] : protobuf 字節(jié)數(shù)據(jù)
byte[] newdata = new byte[buf.readableBytes()];
buf.readBytes(newdata);
//發(fā)送數(shù)據(jù)
ctx.writeAndFlush(Unpooled.copiedBuffer(newdata)); // 必須有flush
}
/**
* 處理服務端發(fā)送過來的消息
*/
@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