读Flask源代码学习Python--config原理

个人学习笔记,水平有限。如果理解错误的地方,请大家指出来,谢谢!第一次写文章,发现好累--!。

起因

  莫名其妙在第一份工作中使用了从来没有接触过的Python,从那之后就对Python有了莫名其妙的好感。前段时间用了Flask做了几个不大不小的项目,项目过程中学到不少以前没注意到的知识点。于是就有了这个系列,希望最后能坚持完成。

Flask是什么?

  根据Flask官网介绍: Flask 是一个用于 Python 的微型网络开发框架。

Flask, HelloWorld!

from flask import Flask
app = Flask(__name__) @app.route("/")
def index():
return "Flask, HelloWorld!" if __name__ == "__main__":
app.run(host="localhost", port=5000, debug=True)

这个系列我会怎样读Flask源码?

  我会根据一个问题的回答顺序进行来进行这个系列的文章。该问题地址

  1. config原理。
  2. import原理。
  3. WSGI接口调用。
  4. 路由原理。
  5. 理解session。
  6. 理解threading.local。
  7. 理解flask自己封装的thread local。
  8. 理解g和request。
  9. 理解app context和request context。

  后续如果个人水平提高了,会从更高的层面继续这个系列。

根据官方教程, Flask中常见Config用法有以下几种。

  1. 直接赋值。
  2. 通过config的update方法一次更新多个属性值。
  3. 部分配置值可以通过属性赋值。
  4. 通过文件读取初始化config信息。

直接赋值

app = Flask(__name__)
app.config['DEBUG'] = True

  从中可以猜测config可能是一个字典或者至少提供一个通过key获取对应值的方法。

class Flask(_PackageBoundObject):
#: The class that is used for the ``config`` attribute of this app.
#: Defaults to :class:`~flask.Config`.
#:
#: Example use cases for a custom class:
#:
#: 1. Default values for certain config options.
#: 2. Access to config values through attributes in addition to keys.
#:
#: .. versionadded:: 1.0
config_class = Config def __init__(self, import_name, static_path=None, static_url_path=None,
static_folder='static', template_folder='templates',
instance_path=None, instance_relative_config=False,
root_path=None):
self.config = self.make_config(instance_relative_config) def make_config(self, instance_relative=False):
"""Used to create the config attribute by the Flask constructor.
The `instance_relative` parameter is passed in from the constructor
of Flask (there named `instance_relative_config`) and indicates if
the config should be relative to the instance path or the root path
of the application. .. versionadded:: 0.8
"""
root_path = self.root_path
if instance_relative:
root_path = self.instance_path
return self.config_class(root_path, self.default_config) def from_envvar(self, variable_name, silent=False):
pass def from_pyfile(self, filename, silent=False):
pass def from_object(self, obj):
pass def from_json(self, filename, silent=False):
pass def from_mapping(self, *mapping, **kwargs):
pass def get_namespace(self, namespace, lowercase=True, trim_namespace=True):
pass

  这是最新的Flask源代码,它出现在app.py文件中的Flask的类定义中。config_class默认值为Config类,Config类又是什么?

  在config.py文件中,有下面这样的代码块。

class Config(dict):
"""Works exactly like a dict but provides ways to fill it from files
or special dictionaries.
""" def __init__(self, root_path, defaults=None):
dict.__init__(self, defaults or {})
self.root_path = root_path

  从Config的构造函数和make_config方法中,可以看到之前猜测config是一个字典的结论确实是正确的。

通过config的update方法一次更新多个属性值。

  有了app的config属性是一个字典这个事实,通过update方法更新多个属性值就很好理解了。

app.config.update(
DEBUG=True,
SECRET_KEY='maeorwmarfomferw')
知识点

  类属性(class attribute)和对象属性(object attribute,有时候也称为实例属性)

  注意,这里config_class是一个Flask的类属性,但是却好像“被当作对象属性”使用。

self.config_class(root_path, self.default_config)

  这又是怎么一回事?首先,先来看看下面这段示例代码。

def attribute_step_001():

    class ClassAttribute(object):

        class_attribute = "class_attribute_001"

        def __init__(self):
