2015年7月30日星期四

利用saltstack 部署lnmp环境(yum版本)

本邮件内容由第三方提供,如果您不想继续收到该邮件,可 点此退订
利用saltstack 部署lnmp环境(yum版本)  阅读原文»

利用saltstack 部署lnmp环境(yum版本)

例行吐槽:有年月没更新博客了,最近太浮躁了,以前一起工作的小伙伴都找到新的东家了,薪资条那叫一个长,不开森了,都不带我[j_0004.gif]

#######################分隔线####################

一、简介

saltstack 是一个新基础设施管理工具,可以看做是强化的Func+弱化puppet的组合,间接的反映出了saltstack的两大功能:远程执行命令与配置管理,

saltstack是使用python开发的,非常简单易用和轻量级的管理工具,由master和minion构成,通过ZeroMQ进行通信

二、安装

安装时需要epel源的支持,请自行安装与当系统匹配的epel源

*:安装依赖包

  yum install python-jinja2 -y  

1、安装master端

  yum -y install salt-master enablerepr=epel-testing  

2、安装minion端

  yum -y install salt-minion enablerepr=epel-testing  

三、配置环境

a、master

1、修改本地绑定地址

  sed -ie 's/^#.*interface:.*/\  interface:  192.168.2.65/g' /etc/salt/master  

注:或是此处写上主机名,并绑定/etc/hosts文件

2、自动接收所有minion的请求

  sed -ie 's@^#\(auto_accept: \)False@\  \1true@g' /etc/salt/master  

b、minion

1、指向master

  sed -ie 's@^#.*master:.*@\  master:  salt@g' /etc/salt/minion  

注:由于saltstack配置文件所限,当启用每一个配置参数时对格式有严格要求,书写时请注意

四、环境测试

1、分别在master/minion中启动服务

  #service salt-master start  #service salt-minion start  

说明:

saltstack的master监听于4505,minion监听于4506;

可以将master与minion部署同一台服务器上(没有什么意义[哈哈~])。

2、salt 测试

  [root@openapi php-fpm]# salt "*" test.ping  192.168.2.36-CentOS.backup.test.backend:      True  192.168.2.30-centos.public.test:      True  

说明:salt的其它命令可用salt --help很简单,在此不多介绍

五、salt 常用的正则表达式(部分)

a、E:正则匹配

wKioL1W4_f7i-jLPAACz8jBS0LA203.jpg

可以在每一个句后面加一个 -l debug来显示命令具体执行过程

  [root@openapi self_userd]# salt -E '(backend81)' test.ping  backend81:      True  [root@openapi self_userd]# salt -E '(backend81)' test.ping -l debug  [DEBUG   ] Reading configuration from /etc/salt/master  [DEBUG   ] Guessing ID. The id can be explicitly in set /etc/salt/minion   Found minion id from generate_minion_id(): openapi.test.dns.com.cn  [DEBUG   ] Missing configuration file: ~/.saltrc  [DEBUG   ] Configuration file path: /etc/salt/master  [DEBUG   ] Reading configuration from /etc/salt/master  [DEBUG   ] Guessing ID. The id can be explicitly in set /etc/salt/minion   Found minion id from generate_minion_id(): openapi.test.dns.com.cn  [DEBUG   ] Missing configuration file: ~/.saltrc  [DEBUG   ] MasterEvent PUB socket URI: ipc:///var/run/salt/master/master_event_pub.ipc  [DEBUG   ] MasterEvent PULL socket URI: ipc:///var/run/salt/master/master_event_pull.ipc  [DEBUG   ] LazyLoaded local_cache.get_load  [DEBUG   ] get_iter_returns for jid 20150727212822728009 sent to set(['backend81']) will timeout at 21:28:27.732956  [DEBUG   ] jid 20150727212822728009 return from backend81  backend81:      True  [DEBUG   ] jid 20150727212822728009 found all minions set(['backend81'])  

b、-L :命令行里面一般是以列表的形式来指定对象的。

wKioL1W4_wXxyIZXAACFo_8-OSc489.jpg

c、-G:这个参数很强大,会根据默认的grain的结果来过滤。(grains也可以自己定义)

wKiom1W4_X6xGjf0AACqkBNK2kM670.jpg

wKioL1W4_4Oiz6lcAACmVYdhGlY139.jpg

