Ucenter 通信

先說一下什麼是Ucenter,顧名思義它是“用戶中心”。

UCenterCom服務器enz旗下各個產品之間信息直接傳遞的一個橋樑,通過UCenter可以無縫整合Com服務器enz系列產品,實現用戶的一站式登錄以及社區其他數據的交互。


Ucenter 通信基本過程如下:


1.從用戶xxx在某一應用程序的login.php,輸入用戶名,密碼講起。
先用uc_user_login函數到uc server驗證此用戶和密碼,如正確,則寫入session,寫入cookies,並更新應用程序會員表中的登錄ip,登錄時間。用戶感覺不到這個過程。
2.然後通過uc_user_synlogin通知uc server用戶xxx登錄成功,這個過程可能使用ajax,用戶感覺不到通知過程。
3.uc server收到這個消息後,馬上命令手下,把xxx登錄的消息,像令牌環一樣,發給所有願意接收(後臺中那個是否開啓同步登錄)這個消息的其它應用程序。其實就是帶參數訪問一下各應用程序的uc.php,用戶感覺不到這個過程。
4.各應用程序靠api下的uc.php來接收uc server發來的消息,並對uc server言聽計從,讓幹什麼就幹什麼。現在,收到讓xxx用戶在你的程序中登錄的命令,馬上執行。並寫本應用程序的session,並且使用p3p, 寫入相同域或不同域的cookie服務器.用戶感覺不到這個過程。
5.最後所有和uc整合的程序,xxx均登錄成功。用戶從www.test.com/bbs登錄後, 跳到www.test.com/news同樣顯示登錄。因爲bbs 和news系統在後臺均已登錄。
6.應用程序與uc server的會話結束。


接下來具體說一下通信原理:

Ucenter 分爲服務器端 uc_服務器erver與客戶端 uc_client,服務器端主要功能是提供服務,和管理應用,Client端主要功能是與服務端通信,及把更新及時傳遞到服務器,並在需要時從服務器端取數據給自己用。


服務器端相對比較複雜,依然採用的是MVC結構。

服務器端程序對各種UCenter所管理的資源都提供了相應管理的界面。
併爲與之相連接的每一個應用請求這些資源而做出了相應的接口,提供與應用的通訊。

我們從簡單的例子來看一下,服務器端大概的結構,以及服務器端是如何與C端進行通訊的吧。
我們就從最簡單的功能看起。

在服務器的後臺,應用管理界面。會列表出所有已經註冊過的應用。
同時會在這些列表的最後一列出現通訊情況。如果通訊成功,則返回綠色的通訊成功,不成功則返回紅色的通訊失敗

這裏,可以肯定的說,服務器與其所有註冊過的應用,都發生了一次通訊。這裏,就是我們的切入點。我們將從這裏切進來,理清整個UCenter的通訊流程。


Client端相對比較簡單,api目錄和uc_client目錄。

   api目錄保存是與服務器通信相關的文件,也是直接與服務器進行通訊的第一道關口,來自於服務器的請求直接被Dspatchapi/uc.php文件裏。

   uc.php就是位於該目錄下,uc.php也是這個目錄裏最主要的文件,主要負責對傳來的code進行解碼,而解碼的唯一依據是UC_KEY.

這也就解釋了,爲什麼當服務器端與客戶端的UC_KEY不一致,會導致所有的接口調用都會失敗(因爲解碼失敗,所有從服務器傳來的數據都亂了,沒辦法繼續走下去)

   uc.php文件裏定義了uc_note類,用這個類實現了服務器端請求客戶端去進行一些操作比如test,deleteuser……等等,這類主要是服務器請求客戶端同步的操作。應用如果需要在請求到來之際,操作自己數據庫或者Cookie,那麼在這裏定義這些操作,是最好的方式。

   uc_client目錄裏有4個子目錄,controldatalibmodel。另外,最主要的文件是client.php文件,並判斷安裝的應用是以數據庫模式,或者是以消息模式來與UCenter通訊的。以此爲依據,來調用相應的函數與UCenter的服務器端進行通訊。

   client.php文件裏定義了很多的函數。如:uc_serializeuc_unserializeuc_authcodeuc_api_post……等等。這個函數基本都是類的成員函數。
