您的位置:首页 > 其它

Ucenter 通信

2014-01-14 12:42 218 查看

先说一下什么是Ucenter,顾名思义它是“用户中心”。
UCenter是Com服务器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目录保存是与服务器通信相关的文件,也是直接与服务器进行通讯的第一道关口,来自于服务器的请求直接被Dspatch到api/uc.php文件里。

uc.php就是位于该目录下,uc.php也是这个目录里最主要的文件,主要负责对传来的code进行解码,而解码的唯一依据是UC_KEY.

这也就解释了,为什么当服务器端与客户端的UC_KEY不一致,会导致所有的接口调用都会失败(因为解码失败,所有从服务器传来的数据都乱了,没办法继续走下去)。

uc.php文件里定义了uc_note类,用这个类实现了服务器端请求客户端去进行一些操作比如test,deleteuser……等等,这类主要是服务器请求客户端同步的操作。应用如果需要在请求到来之际,操作自己数据库或者Cookie,那么在这里定义这些操作,是最好的方式。

uc_client目录里有4个子目录,control、data、lib、model。另外,最主要的文件是client.php文件,并判断安装的应用是以“数据库”模式,或者是以“消息”模式来与UCenter通讯的。以此为依据,来调用相应的函数与UCenter的服务器端进行通讯。

client.php文件里定义了很多的函数。如:uc_serialize,uc_unserialize,uc_authcode,uc_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函数。实际上是在用S与C约定好的UC_KEY,利用authcode函数,来实现对$action,time......等参数的组合加密。

这同样也说明了,为什么当S与C的UC_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_FAILED给S端。
//如果是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)之类的方法。

其实UCenter的MVC并没有完全的分开,不是很规范。如上面的C层里的onadd()应该是不会去直接操作数据库的。但是仍然直接访问了数据库,而且还在应用数据表里添加了一条应用记录。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  ucenter 通信 整合