d、-N:这个参数是基于分组的,前提是你得先分好组。(分组可以定义于主匹配文件/etc/salt/master中,也可以定义于/etc/salt/master.d/*.conf)

wKioL1W5ADKxFDpLAAHISTdTPI8672.jpg

然后可以这样使用

wKiom1W4_mOzPyZWAABgST-EzN4712.jpg

e、-C :表示tagger可是一个复合语句

  [root@openapi salt]# salt -C 'G@virtual:physical and E@backend81' test.ping  backend81:      True  

f、-b :一次操作多少台,也可以使使用百分比来操作(--batch-size)

  [root@openapi salt]# salt "*" -b 3 grains.item os  #salt "*" --batch-size 25%  grains.item os  36 Detected for this batch run  backend81 Detected for this batch run  backend84 Detected for this batch run  zabbix.server.dns.com.cn Detected for this batch run  backend83 Detected for this batch run  webdata.backup Detected for this batch run  Executing run on ['zabbix.server.dns.com.cn', 'webdata.backup', 'backend84']  webdata.backup:      ----------      os:          CentOS  backend84:      ----------      os:          CentOS  zabbix.server.dns.com.cn:      ----------      os:          CentOS  Executing run on ['backend83', 'backend81', '36']  backend81:      ----------      os:          CentOS  backend83:      ----------      os:          CentOS  36:      ----------      os:          CentOS  

注:

更多的模块使用说明可以使用

  salt \* sys.doc |grep <模块名称>  

salt 的每一个子命令都可以用-d来查看具体的用法

六、自定义grains

1、grains的优先级

grains可以保持在minion端、通过master端下发等多个方式来分发。但不同的方法有不同的优先级的:

a. /etc/salt/grains

b. /etc/salt/minion

c./srv/salt/_grains/ master端_grains目录下

优先级顺序依次为存在在minion端/etc/salt/minion配置文件中的同名grains会覆盖/etc/salt/grains文件中的值,而通过master端_grains目录下grains文件下发的值可以会覆盖minion端的所有同名值。比较拗口,总之记得,通过master下发的grains优先级是最高的可,/etc/salt/minion次之,/etc/salt/grains最低(core grains不大懂,就不讨论了,这个比/etc/salt/grains还低)

2、自定义grains

注:

利用Java注解将常量类生成js文件供前端调用  阅读原文»

利用Java注解将常量类生成js文件供前端调用

  注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记,没加,则等于没有某种标记,以后,javac编译器,开发工具和其他程序可以用反射来了解你的类及各种元素上有无何种标记,看你有什么标记,就去干相应的事标记可以加在包,类,字段,方法,方法的参数以及局部变量上。

1)定义一个最简单的注解

public@interfaceMyAnnotation{

2)把注解加在某个类上:

publicclassAnnotationTest{

  我们的常量类注解如下:

packagecom.gaochao.platform.util;
importjava.lang.annotation.ElementType;
importjava.lang.annotation.Retention;
importjava.lang.annotation.RetentionPolicy;
importjava.lang.annotation.Target;
*@authorchao.gao
*@date2013-12-20下午1:02:02
*@version<b>1.0.0</b>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public@interfaceConstantsText{
*数据字典的label,获取中文含义时使用
*@authorchao.gao
*@date2013-12-20下午1:17:19
publicStringtermsLable()default"";
*当数据字典中不存在对面的中文时使用,手动设置
*@authorchao.gao
*@date2013-12-20下午1:17:40
publicStringtext()default"";

  注解的上方有两个元注解,是注解的注解,含义及用法如下:

//Java中提供了四种元注解,专门负责注解其他的注解,分别如下//@Retention元注解,表示需要在什么级别保存该注释信息(生命周期)。可选的RetentionPoicy参数包括:
//RetentionPolicy.SOURCE: 停留在java源文件,编译器被丢掉
//RetentionPolicy.CLASS:停留在class文件中,但会被VM丢弃(默认)
//RetentionPolicy.RUNTIME:内存中的字节码,VM将在运行时也保留注解,因此可以通过反射机制读取注解的信息//@Target元注解,默认值为任何元素,表示该注解用于什么地方。可用的ElementType参数包括
//ElementType.CONSTRUCTOR: 构造器声明
//ElementType.FIELD: 成员变量、对象、属性(包括enum实例)
//ElementType.LOCAL_VARIABLE: 局部变量声明
//ElementType.METHOD: 方法声明
//ElementType.PACKAGE: 包声明
//ElementType.PARAMETER: 参数声明
//ElementType.TYPE: 类、接口(包括注解类型)或enum声明

//@Documented将注解包含在JavaDoc中//@Inheried允许子类继承父类中的注解

在常量类中应用注解:

publicclassRelationshipConstants{
publicstaticfinalStringSTATUS_SAVED="saved";
publicstaticfinalStringSTATUS_FINISHED="finished";
publicstaticfinalintHAVE_YES=1;
publicstaticfinalintHAVE_NO=0;

  添加注解后

@ConstantsText(text="我是手动审核")
publicstaticfinalStringPLANSTATUS_AUDITED="audited";
*计划状态:已作废,从数据字典查找
@ConstantsText(termsLable="planStatus")
publicstaticfinalStringPLANSTATUS_CANCELED="canceled";
@ConstantsText(termsLable="planStatus")
publicstaticfinalStringPLANSTATUS_FINISHED="finished";

阅读更多内容

IOS 通过摄像头读取每一帧的图片,并且做识别做人脸识别(swift) - qg

本邮件内容由第三方提供,如果您不想继续收到该邮件,可 点此退订
IOS 通过摄像头读取每一帧的图片,并且做识别做人脸识别(swift) - qg  阅读原文»

最近帮别人做一个项目,主要是使用摄像头做人脸识别

github地址:https://github.com/qugang/AVCaptureVideoTemplate

要使用IOS的摄像头,需要使用AVFoundation 库,库里面的东西我就不介绍。

启动摄像头需要使用AVCaptureSession 类。

然后得到摄像头传输的每一帧数据,需要使用AVCaptureVideoDataOutputSampleBufferDelegate 委托。

首先在viewDidLoad 里添加找摄像头设备的代码,找到摄像头设备以后,开启摄像头

captureSession.sessionPreset = AVCaptureSessionPresetLow
let devices
= AVCaptureDevice.devices()
for device in devices {
if (device.hasMediaType(AVMediaTypeVideo)) {
if (device.position == AVCaptureDevicePosition.Front) {
captureDevice
= device as?AVCaptureDevice
if captureDevice != nil {
println(
"Capture Device found")
beginSession()
}
}
}
}

beginSession,开启摄像头:

func beginSession() {
var err : NSError
? = nil
captureSession.addInput(AVCaptureDeviceInput(device: captureDevice, error:
&err))
let output
= AVCaptureVideoDataOutput()

let cameraQueue
= dispatch_queue_create("cameraQueue", DISPATCH_QUEUE_SERIAL)
output.setSampleBufferDelegate(self, queue: cameraQueue)
output.videoSettings
= [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA]
captureSession.addOutput(output)


if err != nil {
println(
"error: \(err?.localizedDescription)")
}
previewLayer
= AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer
?.videoGravity = "AVLayerVideoGravityResizeAspect"
previewLayer
?.frame = self.view.bounds
self.view.layer.addSublayer(previewLayer)

captureSession.startRunning()
}

开启以后,实现captureOutput 方法:

func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) {

if(self.isStart)
{
let resultImage
= sampleBufferToImage(sampleBuffer)

let context
= CIContext(options:[kCIContextUseSoftwareRenderer:true])
let detecotr
= CIDetector(ofType:CIDetectorTypeFace, context:context, options:[CIDetectorAccuracy: CIDetectorAccuracyHigh])




let ciImage
= CIImage(image: resultImage)

let results:NSArray
= detecotr.featuresInImage(ciImage,options: ["CIDetectorImageOrientation" : 6])

for r in results {
let face:CIFaceFeature
= r as! CIFaceFeature;
let faceImage
= UIImage(CGImage: context.createCGImage(ciImage, fromRect: face.bounds),scale: 1.0, orientation: .Right)

NSLog(
"Face found at (%f,%f) of dimensions %fx%f", face.bounds.origin.x, face.bounds.origin.y,pickUIImager.frame.origin.x, pickUIImager.frame.origin.y)

dispatch_async(dispatch_get_main_queue()) {
if (self.isStart)
{
self.dismissViewControllerAnimated(
true, completion: nil)
self.didReceiveMemoryWarning()

self.callBack
!(face: faceImage!)
}
self.isStart
= false
}
}
}
}

在每一帧图片上使用CIDetector 得到人脸,CIDetector 还可以得到眨眼,与微笑的人脸,如果要详细使用去官方查看API

上面就是关键代码,设置了有2秒的延迟,2秒之后开始人脸检测。

全部代码:

//
// ViewController.swift
// AVSessionTest
//
// Created by qugang on 15/7/8.
// Copyright (c) 2015年 qugang. All rights reserved.
//

import UIKit
import AVFoundation

class AVCaptireVideoPicController: UIViewController,AVCaptureVideoDataOutputSampleBufferDelegate {

var callBack :((face: UIImage)
->())?
let captureSession
= AVCaptureSession()
var captureDevice : AVCaptureDevice
?
var previewLayer : AVCaptureVideoPreviewLayer
?
var pickUIImager : UIImageView
= UIImageView(image: UIImage(named: "pick_bg"))
var line : UIImageView
= UIImageView(image: UIImage(named: "line"))
var timer : NSTimer
!
var upOrdown
= true
var isStart
= false


override func viewDidLoad() {
super.viewDidLoad()

captureSession.sessionPreset
= AVCaptureSessionPresetLow
let devices
= AVCaptureDevice.devices()
for device in devices {
if (device.hasMediaType(AVMediaTypeVideo)) {
if (device.position == AVCaptureDevicePosition.Front) {
captureDevice
= device as?AVCaptureDevice
if captureDevice != nil {
println(
"Capture Device foundWebdriver配合Tesseract-OCR 自动识别简单的验证码 - to be crazy  阅读原文»


验证码: 如下,在进行自动化测试,遇到验证码的问题,一般有两种方式

1.找开发去掉验证码或者使用万能验证码

2.使用OCR自动识别


使用OCR自动化识别,一般识别率不是太高,处理一般简单验证码还是没问题

这里使用的是Tesseract-OCR,下载地址:https://github.com/A9T9/Free-Ocr-Windows-Desktop/releases

怎么使用呢?

进入安装后的目录:

tesseract.exe test.png test -1


准备一份网页,上面使用该验证码

<html>
<head>
<title>Table test by Young</title>
</head>
<body>
</br>
<h1> Test </h1>
<img src="http://csujwc.its.csu.edu.cn/sys/ValidateCode.aspx?t=1">
</br>
</body>
</html>

要识别验证码,首先得取得验证码,这两款采取对 页面元素部分截图的方式,首先获取整个页面的截图

然后找到页面元素坐标进行截取

/**
* This method for screen shot element
*
*
@param driver
*
@param element
*
@param path
*
@throws InterruptedException
*/
public static void screenShotForElement(WebDriver driver,
WebElement element, String path)
throws InterruptedException {
File scrFile
= ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.FILE);
try {
Point p
= element.getLocation();
int width = element.getSize().getWidth();
int height = element.getSize().getHeight();
Rectangle rect
= new Rectangle(width, height);
BufferedImage img
= ImageIO.read(scrFile);
BufferedImage dest
= img.getSubimage(p.getX(), p.getY(),
rect.width, rect.height);
ImageIO.write(dest,
"png", scrFile);
Thread.sleep(
1000);
FileUtils.copyFile(scrFile,
new File(path));
}
catch (IOException e) {
e.printStackTrace();
}
}