super(ClassAttribute, self).__init__()
self.object_attribute = "object_attribute_002" class ClassAttributeWithOverrideGetAttr(object): class_attribute = "class_attribute_001" def __init__(self):
super(ClassAttributeWithOverrideGetAttr, self).__init__()
self.class_attribute = "object_attribute_001"
self.object_attribute = "object_attribute_002" def __getattr__(self, attributename):
pass print("=== two ===")
two = ClassAttributeWithOverrideGetAttr()
print(two.__dict__)
print(two.class_attribute)
print(two.class_attribute_no_exist) print("=== one ===")
one = ClassAttribute()
print(one.__dict__)
print(one.class_attribute)
print(one.class_attribute_no_exist) #tip001这一行可以注释掉,重新运行一遍这样输出比较清晰。 attribute_step_001()

  执行之后输出的结果是:

=== two ===
{'class_attribute': 'object_attribute_001', 'object_attribute': 'object_attribute_002'}
object_attribute_001
None === one ===
{'object_attribute': 'object_attribute_002'}
class_attribute_001
Traceback (most recent call last):
File "D:\work_space\Dev_For_Python\flask_hello_world\application.py", line 128, in <module>
attribute_step_001()
File "D:\work_space\Dev_For_Python\flask_hello_world\application.py", line 125, in attribute_step_001
print(one.class_attribute_no_exist)
AttributeError: 'ClassAttribute' object has no attribute 'class_attribute_no_exist'

  从结果可以发现,当获取一个对象属性时,如果它没有进行设置的话,默认返回的是这个这个对象的类型的同名类属性的值(如果同名类属性存在的话)。

实际上Python获取一个属性值(objectname.attributename)的搜索流程是(新式类):

1:如果attributename对于对象来说是一个特殊的(比如是Python提供的)属性,直接返回它。

2:从对象的__dict__(obj.__dict__)查找。

3:从对象的类型的__dict__(obj.__class__.__dict__)中查找。

4:从对象类型的基类中obj.__class__.__bases__.__dict__查找,基类的基类,直到object。如果__bases__中有多个值,最后的结果依赖于Python的方法解析顺序(MRO)。

5:如果上面都没有找到并且对象的类定义中重写__getattr__(self, attributename)方法,那么会得到对应的返回值。如果没有重写__getattr__(self, attributename)方法,Python会抛出异常AttributeError。

部分配置值可以通过属性赋值

app = Flask(__name__)
app.debug = True

  这些配置值为什么能直接通过属性赋值?答案还是在Flask类和ConfigAttribute类的定义中。

#Flask类定义代码片段
class Flask(_PackageBoundObject):
debug = ConfigAttribute('DEBUG')
testing = ConfigAttribute('TESTING')
session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME')
send_file_max_age_default = ConfigAttribute('SEND_FILE_MAX_AGE_DEFAULT',
get_converter=_make_timedelta) def __init__(self, import_name, static_path=None, static_url_path=None,
static_folder='static', template_folder='templates',
instance_path=None, instance_relative_config=False,
root_path=None):
pass #ConfigAttribute类定义代码片段
class ConfigAttribute(object):
"""Makes an attribute forward to the config""" def __init__(self, name, get_converter=None):
self.__name__ = name
self.get_converter = get_converter def __get__(self, obj, type=None):
if obj is None:
return self
rv = obj.config[self.__name__]
if self.get_converter is not None:
rv = self.get_converter(rv)
return rv def __set__(self, obj, value):
obj.config[self.__name__] = value

  在Flask类的类定义中可以看到debug被定义成一个ConfigAttribute对象。ConfigAttribute是什么东西?为什么通过app.config["debug"]=false和app.debug=false得到的效果是一样的?这得从Python的描述符说起。

知识点

  • Python描述符(也有文章称为描述器)

  什么是描述符?官方给的定义是:

In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol. Those methods are__get__(), __set__(), and __delete__(). If any of those methods are defined for an object, it is said to be a descriptor.

  简单的说,只要一个对象实现了描述符协议中的任一方法,那么就可以称之为描述符。描述符协议有哪些方法?

