亲自操刀-网易云信SDK


网易云信SDK

作者:dzer <email:358654744@qq.com blog:dzer.me>
创建时间:2015/11/5 19:32:50 
最后修改时间: 2015/11/27 13:27:50    

最近公司需要做IM,经比较用了刚刚发布不久的云信,云信比较其他IM还是有很多优点,功能多,嵌入快,文档还算全,稳定性也不错。公司两个项目都要用IM,一个是TP写的,一个是Yii写的,我一个人负责IM功能总不能写两遍吧,想到一个办法就是写成SDK,这样两个项目都可以导入就用了,官网没php的SDK,正好想尝试些下扩展,只有自己写SDK了。

感觉这次写的扩展思路,规范,格式都还不错,也学习到一些东西,记录下来以后写一些扩展的时候都可以参考。

需求:
  • 方便接入
  • 完全面向对象
  • 有日志记录
  • 方便调用
  • 注释完整
完成步骤:
  1. SDK结构
  2. 代码编写
  3. 测试
  4. 嵌入项目

一、SDK结构

网易云信SDK结构图

二、代码编写

记录下主要几个基础类,业务逻辑类没什么好记录的。

1.入口文件

主要定义根目录,开发模式,日志保存目录,引入配置文件和自动加载等。开发主模式可关闭,关闭后不记录日志。

/**
 * 网易云信入口文件
 * 
 * @version 1.0
 * @author dzer <358654744@qq.com>
 * @date 2015-11-04
 */

date_default_timezone_set("PRC");
//根目录
defined('IM_ROOT') || define('IM_ROOT', str_replace('\\', '/', dirname(__FILE__)));
//是否处于开发模式(开发模式将记录接口请求参数,错误记录等)
defined('IM_DEBUG') || define('IM_DEBUG', true);
//日志保存路径
defined('IM_LOG_DIR') || define('IM_LOG_DIR', IM_ROOT . '/tmp_log/');
//引入配置文件类
include(IM_ROOT . '/ImConfig.php');
//引入自动加载类
include(IM_ROOT . '/basic/ImAutoloader.php');
2. 配置文件

主要嵌入项目时配置appkey等参数。

/**
 * 配置文件类
 * 
 * @author dzer <358654744@qq.com>
 * @date 2015-11-4
 * @version 1.0
 */
class IMConfig {

    //开发者平台分配的appkey
    const APPKEY = '****************';
    //秘钥
    const APPSECRET = '**********';

}
3. 自动加载类

自动加载是个很重要的类,引入各种类就靠它了,php-fig在PSR-4有很友好的规范,但需要命名空间支持,所以本次自动加载遵循psr-0。 将函数注册到SPL__autoload函数栈中。如果该栈中的函数尚未激活,则激活它们。在使用的框架中应该都实现了__autoload函数,所以我的自动加载类必须再显式注册到__autoload栈中。因为 spl_autoload_register()函数会将Zend Engine中的__autoload函数取代为spl_autoload()spl_autoload_call()

/**
 * 自动注册类
 * 
 * @author dx <358654744@qq.com>
 * @date 2015-11-04
 * @version 1.0
 */
ImAutoloader::register();

class ImAutoloader {

    /**
     * 注册自动加载方法
     * 
     * @return bool
     */
    public static function register() {
        if (function_exists('__autoload')) {
            spl_autoload_register('__autoload');
        }
        //获取所有已注册的__autoload()函数并注销
        $functions = spl_autoload_functions();
        foreach ($functions as $function) {
            spl_autoload_unregister($function);
        }
        //加入我们的自动加载方法,并重新注册到__autoload栈中
        $functions = array_merge(array(array('ImAutoloader', 'load')), $functions);
        foreach ($functions as $function) {
            $x = spl_autoload_register($function);
        }
        return $x;
    }

    /**
     * 自动加载类
     *
     * @param string  $className  类名
     */
    public static function load($className) {
        if ((class_exists($className, false)) || (strpos($className, 'Im') !== 0)) {
            return false;
        }
        $classFilePath = IM_ROOT . '/lib/' . $className . '.php';
        if (file_exists($classFilePath) === false) {
            $classFilePath = IM_ROOT . '/basic/' . $className . '.php';
        }
        if ((file_exists($classFilePath) === false) || (is_readable($classFilePath) === false)) {
            return false;
        }

        require($classFilePath);
    }

}
4. 日志类

主要写入日志

/**
 * 日志类
 * @author dx <358654744@qq.com>
 * @date 2015-11-05
 * @version 1.0
 */
class ImLog {

    const LOGFILE = 'im.log';

    public static function write($str) {
        $log = self::isbak();
        if ($fp = fopen($log, 'a')) {
            fwrite($fp, '[' . date('Y-m-d H:i:s') . '] ' . $str . "\r\n");
            fclose($fp);
        }
    }

    public static function isbak() {
        $log = IM_LOG_DIR . self::LOGFILE;
        if (!file_exists(IM_LOG_DIR)) {
            mkdir(IM_LOG_DIR, '0777');
        }
        //判断日志文件是否存在,不存在就创建
        if (!file_exists($log)) {
            touch($log);
            return $log;
        }
        //存在就判断大小,当小于1M时就直接返回
        $size = filesize($log);
        clearstatcache(); //清除文件状态缓存
        if ($size <= 1024 * 1024) {
            return $log;
        }
        //当大于1M时就另存一份
        if (self::bak()) {
            touch($log);
            return $log;
        } else {
            return $log;
        }
    }