截取完元素,就可以调用Tesseract-OCR生成text

// use Tesseract to get strings
Runtime rt = Runtime.getRuntime();
rt.exec(
"cmd.exe /C tesseract.exe D:\\Tesseract-OCR\\test.png D:\\Tesseract-OCR\\test -1 ");

接下来通过java读取txt

/**
* This method for read TXT file
*
*
@param filePath
*/
public static void readTextFile(String filePath) {
try {
String encoding
= "GBK";
File file
= new File(filePath);
if (file.isFile() && file.exists()) { // 判断文件是否存在
InputStreamReader read = new InputStreamReader(
new FileInputStream(file), encoding);// 考虑到编码格式
BufferedReader bufferedReader = new BufferedReader(read);
String lineTxt
= null;
while ((lineTxt = bufferedReader.readLine()) != null) {
System.out.println(lineTxt);
}
read.close();
}
else {
System.out.println(
"找不到指定的文件");
}
}
catch (Exception e) {
System.out.println(
"读取文件内容出错");
e.printStackTrace();
}
}

整体代码如下:

1 package com.dbyl.tests;
2
3 import java.awt.Rectangle;
4 import java.awt.image.BufferedImage;
5 import java.io.BufferedReader;
6 import java.io.File;
7 import java.io.FileInputStream;
8 import java.io.IOException;
9 import java.io.InputStreamReader;
10 import java.io.Reader;
11 import java.util.concurrent.TimeUnit;
阅读更多内容

2015年7月29日星期三

keshen fuel pump

放置简短标题

Dear Friend,

How are you?

 

I am sorry to disturb you, Kindly pls give me five minutes to introduce our company, maybe in this short time you can choose a right company which you always searching.

 

Our Company was founded in 2001, Main products are Fuel pumps. it is located in Wenzhou City Zhengjiang province, The products are marketed and distributed in Asia,Iran,Turkey,India, Dubai , Europe,North America and Russia. We hold high regards on the R& D of products and improvement of products quality. we have complete inspection system and machining production line and assembling line for fuel pump which fully ensure the quality and durability of products. Now, let me explain our products as below to make sure more clear for its performance:

 

- Reliable , Durable , Excellent Performance On Hot Weather.
- Custom Design Your Product Based On Your Own Requirement
- Specialize In In-line Pump And Carbon Commutator Pump

 

Each company want to choose good partner, I dont want explain too much for our quality, because that should be improved by fuel pump itself, if you tested it, the result will show you everything, especially for our lift fuel pump, these products are supper star fuel pump in our factory with low noise and No.1 class performance, most of customer have deeply interesting and satisfied with its high quality.

 

Attached file only send you to reference, All the famous and big show Keshen will join not only to promote our brand in domestic and oversea market but also for meet our esteem customer, for us they are not only customer, due to years communication and mutual trust, Keshen really consider them as our group and family member.

 

