2015年8月26日 星期三

無痛入手網路界的超跑Nginx+PHP-FPM!

筆者平常開的車是toyota altis,對於超跑可說是忘塵莫及;但身為程式設計師,確每人都可入手一網路界的超跑Nginx + PHP-FPM;Nginx是一款高效能的Web Server近幾年來大幅的成長,用來取代Apache,若您開發的網站具有大流量,而且速度要求快,那麼Nginx應是您的首選!

Wikipedia的說明如下。

『Nginx是一款面向效能設計的HTTP伺服器,相較於Apache、lighttpd具有占有記憶體少,穩定性高等優勢。與舊版本(<=2.2)的Apache不同,nginx不採用每客戶機一執行緒的設計模型,而是充分使用非同步邏輯,削減了上下文排程開銷,所以並行服務能力更強。整體採用模組化設計,有豐富的模組庫和第三方模組庫,配置靈活。 在Linux作業系統下,nginx使用epoll事件模型,得益於此,nginx在Linux作業系統下效率相當高。同時Nginx在OpenBSD或FreeBSD作業系統上採用類似於epoll的高效事件模型kqueue。』

有些教學文件是整套安裝完Nginx、MySQL及PHP再測試,建議是分段安裝,有問題可容易發覺;先安裝Nginx測試成功後再安裝PHP,最後才是安裝MySQL。

基本環境

  • 使用AWS EC2 China,系統為Ubuntu14.04;HVM虛擬技術。
  • 登錄EC2後使用sudo su切換到root權限。
  • 先執行apt-update update。

常用指令

指令
說明
cd /etc/nginx/sites-enabled
虛擬網站設定目錄
cd /usr/share/nginx/html
預設網站的文件存放目錄
cd /home/ubuntu
虛擬網站文件存放目錄
cd /etc/nginx/sites-enabled/default
啟用網站目錄
cd /etc/nginx/sites-available/default
所有網站目錄
vi /etc/nginx/nginx.conf
編輯Nginx設定
vi /var/log/nginx/error.log
查看nginx錯誤訊息,很常用!

安裝Nginx

  • 安裝nginx
apt-get install nginx

  • 修改nginx配置文件
vi /etc/nginx/nginx.conf

  • 將user改為EC2用戶名,預設為ubuntu
user ubuntu;

  • 在events配置加上
use epoll;

  • 修改後設定檔如下。

user ubuntu;
worker_processes 4;
pid /run/nginx.pid;

events {
       worker_connections 768;
       use epoll;
       # multi_accept on;
}



  • 若是預設網站不是使用80 port,需要修改預設網站設定。
vi /etc/nginx/sites-enable/default

將80改為其他port,若是AWS China的ICP未通過,80、8080及443是無法使用的。
listen 80 default_server;

  • 需要注意的是default_server及server_name不需要修改,而是在之後用虛擬方式來新增;先確定Nginx在本機環境能測試運作。

測試Nginx

  • 完成後啟動
service nginx start

  • 測試Nginx是否啟動,在EC2上運行;如果輸出Welcome to nginx!字樣,說明nginx安裝成功。

  • 開放外部80端口訪問;進入安全組頁面。

螢幕快照 2015-08-22 07.57.47.png
  • 從外部網址測試,成功畫面如下。
螢幕快照 2015-08-22 08.00.14.png

安裝並配置PHP

由於要使用Nginx,所以這裡選擇安裝帶fpm版本的php
apt-get install php5-fpm

安裝相應的PHP模塊。
apt-get install php5-mysql php5-curl php5-gd php5-intl php-pear php5-imagick php5-imap php5-mcrypt php5-memcache php5-ming php5-ps php5-pspell php5-snmp php5-sqlite php5-tidy php5-xmlrpc php5-xsl

安裝PHP Command Line模組,可以直接用Command Line確認PHP的運作正常。
apt-get install php5-cli

安裝完成後,配置php-fpm,配置中出現的ubuntu為EC2的登陸用戶。
vi /etc/php5/fpm/pool.d/www.conf

修改或新增如下幾項:
user = ubuntu
group = ubuntu
listen.owner = ubuntu
listen.group = ubuntu
listen.mode = 0660