    public static function bak() {
        $log = IM_LOG_DIR . self::LOGFILE;
        $bak = IM_LOG_DIR . 'im.bak';
        return rename($log, $bak);
    }

}
5. 请求类

通过curl发送请求,设置校验header信息,请求body参数等。

/**
 * 基础请求类
 * 
 * @author dx <358654744@qq.com>
 * @date 2015-11-04
 * @version 1.0
 */
class ImRequest {

    //appkey
    private $appKey;
    //秘钥
    private $appSecret;
    //随机数
    private $nonce;
    //cURL允许执行的最长秒数
    private $readTimeout;
    //在发起连接前等待的时间
    private $connectTimeout;

    public function __construct($appKey = '', $appSecret = '') {
        if (empty($appKey) || empty($appSecret)) {
            $this->appKey = IMConfig::APPKEY;
            $this->appSecret = IMConfig::APPSECRET;
        }
        if (empty($this->appKey) || empty($this->appSecret)) {
            throw new Exception('APPkey或appSecret不能为空');
            exit();
        }
        $this->nonce = $this->randString();
    }

    /**
     * 执行请求方法
     * @param string $url
     * @param array $postFields
     * @return void 
     */
    public function exec($url, $postFields = null){
        //当前时间戳
        $curTime = time();
        $headerFields = array(
            'Appkey: ' . $this->appKey,
            'Nonce: ' . $this->nonce,
            'CurTime: ' . $curTime,
            'CheckSum: ' . $this->checkSum($curTime)
        );
        return $this->curl($url, $headerFields, $postFields);
    }

    /**
     * 计算checkSum校验值
     * 
     * @param integer $curTime
     * @return string
     */
    private function checkSum($curTime){
        return sha1($this->appSecret . $this->nonce . $curTime);
    }

    /**
     * 生成随机字符串
     * 
     * @param integer $length 随机字符串长度
     * @return string
     */
    private function randString($length = 20) {
        $string = '1234567890qwertyuiopasdfghjklzxcvbnm~!#$%^&*()_+';
        return substr(str_shuffle($string), 0, $length);
    }


    /**
     * 请求方法
     * 支持http和https请求
     * @param string $url   请求地址
     * @param array $headerFields 请求头参数
     * @param array $postFields 请求体参数
     * @return void 
     * @throws Exception
     */
    private function curl($url, $headerFields = null, $postFields = null) {
        $ch = curl_init();
        //请求url地址
        curl_setopt($ch, CURLOPT_URL, $url);
        //HTTP状态码
        curl_setopt($ch, CURLOPT_FAILONERROR, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        //设置cURL允许执行的最长秒数
        if ($this->readTimeout) {
            curl_setopt($ch, CURLOPT_TIMEOUT, $this->readTimeout);
        }
        //尝试连接等待时间
        if ($this->connectTimeout) {
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connectTimeout);
        }
        curl_setopt($ch, CURLOPT_USERAGENT, "top-sdk-php");
        //https 请求(当请求https的数据时,会要求证书,这时候,加上下面这两个参数,规避ssl的证书检查)
        if (strlen($url) > 5 && strtolower(substr($url, 0, 5)) == "https") {
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        }

        if (is_array($postFields) && 0 < count($postFields)) {
            $postBodyString = "";
            $postMultipart = false;
            foreach ($postFields as $k => $v) {
                if ("@" != substr($v, 0, 1)) {//判断是不是文件上传
                    $postBodyString .= "$k=" . urlencode($v) . "&";
                } else {
                    //文件上传用multipart/form-data,否则用www-form-urlencoded
                    $postMultipart = true;
                }
            }
            unset($k, $v);
            curl_setopt($ch, CURLOPT_POST, true);
            if ($postMultipart) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
                curl_setopt($ch, CURLOPT_HTTPHEADER, $headerFields);
            } else {
                $contentType = "content-type: application/x-www-form-urlencoded; charset=UTF-8";
                array_push($headerFields, $contentType);
                curl_setopt($ch, CURLOPT_HTTPHEADER, $headerFields);
                curl_setopt($ch, CURLOPT_POSTFIELDS, substr($postBodyString, 0, -1));
            }
        }
        $reponse = curl_exec($ch);

        if (curl_errno($ch)) {
            throw new Exception(curl_error($ch), 0);
        } else {
            $httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            if (200 !== $httpStatusCode) {
                throw new Exception($reponse, $httpStatusCode);
            }
        }
        //记录日志
        if (IM_DEBUG) {
            $log = "header: \r\n" . print_r($headerFields, true) . "\r\n"
                 . "body: \r\n" . print_r($postFields, true) . "\r\n"
                 . "response: \r\n" . print_r($reponse, true) . "\r\n";
            ImLog::write($log);
        }
        curl_close($ch);
        return $reponse;
    }

}

三、测试

通过使用,测试各个接口。

四、嵌入项目

打包放入项目扩展文件夹,修改配置文件,加载入口文件,简单方便就可以使用了。

github地址:https://github.com/dzer/NeteaseIm.git


个人总结:感觉很多东西,静下心来写也没那么复杂,这个扩展还有一些需要优化的,比如请求字段还可以自动验证,错误处理也不是很完善,但毕竟只是一个扩展,够用了。这个扩展完全是个人的思路和理解可能很菜B,该项目也放到github了,希望大家多给点意见和建议。


常常是最后一把钥匙打开了门