Hence we sincerely invite you to join Keshen Ship together, with you, we can move more and long distance in the blue sea.

 

OK, five minutes passing by, Hope you are not feel upset to read my long mail. May your life full of colorful sunshine every day. Wish splendid smile are always there on your face to defeat all the tough troubles/

 

Look forward to getting your quickly response

 

Sincerely yours

 

Best regards!

  

James Lee

 

KeShen Fuel Pump

Ruian Keda Auto Parts Co.,Ltd.

ADD:139Tianfeng STR.,Tangxia Town,Ruian City,Zhejiang Province,China.

Mobile phone:0086-13967739698

Tel:0086-577-65371750

SKYPE:keshenfuelpumplee

WECHAT:13967739698

QQ:501806228

www.keshen.cn

 

这是一个文字居中,带有边框的文本块
设计您的邮件
创建一个优雅的邮件很简单的
当您选择了一个模板之后,您可以在邮件编辑区域右侧选择您想要的部件,将之拖放到左侧编辑区域。选中编辑"块",单击"设计"来定义字体、颜色和风格。

If you no longer wish to receive such messages, you can click here to unsubscribe|Report as spam

2015年7月27日星期一

Memcached简介 - loveis715

本邮件内容由第三方提供,如果您不想继续收到该邮件,可 点此退订
Memcached简介 - loveis715  阅读原文»

  在Web服务开发中,服务端缓存是服务实现中所常常采用的一种提高服务性能的方法。其通过记录某部分计算结果来尝试避免再次执行得到该结果所需要的复杂计算,从而提高了服务的运行效率。

  除了能够提高服务的运行效率之外,服务端缓存还常常用来提高服务的扩展性。因此一些大规模的Web应用,如Facebook,常常构建一个庞大的服务端缓存。而它们所最常使用的就是Memcached。

  在本文中,我们就将对Memcached进行简单地介绍。

Memcached简介

  在介绍Memcached之前,让我们首先通过一个示例了解什么是服务端缓存。

  相信大家都玩过一些网络联机游戏吧。在我那个年代(03年左右),这些游戏常常添加了对战功能,并提供了天梯来显示具有最优秀战绩的玩家以及当前玩家在天梯系统中的排名。这是游戏开发商所常常采用的一种聚拢玩家人气的手段。而希望在游戏中证明自己的玩家则会由此激发斗志,进而花费更多时间来在天梯中取得更好的成绩。

  就天梯系统来说,其最主要的功能就是为玩家提供天梯排名的信息,而并不允许玩家对该系统中所记录的数据作任何修改。这样设定的结果就是,整个天梯系统的读操作居多,而写操作很少。反过来,由于一个游戏中的玩家可能有上千万甚至上亿人,而且在线人数常常达到上万人,因此对天梯的访问也会是非常频繁的。这样的话,即使每秒钟只有10个人访问天梯中的排名,对这上亿个玩家的天梯排名进行读取及排序也是一件非常消耗性能的事情。

  一个自然而然的想法就是:在对天梯排名进行一次计算后,我们在服务端将该天梯排名缓存起来,并在其它玩家访问的时候直接返回该缓存中所记录的结果。而在一定时间段之后,如一个小时,我们再对缓存中的数据进行更新。这样我们就不再需要每个小时执行成千上万次的天梯排名计算了。

  而这就是服务端缓存所提供的最重要功能。其既可以提高单个请求的响应速度,又可以降低服务层及数据库层的压力。除此之外,多个服务实例都可以读取该服务端缓存所缓存的信息,因此我们也不再需要担心这些数据在各个服务实例中都保存了一份进而需要彼此同步的问题,也即是提高了扩展性。

  而Memcached就是一个使用了BSD许可的服务端缓存实现。但是与其它服务端缓存实现不同的是,其主要由两部分组成:独立运行的Memcached服务实例,以及用于访问这些服务实例的客户端。因此相较于普通服务端缓存实现中各个缓存都运行在服务实例之上的情况,Memcached服务实例则是在服务实例之外独立运行的:

  从上图中可以看出,由于Memcached缓存实例是独立于各个应用服务器实例运行的,因此应用服务实例可以访问任意的缓存实例。而传统的缓存则与特定的应用实例绑定,因此每个应用实例将只能访问特定的缓存。这种绑定一方面会导致整个应用所能够访问的缓存容量变得很小,另一方面也可能导致不同的缓存实例中存在着冗余的数据,从而降低了缓存系统的整体效率。

  在运行时,Memcached服务实例只需要消耗非常少的CPU资源,却需要使用大量的内存。因此在决定如何组织您的服务端缓存结构之前,您首先需要搞清当前服务中各个服务实例的负载情况。如果一个服务器的CPU使用率非常高,却存在着非常多的空余内存,那么我们就完全可以在其上运行一个Memcached实例。而如果当前服务中的所有服务实例都没有过多的空余内存,那么我们就需要使用一系列独立的服务实例来搭建服务端缓存。一个大型服务常常拥有上百个Memcached实例。而在这上百个Memcached实例中所存储的数据则不尽相同。由于这种数据的异构性,我们需要在访问由Memcached所记录的信息之前决定在该服务端缓存系统中到底由哪个Memcached实例记录了我们所想要访问的数据:

  如上图所示,用户需要通过一个Memcached客户端来完成对缓存服务所记录信息的访问。该客户端知道服务端缓存系统中所包含的所有Memcached服务实例。在需要访问具有特定键值的数据时,该客户端内部会根据所需要读取的数据的键值,如“foo”,以及当前Memcached缓存服务的配置来计算相应的哈希值,以决定到底是哪个Memcached实例记录了用户所需要访问的信息。在决定记录了所需要信息的Memcached实例之后,Memcached客户端将从配置中读取该Memcached服务实例所在地址,并向该Memcached实例发送数据访问请求,以从该Memcached实例中读取具有键值“foo”的信息。在各个论坛的讨论中,这被称为是Memcached的两阶段哈希(Two-stage hash)。

  而对数据的记录也使用了类似的流程:假设用户希望通过服务端缓存记录数据“bar”,并为其指定键值“foo”。那么Memcached客户端将首先对用户所赋予的键值“foo”及当前服务端缓存所记录的可用服务实例个数执行哈希计算,并根据哈希计算结果来决定存储该数据的Memcached服务实例。接下来,客户端就会向该实例发送请求,以在其中记录具有键值“foo”的数据“bar”。

  这样做的好处则在于,每个Memcached服务实例都是独立的,而彼此之间并没有任何交互。在这种情况下,我们可以省略很多复杂的功能逻辑,如各个节点之间的数据同步以及结点之间消息的广播等等。这种轻量级的架构可以简化很多操作。如在一个节点失效的时候,我们仅仅需要使用一个新的Memcached节点替代老节点即可。而在对缓存进行扩容的时候,我们也只需要添加额外的服务并修改客户端配置。

  这些记录在服务端缓存中的数据是全局可见的。也就是说,一旦在Memcached服务端缓存中成功添加了一条新的记录,那么其它使用该缓存服务的应用实例将同样可以访问该记录:

  在Memcached中,每条记录都由四部分组成:记录的键,有效期,一系列可选的标记以及表示记录内容的数据。由于记录内容的数据中并不包含任何数据结构,因此我们在Memcached中所记录的数据需要是经过序列化之后的表示。