"""
描述符中定义的方法
"""
descriptor.__get__(self, obj, type=None) --> value
descriptor.__set__(self, obj, value) --> None
descriptor.__delete__(self, obj) --> None

  这是描述符协议的所有方法。一个对象只要重写了上面任意一个方法就可以称之为描述符。这里还有几个概念需要提的是,如果一个对象同时定义了__get__()__set__(),它叫做数据描述符(data descriptor)。仅定义了__get__()的描述符叫非数据描述符(non-data descriptor)。

"""
这是描述符的示例代码,通过代码了解下描述符。
"""
class DataDescriptor(object):
"""
这是一个数据描述符
""" def __init__(self):
pass def __get__(self, obj, objtype):
return "value from DataDescriptor" def __set__(self, obj, val):
pass def __delete__(self, obj):
pass class NoDataDescriptor(object):
"""
这是一个非数据描述符
""" def __init__(self):
pass def __get__(self, obj, objtype):
return "value from DataDescriptor" def attribute_test_001(): class ClassAttributeWithOverrideGetAttr(object): class_attribute = "class_attribute_001"
class_attribute2 = NoDataDescriptor() def __init__(self):
super(ClassAttributeWithOverrideGetAttr, self).__init__()
self.class_attribute = "object_attribute_001"
self.object_attribute = "object_attribute_002"
self.class_attribute2 = "object_attribute_003" def __getattr__(self, attributename):
return "value from __getattr__" class ClassAttribute(object): class_attribute = "class_attribute_001"
class_attribute2 = DataDescriptor() def __init__(self):
super(ClassAttribute, self).__init__()
self.object_attribute = "object_attribute_001"
self.class_attribute2 = "object_attribute_002" print("=== ClassAttributeWithOverrideGetAttr ===")
a = ClassAttributeWithOverrideGetAttr()
print("[a01]: a.__dict__ = ", a.__dict__)
print("[a02]: a.__class__.__dict__ = ", a.__class__.__dict__)
print("[a03]: a.class_attribute = ", a.class_attribute)
print("[a04]: a.class_attribute2 = ", a.class_attribute2)
print("[a05]: a.class_attribute_no_exist = ", a.class_attribute_no_exist) print("\r\n=== ClassAttribute ===")
b = ClassAttribute()
print("[b01]: b.__dict__ = ", b.__dict__)
print("[b02]: b.__class__.__dict__ = ", b.__class__.__dict__)
print("[b03]: b.class_attribute = ", b.class_attribute)
print("[b04]: b.class_attribute2 = ", b.class_attribute2)
print("[b05]: b.class_attribute_no_exist = ", b.class_attribute_no_exist)
attribute_test_001()

  代码的输出结果是:

=== ClassAttributeWithOverrideGetAttr ===
[a01]: a.__dict__ = {'class_attribute2': 'object_attribute_003', 'class_attribute': 'object_attribute_001', 'object_attribute': 'object_attribute_002'}
[a02]: a.__class__.__dict__ = {'__dict__': <attribute '__dict__' of 'ClassAttributeWithOverrideGetAttr' objects>, '__weakref__': <attribute '__weakref__' of 'ClassAttributeWithOverrideGetAttr' objects>, '__getattr__': <function attribute_test_001.<locals>.ClassAttributeWithOverrideGetAttr.__getattr__ at 0x01929F60>, '__module__': '__main__', '__doc__': None, 'class_attribute2': <__main__.NoDataDescriptor object at 0x0192ED70>, 'class_attribute': 'class_attribute_001', '__init__': <function attribute_test_001.<locals>.ClassAttributeWithOverrideGetAttr.__init__ at 0x01929ED0>}
[a03]: a.class_attribute = object_attribute_001
[a04]: a.class_attribute2 = object_attribute_003
[a05]: a.class_attribute_no_exist = value from __getattr__ === ClassAttribute ===
[b01]: b.__dict__ = {'object_attribute': 'object_attribute_001'}
[b02]: b.__class__.__dict__ = {'__dict__': <attribute '__dict__' of 'ClassAttribute' objects>, '__weakref__': <attribute '__weakref__' of 'ClassAttribute' objects>, '__module__': '__main__', '__doc__': None, 'class_attribute2': <__main__.DataDescriptor object at 0x0192EDD0>, 'class_attribute': 'class_attribute_001', '__init__': <function attribute_test_001.<locals>.ClassAttribute.__init__ at 0x01929FA8>}
[b03]: b.class_attribute = class_attribute_001
[b04]: b.class_attribute2 = value from DataDescriptor
Traceback (most recent call last):
File "D:\work_space\Dev_For_Python\flask_hello_world\hello.py", line 104, in <module>
attribute_test_001()
File "D:\work_space\Dev_For_Python\flask_hello_world\hello.py", line 101, in attribute_test_001
print("[b05]: b.class_attribute_no_exist = ", b.class_attribute_no_exist)
AttributeError: 'ClassAttribute' object has no attribute 'class_attribute_no_exist'
[Finished in 0.1s]