將檔案的/var/run/php5-fpm.sock所屬用戶及用戶組改為ubuntu,不然會有權限問題。
chown ubuntu:ubuntu /var/run/php5-fpm.sock
chmod 0660 /var/run/php5-fpm.sock

繼續以下操作:

vi /etc/php5/fpm/php.ini

short_open_tag = On
cgi.fix_pathinfo=0

重啟php5-fpm

service php5-fpm restart

測試PHP運作正常

  • 先檢查php5-fpm是否有正常運作。

service php5-fpm status
php5-fpm start/running, process 1084

  • 在default目錄新增phpinfo.php測試檔案。

vi /usr/share/nginx/html/phpinfo.php

<html>
<head><title> PHP Test Script </title></head>
<body>

<?php
phpinfo( );
?>
</body>
</html>

  • 單純用Command Line測試php檔案,以確認php是可以正常執行的。

php5 phpinfo.php

<html>
<head>
<title> PHP Test Script </title>
</head>
<body>

phpinfo()
PHP Version => 5.5.9-1ubuntu4.11


</body>
</html>

結合Nginx與PHP5-FPM

架設Nginx + PHP5-FPM最容易出錯的地方就是在結合兩者處,如何將Nginx的HTTP Request轉發給PHP5-FPM;與Apache不同的是PHP是用內建模組方式,但Nginx則是用轉發方式。

最簡單也不容易出錯的設定方式是將/etc/nginx/sites-available/default檔案關於PHP部份註解移除。

       vi /etc/nginx/sites-available/default

       # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
       #
       location ~ \.php$ {
              fastcgi_split_path_info ^(.+\.php)(/.+)$;
       #       # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
       #
              # With php5-cgi alone:
       #       fastcgi_pass 127.0.0.1:9000;
       #       # With php5-fpm:
              fastcgi_pass unix:/var/run/php5-fpm.sock;
              fastcgi_index index.php;
              include fastcgi_params;
       }

前兩行是將副檔名為PHP的檔案會轉到此區段執行。

Nginx將HTTP Request轉給PHP有兩種方式,第一種是用ip,第二種是用socket;建議使用socket的效能會較好,也就是fastcgi_pass unix:/var/run/php5-fpm.sock。

fastcgi_index index.php是定位php首頁,include fastcgi_params是包含fastcgi相關參數,此檔案在/etc/nginx/fastcgi_params。

設定完後使用指令service nginx reload或是service nginx restart指令重新載入設定;接下來使用以下指令確認是否可用網址存取php檔案,若可以則代表Nginx + PHP-FPM成功設定。

curl http://localhost/phpinfo.php
<html>
<head>
<title> PHP Test Script </title>
</head>
<body>

phpinfo()
PHP Version => 5.5.9-1ubuntu4.11

虛擬主機

虛擬主機的備置目錄有兩個。

  • /etc/nginx/sites-available/
  • /etc/nginx/sites-enabled/

將可能會用到的虛擬目錄放在sites-available目錄,而在sites-enabled用連結指向實際要啟用的。

我們嘗試新增一個虛擬目錄,先放在sites-available。


server {
       listen 80;
       root /home/ubuntu/www.v-union.com.cn;
       index index.php index.html index.htm;
       server_name www.v-union.com.cn;

       location / {
               # First attempt to serve request as file, then
               # as directory, then fall back to displaying a 404.
               try_files $uri $uri/ =404;
               # Uncomment to enable naxsi on this location
               # include /etc/nginx/naxsi.rules
       }

       # pass the PHP scripts to FastCGI server listening
       location ~ \.php$ {
               fastcgi_split_path_info ^(.+\.php)(/.+)$;
              # With php5-fpm:
               fastcgi_pass unix:/var/run/php5-fpm.sock;
               fastcgi_index index.php;
               include fastcgi_params;
       }
}

然後在sites-enabled中用連結方式指向此檔案。

cd /etc/nginx/sites-enabled/
ln -s /etc/nginx/sites-available/www.v-union.com.cn www.v-union.com.cn

需要注意的事網站文件要放在/home/ubuntu,不然權限會不足。