内存管理

  在使用缓存时,我们不得不考虑的一个问题就是如何对这些缓存数据的生存期进行管理。这其中包括如何使一个记录在缓存中的数据过期,如何在缓存空间不够时执行数据的替换等。因此在本节中,我们将对Memcached的内存管理机制进行介绍。

  首先我们来看一看Memcached的内存管理模型。通常情况下,一个内存管理算法所最需要考虑的问题就是内存的碎片化(Fragmentation):在长时间地分配及回收之后,被系统所使用的内存将趋向于散落在不连续的空间中。这使得系统很难找到连续内存空间,一方面增大了内存分配失败的概率,另一方面也使得内存分配工作变得更为复杂,降低了运行效率。

  为了解决这个问题,Memcached使用了一种叫Slab的结构。在该分配算法中,内存将按照1MB的大小划分为页,而该页内存则会继续被分割为一系列具有相同大小的内存块:

  因此Memcached并不是直接根据需要记录的数据的大小来直接分配相应大小的内存。在一条新的记录到来时,Memcached会首先检查该记录的大小,并根据记录的大小选择记录所需要存储到的Slab类型。接下来,Memcached就会检查其内部所包含的该类型Slab。如果这些Slab中有空余的块,那么Memcached就会使用该块记录该条信息。如果已经没有Slab拥有空闲的具有合适大小的块,那么Memcached就会创建一个新的页,并将该页按照目标Slab的类型进行划分。

  一个需要考虑的特殊情况就是对记录的更新。在对一个记录进行更新的时候,记录的大小可能会发生变化。在这种情况下,其所对应的Slab类型也可能会发生变化。因此在更新时,记录在内存中的位置可能会发生变化。只不过从用户的角度来说,这并不可见。

  Memcached使用这种方式来分配内存的好处则在于,其可以降低由于记录的多次读写而导致的碎片化。反过来,由于Memcached是根据记录的大小选择需要插入到的块类型,因此为每个记录所分配的块的大小常常大于该记录所实际需要的内存大小,进而造成了内存的浪费。当然,您可以通过Memcached的配置文件来指定各个块的大小,从而尽可能地减少内存的浪费。

  但是需要注意的是,由于默认情况下Memcached中每页的大小为1MB,因此其单个块最大为1MB。除此之外,Memcached还限制每个数据所对应的键的长度不能超过250个字节。

  一般来说,Slab中各个块的大小以及块大小的递增倍数可能会对记录所载位置的选择及内存利用率有很大的影响。例如在当前的实现下,各个Slab中块的大小默认情况下是按照1.25倍的方式来递增的。也就是说,在一个Memcached实例中,某种类型Slab所提供的块的大小是80K,而提供稍大一点空间的Slab类型所提供的块的大小就将是100K。如果现在我们需要插入一条81K的记录,那么Memcached就会选择具有100K块大小的Slab,并尝试找到一个具有空闲块的Slab以存入该记录。

  同时您也需要注意到,我们使用的是100K块大小的Slab来记录具有81K大小的数据,因此记录该数据所导致的内存浪费是19K,即19%的浪费。而在需要存储的各条记录的大小平均分布的情况下,这种内存浪费的幅度也在9%左右。该幅度实际上取决于我们刚刚提到的各个Slab中块大小的递增倍数。在Memcached的初始实现中,各个Slab块的递增倍数在默认情况下是2,而不是现在的1.25,从而导致了平均25%左右的内存浪费。而在今后的各个版本中,该递增倍数可能还会发生变化,以优化Memcached的实际性能。

  如果您一旦知道了您所需要缓存的数据的特征,如通常情况下数据的大小以及各个数据的差异幅度,那么您就可以根据这些数据的特征来设置上面所提到的各个参数。如果数据在通常情况下都比较小,那么我们就需要将最小块的大小调整得小一些。如果数据的大小变动不是很大,那么我们可以将块大小的递增倍数设置得小一些,从而使得各个块的大小尽量地贴近需要存储的数据,以提高内存的利用率。

  还有一个值得注意的事情就是,由于Memcached在计算到底哪个服务实例记录了具有特定键的数据时并不会考虑用来组成缓存系统中各个服务器的差异性。如果每个服务器上只安装了一个Memcached实例,那么各个Memcached实例所拥有的可用内存将存在着数倍的差异。但是由于各个实例被选中的概率基本相同,因此具有较大内存的Memcached实例将无法被充分利用。我们可以通过在具有较大内存的服务器上部署多个Memcached实例来解决这个问题:

  例如上图所展示的缓存系统是由两个服务器组成。这两个服务器中的内存大小并不相同。第一个服务器的内存大小为32G,而第二个服务器的内存大小仅仅有8G。为了能够充分利用这两个服务器的内存,我们在具有32G内存的服务器上部署了4个Memcached实例,而在只有8G内存的服务器上部署了1个Memcached实例。在这种情况下,32G内存服务器上的4个Memcached实例将总共得到4倍于8G服务器所得到的负载,从而充分地利用了32G内存服务器上的内存。

  当然,由于缓存系统拥有有限的资源,因此其会在某一时刻被服务所产生的数据填满。如果此时缓存系统再次接收到一个缓存数据的请求,那么它就会根据LRU(Least recently used)算法以及数据的过期时间来决定需要从缓存系统中移除的数据。而Memcached所使用的过期算法比较特殊,又被称为延迟过期(Lazy expiration):当用户从Memcached实例中读取数据的时候,其将首先通过配置中所设置的过期时间来决定该数据是否过期。如果是,那么在下一次写入数据却没有足够空间的时候,Memcached会选择该过期数据所在的内存块作为新数据的目标地址。如果在写入时没有相应的记录被标记为过期,那么LRU算法才被执行,从而找到最久没有被使用的需要被替

