如何用PHP/MySQL為 iOS App 寫一個簡單的web服務器(譯) PART1


原文:http://www.raywenderlich.com/2941/how-to-write-a-simple-phpmysql-web-service-for-an-ios-app

  作為一個iPhone/iPad開發者,能夠自己寫一個簡單的web服務器將是很有用的。

  例如,你可能希望在軟件啟動時顯示一些來自服務器的更新,或者在服務器端保存一些用戶數據。除了你的想象力,沒有什么能限制你了。

  在第一篇中,我們將會一步一步的建立一個web服務器,基於promo code system(促銷碼系統),我在我的第一個軟件中使用的,Wild Fables.在第二篇中,我們將會寫一個iOS App來和它進行交互。

為了完成這個教程,你將需要一個web服務器,並裝有MySQL和PHP。如果你沒有,那么你有以下幾種選擇:

  • 如果你想在你的Mac(free)上運行Apache/MySQL/PHP,有很多教程可以幫你。這里有一個教程
  • 如果你想租一個服務器(需要花錢),這里有一個教程
  • 或者你很懶,以上兩種你都不想做,那么你可以使用我在本教程PART2做的服務器。

你不需要有PHP和MySQL的經驗(當然有更好)因為這個教程包含了所有你需要的代碼。

 

你將做什么

也許你已經知道了,如果為你的App添加了內購功能,蘋果並沒有提供內置的系統來提供內購的促銷碼。

然而,建立你自己的內購促銷碼將會很有用。

如果你不需要建立這個特殊的系統也沒關系,你會學到怎么建立web服務器並與App交互。

建立數據庫:

第一步時建立一個數據庫表。這個教程你需要3個表:

  • rw_app:保存需要使用促銷碼系統的軟件列表。這樣,你就可以為不同的App使用相同的表


    id
: app的唯一標示.

    app_id:  app 的唯一字符串標示.

  • w_promo_code:保存可用促銷碼的表
    •   id:唯一表示.
    •   rw_app_id: 對應的App.
    •   code: 用戶輸入的促銷碼字符.
    •   unlock_code: 返回給App的促銷碼字符.
    •   uses_remaining:促銷碼剩余可使用次數.
  • rw_promo_code_redeemed:保存促銷碼兌取后的信息。為了防止一個設備用一個促銷碼兌取多次。
    • id: 唯一標示.
    • rw_promo_code_id: 已經兌取的促銷碼ID (from rw_promo_code).
    • device_id: 已經兌取的設備ID.
    • redeemed_time: 兌取的時間.

這是建表的SQL代碼:

DROP TABLE IF EXISTS rw_promo_code;
DROP TABLE IF EXISTS rw_app;
DROP TABLE IF EXISTS rw_promo_code_redeemed;

CREATE TABLE rw_promo_code (
    id mediumint NOT NULL AUTO_INCREMENT PRIMARY KEY,    
    rw_app_id tinyint NOT NULL, 
    code varchar(255) NOT NULL,
    unlock_code varchar(255) NOT NULL,
    uses_remaining smallint NOT NULL
);

CREATE TABLE rw_app (
    id mediumint NOT NULL AUTO_INCREMENT PRIMARY KEY,    
    app_id varchar(255) NOT NULL
);