App設定

需要注意的事,我們可能會安裝各種App在網站上,其設定檔可能會不一樣;需要再尋找該App的相關資源進行設定,舉例而言筆者有使用magento電子商務網站,其設定檔如下參考。

server {
   listen 80;
   ## SSL directives might go here
   ## Domain is here twice so server_name_in_redirect will favour the www
   ## Forcibly prepend a www;
   server_name buy.v-union.com.cn;
   root /home/ubuntu/buy.v-union.com.cn/;    ## Forcibly prepend a www;
   location / {
       index index.html index.php;  ## Allow a static html file to be shown first
       try_files $uri $uri/ @handler; ## If missing pass the URI to Magento's front handler
       expires 30d; ## Assume all files are cachable
   }

   ## These locations would be hidden by .htaccess normally
   location ^~ /app/                   { deny all; }
   location ^~ /includes/              { deny all; }
   location ^~ /lib/                   { deny all; }
   location ^~ /media/downloadable/ { deny all; }
   location ^~ /pkginfo/               { deny all; }
   location ^~ /report/config.xml   { deny all; }
   location ^~ /var/                   { deny all; }

   location /var/export/ { ## Allow admins only to view export folder
       auth_basic           "Restricted"; ## Message shown in login window
       auth_basic_user_file htpasswd; ## See /etc/nginx/htpassword
       autoindex               on;
   }

   location  /. { ## Disable .htaccess and other hidden files
       return 404;
   }

   location @handler { ## Magento uses a common front handler
       rewrite / /index.php;
   }

   location ~ .php/ { ## Forward paths like /js/index.php/x.js to relevant handler
       rewrite ^(.*.php)/ $1 last;
   }

   location ~ .php$ { ## Execute PHP scripts
       if (!-e $request_filename) { rewrite / /index.php last; } ## Catch 404s that try_files miss

       expires         off; ## Do not cache dynamic content
       fastcgi_pass   unix:/var/run/php5-fpm.sock;
      # fastcgi_param  HTTPS $fastcgi_https;
       fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
       ## Store code is defined in administration > Configuration > Manage Stores
       ##fastcgi_param  MAGE_RUN_CODE default;
       ##fastcgi_param  MAGE_RUN_TYPE store;
       ## See /etc/nginx/fastcgi_params
       include         fastcgi_params;
   }
}

偵錯

若您在設定過程中遇到問題,可以依以下方式檢查。
  • 單獨測試Nginx或PHP是否成功,也不要先設虛擬目錄,成功後再慢慢加入設定。
  • 錯誤信息可以參考nginx錯誤信息檔案,指令為vi /var/log/nginx/error.log。
  • 先不要架設App,因為App通常是架在Apache上比較多,對於Nginx通常需額外設定。
  • 權限設定,是否有將Nginx執行者改為ubuntu;是否有修改/var/run/php5-fpm.sock的權限。
  • 先用localhost的環境測試是否成功,再用外面的網址測試,可確認是否防火牆沒開。

參考文件

2015年2月19日 星期四

UIView、UIWindow、UIScreen初探

UIView定義螢幕上一個矩型區域,並且管理該區域的內容;在執行時期UIView物件負責呈現該區域所有內容,並且處理與該區域的所有互動;一個UIView也可包含多個UIView物件,形成階段式的UIView(UIView Hierarchy)。


簡單來說螢幕上看到的都是UIView。


UIWindow管理及協調在App中的所有UIView物件,並與UIScreen合作顯示在實際的螢幕上,除非外接螢幕App只會有一個UIWindow物件,UIWidnow兩個主要的功能,一是容器顯示其下所有UIView物件,二是將事件分派到其下的UIView物件;UIWidnow也繼承至UIView,擁有UIView的所有特性。


簡單來說一個App只會有一個UIWindow,負責管理所有的UIVew,及分派事件給它們。


UIScreen定義實際螢幕屬性,iOS裝置有主要螢幕,及零或多個附加螢幕;UIScreen可用來取得主要的螢幕物件,及其他附加螢幕物件。每個UIScreen會定義螢幕的長寬及其他螢幕屬性,像是亮度。