Android图像格式类及图像转换方法 - 路上的脚印  阅读原文»

  

  Android图像格式类及图像转换方法介绍  一款软件的开发和图像密切相关,特别是移动应用程序,在视觉效果等方面是至关重要的,因为这直接关系到用户的体验效果。在Android程序开发的过程中,了解存在哪些图像格式类(ImageFormat、PixelFormat及BitmapConfig等)及图像(JPG、PNG及BMP等)的转换方法,对以后的开发多多少少会有些帮助。

  关于图像格式类,介绍以下三个:ImageFormat、PixelFormat及BitmapConfig。

  1、ImageFormat(android.graphics.ImageFormat),格式参数有以下几种:

int JPEG ,Encoded formats,常量值: 256 (0x00000100)

int NV16,YCbCr format, used for video,16 (0x00000010)

int NV21,YCrCb format used for images, which uses the NV21 encoding format,常量值: 17 (0x00000011)

int RGB_565,RGB format used for pictures encoded as RGB_565,常量值: 4 (0x00000004)

int UNKNOWN, 常量值:0 (0x00000000)

int YUY2,YCbCr format used for images,which uses YUYV (YUY2) encoding format,20 (0x00000014)

int YV12,Android YUV format,This format is exposed to software decoders and applications,

YV12 is a 4:2:0 YCrCb planar format comprised of a WxH Y plane followed by (W/2) x (H/2) Cr and Cb planes

  解释总是英文的最通俗易懂,这里就不献丑翻译了。用法举例,在构建ImageReader类的对象时,会用到ImageFormat类的图像格式对象。如

1 ImageReader imageReader = ImageReader.newInstance(width, height, ImageFormat.RGB_565, 2);

  imageReader对象表示其缓存中最多存在宽高分别为width和height、RGB_565格式的图像流两帧。

在需求中用哪一种图像格式,要视实际情况而定,后面的类似。

2、PixelFormat(android.graphics.PixelFormat),格式参数有以下几种:

int A_8,常量值:8 (0x00000008)

int JPEG,常量值:256 (0x00000100),constant,已声明不赞成使用,use ImageFormat.JPEG instead.

int LA_88,常量值:10 (0x0000000a)

int L_8, 常量值:9 (0x00000009)

int OPAQUE,常量值: -1 (0xffffffff),System chooses an opaque format (no alpha bits required)

int RGBA_4444,常量值:7 (0x00000007)

int RGBA_5551,常量值:6 (0x00000006)

int RGBA_8888,常量值:1 (0x00000001)

int RGBX_8888,常量值:2 (0x00000002)

int RGB_332,常量值:11 (0x0000000b)

int RGB_565,常量值:4 (0x00000004)

int RGB_888,常量值:3 (0x00000003)

int TRANSLUCENT,常量值: -3 (0xfffffffd),System chooses a format that supports translucency (many alpha bits)

int TRANSPARENT,常量值:-2 (0xfffffffe),System chooses a format that supports transparency (at least 1 alpha bit)

int UNKNOWN,常量值: 0 (0x00000000)

int YCbCr_420_SP,常量值:17 (0x00000011),constant 已声明不赞成使用 use ImageFormat.NV21 instead

int YCbCr_422_I,常量值: 20 (0x00000014),constant 已声明不赞成使用 use ImageFormat.YUY2 instead

int YCbCr_422_SP,常量值:16 (0x00000010),constant 已声明不赞成使用 use ImageFormat.NV16 instead

  注意,有四种图像格式已被声明不赞成使用,可以用ImaggFormat相对应的格式进行代替。由此可知,两种图像格式之间存在相通之处。用法举例,让窗口实现渐变的效果,如

1 getWindow().setFormat(PixelFormat.RGBA_8888);

  补充说明:RGBA_8888为android的一种32位颜色格式,R、G、B、A分别用八位表示,Android默认的图像格式是PixelFormat.OPAQUE,其是不带Alpha值的。

  3、Bitmap.Config(Android.graphics.Bitmap内部类)

  Possible bitmap configurations。A bitmap configuration describes how pixels are stored。This affects the quality (color depth) as well as the ability to display transparent/translucent colors。(官网介绍,大致意思是说:影响一个图片色彩色度显示质量主要看位图配置,显示图片时透明还是半透明)。

    ALPHA_8:Each pixel is stored as a single translucency (alpha) channel。(原图的每一个像素以半透明显示)

    ARGB_4444:This field was deprecated in API level 13。Because of the poor quality of this configuration, it is advised to use ARGB_8888 instead。(在API13以后就被弃用了,建议使用8888)。

    ARGB_8888 :Each pixel is stored on 4 bytes。 Each channel (RGB and alpha for translucency) is stored with 8 bits of precision (256 possible values) 。This configuration is very flexible and offers the best quality。 It should be used whenever possible。(每个像素占4个字节,每个颜色8位元,反正很清晰,看着很舒服)。

    RGB_565:Each pixel is stored on 2 bytes and only the RGB channels are encoded:red is stored with 5 bits of precision (32 possible values),green is stored with 6 bits of precision (64 possible values) and blue is stored with 5 bits of precision。(这个应该很容易理解了)。

  用法举例,构建Bitmap对象时,会用到BitmapConfig类图像格式对象,如:

1 Bitmap bitmap = Bit

阅读更多内容

2015年7月26日星期日

你了解实时计算吗? - foreach_break

本邮件内容由第三方提供,如果您不想继续收到该邮件,可 点此退订
你了解实时计算吗? - foreach_break  阅读原文»

实时计算是什么?

请看下面的图:

这里写图片描述

我们以热卖产品的统计为例,看下传统的计算手段:

  1. 将用户行为、log等信息清洗后保存在数据库中.
  2. 将订单信息保存在数据库中.
  3. 利用触发器或者协程等方式建立本地索引,或者远程的独立索引.
  4. join订单信息、订单明细、用户信息、商品信息等等表,聚合统计20分钟内热卖产品,并返回top-10.
  5. web或app展示.