从两组输出我们可以得出的结论有:

1: 对比a01, a02, a03 ===> 实例字典和类属性中都存在同样key的时候,实例字典(obj.__dict__) > 类属性(obj.__class__.__dict__)

2: 对比b01, b02, b03 ===> 实例字典不存在key的时候,会返回同名key的类属性的值。

3: 对比a05, b05 ===> 实例字典和类属性都不存在key的时候,会返回重写的(__getattr__)函数的返回值,如果没有重写的话,会抛出异常AttributeError。

4: 对比a04, a04 ===> 实例字典和类属性都存在key的时候,数据描述符 > 实例字典(obj.__dict__) > 非数据描述符。

  描述符的调用逻辑。

  当Python获取一个属性时(objectname.attributename),** 发现要获取的属性是描述符时 **,它的搜索流程会有所改变,转而调用描述的方法。注意,描述符只适用于新式的类和对象。

  1. 如果attributename对于对象来说是一个特殊的(比如是Python提供的)属性,直接返回它。
  2. objectname.__class__.__dict__查找attributename,如果找到并且attributename是一个** 数据描述符 **,那么就返回数据描述符的执行结果。(objectname.__class__.__dict__["attributename"].__get__(objectname, type(objectname)))。在objectname.__class__全部基类中进行同样的搜索。
  3. 从对象的__dict__(objectname.__dict__)查找,找到就返回,不管是否为数据描述符。唯一的区别是,如果是数据描述符就返回描述符(__get__逻辑的返回值)。
  4. 从对象的类型的__dict__(objectname.__class__.__dict__)中查找。
  5. 从对象类型的基类中objectname.__class__.__bases__.__dict__查找,找到就返回,不管是否为数据描述符。唯一的区别是,如果是非数据描述符就返回描述符(__get__逻辑的返回值)。PS:这里不用考虑搜索到数据描述符的情况,因为第二步已经把所有数据描述符的情况考虑在内了。
  6. 如果上面都没有找到并且对象的类定义中重写__getattr__(self, attributename)方法,那么会得到对应的返回值。如果没有重写__getattr__(self, attributename)方法,Python会抛出异常AttributeError。

  通过上面的知识点,可以清楚的知道,首先app.debug是一个数据描述符,其次:当通过app.debug = True对配置值就行修改的时候,实际上调用的是描述符的逻辑type(app).__dict__["debug"].__get__(app, type(app)),最后通过ConfigAttribute中重写的__get__逻辑,可以看出还是修改了app.config字典中key为debug的值。

  最后,对Python涉及到的几点进行总结。

1:在没有描述符出现的的情况下,实例字典(obj.__dict__) > 类属性(obj.__class__.__dict__) > __getattr__()方法 > 抛出异常AttributeError

2:数据描述符 > 实例字典(obj.__dict__) > 非数据描述符。

3:Python中有几个内置的描述符:函数,属性(property), 静态方法(static method) 感兴趣的自行查找相关文章研究下。

通过文件读取初始化config信息。

app = Flask(__name__)
app.config.from_object('yourapplication.default_settings')
app.config.from_envvar('YOURAPPLICATION_SETTINGS')

通过上面这几种方法初始化config信息的源代码都相对简单。个人觉得没有什么好分析的。