UIWindow有screen屬性(UIScreen物件),代表顯示內容的螢幕;而UIView有window屬性,代表所屬的UIWindow。參考下圖會更清楚UIWindow、UIView在App中的位置。


core_objects_2x.png


以下介紹如何取得UIWindow及UIScreen,及它們常用的方法。

取得UIWindow



var window = UIApplication.sharedApplication().keyWindow


var window = UIApplication.sharedApplication().delegate?.window?


若在UIViewController中可以用以下方法取得UIWindow。


var window = self.view.window


取得UIWindow後可用下面的方法測試。


println("\(window!.subviews.count)")


UIWindow與UIView關係



若用Single View Application的subviews.count輸出其值是1,我們將此UIView描述輸出。


println("UIWindow First Subview Description:\(window!.subviews[0].description!)")


UIWindow First Subview Description:<UIView: 0x7fb83ae34540; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb83ae34810>>
<UIScreen: 0x7fb83ad1bf20; bounds = {{0, 0}, {375, 667}}; mode = <UIScreenMode: 0x7fb83ac12dd0; size = 750.000000 x 1334.000000>>


下圖示意Single View Application,最底層是UIWindow,接下來放置UIView,最後才放置其它視覺化物件。


UIWindow及UIView.png


新建UIWindow



App建立時會自動為我們建立UIWindow,我們也可以建立自己的UIWindow來取代原本的UIWindow,參考以下程式碼。


func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {


self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window!.backgroundColor = UIColor.redColor()
self.window!.makeKeyAndVisible()
return true
}

UIWindow的rootViewController屬性



UIWindow要替換其中所有的內容,建議方式是新建一個ViewController,然後指定到rootViewController屬性,而不是直接新增Subviews,若直接新增則需自行維持View與ViewController之間的關連。以下程式碼將原本ViewController置換。


func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {


var navController: UINavigationController = UINavigationController()
self.window!.rootViewController = navController
let myLabel: UILabel = UILabel(frame: CGRectMake(0,0, 100,100))
myLabel.text = "Hello World"
navController.view.addSubview(myLabel)
navController.setToolbarHidden(false, animated: true)
return true
}


下圖說明rootViewController與subviews之間的差異。


rootViewController.png


取得UIScreen


var screen = UIScreen.mainScreen()


取得螢幕大小



bounds取得包含statusbar的App大小,applicationFrame取得不包含statusbar的App大小;可用以下程式碼做實驗。


var r = UIScreen.mainScreen().applicationFrame


println(r)
       
var r1 = UIScreen.mainScreen().bounds
       
println(r1)


輸出結果如下ApplicationFrame輸出寬度是375、高度是647,Y軸位置是20;而Bounds輸出寬度是375、高度是667,Y軸位置是0。


(0.0,20.0,375.0,647.0)  -> ApplicationFrame
(0.0,0.0,375.0,667.0)    -> Bounds


需要注意的是在iOS8後輸出的寬度及高度並非不變的,而是根據螢幕旋轉而變動,我們將螢幕旋轉90度後,輸出的值如下。


(0.0,0.0,667.0,375.0) ->  ApplicationFrame
(0.0,0.0,667.0,375.0) ->  Bounds


所以我們可以這特性很簡單的判斷螢幕是直向還是橫向。


      if (r.width < r.height)
      {


           println("Portrait 直向")


      }
      else
      {
           println("Landscape 橫向")
      }


截取螢幕畫面


UIScreen提供螢幕截取方法snapshotViewAfterScreenUpdates,其中afterUpdates是否要將最近的變動更新後再截取,常用的值是false要立即截取目前畫面;傳回的是UIView,需要自行轉存成影像檔案,並存到相簿中,參考以下程式碼。


       //截取螢幕畫面
       var currentview = UIScreen.mainScreen().snapshotViewAfterScreenUpdates(false)


      //建立UIImage
       UIGraphicsBeginImageContext(currentview.frame.size)
       currentview.layer.renderInContext(UIGraphicsGetCurrentContext())
       let image = UIGraphicsGetImageFromCurrentImageContext()
       UIGraphicsEndImageContext()


       //儲存到相簿
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)