这是一个假想的场景,但假设你具有处理类似场景的经验,应该会体会到这样一些问题和难处:

  1. 水平扩展问题(scale-out)
    显然,如果是一个具有一定规模的电子商务网站,数据量都是很大的。而交易信息因为涉及事务,所以很难直接舍弃关系型数据库的事务能力,迁移到具有更好的scale-out能力的NoSQL数据库中。

    那么,一般都会做sharding。历史数据还好说,我们可以按日期来归档,并可以通过批处理式的离线计算,将结果缓存起来。
    但是,这里的要求是20分钟内,这很难。

  2. 性能问题
    这个问题,和scale-out是一致的,假设我们做了sharding,因为表分散在各个节点中,所以我们需要多次入库,并在业务层做聚合计算。

    问题是,20分钟的时间要求,我们需要入库多少次呢?
    10分钟呢?
    5分钟呢?
    实时呢?
    而且,业务层也同样面临着单点计算能力的局限,需要水平扩展,那么还需要考虑一致性的问题。
    所以,到这里一切都显得很复杂。

  3. 业务扩展问题
    假设我们不仅仅要处理热卖商品的统计,还要统计广告点击、或者迅速根据用户的访问行为判断用户特征以调整其所见的信息,更加符合用户的潜在需求等,那么业务层将会更加复杂。

也许你有更好的办法,但实际上,我们需要的是一种新的认知:

这个世界发生的事,是实时的。
所以我们需要一种实时计算的模型,而不是批处理模型。
我们需要的这种模型,必须能够处理很大的数据,所以要有很好的scale-out能力,最好是,我们都不需要考虑太多一致性、复制的问题。

那么,这种计算模型就是实时计算模型,也可以认为是流式计算模型。

现在假设我们有了这样的模型,我们就可以愉快地设计新的业务场景:

  1. 转发最多的微博是什么?
  2. 最热卖的商品有哪些?
  3. 大家都在搜索的热点是什么?
  4. 我们哪个广告,在哪个位置,被点击最多?

或者说,我们可以问:

这个世界,在发生什么?

最热的微博话题是什么?

我们以一个简单的滑动窗口计数的问题,来揭开所谓实时计算的神秘面纱。

假设,我们的业务要求是:

统计20分钟内最热的10个微博话题。

解决这个问题,我们需要考虑:

  1. 数据源
    这里,假设我们的数据,来自微博长连接推送的话题。
  2. 问题建模
    我们认为的话题是#号扩起来的话题,最热的话题是此话题出现的次数比其它话题都要多。
    比如:@foreach_break : 你好,#世界#,我爱你,#微博#。
    "世界"和"微博"就是话题。
  3. 计算引擎
    我们采用storm。
  4. 定义时间

如何定义时间?

时间的定义是一件很难的事情,取决于所需的精度是多少。
根据实际,我们一般采用tick来表示时刻这一概念。

在storm的基础设施中,executor启动阶段,采用了定时器来触发"过了一段时间"这个事件。
如下所示:

(defn setup-ticks! [worker executor-data]
(let [storm-conf (:storm-conf executor-data)
tick-time-secs (storm-conf TOPOLOGY-TICK-TUPLE-FREQ-SECS)
receive-queue (:receive-queue executor-data)
context (:worker-context executor-data)]
(when tick-time-secs
(if (or (system-id? (:component-id executor-data))
(and (= false (storm-conf TOPOLOGY-ENABLE-MESSAGE-TIMEOUTS))
(= :spout (:type executor-data))))
(log-message "Timeouts disabled for executor " (:component-id executor-data) ":" (:executor-id executor-data))
(schedule-recurring
(:user-timer worker)
tick-time-secs
tick-time-secs
(fn []
(disruptor/publish
receive-queue
[[nil (TupleImpl. context [tick-time-secs] Constants/SYSTEM_TASK_ID Constants/SYSTEM_TICK_STREAM_ID)]]
)))))))

之前的博文中,已经详细分析了这些基础设施的关系,不理解的童鞋可以翻看前面的文章。

每隔一段时间,就会触发这样一个事件,当流的下游的bolt收到一个这样的事件时,就可以选择是增量计数还是将结果聚合并发送到流中。

bolt如何判断收到的tuple表示的是"tick"呢?
负责管理bolt的executor线程,从其订阅的消息队列消费消息时,会调用到bolt的execute方法,那么,可以在execute中这样判断:

public static boolean isTick(Tuple tuple) {
return tuple != null
&& Constants.SYSTEM_COMPONENT_ID .equals(tuple.getSourceComponent())
&& Constants.SYSTEM_TICK_STREAM_ID.equals(tuple.getSourceStreamId());
}

结合上面的setup-tick!的clojure代码,我们可以知道SYSTEM_TICK_STREAM_ID在定时事件的回调中就以构造函数的参数传递给了tuple,那么SYSTEM_COMPONENT_ID是如何来的呢?
可以看到,下面的代码中,SYSTEM_TASK_ID同样传给了tuple:

;; 请注意SYSTEM_TASK_ID和SYSTEM_TICK_STREAM_ID
(TupleImpl. context [tick-time-secs] Constants/SYSTEM_TASK_ID Constants/SYSTEM_TICK_STREAM_ID)

然后利用下面的代码,就可以得到SYSTEM_COMPONENT_ID:

public String getComponentId(int taskId) {
if(taskId==Constants.SYSTEM_TASK_ID) {
return Constants.SYSTEM_COMPONENT_ID;
} else {
return _taskToComponent.get(taskId);
}
}

滑动窗口

有了上面的基础设施,我们还需要一些手段来完成"工程化",将设想变为现实。

这里,我们看看Michael G. Noll的滑动窗口设计。

这里写图片描述
注:图片来自http://www.michael-noll.com/blog/2013/01/18/implementing-real-time-trending-topics-in-storm/

Topology