"""
这是Flask中通过文件或者对象初始化涉及到的源代码
"""
def from_envvar(self, variable_name, silent=False):
"""Loads a configuration from an environment variable pointing to
a configuration file. This is basically just a shortcut with nicer
error messages for this line of code:: app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) :param variable_name: name of the environment variable
:param silent: set to ``True`` if you want silent failure for missing
files.
:return: bool. ``True`` if able to load config, ``False`` otherwise.
"""
rv = os.environ.get(variable_name)
if not rv:
if silent:
return False
raise RuntimeError('The environment variable %r is not set '
'and as such configuration could not be '
'loaded. Set this variable and make it '
'point to a configuration file' %
variable_name)
return self.from_pyfile(rv, silent=silent) def from_pyfile(self, filename, silent=False):
"""Updates the values in the config from a Python file. This function
behaves as if the file was imported as module with the
:meth:`from_object` function. :param filename: the filename of the config. This can either be an
absolute filename or a filename relative to the
root path.
:param silent: set to ``True`` if you want silent failure for missing
files. .. versionadded:: 0.7
`silent` parameter.
"""
filename = os.path.join(self.root_path, filename)
d = types.ModuleType('config')
d.__file__ = filename
try:
with open(filename) as config_file:
exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
self.from_object(d)
return True def from_object(self, obj):
"""Updates the values from the given object. An object can be of one
of the following two types: - a string: in this case the object with that name will be imported
- an actual object reference: that object is used directly Objects are usually either modules or classes. Just the uppercase variables in that object are stored in the config.
Example usage:: app.config.from_object('yourapplication.default_config')
from yourapplication import default_config
app.config.from_object(default_config) You should not use this function to load the actual configuration but
rather configuration defaults. The actual config should be loaded
with :meth:`from_pyfile` and ideally from a location not within the
package because the package might be installed system wide. :param obj: an import name or object
"""
if isinstance(obj, string_types):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key) def from_json(self, filename, silent=False):
"""Updates the values in the config from a JSON file. This function
behaves as if the JSON object was a dictionary and passed to the
:meth:`from_mapping` function. :param filename: the filename of the JSON file. This can either be an
absolute filename or a filename relative to the
root path.
:param silent: set to ``True`` if you want silent failure for missing
files. .. versionadded:: 1.0
"""
filename = os.path.join(self.root_path, filename) try:
with open(filename) as json_file:
obj = json.loads(json_file.read())
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
return self.from_mapping(obj) def from_mapping(self, *mapping, **kwargs):
"""Updates the config like :meth:`update` ignoring items with non-upper
keys. .. versionadded:: 1.0
"""
mappings = []
if len(mapping) == 1:
if hasattr(mapping[0], 'items'):
mappings.append(mapping[0].items())
else:
mappings.append(mapping[0])
elif len(mapping) > 1:
raise TypeError(
'expected at most 1 positional argument, got %d' % len(mapping)
)
mappings.append(kwargs.items())
for mapping in mappings:
for (key, value) in mapping:
if key.isupper():
self[key] = value
return True

在项目中如何使用初始化config?

推荐一篇相关文章

最后分享下写这篇文章学习到的几个Markdown的小技巧。

  1. 段落缩进使用.
&emsp;&emsp;
  1. 类似__set__, __delete__, __init__ 双下划线的文本,而且又不想放在代码块的情况。可以把在下划线前面加上\,这样就不会被吃了。

参考资料

  1. Descriptior HowTo Guide
  2. Python Types and Objects
  3. Python Attributes and Methods
  4. 编写高质量代码 改善Python程序的91个建议 (第58,59,60建议)这里有关于属性拦截和获取更详细的解读,当中涉及到__getattribute__, __getattr__

