引言
由于現(xiàn)在java web太卷了,所以各位同行可以考慮換一個(gè)賽道,做游戲還是很開心的。
本篇教程給新人用于學(xué)習(xí)游戲服務(wù)器的基本知識(shí),給新人們一些學(xué)習(xí)方向,有什么錯(cuò)誤的地方歡迎各位同行進(jìn)行討論。
技術(shù)選型
本篇教程預(yù)計(jì)使用Java+Redis+Mongo
正文
本著先完成再完美的原則,從最簡單的echo服務(wù)器開始。
Echo服務(wù)器就是,客戶端發(fā)什么數(shù)據(jù),服務(wù)端就原樣返回回去。
創(chuàng)建基礎(chǔ)架構(gòu)
IDEA創(chuàng)建項(xiàng)目
我這邊用Gradle進(jìn)行依賴管理,使用的版本為 gradle8.0.2, openjdk19.
修改build.gradle導(dǎo)入幾個(gè)基礎(chǔ)開發(fā)包。
dependencies {
//網(wǎng)絡(luò)
implementation group: 'io.netty', name: 'netty-all', version: '4.1.90.Final'
//spring
implementation group: 'org.springframework', name: 'spring-context', version: '6.0.6'
//log
implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.36'
implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.2.11'
implementation group: 'ch.qos.logback', name: 'logback-access', version: '1.2.11'
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.11'
//lombok
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.24'
}
創(chuàng)建Bean配置類
@Configuration
@ComponentScan(basePackages = {"com.wfgame"})
public class GameBeanConfiguration {
}
創(chuàng)建主類
@Component
@Slf4j
public class GameMain {
public static void main(String[] args) {
// 初始化Spring
AnnotationConfigApplicationContext springContext = new AnnotationConfigApplicationContext(GameBeanConfiguration.class);
springContext.start();
log.info("server start!");
}
}
運(yùn)行一下,正常輸出server start!
我們會(huì)發(fā)現(xiàn),程序執(zhí)行后馬上停止了,對于游戲服務(wù)器來說,我們需要保持運(yùn)行狀態(tài),等待玩家接入進(jìn)行游戲。所以我們main中增加一個(gè)循環(huán),不停讀取控制臺(tái)輸入,當(dāng)讀取到控制臺(tái)輸入stop時(shí),我們再進(jìn)行停服。
修改main方法如下:
public static void main(String[] args) {
// 初始化Spring
AnnotationConfigApplicationContext springContext = new AnnotationConfigApplicationContext(GameBeanConfiguration.class);
springContext.start();
log.info("server start!");
//region 處理控制臺(tái)輸入,每秒檢查一遍 stopFlag,為true就跳出循環(huán),執(zhí)行關(guān)閉操作
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 設(shè)置循環(huán)使服務(wù)器不立刻停止
while (true) {
if (stopFlag) {
log.info("receive stop flag, server will stop!");
break;
}
// 每次循環(huán)停止一秒,避免循環(huán)頻率過高
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
//處理控制臺(tái)指令
try {
if (br.ready()) {
String cmd = br.readLine().trim();
if (cmd.equals("stop")) {//正常關(guān)服
stopFlag = true;
log.info("Receive stop flag, time to stop.");
} else {
log.info("Unsupported cmd:{}", cmd);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
//停掉虛擬機(jī)
System.exit(0);
}
這樣我們就獲得了一個(gè)可以控制停服的服務(wù)器。當(dāng)我們控制臺(tái)輸入stop時(shí),程序結(jié)束運(yùn)行。
添加Netty監(jiān)聽端口
要與客戶端進(jìn)行TCP連接,需要建立socket通道,然后通過socket通道進(jìn)行數(shù)據(jù)交互。
傳統(tǒng)BIO一個(gè)線程一個(gè)連接,有新的連接進(jìn)來時(shí)就要?jiǎng)?chuàng)建一個(gè)線程,并持續(xù)讀取數(shù)據(jù)流,當(dāng)這個(gè)連接發(fā)送任何請求時(shí),會(huì)對性能造成嚴(yán)重浪費(fèi)。
NIO一個(gè)線程通過多路復(fù)用器可以監(jiān)聽多個(gè)連接,通過輪詢判斷連接是否有數(shù)據(jù)請求。
Netty對java原生NIO進(jìn)行了封裝,簡化了代碼,便于我們的使用。
Netty的包我們之前已經(jīng)導(dǎo)入過了,直接拿來用即可。
首先我們創(chuàng)建一個(gè)Netty自定義消息處理類。
@Sharable
public class NettyMessageHandler extends SimpleChannelInboundHandler<Object> {
/**
* 讀取數(shù)據(jù)
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
this.doRead(ctx, msg);
}
private void doRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("received msg = : " + msg);
// 馬上將原數(shù)據(jù)返回
ctx.writeAndFlush(msg);
}
}
然后編寫Netty服務(wù)器啟動(dòng)代碼,我們修改GameMain類的代碼
@Component
@Slf4j
public class GameMain {
// 停服標(biāo)志
private static boolean stopFlag = false;
public static void main(String[] args) {
// 初始化Spring
AnnotationConfigApplicationContext springContext = new AnnotationConfigApplicationContext(GameBeanConfiguration.class);
springContext.start();
// 啟動(dòng)Netty服務(wù)器
try {
startNetty();
log.info("Netty server start!");
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("server start!");
//region 處理控制臺(tái)輸入,每秒檢查一遍 stopFlag,為true就跳出循環(huán),執(zhí)行關(guān)閉操作
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 設(shè)置循環(huán)使服務(wù)器不立刻停止
while (true) {
if (stopFlag) {
log.info("receive stop flag, server will stop!");
break;
}
// 每次循環(huán)停止一秒,避免循環(huán)頻率過高
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
//處理控制臺(tái)指令
try {
if (br.ready()) {
String cmd = br.readLine().trim();
if (cmd.equals("stop")) {//正常關(guān)服
stopFlag = true;
log.info("Receive stop flag, time to stop.");
} else {
log.info("Unsupported cmd:{}", cmd);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
//停掉虛擬機(jī)
System.exit(0);
}
/**
* 啟動(dòng)netty服務(wù)器
*/
private static void startNetty() throws InterruptedException {
int port = 2333;
log.info("Netty4SocketServer start---Normal, port = " + port);
final NioEventLoopGroup bossGroup = new NioEventLoopGroup(2);
final NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.option(ChannelOption.SO_REUSEADDR, true);//允許重用端口
bootstrap.option(ChannelOption.SO_BACKLOG, 512);//允許多少個(gè)新請求進(jìn)入等待
bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);//是否使用內(nèi)存池
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true); // 保持連接活動(dòng)
bootstrap.childOption(ChannelOption.TCP_NODELAY, false); // 禁止Nagle算法等待更多數(shù)據(jù)合并發(fā)送,提高信息及時(shí)性
bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);//是否使用內(nèi)存池
final NettyMessageHandler handler = new NettyMessageHandler();
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline cp = ch.pipeline();
cp.addLast(new StringDecoder());
cp.addLast(new StringEncoder());
cp.addLast("handler", handler);
}
});
// 綁定并監(jiān)聽端口
bootstrap.bind(port).sync();//線程同步阻塞等待服務(wù)器綁定到指定端口
// 優(yōu)雅停機(jī)
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}));
log.info("Netty4SocketServer ok,bind at :" + port);
}
我們先創(chuàng)建了一個(gè)startNetty()方法,用于啟動(dòng)Netty服務(wù)器,同時(shí)綁定了端口2333
我們要注意一下initChannel這塊代碼,我們注冊了String編碼解碼器,他們是用換行符作為一個(gè)消息的結(jié)束標(biāo)志,因此我們等下通過客戶端發(fā)送消息過來需要在行尾添加換行符。同時(shí)將我們自定義的消息處理類也注冊進(jìn)pipeline中,當(dāng)客戶端發(fā)送消息過來,先通過StringDecoder進(jìn)行解碼,然后流入自定義處理類中進(jìn)行下一步處理。
至此服務(wù)端Netty接入完畢,我們下面編寫一個(gè)客戶端進(jìn)行測試。
編寫客戶端進(jìn)行測試
我們增加了ClientMain類,用socket與服務(wù)器進(jìn)行連接,讀取控制臺(tái)輸入上行到服務(wù)器,同時(shí)接受服務(wù)器下行的消息。
public class ClientMain {
private static Socket socket = null;
private static BufferedReader br = null;
private static BufferedWriter writer = null;
private static BufferedReader receivedBufferedReader = null;
public static void main(String[] args) {
// 新增連接到服務(wù)器
startSocket();
}
/**
* 啟動(dòng)socket連接
*/
private static void startSocket() {
try {
socket = new Socket("127.0.0.1", 2333);
br = new BufferedReader(new InputStreamReader(System.in));
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
receivedBufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
new Thread(() -> {
try {
while (true) {
Thread.sleep(1000L);
String s = receivedBufferedReader.readLine();
if (s!=null && !s.equals("")) {
System.out.println("receive: " + s);
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}).start();
while (true) {
Thread.sleep(1000L);
if (br.ready()) {
writer.write(br.readLine().trim() + "\n");
writer.flush();
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
try {
if (receivedBufferedReader != null) {
receivedBufferedReader.close();
}
if (writer != null) {
writer.close();
}
if (br != null) {
br.close();
}
if (socket != null) {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
測試一下,我們先運(yùn)行服務(wù)器,再運(yùn)行客戶端。
在客戶端控制臺(tái)下輸入測試信息。
可以成功進(jìn)行信息交互
總結(jié)
本節(jié)一共做了這么幾件事:文章來源:http://www.zghlxwxcb.cn/news/detail-497691.html
- 項(xiàng)目的初步創(chuàng)建,通過build.gradle進(jìn)行依賴包的管理。
- Netty服務(wù)器的啟動(dòng),并且不斷監(jiān)聽控制臺(tái)輸入,客戶端上行數(shù)據(jù)的讀取。
- 編寫測試用客戶端,與服務(wù)器進(jìn)行數(shù)據(jù)交互。
下一節(jié)將進(jìn)行注冊登錄的開發(fā),內(nèi)容將會(huì)比較多,感興趣的點(diǎn)點(diǎn)關(guān)注或者留言評論。文章來源地址http://www.zghlxwxcb.cn/news/detail-497691.html
到了這里,關(guān)于從零開始搭建游戲服務(wù)器 第一節(jié) 創(chuàng)建一個(gè)簡單的服務(wù)器架構(gòu)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!