String spoutId = "wordGenerator";
String counterId = "counter";
String intermediateRankerId = "intermediateRanker";
String totalRankerId = "finalRanker";
// 这里,假设TestWordSpout就是我们发送话题tuple的源
builder.setSpout(spoutId, new TestWordSpout(), 5);
// RollingCountBolt的时间窗口为9秒钟,每3秒发送一次统计结果到下游
builder.setBolt(counterId, new RollingCountBolt(9, 3), 4).fieldsGrouping(spoutId, new Fields("word"));
// IntermediateRankingsBolt,将完成部分聚合,统计出top-n的话题
builder.setBolt(intermediateRankerId, new IntermediateRankingsBolt(TOP_N), 4).fieldsGrouping(counterId, new Fields(
"obj"));
// TotalRankingsBolt, 将完成完整聚合,统计出top-n的话题
builder.setBolt(totalRankerId, new TotalRankingsBolt(TOP_N)).globalGrouping(intermediateRankerId);

上面的topology设计如下:

这里写图片描述
注:图片来自http://www.michael-noll.com/blog/2013/01/18/implementing-real-time-trending-topics-in-storm/

将聚合计算与时间结合起来

前文,我们叙述了tick事件,回调中会触发bolt的execute方法,那可以这么做:

RollingCountBolt:

@Override
public void execute(Tuple tuple) {
if (TupleUtils.isTick(tuple)) {
LOG.debug("Received tick tuple, triggering emit of current window counts");
// tick来了,将时间窗口内的统计结果发送,并让窗口滚动
emitCurrentWindowCounts();
}
else {
// 常规tuple,对话题计数即可
countObjAndAck(tuple);
}
}

// obj即为话题,增加一个计数 count++
// 注意,这里的速度基本取决于流的速度,可能每秒百万,也可能每秒几十.
// 内存不足? bolt可以scale-out.
private void countObjAndAck(Tuple tuple) {
Object obj = tuple.getValue(0);
counter.incrementCount(obj);
collector.ack(tuple);
}

// 将统计结果发送到下游
private void emitCurrentWindowCounts() {
Map<Object, Long> counts = counter.getCountsThenAdvanceWindow();
int actualWindowLengthInSeconds = lastModifiedTracker.secondsSinceOldestModification();
lastModifiedTracker.markAsModified();
if (actualWindowLengthInSeconds != windowLengthInSeconds) {
LOG.warn(String.format(WINDOW_LENGTH_WARNING_TEMPLATE, actualWindowLengthInSeconds, windowLengthInSeconds));
}
emit(counts, actualWindowLengthInSeconds);
}

上面的代码可能有点抽象,看下这个图就明白了,tick一到,窗口就滚动:

这里写图片描述
注:图片来自http://www.michael-noll.com/blog/2013/01/18/implementing-real-time-trending-topics-in-storm/

IntermediateRankingsBolt & TotalRankingsBolt:

public final void execute(Tuple tuple, BasicOutputCollector collector) {
if (TupleUtils.isTick(tuple)) {
getLogger().debug("Received tick tuple, triggering emit of current rankings");
// 将聚合并排序的结果发送到下游
emitRankings(collector);
}
else {
// 聚合并排序
updateRankingsWithTuple(tuple);
}
}

其中,IntermediateRankingsBolt和TotalRankingsBolt的聚合排序方法略有不同:

IntermediateRankingsBolt的聚合排序方法:

// IntermediateRankingsBolt的聚合排序方法:
@Override
void updateRankingsWithTuple(Tuple tuple) {
// 这一步,将话题、话题出现的次数提取出来
Rankable rankable = RankableObjectWithFields.from(tuple);
// 这一步,将话题出现的次数进行聚合,然后重排序所有话题
super.getRankings().updateWith(rankable);
}

TotalRankingsBolt的聚合排序方法:

// TotalRankingsBolt的聚合排序方法
@Override
void updateRankingsWithTuple(Tuple tuple) {
// 提出来自IntermediateRankingsBolt的中间结果
Rankings ranking
dotNET跨平台相关文档整理 - 张善友  阅读原文»

一直在从事C#开发的相关技术工作,从C# 1.0一路用到现在的C# 6.0, 通常情况下被局限于Windows平台,Mono项目把我们C#程序带到了Windows之外的平台,在工作之余花了很多时间在Mono的学习研究和推广,从《国内 Mono 相关文章汇总》你可以看到博客园有很多的同仁在探索学习,逐步形成了一个小圈子,这个圈子里的很多都是非Windows平台上运行C#程序,特别是MVP 刘冰的Web服务器Jexus 为我们dotNET跨平台提供了一个工业级的应用服务器,这个圈子里的同仁对于Mono,Jexus的使用都很熟悉,平时也在QQ群里讨论相关的问题,我会把相关讨论记录下来。随着去年微软全面拥抱开源以来,越来越多的人开始走出windows,开始接触Linux/Mac等非windows平台上的.NET 体验,像是运用最近火红的 Docker来试试跑跑 ASP.NET 5的应用程序,或是在你熟悉的 Sublime Text 3、Vim 等编辑器上安装 OmniSharp.NET的 plugin,看看在非 Visual Studio 下开发 .NET 应用程序的感觉;在体验过这些东西之后,其实你会发现 .NET 的开源其实是让 .NET 开发人员有更多发挥的舞台,就算你原本不是使用 Windows/.NET/Visual Studio 的开发人员,也可以接触新时代的 .NET。

很多人对微软这些年的失落,微软ceo纳德拉在将微软拉到正确的轨道上来,我们所做的是积极拥抱变化,我一直看好dotNET跨平台,也在社区一直推动dotNET跨平台在国内的发展,希望对Windows上的.NET开发人员顺利跨入Linux 的Mono平台开发提供帮助。对于Linux平台上的Mono开发人员也有借鉴意义。平时工作中我主要使用的RedHat系的CentOS,整理的dotNET跨平台研究的相关文档,主要针对的Linux 发行版是CentOS 6和 7,主要是在CentOS平台上进行dotNET跨平台开发的相关文档。将整理的文档放在Github: https://github.com/geffzhang/opendotnet 希望大家能够一起来完善这方面的文档。目前完成的内容主要是两大块,将来会增加更多的内容,下面简要介绍下已经完成的内容:

  1. Linux简要:介绍Linux的常用命令使用方法和 从一个Windows系统的使用者如何快速学习CentOS 系统,为我们在CentOS上开发,运行dotNET程序打下良好的基础,其中包括了我在公司针对这一部分的培训ppt。

  2. dotNET环境部署:介绍在CentOS 上部署Mono& Jexus 和 CoreCLR的相关内容,其中包含最完整的 Jexus web服务器资料:

    贴下这个文档的部分目录:


本文链接:dotNET跨平台相关文档整理,转载请注明。

阅读更多内容