client.php文件以include_once的形式,分別從4個子目錄裏引用需要的類定義。來實現以上這些函數。

這些函數的主要實現的都是比較核心的功能。比如序列化、反序列化、加密、解密……等等。
總之,uc_client裏面的文件主要實現的功能是與服務器端進行通訊。


Ucenter “應用管理的界面uc_server\control\admin\app.php

這裏是控制器層:

class control extends adminbase {

………………………………

這個類的函數onadd,onls,onping……分別實現了添加新應用、應用列表、測試應用連接……功能。
我們要說的就是從這個onping函數開始。


function onping() {
               $ip =getgpc('ip');
               $url =getgpc('url');
               $appid =intval(getgpc('appid'));
               $app =$_ENV['app']->get_app_by_appid($appid);
               $status = '';
              if($app['extra']['apppath'] && @include$app['extra']['apppath'].'./api/'.$app['apifilename']) {
                      $uc_note = new uc_note();
                      $status = $uc_note->test($note['getdata'],$note['postdata']);
                     //WriteToLog("ping.log",'\$uc_note='.$uc_note,'');
               } else {
                      $this->load('note');
$url =$_ENV['note']->get_url_code('test', '', $appid);
$status =$_ENV['app']->test_api($url, $ip);
                     //WriteToLog("ping.log",'URL='.$url,'');
                      file_put_contents("ping.log",strtotime("now").'  '.'URL='.$url."\r",FILE_APPEND);
               }
               if($status == '1' ) {
                      echo'document.getElementById(\'status_'.$appid.'\').innerHTML = "<imgsrc=\'images/correct.gif\' border=\'0\' class=\'statimg\' \/><spanclass=\'green\'>'.$this->lang['app_connent_ok'].'</span>";testlink();';
                     WriteToLog("ping.log",$appid.'  '.$this->lang['app_connent_ok'],'');
               } else {
                      echo 'document.getElementById(\'status_'.$appid.'\').innerHTML= "<img src=\'images/error.gif\' border=\'0\' class=\'statimg\'\/><spanclass=\'red\'>'.$this->lang['app_connent_false'].'</span>";testlink();';
                     WriteToLog("ping.log",$appid.'  '.$this->lang['app_connent_false'],'');
               }

       }


注意以上代碼紅色處,$status 的值是由函數$_ENV['app']->test_api($url, $ip);返回的。
這表示調用uc_server\model\app.php裏的test_api函數。

我們來看一下test_api函數。

function test_api($url, $ip = '') {
 $this->base->load('misc');
 if(!$ip) {
  $ip = $_ENV['misc']->get_host_by_url($url);//在這裏調用了misc模塊的get_host_by_url($url)來獲得正確的應用的IP地址以實現通訊。
 }
 if($ip < 0) {
  return FALSE;
 }

 file_put_contents("test_api.log",strtotime("now").'  '.'ip>=0and url='.$url.'ip='.$ip."\r",FILE_APPEND);

 $myreturn=$_ENV['misc']->dfopen($url, 0, '', '', 1, $ip);//在這裏調用了misc模塊的dfopen()來嚮應用發送調用。

   file_put_contents("test_api.log",strtotime("now").'  '.'\$_ENV[\'misc\']->dfopen'.$myreturn."\r",FILE_APPEND);
 return $myreturn;
}

那麼我們跟進\uc_client\model\'misc'模塊的dfopen函數。

function dfopen($url, $limit = 0, $post = '', $cookie = '', $bysocket = FALSE ,$ip = '', $timeout = 15, $block = TRUE, $encodetype  = 'URLENCODE') {
 //error_log("[uc_client]\r\nurl: $url\r\npost:$post\r\n\r\n", 3, 'c:/log/php_fopen.txt');
 $return = '';
 $matches = parse_url($url);
 $host = $matches['host'];
 $path = $matches['path'] ? $matches['path'].($matches['query'] ?'?'.$matches['query'] : '') : '/';
 $port = !empty($matches['port']) ? $matches['port'] : 80;
 if($post) {
  $out = "POST $path HTTP/1.0\r\n";
  $out .= "Accept: */*\r\n";
  //$out .= "Referer: $boardurl\r\n";
  $out .= "Accept-Language: zh-cn\r\n";
  $boundary = $encodetype == 'URLENCODE' ? '' : ';'.substr($post, 0,trim(strpos($post, "\n")));
  $out .= $encodetype == 'URLENCODE' ? "Content-Type:application/x-www-form-urlencoded\r\n" : "Content-Type:multipart/form-data$boundary\r\n";
  $out .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n";
  $out .= "Host: $host\r\n";
  $out .= 'Content-Length: '.strlen($post)."\r\n";
  $out .= "Connection: Close\r\n";
  $out .= "Cache-Control: no-cache\r\n";
  $out .= "Cookie: $cookie\r\n\r\n";
  $out .= $post;
 } else {
  $out = "GET $path HTTP/1.0\r\n";
  $out .= "Accept: */*\r\n";
  //$out .= "Referer: $boardurl\r\n";
  $out .= "Accept-Language: zh-cn\r\n";
  $out .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n";
  $out .= "Host: $host\r\n";
  $out .= "Connection: Close\r\n";
  $out .= "Cookie: $cookie\r\n\r\n";
 }
 $fp = @fsockopen(($ip ? $ip : $host), $port, $errno, $errstr,$timeout);
 if(!$fp) {
  return '';
 } else {
  stream_set_blocking($fp, $block);
  stream_set_timeout($fp, $timeout);
  @fwrite($fp, $out);
  $status = stream_get_meta_data($fp);
  if(!$status['timed_out']) {
   while (!feof($fp)) {
    if(($header = @fgets($fp)) && ($header =="\r\n" ||  $header == "\n")) {
     break;
    }
   }
   $stop = false;
   while(!feof($fp) && !$stop) {
    $data = fread($fp, ($limit == 0 || $limit > 8192 ?8192 : $limit));
    $return .= $data;
    if($limit) {
     $limit -= strlen($data);
     $stop = $limit <= 0;
    }
   }
  }
  @fclose($fp);
  return $return;
 }
}

我們可以看到,dfopen其實在最直接的一個嚮應用端(S端)發送http1.1 Request的函數。所有的請求,都會由它先編碼,然後發送出去。

我們截獲由dfopen發送的字串,發現是這樣的:
GET/test/discuzx/api/uc.php?code=04e27HZ%2FyCLXqy%2BfWDKM57uRzvFbIdA0Oky2sVXCrbdxH%2FOQc9xGGz0ZQklzbrFFycAQxwDzsDwHTTP/1.0
Accept: */*
Accept-Language: zh-cn
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; GTB6.5;.NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR3.0.4506.2152; .NET CLR 3.5.30729; 360SE)
Host: localhost:80
Connection: Close
Cookie:

也就是說,所有的請求,都被髮送到了應用的 ./api/uc.php文件裏。
S的請求,全部交由應用的uc.php來處理。
由此證明,服務端(S)發送到應用端(C)與的通信,第一個關口就是S端的uc.php文件。

那麼那段uc.php?code=04e27HZ%2FyCLXqy%2BfWDKM57uRzvFbIdA0Oky2sVXCrbdxH%2FOQc9xGGz0ZQklzbrFFycAQxwDzsDw
中的code=.....到底包含了哪些數據,到底是什麼東西呢?

讓我們返回到: uc_server\control\admin\app.php裏的onping()函數。

function onping() {
               $ip =getgpc('ip');
               $url =getgpc('url');
               $appid =intval(getgpc('appid'));
               $app =$_ENV['app']->get_app_by_appid($appid);
               $status = '';
              if($app['extra']['apppath'] && @include$app['extra']['apppath'].'./api/'.$app['apifilename']) {
                      $uc_note = new uc_note();
                      $status = $uc_note->test($note['getdata'],$note['postdata']);
                     //WriteToLog("ping.log",'\$uc_note='.$uc_note,'');
               } else {
                      $this->load('note');
$url =$_ENV['note']->get_url_code('test', '', $appid);
$status =$_ENV['app']->test_api($url, $ip);

)很明顯,那段code就是$url,而這段$url來自於'note'模型的get_url_code的函數。

我們跟進\uc_client\model\'note'模型的get_url_code函數。

function get_url_code($operation, $getdata, $appid) {
 $app = $this->apps[$appid];
$authkey = UC_KEY;
 $url = $app['url'];
 $apifilename = isset($app['apifilename']) &&$app['apifilename'] ? $app['apifilename'] : 'uc.php';
 $action = $this->operations[$operation][1];
$code =urlencode($this->base->authcode("$action&".($getdata ?"$getdata&" : '')."time=".$this->base->time,'ENCODE', $authkey));
 return $url."/api/$apifilename?code=$code";
}

注意以上兩段紅色的代碼,說明get_url_code函數。實際上是在用SC約定好的UC_KEY,利用authcode函數,來實現對$action,time......等參數的組合加密。

這同樣也說明了,爲什麼當SCUC_KEY不一至時,所有的S發送到C的請求,都會失敗的原因。因爲C沒有辦法對傳過來的code正確的解密、解析。所以根本不知道,S端傳過來的是什麼。


S端向C端發送請求的流程到此已經結束。
下面我們來分析一下,C端是如何去處理S端發送過來的請求的。

'因爲所有的S端的請求,是發往uc.php的。我們來分析一下uc.php文件。
uc.php文件分爲幾個主要段落。
第一是define段。用來定義UCenter版本號、發行號;哪些同步方法的是否打開,哪些關閉;文件主目錄;

define('UC_CLIENT_VERSION', '1.5.1');
define('UC_CLIENT_RELEASE', '20100501');
define('API_DELETEUSER', 1);
define('API_RENAMEUSER', 1);
define('API_GETTAG', 1);
define('API_SYNLOGIN', 1);
define('API_SYNLOGOUT', 1);
define('API_UPDATEPW', 1);
define('API_UPDATEBADWORDS', 1);
define('API_UPDATEHOSTS', 1);
define('API_UPDATEAPPS', 1);
define('API_UPDATECLIENT', 1);
define('API_UPDATECREDIT', 1);
define('API_GETCREDIT', 1);
define('API_GETCREDITSETTINGS', 1);
define('API_UPDATECREDITSETTINGS', 1);
define('API_ADDFEED', 1);
define('API_RETURN_SUCCEED', '1');
define('API_RETURN_FAILED', '-1');
define('API_RETURN_FORBIDDEN', '1');
define('IN_DISCUZ', true);
define('IN_UC',true);
define('DISCUZ_ROOT', dirname(dirname(__FILE__)).'/');
define('CURSCRIPT', 'api');


這裏的define一般不會出錯,都會順利執行下去,如果有問題,那麼無非是定義賦值錯誤,或者遺漏了某些常量的定義。

接下來是require段。用來引入一些類、函數的定義文件。
require_once DISCUZ_ROOT.'./config/config_global.php';
require_once DISCUZ_ROOT.'./config/config_ucenter.php';
require_once DISCUZ_ROOT.'./source/function/function_core.php';
require_once DISCUZ_ROOT.'./source/class/class_core.php';
$discuz = & discuz_core::instance();
$discuz->init();
require DISCUZ_ROOT.'./config/config_ucenter.php';
require_once DISCUZ_ROOT.'./data/config.inc.php';

require段,要注意,雖然require一個不存在的文件,不會影響程序往下執行,但是:如果文件沒有被正確引入,那麼段內的紅色的兩行語句,會因爲找不到對應的類文件而無法生成實例。這是很嚴重的錯誤,因爲這會導致程序直接在此處停止。不同的應用,大部分是因爲這裏出錯而導致C端停止解析S端的請求。

繼續往下看。
$get = $post = array();//定義數組。
$code = @$_GET['code'];//得到S端傳來的code=92fjd892fhidf2fop2fl22f

繼續往下看。
//以下這句很重要,調用了uc_authcode,來解密code字串。同時使用應用端的UC_KEY做爲解密的參數。因爲傳來的code字串,正是用UC_KEY來加密的。並將解密的結果(是一個數組),放到$get變量裏。
parse_str(uc_authcode($code, 'DECODE', UC_KEY), $get);
//那麼放入$get變量裏的數組是什麼形式的呢?
//我們已經捕獲了。
//這裏給大家看一下$get變量的大概內容包括哪些。如:
//action=synlogin&username=admin&uid=1&password=f03a9e498589d7b882786b5f70e49a75&time=1283314514
//action=test&time=1283314644
//現在理解了吧。至少包含action,time這兩個數組元素。
//此處看看請求是否超時
if(time() - $get['time'] > 3600) {
exit('Authracation has expiried');
file_put_contents("uc.log",strtotime("now").'  '.'請求錯誤,已經超時'."\r",FILE_APPEND);
}
//此處看看是否解析code成功,如果解析code字串成功,那麼empty($get)就不會爲null
if(empty($get)) {
exit('Invalid Request');
file_put_contents("uc.log",strtotime("now").'  '.'錯誤的請求'.$code."\r",FILE_APPEND);
}


繼續往下看。
include_once DISCUZ_ROOT.'./uc_client/lib/xml.class.php';//引入XML序列化類準備對$post反序列化
file_put_contents("uc.log",strtotime("now").'  '.'開始反序列化......'.$code."\r",FILE_APPEND);
$post = xml_unserialize(file_get_contents('php://input'));//反序列化$post
file_put_contents("uc.log",strtotime("now").'  '.'反序列化成功!'.$code."\r",FILE_APPEND);
//以下判斷一下,解析出來的$get['action']是否是uc.php裏列出的幾種函數名之後,如果不是直接拋出一個API_RETURN_FAILEDS端。
//如果是uc.php列出的函數之後,那麼生成一個uc_note()實例,然後調用uc_note->$get['action']($get,$post);來把請求交給uc_note實例來處理。
if(in_array($get['action'], array('test', 'deleteuser', 'renameuser', 'gettag','synlogin', 'synlogout', 'updatepw', 'updatebadwords', 'updatehosts','updateapps', 'updateclient', 'updatecredit', 'getcredit', 'getcreditsettings','updatecreditsettings', 'addfeed'))) {
$uc_note = new uc_note();
echo $uc_note->$get['action']($get, $post);
exit();
} else {
exit(API_RETURN_FAILED);
}

OK,以上uc.php主要完成了對S端傳來的code進行解析、檢測的過程。並生成了一個uc_note類來處理這些請求。

繼續往下,就是class uc_note()的定義了。相信有一點php基礎的人都可以看懂了。這裏就列出個大概了,想了解的,請自己去看uc.php
S端發往C端,以及C端處理S端的流程。就介紹到這裏。告破。

class uc_note {
var $dbconfig = '';
var $db = '';
var $tablepre = '';
var $appdir = '';

function _serialize($arr, $htmlon = 0) {
if(!function_exists('xml_serialize')) {
include_once DISCUZ_ROOT.'./uc_client/lib/xml.class.php';
}
 return xml_serialize($arr,$htmlon);
}

function uc_note() {
}

function test($get, $post) {
 return API_RETURN_SUCCEED;
}

…………

服務器端的模型層即M層在哪裏呢?
uc_server\model\app.php文件裏。
定義瞭如:delete_apps($appids)之類的方法。

其實UCenterMVC並沒有完全的分開,不是很規範。如上面的C層裏的onadd()應該是不會去直接操作數據庫的。但是仍然直接訪問了數據庫,而且還在應用數據表裏添加了一條應用記錄。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章