鸿蒙飞机大战小游戏(包含前后端通信)

成果展示

该项目基于开源的飞机大战小程序,构建了登陆验证机制,主要是完成了在鸿蒙中的网络通信和前后端交互功能,可以实现注册用户并登陆,用户游戏记录保存和展示功能,项目源码在此,部分运行成果如图:
成果

成果1

成果2

构建思路

飞机大战应该很容易找到开源的素材,拿来修修改改应该是最快的, 但这样就成了只写前端的脚本小子,所以目前的想法是通过登陆界面,实现后端保存记录的功能,打通前后端交互这条路。目前计划的流程图如下:程序流程图
看起来也很简单,但是很多操作以前根本没有实践过,不能以现在的视角嘲笑过去幼稚的自己,通过这个比较简单的例子,真正打通前后端,又是基于鸿蒙体系,对我来说也是一次比较大的实践了。

前端——登陆界面

这里设置三个界面,分别开放登陆、注册和忘记密码,登陆入口主要实现不同页面的跳转,通过注册字符和按钮的点击事件跳转到响应界面,并给予响应提示。

在所有任务之前,因为项目需要网络通信,所有首先把网络权限赋给该项目,在module.json5module中增加如下代码

    requestPermissions:[
      {
        name: "ohos.permission.INTERNET"
      },]

图标配置

应用生成后默认是一个名为lable有四个方框的图标,我们可以在src\main\module.json5中修改配置文件实现图标自定义,在其中abilities模块中分别修改iconlabel字段,按住ctrl步入字符常量,
修改位置
图标和标签分别修改如下:
图标修改

标签修改
修改后的结果展示如图:
示例

登陆入口

常规登陆入口由一个图标,用户名和密码输入栏,以及登陆按钮组成,完成后的界面如下:
登陆界面展示
该部分可用列排列,分别包含图片,两个输入框,登陆按钮和注册字符,其中注册字符是横向排列的两个字符串,所以可以额外定义一个行排列,大致框架代码如下:

import { router } from '@kit.ArkUI';
import {req} from "./http"

@Entry
@Component
struct Index {
  @State userName:string="admin"
  @State passWord:string="";
  build() {

    //列边距20
    Column({space: 20}) {
      Image($r("app.media.huawei_logo")).width('30%')
        // 顶部距离
        .margin({top: '300px'})
      Text("欢迎登陆")
      TextInput({
      	// 设置默认登陆用户名
        placeholder:"请输入用户名",text:this.userName
      })//输入框内的值给用户名
        .onChange((value) =>{
        this.userName=value
      })
      TextInput({
        placeholder:"请输入密码"
      }).type(InputType.Password)
      //设置密码格式
        .onChange((value) =>{
          this.passWord=value
        })
      Button("登陆").width('75%')
        .onClick(() => {
        //登陆事件代码
          }
        )
      Row({space: 20}){
        Text("注册").fontSize(15)
          .onClick(() => {
            //点击事件跳转路由,到注册界面
            router.pushUrl({
              url:"pages/Sigin"
            })
          })
        Text("忘记密码").fontSize(15)
          .onClick(() => {
            //点击事件跳转路由,到修改密码界面
            router.pushUrl({
              url:"pages/ForgetPassport"
            })
            })
      }
    }//默认具中,设置宽度即可实现
      .width('100%')
      //内边距
      .padding(20);
   }
}

登陆验证——同步处理

该部分需要构造网络请求发送给服务器,可以通过自带的http模块实现,大概借助如下方法即可:

// 首先声明该方法并不能达成效果!!! 
// 实例化http请求对象
 let httpRequest = http.createHttp();
 // 构造http请求
 httpRequest.request(
      // 填写http请求的url地址,
      url,
      {
        method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
        // 当使用POST请求时此字段用于传递内容
        header: {
          'Content-Type': 'application/json',
        },
        extraData: {
          "request_type":type,
          "username": username,
          "password": password,
        },
      },
      (err, data) => {
      if (!err) {
      // 处理正常逻辑
      }
      else{
      // 异常处理
      }
            }
    )
    }

