原文: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。如果你没有,那么你有以下几种选择:
你不需要有PHP和MySQL的经验(当然有更好)因为这个教程包含了所有你需要的代码。
你将做什么
也许你已经知道了,如果为你的App添加了内购功能,苹果并没有提供内置的系统来提供内购的促销码。
然而,建立你自己的内购促销码将会很有用。
如果你不需要建立这个特殊的系统也没关系,你会学到怎么建立web服务器并与App交互。
建立数据库:
第一步时建立一个数据库表。这个教程你需要3个表:
id: app的唯一标示.
app_id: app 的唯一字符串标示.
这是建表的SQL代码:
DROP TABLE <span style="color: #0000ff;">IF</span><span style="color: #000000;"> EXISTS rw_promo_code; DROP TABLE </span><span style="color: #0000ff;">IF</span><span style="color: #000000;"> EXISTS rw_app; DROP TABLE </span><span style="color: #0000ff;">IF</span><span style="color: #000000;"> EXISTS rw_promo_code_redeemed; CREATE TABLE rw_promo_code ( id mediumint NOT </span><span style="color: #0000ff;">NULL</span> AUTO_INCREMENT PRIMARY <span style="color: #008080;">KEY</span>,<span style="color: #000000;"> rw_app_id tinyint NOT </span><span style="color: #0000ff;">NULL</span>,<span style="color: #000000;"> code varchar(</span>255) NOT <span style="color: #0000ff;">NULL</span>,<span style="color: #000000;"> unlock_code varchar(</span>255) NOT <span style="color: #0000ff;">NULL</span>,<span style="color: #000000;"> uses_remaining smallint NOT </span><span style="color: #0000ff;">NULL</span><span style="color: #000000;"> ); CREATE TABLE rw_app ( id mediumint NOT </span><span style="color: #0000ff;">NULL</span> AUTO_INCREMENT PRIMARY <span style="color: #008080;">KEY</span>,<span style="color: #000000;"> app_id varchar(</span>255) NOT <span style="color: #0000ff;">NULL</span><span style="color: #000000;"> ); CREATE TABLE rw_promo_code_redeemed ( id mediumint NOT </span><span style="color: #0000ff;">NULL</span> AUTO_INCREMENT PRIMARY <span style="color: #008080;">KEY</span>,<span style="color: #000000;"> rw_promo_code_id mediumint NOT </span><span style="color: #0000ff;">NULL</span>,<span style="color: #000000;"> device_id varchar(</span>255) NOT <span style="color: #0000ff;">NULL</span>,<span style="color: #000000;"> redeemed_time TIMESTAMP </span><span style="color: #0000ff;">DEFAULT</span><span style="color: #000000;"> CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );</span>
在你的web服务器上,你需要建立一个MySQL数据库并建立这三张表。这里是完成的命令:
保存上面的代码到一个名为create.sql的文件,然后:
rwenderlich@kermit:~$ <span style="color: #008080;">mysql</span> -u root -<span style="color: #000000;">p Enter password</span>:<span style="color: #000000;"> Welcome to the </span><span style="color: #008080;">MySQL</span> monitor. Commands <span style="color: #008080;">end</span> with ; or \g.<span style="color: #000000;"> Your </span><span style="color: #008080;">MySQL</span> connection id is 1286<span style="color: #000000;"> Server version</span>: 5.1.37-1ubuntu5.1-<span style="color: #008080;">log</span><span style="color: #000000;"> (Ubuntu) Type </span>'help;' or '\h' <span style="color: #0000ff;">for</span> help. Type '\c' to clear the <span style="color: #008080;">current</span> input statement. <span style="color: #008080;">mysql</span>><span style="color: #000000;"> create database promos; Query OK</span>, 1 row affected (0.00<span style="color: #000000;"> sec) </span><span style="color: #008080;">mysql</span>> <span style="color: #0000ff;">use</span><span style="color: #000000;"> promos; Database changed </span><span style="color: #008080;">mysql</span>> grant all privileges on promos.* to 'username'@'localhost' identified by 'password'<span style="color: #000000;">; Query OK</span>, 0 rows affected (0.00<span style="color: #000000;"> sec) </span><span style="color: #008080;">mysql</span>> <span style="color: #0000ff;">exit</span><span style="color: #000000;"> Bye rwenderlich@kermit</span>:~$ <span style="color: #008080;">mysql</span> -u username -p promos sql Enter password:<span style="color: #000000;"> rwenderlich@kermit</span>:~$ <span style="color: #008080;">mysql</span> -u root -<span style="color: #000000;">p Enter password</span>:<span style="color: #000000;"> Welcome to the </span><span style="color: #008080;">MySQL</span> monitor. Commands <span style="color: #008080;">end</span> with ; or \g.<span style="color: #000000;"> Your </span><span style="color: #008080;">MySQL</span> connection id is 1417<span style="color: #000000;"> Server version</span>: 5.1.37-1ubuntu5.1-<span style="color: #008080;">log</span><span style="color: #000000;"> (Ubuntu) Type </span>'help;' or '\h' <span style="color: #0000ff;">for</span> help. Type '\c' to clear the <span style="color: #008080;">current</span> input statement. <span style="color: #008080;">mysql</span>> <span style="color: #0000ff;">use</span><span style="color: #000000;"> promos; Database changed </span><span style="color: #008080;">mysql</span>><span style="color: #000000;"> show tables ; </span>+------------------------+ | 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'<span style="color: #000000;">); INSERT INTO rw_promo_code VALUES(</span>1, 1, 'test', 'com.razeware.test.unlock.cake', 10000);
好的。现在数据库已经连接,可以写PHP服务器了。
验证PHP/MySQL
在开始实现PHP服务器之前,首先检查PHP是否在你的服务器上运行正常。在你的服务器上建立一个叫promos的文件夹,在里面建立一个叫index.php的文件:
<span style="color: #000000;">php </span><span style="color: #0000ff;">class</span><span style="color: #000000;"> RedeemAPI { </span><span style="color: #008000;">//</span><span style="color: #008000;"> Main method to redeem a code</span> <span style="color: #0000ff;">function</span><span style="color: #000000;"> redeem() { </span><span style="color: #0000ff;">echo</span> "Hello, PHP!"<span style="color: #000000;">; } } </span><span style="color: #008000;">//</span><span style="color: #008000;"> 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</span> <span style="color: #800080;">$api</span> = <span style="color: #0000ff;">new</span><span style="color: #000000;"> RedeemAPI; </span><span style="color: #800080;">$api</span>-><span style="color: #000000;">redeem(); </span>?>
你可以用你的服务器的URL测试,也可以像下面这样在命令行测试:
Ray-Wenderlichs-Mac-mini-2:~ rwenderlich$ curl http:<span style="color: #008000;">//</span><span style="color: #008000;">www.wildfables.com/promos/</span> Hello, PHP!
下一步,扩展这个类,确保你的服务器可以连接到数据库:
<span style="color: #0000ff;">class</span><span style="color: #000000;"> RedeemAPI { </span><span style="color: #0000ff;">private</span> <span style="color: #800080;">$db</span><span style="color: #000000;">; </span><span style="color: #008000;">//</span><span style="color: #008000;"> Constructor - open DB connection</span> <span style="color: #0000ff;">function</span><span style="color: #000000;"> __construct() { </span><span style="color: #800080;">$this</span>->db = <span style="color: #0000ff;">new</span> mysqli('localhost', 'username', 'password', 'promos'<span style="color: #000000;">); </span><span style="color: #800080;">$this</span>->db->autocommit(<span style="color: #0000ff;">FALSE</span><span style="color: #000000;">); } </span><span style="color: #008000;">//</span><span style="color: #008000;"> Destructor - close DB connection</span> <span style="color: #0000ff;">function</span><span style="color: #000000;"> __destruct() { </span><span style="color: #800080;">$this</span>->db-><span style="color: #000000;">close(); } </span><span style="color: #008000;">//</span><span style="color: #008000;"> Main method to redeem a code</span> <span style="color: #0000ff;">function</span><span style="color: #000000;"> redeem() { </span><span style="color: #008000;">//</span><span style="color: #008000;"> Print all codes in database</span> <span style="color: #800080;">$stmt</span> = <span style="color: #800080;">$this</span>->db->prepare('SELECT id, code, unlock_code, uses_remaining FROM rw_promo_code'<span style="color: #000000;">); </span><span style="color: #800080;">$stmt</span>-><span style="color: #000000;">execute(); </span><span style="color: #800080;">$stmt</span>->bind_result(<span style="color: #800080;">$id</span>, <span style="color: #800080;">$code</span>, <span style="color: #800080;">$unlock_code</span>, <span style="color: #800080;">$uses_remaining</span><span style="color: #000000;">); </span><span style="color: #0000ff;">while</span> (<span style="color: #800080;">$stmt</span>-><span style="color: #000000;">fetch()) { </span><span style="color: #0000ff;">echo</span> "<span style="color: #800080;">$code</span> has <span style="color: #800080;">$uses_remaining</span> uses remaining!"<span style="color: #000000;">; } </span><span style="color: #800080;">$stmt</span>-><span style="color: #000000;">close(); } }</span>
这里添加了一个构造函数来连接给定用户名和密码的数据库,一个 析构函数来关闭数据库。现在你可以测试一下了:
Ray-Wenderlichs-Mac-mini-2:~ rwenderlich$ curl http:<span style="color: #008000;">//</span><span style="color: #008000;">www.wildfables.com/promos/</span> test has 10000 uses remaining!
服务器策略:GET还是POST:
好的,现在是时候来实现完成的功能了。但首先,让我们来谈谈web服务器的策略。
我们知道我们需要向服务器发送一些数据,包括app的ID,兑换吗,要兑换的设备ID。
如何发送呢?有两种方法:GET(普通方法)和POST(用于发送表单)
每个都能满足你的需求,但是当你要尝试做些什么的时候,比如兑换促销码,还是用POST比较好。这也是我将要做的。
这是什么意思呢?如果我们想在PHP中访问这些参数,我们可以通过内建的$_POST 数组:
<span style="color: #800080;">$_POST</span>["rw_app_id"]
我们将会用ASIHTTPRequest来连接服务器,用ASIFormDataRequest类发送一个POST请求:
ASIFormDataRequest *request =<span style="color: #000000;"> [ASIFormDataRequest requestWithURL:url]; [request setPostValue:</span><span style="color: #800000;">@"</span><span style="color: #800000;">1</span><span style="color: #800000;">"</span> forKey:<span style="color: #800000;">@"</span><span style="color: #800000;">rw_app_id</span><span style="color: #800000;">"</span>];
更多GET和POST的信息,请看Wikipedia entry。
更新:请看@smpdawg’s的精彩评论forum topic
web服务器策略:算法
下一步,我们要看看将要使用的web服务器的算法:
web服务器的实现
首先,添加一个辅助方法用于返回HTTP状态信息:
<span style="color: #008000;">//</span><span style="color: #008000;"> 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/ </span> <span style="color: #0000ff;">function</span> getStatusCodeMessage(<span style="color: #800080;">$status</span><span style="color: #000000;">) { </span><span style="color: #008000;">//</span><span style="color: #008000;"> these could be stored in a .ini file and loaded // via parse_ini_file()... however, this will suffice // for an example</span> <span style="color: #800080;">$codes</span> = <span style="color: #0000ff;">Array</span><span style="color: #000000;">( </span>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'<span style="color: #000000;"> ); </span><span style="color: #0000ff;">return</span> (<span style="color: #0000ff;">isset</span>(<span style="color: #800080;">$codes</span>[<span style="color: #800080;">$status</span>])) ? <span style="color: #800080;">$codes</span>[<span style="color: #800080;">$status</span>] : ''<span style="color: #000000;">; } </span><span style="color: #008000;">//</span><span style="color: #008000;"> Helper method to send a HTTP response code/message</span> <span style="color: #0000ff;">function</span> sendResponse(<span style="color: #800080;">$status</span> = 200, <span style="color: #800080;">$body</span> = '', <span style="color: #800080;">$content_type</span> = 'text/html'<span style="color: #000000;">) { </span><span style="color: #800080;">$status_header</span> = 'HTTP/1.1 ' . <span style="color: #800080;">$status</span> . ' ' . getStatusCodeMessage(<span style="color: #800080;">$status</span><span style="color: #000000;">); </span><span style="color: #008080;">header</span>(<span style="color: #800080;">$status_header</span><span style="color: #000000;">); </span><span style="color: #008080;">header</span>('Content-type: ' . <span style="color: #800080;">$content_type</span><span style="color: #000000;">); </span><span style="color: #0000ff;">echo</span> <span style="color: #800080;">$body</span><span style="color: #000000;">; }</span>
如果你不理解为什么我们不要这个,那是因为这是一个遵守HTTP协议的web服务器,当你发送一个相应你可以制定一个包含错误码和详细描述的头。有标准错误码可以用,这些方法不过是用起来更方便。
正如你看到的,我防线了一个可以把状态吗转换成HTML信息的教程。
下一步,就是真正的实现了!
function redeem() { // Check for required parameters if (isset(<span style="color: #800080;">$_POST</span>["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 ) { sendResponse(400, 'Invalid code'); return false; } // Bail if code already used if ($uses_remaining ) { 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。这里有一些事情我需要指出:
现在,你的web服务器就已经可以工作了。你可以用下面命令来测试:
curl -F "rw_app_id=1" -F "code=test" -F "device_id=test" http:<span style="color: #008000;">//</span><span style="color: #008000;">www.wildfables.com/promos/</span> {"unlock_code":"com.razeware.wildfables.unlock.test"}
注意,如果你在我的服务器上测试,如果你得到“code already used”的错误,你应该更改你的device_id。
你可能希望进入你的数据库看看那里是否有一个rw_promo_code_redeemed的入口,uses_remaining是否减一等等。
下一步?
这里是源码source code。
敬请期待PART2.