學號 20175313 《實驗五 網絡編程與安全》實驗報告番外篇

1、成果展現

碼雲連接java

  • 實現帶括號的四則運算
    android

  • 能夠輸入小數(".")、正負數(按"±"鍵)、對結果開根("√")、輸入上次計算的結果("Mr")、對算式和結果清零("CE")、退格("←")
    git

  • 處理異常,包括算式格式錯誤,缺失左右括號,除0錯誤等
    算法

  • 切換有理數模式和分數模式
    數據庫

  • 登陸、註冊(還沒有完成後端鏈接數據庫)
    express

2、實驗內容及步驟

一、需求分析 & 頁面佈局

頁面佈局參考iPhone自帶的計算器,可是要實現括號按鈕,發現排不成好看的矩形。。因而多加了MR和開根的功能。
考慮到要知足有理數計算和分數計算,因此設計一個菜單來切換模式。同時分數的計算沒法處理浮點數,正好將小數點鍵改成/
順便作個登陸功能,計劃只有用戶成功登陸之後才能使用分數模式,目前還沒有完成。小程序

綜上,須要三個Activity,MainActivity實現計算器,LoginActivity實現登陸,RegisterActivity實現註冊。重點是MainActivity後端

清單文件以下:服務器

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="cn.edu.besti.is.onlinecalculator">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".LoginActivity"
            android:label="@string/login_screen_title"
            android:parentActivityName=".MainActivity">//ActionBar出現返回鍵,設置上一級界面
        </activity>
        <activity android:name=".RegisterActivity"
            android:label="註冊"
            android:parentActivityName=".LoginActivity">
        </activity>
    </application>
    <uses-permission android:name="android.permission.INTERNET" />//容許該應用程序連接網絡
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

</manifest>

佈局文件activity_main.xml以下:

<?xml version="1.0" encoding="utf-8"?>
<GridLayout
    xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:useDefaultMargins="false"
    android:alignmentMode="alignBounds"
    android:columnOrderPreserved="false"
    android:layout_gravity="center_horizontal"
    android:background="#111"
    android:columnCount="4"
    android:rowCount="7"
    >

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="200sp"
        android:layout_row="0"
        android:layout_column="3">

        <TextView
            android:id="@+id/textView1"
            android:layout_width="match_parent"
            android:ellipsize="start"
            android:singleLine="true"
            android:gravity="center|start"
            android:layout_height="90sp"
            android:layout_gravity="center_horizontal"
            android:background="#111"
            android:text=""
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:textColor="#fff"
            android:textSize="45sp" />

        <TextView
            android:id="@+id/textView2"
            android:layout_width="match_parent"
            android:layout_height="110sp"
            android:layout_gravity="bottom"
            android:gravity="end|center"
            android:background="#000"
            android:text=""
            android:singleLine="true"
            android:textColor="#fff"
            android:textSize="60sp" />
    </FrameLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_row="1"
        android:layout_column="3"
        android:layout_gravity="top"
        android:orientation="horizontal"
        >
        
        <Button
            android:id="@+id/button1_1"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:layout_margin="3sp"
            
            android:textSize="45sp"
            android:text="CE"
            android:background="@drawable/button_style1"
            />


        <Button
            android:id="@+id/button1_2"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:layout_margin="3sp"

            android:textSize="45sp"
            android:background="@drawable/button_style1"
            android:text="±" />

        <Button
            android:id="@+id/button1_3"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:layout_margin="3sp"

            android:textSize="45sp"
            android:background="@drawable/button_style1"
            android:text="←" />

        <Button
            android:id="@+id/button1_4"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:layout_margin="3sp"

            android:textSize="45sp"
            android:textColor="#fff"
            android:background="@drawable/button_style2"
            android:text="√" />


    </LinearLayout>

    ···

</GridLayout>

使用GridLayout配合LinearLayout和FrameLayout,FrameLayout包含兩個TextView,分別是用戶輸入的表達式和計算的結果。
每一個LinearLayout表明一行按鈕,不一樣的按鈕設置不同的樣式,以button_style1.xml爲例:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:state_pressed="true">//按下時的樣式
        <shape android:shape="rectangle">//圓角按鈕
            <solid android:color="#eee"/>//顏色
            <corners android:radius="8dip"/>//圓角程度
        </shape>
    </item>

    <item android:state_pressed="false">//鬆開時的樣式
        <shape android:shape="rectangle">
            <solid android:color="#bbb"/>
            <corners android:radius="8dip"/>
        </shape>
    </item>

</selector>

主界面效果以下

其餘頁面的佈局見碼雲連接

二、MainActivity

package cn.edu.besti.is.onlinecalculator;