读Flask源代码学习Python--config原理的更多相关文章

  1. python flask框架学习(二)——第一个flask程序

    第一个flask程序 学习自:知了课堂Python Flask框架——全栈开发 1.用pycharm新建一个flask项目 2.运行程序 from flask import Flask # 创建一个F ...

  2. 在学习python的Django\Flask\Tornado前你需要知道的,what is web?

    我们都在讲web开发web开发,那到底什么是web呢? 如果你正在学习python三大主流web框架,那这些你必须要知道了 软件开发架构: C/S架构:Client/Server    客户端与服务端 ...

  3. 关于sqlmap当中tamper脚本编码绕过原理的一些总结(学习python没多久有些地方肯定理解有些小问题)

    sqlmap中tamper脚本分析编写 置十对一些编码实现的脚本,很多sqlmap里面需要引用的无法实现,所以有一部分例如keywords就只写写了几个引用了一下,其实这里很多脚本运用是可以绕过安全狗 ...

  4. 编程零基础应当如何开始学习 Python?

    提前说一下,这篇福利多多,别的不说,直接让你玩回最有手感的怀旧游戏,参数贴图很方便自己可以根据喜好修改哦. 本篇通过以下四块展开,提供大量资源对应. 选一个好版本 有没有看过<在下坂本,有何贵干 ...

  5. 学习Python要知道哪些重要的库和工具

    本文转自:https://github.com/jobbole/awesome-python-cn 环境管理 管理 Python 版本和环境的工具 p:非常简单的交互式 python 版本管理工具. ...

  6. 国内某Python大神自创完整版,系统性学习Python

    很多小伙伴纠结于这个一百天的时间,我觉得完全没有必要,也违背了我最初放这个大纲上来的初衷,我是觉得这个学习大纲还不错,自学按照这个来也能相对系统的学习知识,而不是零散细碎的知识最后无法整合,每个人的基 ...

  7. 学习 Python,这 22 个包怎能不掌握?

    如今全球各个行业内 Python 的使用状况怎么样呢? 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手.很多已经做案例的人,却不知道如何去 ...

  8. 学习Python的三种境界

    前言 王国维在<人间词话>中将读书分为了三种境界:"古今之成大事业.大学问者,必经过三种之境界:'昨夜西风凋碧树,独上高楼,望尽天涯路'.此第一境也.'衣带渐宽终不悔,为伊消得人 ...

  9. 学习Python的第一课(简单的单元测试)

    由于有C#开发基础,感觉学习Python应该不难,主要是一些语法了,再加上现在互联网这么发达. 感觉还是要有思路,否则学什么也只能是什么. 话不多说,简单发下这几天的学习成果吧: 第一次写博客,大家不 ...

随机推荐

  1. sql linq lambda 对比

    . 查询Student表中的所有记录的Sname.Ssex和Class列. select sname,ssex,class from student Linq: from s in Students ...

  2. hdu2222Keywords Search

    Problem Description In the modern time, Search engine came into the life of everybody like Google, B ...

  3. linux自旋锁

    一.前言 在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机 ...

  4. Nhibernate 映射关系,一对多 多对一与多对手在映射文件中的体现。

    今天做了第一个Nhibernate项目,摸着石头过河,学到了一些东西,在这里将自己总结体会到的一些映射关系写出来,与大家分享,由于是初学者,如果有不对的地方希望大家能够指出来. 首先要说明要建立的几张 ...

  5. SQL Server 查看指定表上的索引

    解决方案: sys.indexs; ---------------------------------------------------------------------------------- ...

  6. 银行卡检测中心BCTC

    BCTC是Banking Card Test Center的缩写. 银行卡检测中心(下称中心)经中国人民银行总行批准成立于1998年4月,作为一个独立的第三方专业技术检测机构,其主要职责是按照国际.国 ...

  7. 海康威视研究院ImageNet2016竞赛经验分享

    原文链接:https://zhuanlan.zhihu.com/p/23249000 目录 场景分类 数据增强 数据增强对最后的识别性能和泛化能力都有着非常重要的作用.我们使用下面这些数据增强方法. ...

  8. 滴滴专车司机苹果手机ios客户端可以下载了

    申请过滴滴专车司机的朋友都知道,滴滴专车就是滴滴打车的司机端,但是只有安卓的,一直没有苹果IOS的版本,很多申请通过审核的车主恼于没有IOS版本而暂无法使用滴滴专车司机客户端,也就意味着不能上线接单. ...

  9. Android消息推送的服务端

    2.Android消息推送 MQTT服务器采用mosquito  http://mosquitto.org/ PHP管理包采用phpmqttclient:https://github.com/toku ...

  10. SharePoint 2013 讨论板列表"Connect to Outlook" 不可用解决方案

    本文讲述 SharePoint 2013 讨论板列表"Connect to Outlook" 不可用解决方案. SharePoint中的讨论板列表是可以集成到Outlook里面去的 ...