但这里算是我在项目中踩了最大的一个坑,卡了我几乎一天,就是同步问题,问题表现为在按钮的onclick事件调用该方法并不能获得任何信息,对函数和按钮事件分别进行输出后发现,函数的输出晚于按钮事件的输出,这说明函数体反而是后执行完毕的。

这里我去网上查了资料,说request请求是异步的,也就是说执行到请求后程序会直接往下执行,而不等请求信息,出发点是避免网络拥堵造成程序等待,但很多时候我们的信息是有上下文依赖的,这种情况下我们需要手动设置,使程序等待请求结果。

在该阶段我分别尝试了弹窗 AlertDialog.show({ message:"密码或用户名错误,请重新输入" })等待,设置延迟setTimeout方法,发现都不好用,这些电脑端编程的方法都不好用,移动端似乎异步为主,所以只能学习正规的同步手段。

以嵌套读取新闻等背景大致介绍几种方法,比如读取网页标签,进入不同网页后读取分页面,最终读取文本信息的三层嵌套。
该部分的讲解在此,这里只是做简单记录。

传统回调——STEP ONE

每次读取的结果res中写入下一步处理方法,如

load((url){
	success(res=>{
		// 请求成功获取id
		let id = get(data[0].id)
		request(url){
			// 新请求的内容获取id
			data:aid=id
			request(url){
				}
			}
	})
}
)

即每次请求成功的处理函数中连接新的请求,该方法可读性很差,不易维护,最好能将每次处理独立出来,使得结构更清晰,避免将所有处理写在一起,由此产生了第二种方法——回调函数。

回调函数——STEP TWO

基于传统调用的问题,有了将处理写为回调函数的方法,即在函数参数中引入函数,最后的结果用参数函数处理,具体代码如下:


load(){
	this.request1(res=>{
	id=res.data[0].id
	this.request2(id,res=>{
		aid=res.data[0].id
		})
	})
}

