zoukankan      html  css  js  c++  java
  • (转)如何用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. 

     
     
    分类: iOSPHP
  • 相关阅读:
    Delphi中WebBbrowser的编程 转
    博客园设置目录
    iTerm
    python
    谷歌浏览器插件的导出导入
    Chapter10 属性
    WPF之Binding
    ASP.NET 路由系统
    Silverlight中使用Application.GetResourceStream方法加载资源时得到的总是null
    基于IoC的ControllerFactory
  • 原文地址:https://www.cnblogs.com/huanglong/p/3117350.html
Copyright © 2011-2022 走看看