import android.content.Intent;
import android.os.StrictMode;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import java.util.LinkedList;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button[] buttons = new Button[23];
    private int[] ids = new int[]{
            R.id.button1_1, R.id.button1_2, R.id.button1_3, R.id.button1_4,
            R.id.button2_1, R.id.button2_2, R.id.button2_3, R.id.button2_4,
            R.id.button3_1, R.id.button3_2, R.id.button3_3, R.id.button3_4,
            R.id.button4_1, R.id.button4_2, R.id.button4_3, R.id.button4_4,
            R.id.button5_1, R.id.button5_2, R.id.button5_3, R.id.button5_4,
            R.id.button6_1, R.id.button6_2, R.id.button6_3
    };
    private TextView textView1, textView2;
    private String result = "0";
    private LinkedList<String> expr = new LinkedList<>();
    private String Mod = "Rational";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().detectNetwork().penaltyLog().build());
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects().detectLeakedClosableObjects().penaltyLog().penaltyDeath().build());
        for (int i = 0; i < ids.length; i++) {
            buttons[i] = findViewById(ids[i]);
            buttons[i].setOnClickListener(this);
        }
        this.textView1 = findViewById(R.id.textView1);
        this.textView2 = findViewById(R.id.textView2);
    }

    //onClick方法處理各類點擊事件
    @Override
    public void onClick(View view) {
        int id = view.getId();
        Button button = view.findViewById(id);
        String current = button.getText().toString();
        String token;
        StringBuilder expression = new StringBuilder();
        if (current.equals("CE")) {
            expr.clear();
            result = "0";
        } else if (current.equals("±")) {
            if (!expr.isEmpty()) {
                token = expr.pollLast();
                if (!calcArithmatic.isOperator(token)) {
                    if (token.contains("-")) {
                        token = token.replaceAll("-", "");
                    } else {
                        token = "-" + token;
                    }
                }
                expr.offerLast(token);
            }
        } else if (current.equals("←")) {
            expr.pollLast();
        } else if (current.equals(".") || current.equals("/")) {
            if (!expr.isEmpty()) {
                token = expr.pollLast();
                if (!calcArithmatic.isOperator(token)) {
                    if (!token.contains(current)) {
                        token += current;
                    }
                }
                expr.offerLast(token);
            }
        } else if (current.equals("=")) {//按下等號時,在本地將中綴表達式轉爲後綴表達式,傳輸給服務端,接收服務器的計算結果
            if (!expr.isEmpty()) {
                for (String s : expr) {
                    expression.append(" ").append(s);
                }
                try {
                    MyBC myBC = new MyBC();
                    final String formula = myBC.getEquation(expression.toString().trim());
                    try {
                        result = Client.Connect(formula, Mod);
                    } catch (Exception e) {
                        Toast.makeText(this, "請檢查網絡鏈接", Toast.LENGTH_SHORT).show();
                    }
                } catch (ExprFormatException e) {
                    result = e.getMessage();
                } catch (ArithmeticException e0) {
                    result = "Divide Zero Error";
                } finally {
                    expr.clear();
                }
            }
        } else if (current.equals("√")) {
            if (Mod.equals("Rational")) {
                result = String.valueOf(Math.sqrt(Double.parseDouble(result)));
            }
        } else if (current.equals("Mr")) {
            if (result.matches("[0-9.\\-/]+")) {
                current = result;
                expr.offerLast(current);
            }
        } else if (calcArithmatic.isOperator(current)) {
            expr.offerLast(current);
        } else {
            if (!expr.isEmpty()) {
                token = expr.pollLast();
                if (calcArithmatic.isOperator(token)) {
                    expr.offerLast(token);
                    expr.offer(current);
                } else {
                    token += current;
                    expr.offerLast(token);
                }
            } else {
                expr.offerLast(current);
            }
        }
        for (String s : expr) {
            expression.append(" ").append(s);
        }
        textView1.setText(expression.toString().trim());
        textView2.setText(result);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }


    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.option1:
                Intent intent = new Intent(this, LoginActivity.class);
                startActivity(intent);
                return true;
            case R.id.option2:
                expr.clear();
                result = "";
                if (item.getTitle().equals("分數模式")) {
                    buttons[21].setText("/");
                    item.setTitle("有理數模式");
                    Mod = "Fraction";
                } else {
                    buttons[21].setText(".");
                    item.setTitle("分數模式");
                    Mod = "Rational";
                }
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }
}
  • 使用private LinkedList<String> expr = new LinkedList<>();來處理用戶的每次點擊形成的輸入,方便將數字和操做符分開,若是隊尾元素是數字或小數點,而當前值是數字或小數點,則對隊尾元素進行字符串拼接;若是隊尾元素是操做符,就直接入隊。最後計算結果的時候,依次拼接隊中元素,造成中綴表達式。

  • 輸入等號後以下鏈接到客戶端,捕獲不一樣異常來輸出不一樣結果,若是網絡鏈接超時,彈出Toast提示
try {
    MyBC myBC = new MyBC();
    final String formula = myBC.getEquation(expression.toString().trim());
    try {
            result = Client.Connect(formula, Mod);
        } catch (Exception e) {
            Toast.makeText(this, "請檢查網絡鏈接",Toast.LENGTH_SHORT).show();
        }
    } catch (ExprFormatException e) {
        result = e.getMessage();
    } catch (ArithmeticException e0) {
        result = "Divide Zero Error";
    } finally {
        expr.clear();
    }
}

正常來講進行網絡請求必須在線程中進行,可是由於咱們只是一個小程序,阻塞一下沒有什麼問題,因此我就直接在主進程裏面發送請求,需在MainActivity裏面加上以下代碼

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().detectNetwork().penaltyLog().build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects().detectLeakedClosableObjects().penaltyLog().penaltyDeath().build());

同時Android應用默認不開啓網絡鏈接,要在清單文件裏聲明
<uses-permission android:name="android.permission.INTERNET" />

  • 如下代碼實現選擇菜單功能
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.option1:
            Intent intent = new Intent(this, LoginActivity.class);
            startActivity(intent);
            return true;
        case R.id.option2:
            expr.clear();
            result = "";
            if (item.getTitle().equals("分數模式")) {
                buttons[21].setText("/");
                item.setTitle("有理數模式");
                Mod = "Fraction";
            } else {
                buttons[21].setText(".");
                item.setTitle("分數模式");
                Mod = "Rational";
            }
            return true;
        default:
            return super.onOptionsItemSelected(item);
    }
}

點擊切換模式之後,僅僅是將"."按鈕的值改爲"/",由於在處理點擊事件的時候也是根據被點擊按鈕的值來決定行爲的。
同時切換模式後相應的改變菜單裏模式按鈕的文字。
menu.xml以下,app:showAsAction="never"決定該菜單按鈕的位置,never表明永遠摺疊在菜單中

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item android:id="@+id/option1"
        android:title="登陸"
        app:showAsAction="never" />

    <item android:id="@+id/option2"
        android:title="分數模式"
        app:showAsAction="never"/>
</menu>

三、Client類

個人搭檔定義了Client類來完成發送請求和收發數據,在這個過程當中進行加密傳輸。可是由於個人搭檔沒法解決文件權限的問題,因此我只能不通過文件來進行密鑰的傳輸。我在個人博客任務4的基礎上,修改代碼以下:

package cn.edu.besti.is.onlinecalculator;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import java.io.*;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.net.*;

public class Client {
    public static String Connect(String formula, String mod) throws Exception {
        String mode = "AES";
        Socket mysocket;
        DataInputStream in;
        DataOutputStream out;
        mysocket = new Socket();
        mysocket.connect(new InetSocketAddress("172.30.7.19", 2010),5000);
        mysocket.setSoTimeout(5000);
        in = new DataInputStream(mysocket.getInputStream());
        out = new DataOutputStream(mysocket.getOutputStream());
        //使用AES進行後綴表達式的加密
        KeyGenerator kg = KeyGenerator.getInstance(mode);
        kg.init(128);
        SecretKey k = kg.generateKey();//生成密鑰
        byte[] mkey = k.getEncoded();
        Cipher cp = Cipher.getInstance(mode);
        cp.init(Cipher.ENCRYPT_MODE, k);
        byte[] ptext = formula.getBytes("UTF8");
        byte[] ctext = cp.doFinal(ptext);

        //將加密後的後綴表達式傳送給服務器
        String out1 = B_H.parseByte2HexStr(ctext);
        out.writeUTF(out1);

        //建立客戶端DH算法公、私鑰
        KeyPair keyPair = Key_DH5_6.createPubAndPriKey();
        PublicKey pbk = keyPair.getPublic();//Client公鑰
        PrivateKey prk = keyPair.getPrivate();//Client私鑰

        //將公鑰傳給服務器
        byte[] cpbk = pbk.getEncoded();
        String CpubKey = B_H.parseByte2HexStr(cpbk);
        out.writeUTF(CpubKey);
        Thread.sleep(1000);

        //接收服務器公鑰
        String SpubKey = in.readUTF();
        byte[] spbk = H_B.parseHexStr2Byte(SpubKey);
        KeyFactory kf = KeyFactory.getInstance("DH");
        PublicKey serverPub = kf.generatePublic(new X509EncodedKeySpec(spbk));

        //生成共享信息,並生成AES密鑰
        SecretKeySpec key = KeyAgree5_6.createKey(serverPub, prk);

        //對加密後綴表達式的密鑰進行加密,並傳給服務器
        cp.init(Cipher.ENCRYPT_MODE, key);
        byte[] ckey = cp.doFinal(mkey);
        String Key = B_H.parseByte2HexStr(ckey);
        out.writeUTF(Key);

        out.writeUTF(mod);
        //接收服務器回答
        return in.readUTF();
    }
}