request1((callback){
	success(res=>{
		// 请求成功获取id
		callback(res)
	})
}
request2((id,callback){
	success(res=>{
		// 请求成功获取id
		data:cid=id
		callback(res)
	})
}
)

该方法下的代码相对易懂易维护,主函数中只有处理数据的代码而没有具体步骤,需要修改时查阅具体函数即可,但其本质上还是嵌套调用,查询次数多时代码很冗余,在此基础上更改进的版本为promise

Promise方法——STEP THREE

借助一个promise对象跳出循环嵌套,该对象有三种状态,分别为pendingresolvedrejected,本身有thencatch方法,其中resolvereject分别对应处理成功和失败两种方法,then表示处理后对象的返回值,在此基础上用promise方法重构代码如下:

init(){
	this.request1().then(res=>{
		id=res[0].id
		// 返回request有id参数的处理后的promise对象
		retrun request2(id)
	})
	.then(res=>{
	})
}
request1(){
	// 直接返回promise对象
	return new Promise((resolve,reject)=>{ 
	request(url)
	success:res=>{
	// 对象返回res信息,状态切换为resolved
		resolve(res)
	}
	}fail:err{
		// 返回err信息,状态切换为rejected
		reject(err)
	}
}
request2(id){
	return new Promise((resolve,reject)=>{ 
	request(url,id)
	success:res=>{
	// 对象返回res信息,状态切换为resolved
		resolve(res)
	}
	}fail:err{
		// 返回err信息,状态切换为rejected
		reject(err)
	}
}

使用该方法可以将原有嵌套调用的方法转为链式调用,即不断生成新的promise对象,并不断处理。

异步处理同步化async/await——STEP FOUR

使用两个关键字asyncawait分别修饰函数和promise方法,即可实现同步执行,后续代码会等待await执行完毕后再继续执行,实现函数同步执行,加载函数直接修改为:

asyc load(){
	res = await request1()
	id=res[0].id
	res = await request2(id)
}

基于promise的修改版

当时只学到promise,所以目前的代码只是第三版,http请求方法的最终方案为:

import http from '@ohos.net.http';

//request为异步方法,不等待回调函数执行结束,需要手动设置
export async function req(url:string, username:string, password:string,type:number):Promise<string> {
  return await new Promise((resolve, reject) => {
    let httpRequest = http.createHttp();
    httpRequest.request(
      // 填写http请求的url地址,
      url,
      {
        method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
        // 当使用POST请求时此字段用于传递内容
        header: {
          'Content-Type': 'application/json',
        },
        extraData: {
          "request_type":type,
          "username": username,
          "password": password,
        },
      },
      (err, data) => {
        if (!err) {
          // data.result为http响应内容
          console.info('Result:' + data.result);
          console.info('code:' + data.responseCode);
          resolve(data.result.toString())
        } else {
          console.info('error:' + JSON.stringify(err));
          // 当该请求使用完毕时,调用destroy方法主动销毁。
          httpRequest.destroy();
          reject(data.result)
        }
      }
    )
  })
}

登陆按键绑定事件为,发送账号密码给服务器,接收返回的报文,如果标识为pass或不为reject则验证通过跳转页面,否则提示错误信息。

      Button("登陆").width('75%')
        .onClick(() => {
          console.log("正在验证");
          // 发送账号密码给服务器
          req( this.url, this.userName, this.passWord,1).then(res=>{
            console.log("包含信息为:"+res)

            if (res !="reject") {
            // 登陆时获取用户最高成绩记录
              this.record=res
              console.log("验证通过")
              // 验证通过进入下一页面
              router.pushUrl({
                url: "pages/Plane_Game",
                // 传递信息
                params:{
                  username:this.userName,
                  record:this.record
                }
              })
              console.log("传递用户名给下一页:"+this.userName)
            }
            else{
              AlertDialog.show({
                message:"密码或用户名错误,请重新输入"
              })
            }
          }).toString()
          }

注册界面

该部分与登陆界面基本类似,不同的就是密码需要输两遍,像市面上的产品一样,防止用户第一次把密码打错了后面登不上去,两次输入结果如果不同的话就直接弹窗报错,如果一致的话,点击按钮再把用户名和密码发送到后端,让其进行持久化。

逻辑基本类似,但为了记录,直接展示其页面源代码:

import {req} from "./http"
import { router } from '@kit.ArkUI';
@Entry
@Component
struct Sign {
  @State userName:string=""
  @State passWord:string=""
  @State SecondPassWord:string=""
  url: string = "http://127.0.0.1:1111/"
  build() {
    Column({space: 20}) {
      Image($r("app.media.huawei_logo")).width('30%')
        // 顶部距离
        .margin({top: '300px'})
      Text("欢迎注册")
      TextInput({
        placeholder:"请输入用户名"
        })
        .onChange((value) =>{
          this.userName=value
        })
      TextInput({
        placeholder:"请输入密码"
      })//设置密码格式
        .type(InputType.Password)
        .onChange((value) =>{
          this.passWord=value
        })
      TextInput({
        placeholder:"请确认密码"
      })//设置密码格式
        .type(InputType.Password)
        .onChange((value) =>{
          this.SecondPassWord=value
        })

      Button("确认注册").width('75%')
        .onClick(()=>{
          //确认密码不相同
          if(this.passWord!=this.SecondPassWord){
            console.log(this.passWord+" 两次密码分别为"+this.SecondPassWord)
            AlertDialog.show({
              message:"两次密码不相同,请重新输入"
            })
          }
          else{
            //发送账号密码给服务器
            req(this.url,this.userName,this.passWord,2).then(res=> {
              console.log("注册获得返回信息:" + res);
              if (res === "pass") {
                console.log("验证通过")
                // 验证通过进入下一页面
                router.pushUrl({
                  url: "pages/Index"
                })
              } else {
                AlertDialog.show({
                  message: "发生错误"
                })
              }
            })
          }
        })
    }//默认具中,设置宽度即可实现
    .width('100%')
    //内边距
    .padding(20);
  }
}

形成的界面如下:
注册页面

前端——飞机大战

该部分为找的开源项目,其他类似实现思路也有借助画布的,但因为没有代码,故最终采取了现在的版本。

该项目使用重复刷新初始化的方法,即每次逻辑运算产生飞机、子弹、敌机,和各种状态判断等信息,通过不断初始化产生动画效果,ai生成的控制流程图如下:

flowchart TD
    Start[初始化] --> A[设置屏幕宽高、刷新周期等]
    A --> B[初始化玩家飞机、敌机、子弹等状态]
    B --> C[启动定时器]
    C --> D{是否游戏结束}
    D -->|是| E[显示得分和重新开始按钮]
    E --> F[等待用户点击重新开始]
    F --> G[重新初始化游戏]
    G --> C
    D -->|否| H[更新帧数]
    H --> I{帧数 % 50 == 0}
    I -->|是| J[生成敌机]
    J --> K[更新敌机位置]
    I --> K
    K --> L{帧数 % 5 == 0}
    L -->|是| M[生成子弹]
    M --> N[更新子弹位置]
    L --> N
    N --> O[检测子弹与敌机碰撞]
    O --> P[检测玩家飞机与敌机碰撞]
    P --> Q[检测屏幕外的敌机和子弹]
    Q --> R[回收屏幕外的敌机和子弹]
    R --> S[帧数加1]
    S --> C

项目整体采用stack堆叠布局,这里只解读一些关键代码

触摸移动

    .onTouch((event: TouchEvent) => {
      if (event.type === TouchType.Move) {
        this.myPlane.x = event.touches[0].x
        this.myPlane.y = event.touches[0].y
      }
    })

堆叠布局的触摸事件代码,在拖动事件中绑定,飞机的坐标为当前触摸坐标,就是有时候会瞬移。

歼敌判定

checkCollision() {
	// 修改 this.bullets 和 this.enemyPlanes 数组,以及 this.score 变量
	// 存储发生碰撞的子弹的索引
    let  bulletIndexes: number[] = [];
    // 存储发生碰撞的敌机的索引
    let  enemyPlaneIndexes: number[] = [];
    for (let  i = 0; i < this.bullets.length; i++) {
      let  bullet = this.bullets[i]
      for (let  j = 0; j < this.enemyPlanes.length; j++) {
      // 使用 bullet.isCollisionWith(this.enemyPlanes[j]) 方法检查当前子弹是否与当前敌机发生碰撞
        if (bullet.isCollisionWith(this.enemyPlanes[j])) {
          bulletIndexes.push(i)
          // 未添加到碰撞敌机数组,则加分
          if (!enemyPlaneIndexes.includes(j)) {
            enemyPlaneIndexes.push(j)
            this.score++
          }
          break
        }
      }
      this.returnToPool(this.bullets, bulletIndexes)
      this.returnToPool(this.enemyPlanes, enemyPlaneIndexes)
    }
  }

该部分用于处理子弹和敌机的碰撞,通过遍历子弹来判断子弹和飞机体积的位置是否有重合,重合说明击中敌机,就增加分数,将子弹和敌机分别回收到对象池中。

死亡判定也类似,判定自己的飞机是否与敌机碰撞。

离屏回收

该项目其实有一定优化思想,并不是无止境地生成敌机和子弹,而是屏幕中生成固定数量的飞机和子弹,离屏后自动销毁回收,详细代码如下:

  checkOffScreenSprites(sprites: Sprite[]) {
  // 定义离屏数组
    let  offScreenIndexes: number[] = [];
    for (let  i = 0; i < sprites.length; i++) {
      let  sprite = sprites[i]
      // 超出屏幕范围的加入离屏数组
      if (sprite.isOffScreen(this.screenWidth, this.screenHeight)) {
        offScreenIndexes.push(i)
      }
    }
    // 离屏范围数组的对象返回池中
    this.returnToPool(sprites, offScreenIndexes)
  }

其中isOffScreen很简单,只要判断当前坐标与屏幕大小即可,其余逻辑为:
遍历检查所有对象,屏幕外的用索引记录,用于后续回收使用更新信息。

初始化界面

init() {
    this.frames = 0
    this.enemyPlanes = []
    this.bullets = []
    this.isGameOver = false
    this.initMyPlane()
    this.spriteManager.init(this.screenWidth, this.screenHeight)
    let  _this = this


    this.pageRefreshIntervalId = setInterval(() => {
      if (_this.frames % 50 == 0) {
        _this.createEnemyPlane();
      }
      if (_this.frames % 5 == 0) {
        _this.createBullet(_this.myPlane.x, _this.myPlane.y - _this.myPlane.height / 2);
      }
      _this.enemyPlanes.forEach((item) => {
        item.update();
      });
      _this.bullets.forEach((item) => {
        item.update();
      });
      _this.checkCollision();
      _this.checkMyPlaneCollision();
      _this.checkOffScreenSprites(_this.bullets);
      _this.checkOffScreenSprites(_this.enemyPlanes);
      _this.frames++;
    }, this.pageRefreshInterval)
  }

初始化页面函数,每50帧生成一个敌机,5帧生成一颗子弹,遍历二者数组,更新位置,随后检测碰撞与屏幕外子弹和飞机,最后帧数自增。

主程序

 build() {
    Stack({ alignContent: Alignment.TopStart }) {
      Image($r('app.media.bg')).width('100%').height('100%')
      if (this.isGameOver) {
        Column() {
          Button({ type: ButtonType.Capsule, stateEffect: true }) {
            Text('重新开始').fontSize(30).fontColor(0xffffff).padding({ left: 30, right: 30, top: 10, bottom: 10 })
          }
          .onClick((event: ClickEvent) => {
            this.init()
          })
          Text("得分: " + this.score).margin({ top: 10, left: 20 }).fontSize(20)
        }.justifyContent(FlexAlign.Center).width('100%').height('100%')
      } else {
        Image($r('app.media.my_plane'))
          .width(this.myPlane.width).height(this.myPlane.height)
          .margin({ top: this.myPlane.y - this.myPlane.height / 2 + 10, left: this.myPlane.x - this.myPlane.width / 2 })

        ForEach(this.enemyPlanes, (item:Sprite) => {
          Image($r('app.media.enemy_plane'))
            .width(item.width).height(item.height)
            .margin({ top: item.y, left: item.x })
        })

        ForEach(this.bullets, (item:Sprite) => {
          Image($r('app.media.bullet'))
            .width(item.width).height(item.height)
            .margin({ top: item.y, left: item.x })
        })
      }
    }.width('100%').height('100%')

根据游戏状态判定是否结束,是则弹出按钮和游戏记录,按钮的点击事件为初始化函数,否则一次加载,分别显示飞机,敌机和子弹。

帧数不断增加会不会导致溢出呢?
帧数问题
其实这里应该加一步定时清零操作。

回传记录

该部分负责将本次游戏记录回传给服务端,用得分作为记录,安排在重新开始按钮的触发函数中,当本次记录大于登陆页面传来的最高记录才更新,代码如下:

          .onClick((event: ClickEvent) => {
            console.log("获取用户名为:"+this.username);
            // 发送记录给后台,更新数据
            if(this.username){
              // 判定,username不为空才执行
              if(this.score>Number(this.record)){
                req(this.url, this.username, this.score.toString(),3)
                console.log("更新记录成功");
              }else{
                console.log("未更新记录");
              }
            }

后端——数据处理

因为之前学过Java连接数据库,这是很基础但是很重要的一步,而且也听说Java对前后端的处理很完善,所以该项目继续使用Java作为完成的工具。

数据库连接

参照JDBC连接数据库文件即可,分别完成注册驱动,获取数据库连接对象,获取数据库操作对象,执行sql语句,获取结果集,回收资源,在项目中直接返回操作对象的代码整合为一个函数,展示如下:

    public Statement build_connection() {
        try {
            // 注册驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        try{
            // 获取数据库连接对象
            Connection connection= DriverManager.getConnection("jdbc:mysql://localhost:3306/InfoForUser?characterEncoding=utf8&serverTimezone=GMT%2B8"
                    ,"root",null);
            connection.setAutoCommit(false);
            // 获取数据库操作对象
            Statement statement=connection.createStatement();
            System.out.println("获取对象成功");
            return statement;
        }
        catch (SQLException e) {

            throw new RuntimeException(e);
        }
    }

数据处理

该部分负责把前端传来的用户名和密码在数据库中查询,返回查询结果,或在数据库中添加数据,定义为三个函数,分别接收数据库操作对象statementusernamepassword,区别是函数体中执行sql语句不同,
1,登陆检验功能是将数据在数据库中检索,检索得到说明口令合法,返回ture
2,新增数据是直接在数据库中insert一条记录。
3,更新记录负责对用户的record字段进行更新。该步逻辑稍微复杂,数据库不是很扎实的可以分两步走,首先获得用户记录,再进行比较,如果新值较大则更新。

具体展示如下:

    //数据库检查用户名密码是否正确
    public boolean check_user(Statement statement,String user, String password) {
        try {
            ResultSet resultSet = statement.executeQuery("select * from UserInfo where username='" + user + "' and password='" + password + "';");
            if (resultSet.next()) {
                System.out.println("用户名密码正确");
                return true;
            } else {
                System.out.println("用户名密码错误");
                return false;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    // 新增用户
    public boolean add_user(Statement statement,String user, String password) {
        try {
            statement.executeUpdate("insert into UserInfo values('" + user + "','" + password + "');");
            return true;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
        // 获取用户记录
    public String get_userRecord(Statement statement,String user) {
        try {
            ResultSet resultSet = statement.executeQuery("select record from userinfo where username='" + user + "';");
            if (resultSet.next()) {
                return resultSet.getString("record");
            } else {
                System.out.println("用户不存在");
                return null;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
    // 更新用户成绩
    public boolean update_userRecord(Statement statement,String user, String record) {
        try {
            statement.executeUpdate("update userinfo set record='" + record + "' where username='" + user + "';");
            return true;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

网络连接

该部分需要Java负责完成针对特定网络端口的监听,接收报文并响应,目前的逻辑是用一个缓冲区接收所有信息,然后在函数中根据字符串查找,首先获取用户名和密码,然后判断请求类型,是登陆还是注册,登陆就在数据库中查询是否有该记录,有就返回pass允许登陆,否则reject,注册请求也类似,新增记录到数据库中,成功返回pass,否则返回reject

该模块大致流程如下:
网络模块流程图
监听模块代码如下:

//创建socket,绑定监听端口1111
ServerSocket serverSocket = new ServerSocket(1111);
System.out.println("Server started on port 1111");
while (true) {
            //循环监听等待请求
            Socket socket = serverSocket.accept();
            System.out.println("New client connected");

            byte[] buffer =new byte[1024];
            int a = socket.getInputStream().read(buffer);
            //响应对象
            OutputStream resp = socket.getOutputStream();
            //报文转化成字符串
            String requestLine = new String(buffer, 0, a);
            System.out.println("报文内容为:"+requestLine);
            }

一次通信的报文信息展示如下:
通信信息展示
而更新部分并不强制要求响应,故延续即可,无需规定。

报文解析

本来是有现成的框架比如Springtomcat等等一大堆,但是因为现在写的只算是一个小demo,暂时没必要学太深,所以只从字符串处理的角度来完成。

该部分需要三个方法,分别解析post消息体中的请求类型、用户名和密码,因为psot消息体大多使用JSON格式,键值对对应数据的格式,所以在字符串中,我们只需要找到其键值位置,往后加几位读取即可,比如indexOf("password")即可找到password关键字的第一个位置,然后设置start的值为indexof+11跳过其中的字段及报文格式信息,end结束字段为报文末尾,去掉格式两个字符,即length()-2,最后通过String password = requestLine.substring(start, end);截取报文内容即可获得密码。

类似的,三个字段的获取代码如下:

	//获取请求类型
	public static String getPosttype(String requestLine) {
        //加14位因为还有request_type":
        int start =requestLine.indexOf("request_type")+14;
        System.out.println("开始位置为:"+start);
        //只读一位即可
        int end =start+1;
        System.out.println("结束位置为:"+end);
        String posttype = requestLine.substring(start, end);
        System.out.println("获取到的post类型为:" + posttype);
        return posttype;
    }
    //获取用户名
    public static String getUsername(String requestLine) {
        //加11位因为还有“和:
        int start =requestLine.indexOf("username")+11;
        System.out.println("开始位置为:"+start);
        //减一位因为还有,
        int end =requestLine.indexOf("password")-3;
        System.out.println("结束位置为:"+end);
        String username = requestLine.substring(start, end);

        System.out.println("获取到的用户名为:" + username);
        return username;
    }
    //获取密码
    public static String getPassword(String requestLine) {
        //加3位因为":"
        int start =requestLine.indexOf("password")+11;
        //减2位因为还有“}
        int end =requestLine.length()-2;

        String password = requestLine.substring(start, end);
        System.out.println("获取到的密码为:" + password);
        return password;
    }

其中获取密码方法可以直接用于获取记录,只需要服务端发包时将记录放在密码参数中即可。

报文响应

接收消息并能识别请求信息后,要根据请求内容对其进行响应,同样返回一个http报文,该步骤借助OutputStream输出流实现,输入输出流基于socket套接字,自动绑定端口进行监听回应,构造构造消息后调用输出流的wirte方法即可实现报文响应,具体代码如下:

    public static boolean Response(OutputStream resp,String body){
        StringBuilder response = new StringBuilder();
        // 构造响应消息头
        response.append("HTTP/1.1 ").append(200).append("\r\n")
                .append("Content-Type: text/plain\r\n")
                .append("Content-Length: ").append(body.length()).append("\r\n")
                .append("\r\n");
        try{// 开始响应
            resp.write(response.toString().getBytes());
            resp.write(body.getBytes());
            return true;
        }
        catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

响应消息构建

该函数负责对不同的请求信息和请求内容进行消息响应,主要是http报文格式的构建和发送过程,具体消息体目前很简单,只要返回通过pass或是拒绝reject两种报文即可。

具体实现流程为,构建http报文头,获取消息体body,在输出流write函数中写入,具体代码如下:

    public static boolean Response(OutputStream resp,String body){
        StringBuilder response = new StringBuilder();
        // 构造响应消息头
        response.append("HTTP/1.1 ").append(200).append("\r\n")
                .append("Content-Type: text/plain\r\n")
                .append("Content-Length: ").append(body.length()).append("\r\n")
                .append("\r\n");
        try{// 开始响应
            resp.write(response.toString().getBytes());
            resp.write(body.getBytes());
            return true;
        }
        catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

应用中,检查用户名密码或注册成功状态后,分别给消息体body中填充不同的字段即可,过程代码如下:

           if(posttype.equals("1")){//登陆逻辑
                System.out.println("请求类型为:登录");

                //判断用户名和密码是否正确
                //正确返回用户记录,错误返回reject
                if( test.check_user(t,username,password)){
                    Response(resp,record);
                }
                else{
                    Response(resp,"reject");
                }
            }
            else if(posttype.equals("2")) {//注册逻辑
                System.out.println("请求类型为:注册");
                if( test.add_user(t,username,password)){
                    Response(resp,"pass");
                    System.out.println("注册成功");
                }else{
                    Response(resp,"reject");
                }
            }
            else if(posttype.equals("3")){
                System.out.println("请求类型为:更新");
                if(test.update_userRecord(t,username,password)){
                        Response(resp,"pass");
                        System.out.println("更新记录成功");
                }
            }
            else{
                System.out.println("请求类型为:其他");
            }

总结

该项目断断续续也算搞了小两周,费事的主要难点在于:
1,前端语法不熟悉。构建框架,组件使用,几乎每一个都要上网查一下才能确定,这导致进展缓慢。
2,实际应用场景复杂。网络通信相互依赖时的同步处理,这是我印象最深,卡的最久的,另外数据库操作是否立即提交,页面跳转传递信息等,这都是很小很细节的点,但如果考虑不到都会成为一个坑。

目前想到后续的改进工作如下:
1,手动编写的登陆验证可以直接拉起微信接口,更贴近实际。
2,后端数据更多的展示,好友排行榜等提示信息,增加社交属性。
3,游戏方案优化,增加更多动画,子弹和敌机,丰富场景。

虽然项目还很稚嫩,但对我来说作为一个打通前后端交互的小demo还是学到了很多,前端页面大致的build过程,报文传递与解析,还有同步化,第一次写前端,如果以后不需要从事该岗位的话,了解到此应该也足够了吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值