CREATE TABLE rw_promo_code_redeemed (
    id mediumint NOT NULL AUTO_INCREMENT PRIMARY KEY,    
    rw_promo_code_id mediumint NOT NULL,
    device_id varchar(255) NOT NULL,
    redeemed_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

在你的web服務器上,你需要建立一個MySQL數據庫並建立這三張表。這里是完成的命令:

保存上面的代碼到一個名為create.sql的文件,然后:

rwenderlich@kermit:~$ mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1286
Server version: 5.1.37-1ubuntu5.1-log (Ubuntu)

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> create database promos;
Query OK, 1 row affected (0.00 sec)

mysql> use promos;
Database changed
mysql> grant all privileges on promos.* to 'username'@'localhost' identified by 'password';
Query OK, 0 rows affected (0.00 sec)

mysql> exit
Bye

rwenderlich@kermit:~$ mysql -u username -p promos < create.sql
Enter password: 
rwenderlich@kermit:~$ mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1417
Server version: 5.1.37-1ubuntu5.1-log (Ubuntu)

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use promos;
Database changed
mysql> show tables ;
+------------------------+
| Tables_in_promos       |
+------------------------+
| rw_app                 | 
| rw_promo_code          | 
| rw_promo_code_redeemed | 
+------------------------+
3 rows in set (0.00 sec)

現在已有了三張空表。下一步,建立一個測試的app:

INSERT INTO rw_app VALUES(1, 'com.razeware.test');
INSERT INTO rw_promo_code VALUES(1, 1, 'test', 'com.razeware.test.unlock.cake', 10000);

好的。現在數據庫已經連接,可以寫PHP服務器了。

驗證PHP/MySQL

在開始實現PHP服務器之前,首先檢查PHP是否在你的服務器上運行正常。在你的服務器上建立一個叫promos的文件夾,在里面建立一個叫index.php的文件:

<?php
 
class RedeemAPI {
    // Main method to redeem a code
    function redeem() {
        echo "Hello, PHP!";
    }
}
 
// This is the first thing that gets called when this page is loaded
// Creates a new instance of the RedeemAPI class and calls the redeem method
$api = new RedeemAPI;
$api->redeem();
 
?>

你可以用你的服務器的URL測試,也可以像下面這樣在命令行測試:

Ray-Wenderlichs-Mac-mini-2:~ rwenderlich$ curl http://www.wildfables.com/promos/
Hello, PHP!

下一步,擴展這個類,確保你的服務器可以連接到數據庫:

class RedeemAPI {
    private $db;
 
    // Constructor - open DB connection
    function __construct() {
        $this->db = new mysqli('localhost', 'username', 'password', 'promos');
        $this->db->autocommit(FALSE);
    }
 
    // Destructor - close DB connection
    function __destruct() {
        $this->db->close();
    }
 
    // Main method to redeem a code
    function redeem() {
        // Print all codes in database
        $stmt = $this->db->prepare('SELECT id, code, unlock_code, uses_remaining FROM rw_promo_code');
        $stmt->execute();
        $stmt->bind_result($id, $code, $unlock_code, $uses_remaining);
        while ($stmt->fetch()) {
            echo "$code has $uses_remaining uses remaining!";
        }
        $stmt->close();
    }
}

這里添加了一個構造函數來連接給定用戶名和密碼的數據庫,一個 析構函數來關閉數據庫。現在你可以測試一下了:

Ray-Wenderlichs-Mac-mini-2:~ rwenderlich$ curl http://www.wildfables.com/promos/
test has 10000 uses remaining!

服務器策略:GET還是POST:

 好的,現在是時候來實現完成的功能了。但首先,讓我們來談談web服務器的策略。

我們知道我們需要向服務器發送一些數據,包括app的ID,兌換嗎,要兌換的設備ID。

如何發送呢?有兩種方法:GET(普通方法)和POST(用於發送表單)

  • 如果你選擇GET,那么參數是URL的一部分,就是把參數發到URL里,然后向服務器發送請求。
  • 如果你選擇POST,參數被放到request body中

每個都能滿足你的需求,但是當你要嘗試做些什么的時候,比如兌換促銷碼,還是用POST比較好。這也是我將要做的。

這是什么意思呢?如果我們想在PHP中訪問這些參數,我們可以通過內建的$_POST 數組:

$_POST["rw_app_id"]

我們將會用ASIHTTPRequest來連接服務器,用ASIFormDataRequest類發送一個POST請求:

ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:@"1" forKey:@"rw_app_id"];

更多GET和POST的信息,請看Wikipedia entry

更新:請看@smpdawg’s的精彩評論forum topic

 

web服務器策略:算法 

下一步,我們要看看將要使用的web服務器的算法:

  1. 確保需要的參數是通過POST發送的。
  2. 確保數據庫里有兌換碼。
  3. 確保兌換碼剩余可使用次數。
  4. 確保設備沒有使用過兌換碼。
  5. 到這一步,就要完成了
    • 添加一個rw_promo_code_redeemed的入口來記錄兌換
    • 將在rw_promo_code的uses_remaining減去一
    • 返回unlock_code給App。

web服務器的實現

 首先,添加一個輔助方法用於返回HTTP狀態信息:

// Helper method to get a string description for an HTTP status code
// From http://www.gen-x-design.com/archives/create-a-rest-api-with-php/ 
function getStatusCodeMessage($status)
{
    // these could be stored in a .ini file and loaded
    // via parse_ini_file()... however, this will suffice
    // for an example
    $codes = Array(
        100 => 'Continue',
        101 => 'Switching Protocols',
        200 => 'OK',
        201 => 'Created',
        202 => 'Accepted',
        203 => 'Non-Authoritative Information',
        204 => 'No Content',
        205 => 'Reset Content',
        206 => 'Partial Content',
        300 => 'Multiple Choices',
        301 => 'Moved Permanently',
        302 => 'Found',
        303 => 'See Other',
        304 => 'Not Modified',
        305 => 'Use Proxy',
        306 => '(Unused)',
        307 => 'Temporary Redirect',
        400 => 'Bad Request',
        401 => 'Unauthorized',
        402 => 'Payment Required',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allowed',
        406 => 'Not Acceptable',
        407 => 'Proxy Authentication Required',
        408 => 'Request Timeout',
        409 => 'Conflict',
        410 => 'Gone',
        411 => 'Length Required',
        412 => 'Precondition Failed',
        413 => 'Request Entity Too Large',
        414 => 'Request-URI Too Long',
        415 => 'Unsupported Media Type',
        416 => 'Requested Range Not Satisfiable',
        417 => 'Expectation Failed',
        500 => 'Internal Server Error',
        501 => 'Not Implemented',
        502 => 'Bad Gateway',
        503 => 'Service Unavailable',
        504 => 'Gateway Timeout',
        505 => 'HTTP Version Not Supported'
    );
 
    return (isset($codes[$status])) ? $codes[$status] : '';
}
 