以下設置鏈接請求超時時間爲5秒
mysocket.connect(new InetSocketAddress("172.30.7.19", 2010),5000);
以下設置收發數據超時時間爲5秒
mysocket.setSoTimeout(5000);

四、服務器端

服務器端簡單的用Java實現,一樣在個人博客任務4的基礎上修改代碼以下:

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;

public class Server extends Thread {
    Socket socketOnServer;

    public Server(Socket socketOnServer) {
        super();
        this.socketOnServer = socketOnServer;
    }

    public static void main(String[] args) {
        ServerSocket serverForClient;
        try {
            serverForClient = new ServerSocket(2010);
            while (true) {
                System.out.println(currentThread()+"等待客戶呼叫:");
                Socket socketOnServer = serverForClient.accept();
                new Server(socketOnServer).start();
            }
        } catch (IOException e1) {
            System.out.println(e1.getMessage());
        }

    }

    @Override
    public void run() {
        String mode = "AES";

        DataOutputStream out = null;
        DataInputStream in = null;

        String result;
        try {
            out = new DataOutputStream(socketOnServer.getOutputStream());
            in = new DataInputStream(socketOnServer.getInputStream());

            //接收加密後的後綴表達式
            String cformula = in.readUTF();
            byte cipher[] = H_B.parseHexStr2Byte(cformula);

            //接收Client端公鑰
            String push = in.readUTF();
            byte np[] = H_B.parseHexStr2Byte(push);
            KeyFactory kf = KeyFactory.getInstance("DH");
            PublicKey ClientPub = kf.generatePublic(new X509EncodedKeySpec(np));

            //建立服務器DH算法公、私鑰
            KeyPair keyPair = Key_DH5_6.createPubAndPriKey();
            PublicKey pbk = keyPair.getPublic();//Server公鑰
            PrivateKey prk = keyPair.getPrivate();//Server私鑰

            //將服務器公鑰傳給Client端
            byte cpbk[] = pbk.getEncoded();
            String CpubKey = B_H.parseByte2HexStr(cpbk);
            out.writeUTF(CpubKey);
            Thread.sleep(1000);

            //生成共享信息,並生成AES密鑰
            SecretKeySpec key = KeyAgree5_6.createKey(ClientPub, prk);

            String k = in.readUTF();//讀取加密後密鑰
            byte[] encryptKey = H_B.parseHexStr2Byte(k);
            String mod = in.readUTF();
            //對加密後密鑰進行解密
            Cipher cp = Cipher.getInstance(mode);
            cp.init(Cipher.DECRYPT_MODE, key);
            byte decryptKey[] = cp.doFinal(encryptKey);

            //對密文進行解密
            SecretKeySpec plainkey = new SecretKeySpec(decryptKey, mode);
            cp.init(Cipher.DECRYPT_MODE, plainkey);
            byte[] plain = cp.doFinal(cipher);

            //計算後綴表達式結果
            String formula = new String(plain);
            MyDC myDC = new MyDC(mod);
            try {
                result = myDC.calculate(formula);
                //後綴表達式formula調用MyDC進行求值
            } catch (ExprFormatException e) {
                result = e.getMessage();
            } catch (ArithmeticException e0) {
                result = "Divide Zero Error";
            }
            //將計算結果傳給Client端
            out.writeUTF(result);
        } catch (Exception e) {
            System.out.println("客戶已斷開" + e);
        }
    }
}
  • 服務端必須使用多線程來防止阻塞
  • MyDC流程大體以下,真正在計算時會根據是有理數模式仍是分數模式使用不一樣的計算規則

五、登陸、註冊部分

要實現ActionBar出現返回鍵,在清單文件中相應的Activity下設置parentActivityName

<activity android:name=".LoginActivity"
    android:label="@string/login_screen_title"
    android:parentActivityName=".MainActivity">//ActionBar出現返回鍵,設置上一級界面
</activity>

3、遇到問題及解決

  • 問題1:測試時client嘗試鏈接127.0.0.1一直鏈接不上,服務端一直在等待客戶呼叫,沒有任何反應。
  • 問題1解決:說明根本就沒有向我主機發送請求,原來Android程序中嘗試鏈接localhost,程序會將Android手機做爲主機,固然連不到我服務端所在的電腦。應該將地址改成內網地址

  • 問題2:使用DH算法協商密鑰時須要寫入文件,可是Android虛擬手機沒有寫權限。。
  • 問題2解決(並無):沒有找到改權限的方法,因此只能直接傳輸密鑰,不通過文件。

4、碼雲連接

相關文章
相關標籤/搜索