// Helper method to send a HTTP response code/message
function sendResponse($status = 200, $body = '', $content_type = 'text/html')
{
    $status_header = 'HTTP/1.1 ' . $status . ' ' . getStatusCodeMessage($status);
    header($status_header);
    header('Content-type: ' . $content_type);
    echo $body;
}

如果你不理解為什么我們不要這個,那是因為這是一個遵守HTTP協議的web服務器,當你發送一個相應你可以制定一個包含錯誤碼和詳細描述的頭。有標准錯誤碼可以用,這些方法不過是用起來更方便。

正如你看到的,我防線了一個可以把狀態嗎轉換成HTML信息的教程

下一步,就是真正的實現了!

function redeem() {
 
    // Check for required parameters
    if (isset($_POST["rw_app_id"]) && isset($_POST["code"]) && isset($_POST["device_id"])) {
 
        // Put parameters into local variables
        $rw_app_id = $_POST["rw_app_id"];
        $code = $_POST["code"];
        $device_id = $_POST["device_id"];
 
        // Look up code in database
        $user_id = 0;
        $stmt = $this->db->prepare('SELECT id, unlock_code, uses_remaining FROM rw_promo_code WHERE rw_app_id=? AND code=?');
        $stmt->bind_param("is", $rw_app_id, $code);
        $stmt->execute();
        $stmt->bind_result($id, $unlock_code, $uses_remaining);
        while ($stmt->fetch()) {
            break;
        }
        $stmt->close();
 
        // Bail if code doesn't exist
        if ($id <= 0) {
            sendResponse(400, 'Invalid code');
            return false;
        }
 
        // Bail if code already used        
        if ($uses_remaining <= 0) {
            sendResponse(403, 'Code already used');
            return false;
        }    
 
        // Check to see if this device already redeemed    
        $stmt = $this->db->prepare('SELECT id FROM rw_promo_code_redeemed WHERE device_id=? AND rw_promo_code_id=?');
        $stmt->bind_param("si", $device_id, $id);
        $stmt->execute();
        $stmt->bind_result($redeemed_id);
        while ($stmt->fetch()) {
            break;
        }
        $stmt->close();
 
        // Bail if code already redeemed
        if ($redeemed_id > 0) {
            sendResponse(403, 'Code already used');
            return false;
        }
 
        // Add tracking of redemption
        $stmt = $this->db->prepare("INSERT INTO rw_promo_code_redeemed (rw_promo_code_id, device_id) VALUES (?, ?)");
        $stmt->bind_param("is", $id, $device_id);
        $stmt->execute();
        $stmt->close();
 
        // Decrement use of code
        $this->db->query("UPDATE rw_promo_code SET uses_remaining=uses_remaining-1 WHERE id=$id");
        $this->db->commit();
 
        // Return unlock code, encoded with JSON
        $result = array(
            "unlock_code" => $unlock_code,
        );
        sendResponse(200, json_encode($result));
        return true;
    }
    sendResponse(400, 'Invalid request');
    return false;
 
}

你應該能夠讀懂這段代碼,否則的話查看以下這個教程Mysqli reference。這里有一些事情我需要指出:

  • isset是一個用於檢測變量是否已經設置了的PHP函數。我們這里用它來確保所有需要的POST參數都發送了。
  • 注意我們沒有自己把傳進來的變量放到SQL語句中,而是使用bind_param方法。這是更安全的方法,否則你可能使自己易受SQL injection的攻擊。
  • 注意 unlock_code 用JSON返回。我們當然可以直接用字符串返回因為我們只返回一個信息,但是用JSON便於以后擴展。

現在,你的web服務器就已經可以工作了。你可以用下面命令來測試:

curl -F "rw_app_id=1" -F "code=test" -F "device_id=test" http://www.wildfables.com/promos/
{"unlock_code":"com.razeware.wildfables.unlock.test"}

注意,如果你在我的服務器上測試,如果你得到“code already used”的錯誤,你應該更改你的device_id。

你可能希望進入你的數據庫看看那里是否有一個rw_promo_code_redeemed的入口,uses_remaining是否減一等等。

下一步?

這里是源碼source code

敬請期待PART2. 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM