0%

C++基于编译时间自动生成版本号_c++自动生成版本号-CSDN博客

Excerpt

文章浏览阅读5.1k次,点赞3次,收藏21次。我们希望每次编译发布程序都有不同的版本号。但是每次编译都需要修改版本号特别麻烦。本文介绍一种基于编译时间生成版本号的方法。C++内置宏C/C++编译器会内置有两个获取编译时间的宏:DATE__和__TIME;__DATE__:在源文件中插入当前的编译日期__TIME__:在源文件中插入当前编译时间;#include <stdio.h>int main(void) { printf(“Date : %s\n”, DATE); _c++自动生成版本号


我们希望每次编译发布程序都有不同的版本号。但是每次编译都需要修改版本号特别麻烦。本文介绍一种基于编译时间生成版本号的方法。

C++内置宏

C/C++编译器会内置有两个获取编译时间的宏:__DATE__和__TIME__;

  • __DATE__:在源文件中插入当前的编译日期

  • __TIME__:在源文件中插入当前编译时间;

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void)

{

printf("Date : %s\n", __DATE__);

printf("Time : %s\n", __TIME__);

}

输出:

1
2
3
Date : May 14 2020

Time : 19:34:54

日期转化成数字表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#define BUILD_YEAR_CH0 (__DATE__[ 7])

#define BUILD_YEAR_CH1 (__DATE__[ 8])

#define BUILD_YEAR_CH2 (__DATE__[ 9])

#define BUILD_YEAR_CH3 (__DATE__[10])

#define BUILD_MONTH_IS_JAN (__DATE__[0] == 'J' && __DATE__[1] == 'a' && __DATE__[2] == 'n')

#define BUILD_MONTH_IS_FEB (__DATE__[0] == 'F')

#define BUILD_MONTH_IS_MAR (__DATE__[0] == 'M' && __DATE__[1] == 'a' && __DATE__[2] == 'r')

#define BUILD_MONTH_IS_APR (__DATE__[0] == 'A' && __DATE__[1] == 'p')

#define BUILD_MONTH_IS_MAY (__DATE__[0] == 'M' && __DATE__[1] == 'a' && __DATE__[2] == 'y')

#define BUILD_MONTH_IS_JUN (__DATE__[0] == 'J' && __DATE__[1] == 'u' && __DATE__[2] == 'n')

#define BUILD_MONTH_IS_JUL (__DATE__[0] == 'J' && __DATE__[1] == 'u' && __DATE__[2] == 'l')

#define BUILD_MONTH_IS_AUG (__DATE__[0] == 'A' && __DATE__[1] == 'u')

#define BUILD_MONTH_IS_SEP (__DATE__[0] == 'S')

#define BUILD_MONTH_IS_OCT (__DATE__[0] == 'O')

#define BUILD_MONTH_IS_NOV (__DATE__[0] == 'N')

#define BUILD_MONTH_IS_DEC (__DATE__[0] == 'D')

#define BUILD_MONTH_CH0 \

((BUILD_MONTH_IS_OCT || BUILD_MONTH_IS_NOV || BUILD_MONTH_IS_DEC) ? '1' : '0')

#define BUILD_MONTH_CH1 \

( \

(BUILD_MONTH_IS_JAN) ? '1' : \

(BUILD_MONTH_IS_FEB) ? '2' : \

(BUILD_MONTH_IS_MAR) ? '3' : \

(BUILD_MONTH_IS_APR) ? '4' : \

(BUILD_MONTH_IS_MAY) ? '5' : \

(BUILD_MONTH_IS_JUN) ? '6' : \

(BUILD_MONTH_IS_JUL) ? '7' : \

(BUILD_MONTH_IS_AUG) ? '8' : \

(BUILD_MONTH_IS_SEP) ? '9' : \

(BUILD_MONTH_IS_OCT) ? '0' : \

(BUILD_MONTH_IS_NOV) ? '1' : \

(BUILD_MONTH_IS_DEC) ? '2' : \

'?' \

)

#define BUILD_DAY_CH0 ((__DATE__[4] >= '0') ? (__DATE__[4]) : '0')

#define BUILD_DAY_CH1 (__DATE__[ 5])

#define BUILD_HOUR_CH0 (__TIME__[0])

#define BUILD_HOUR_CH1 (__TIME__[1])

#define BUILD_MIN_CH0 (__TIME__[3])

#define BUILD_MIN_CH1 (__TIME__[4])

#define BUILD_SEC_CH0 (__TIME__[6])

#define BUILD_SEC_CH1 (__TIME__[7])

上述代码讲日期和时间转化成单个数字表示,方便后续根据特定的格式生成版本号。

如果编译日期时间:May 14 2020 19:34:54

转化后结果如下表:

对应的值

BUILD_YEAR_CH0

2

BUILD_YEAR_CH1

0

BUILD_YEAR_CH2

2

BUILD_YEAR_CH3

0

BUILD_MONTH_CH0

0

BUILD_MONTH_CH2

5

BUILD_DAY_CH0

1

BUILD_DAY_CH1

4

BUILD_HOUR_CH0

1

BUILD_HOUR_CH1

9

BUILD_MIN_CH0

3

BUILD_MIN_CH1

4

组成版本号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#define VERSION_MAJOR 1

#define VERSION_MINOR 3

#if VERSION_MAJOR > 100

#define VERSION_MAJOR_INIT \

((VERSION_MAJOR / 100) + '0'), \

(((VERSION_MAJOR % 100) / 10) + '0'), \

((VERSION_MAJOR % 10) + '0')

#elif VERSION_MAJOR > 10

#define VERSION_MAJOR_INIT \

((VERSION_MAJOR / 10) + '0'), \

((VERSION_MAJOR % 10) + '0')

#else

#define VERSION_MAJOR_INIT \

(VERSION_MAJOR + '0')

#endif

#if VERSION_MINOR > 100

#define VERSION_MINOR_INIT \

((VERSION_MINOR / 100) + '0'), \

(((VERSION_MINOR % 100) / 10) + '0'), \

((VERSION_MINOR % 10) + '0')

#elif VERSION_MINOR > 10

#define VERSION_MINOR_INIT \

((VERSION_MINOR / 10) + '0'), \

((VERSION_MINOR % 10) + '0')

#else

#define VERSION_MINOR_INIT \

(VERSION_MINOR + '0')

#endif

#define COMLETE_VERSION {\

VERSION_MAJOR_INIT,\

'.',\

VERSION_MINOR_INIT,\

'.',\

BUILD_YEAR_CH2, BUILD_YEAR_CH3,\

BUILD_MONTH_CH0, BUILD_MONTH_CH1,\

BUILD_DAY_CH0, BUILD_DAY_CH1,\

'.',\

BUILD_HOUR_CH0, BUILD_HOUR_CH1,\

BUILD_MIN_CH0, BUILD_MIN_CH1,\

'\0'\

}

述代码定义了大版本号为1,小版本号3,然后将编译日期和时间作为版本号的第3第4位。

最后COMLETE_VERSION 为 1.3.200514.1934。

每次编译版本号的第3第4位自动设置为编译日期和时间。

C/C++编译器的一些易混淆概念,总结一下。

关于什么是Unix-like操作系统,常见操作系统间差异,什么是操作系统接口等等,请参考《操作系统宝鉴》。

C/C++编译器有哪些?

首先是如雷贯耳的这几位仁兄,MSVC、GCC、Cygwin、MingW(Cygwin和MingW的英文发音),另外还有些小众和新秀,像ICC(Intel C/C++ Compiler)、BCC(Borland C/C++ Compiler,快销声匿迹了)、RVCT(ARM的汇编/C/C++编译器,内置在ARM的IDE——RVDS中)、Pgi编译器……其实有一大串,我们只要熟悉常用的最强大的几款就可以了。

主流C/C++编译器|编译环境简介

MSVC

MSVC是微软Windows平台Visual Studio自带的C/C++编译器。

_优点_:对Windows平台支持好,编译快。

_缺点_:对C++的新标准支持得少。

GCC

GCC原名GNU C Compiler,后来逐渐支持更多的语言编译(C++、Fortran、Pascal、Objective-C、Java、Ada、Go等),所以变成了GNU Compiler Collection(GNU编译器套装),是一套由GNU工程开发的支持多种编程语言的编译器。GCC是自由软件发展过程中的著名例子,由自由软件基金会以GPL协议发布,是大多数类Unix(如Linux、BSD、Mac OS X等)的标准编译器,而且适用于Windows(借助其他移植项目实现的,比如MingW、Cygwin等)。GCC支持多种计算机体系芯片,如x86、ARM,并已移植到其他多种硬件平台。

_优点_:类Unix下的标准编译器,支持众多语言,支持交叉编译。

**缺点**:默认不支持Windows,需要第三方移植才可用于Windows。

Cygwin

Cygwin是一个Windows下Unix-like模拟环境,具体说就是Unix-like接口(OS API,命令行)重定向层,其目的是不修改软件源码仅重新编译就可以将Unix-like系统上的软件移植到Windows上(这个移植也许还算不上严格意义上的无缝移植)。始于1995年,最初作为Cygnus软件公司工程师Steve Chamberlain的一个项目。

**和GCC的关系**:Cygwin是让Windows拥有Unix-like环境的软件而不是编译器,GCC是安装在Cygwin上的编译器。

_优点_:可以比MingW移植更多的软件到Windows上,对Linux接口模拟比MingW全面。

_缺点_:软件运行依赖cygwin1.dll,速度受点影响。

**注意**:Unix-like模拟环境不是Unix虚拟环境,很多论述中都声称Cygwin是在Windows上尽可能模拟类Unix环境,这容易造成误解,好像类Unix的elf程序可以直接运行在安装了Cygwin的Windows上一样。Cygwin和Wine的思路是不同的。在Windows+Cygwin上你可以像类Unix那样使用命令行和编程,但elf等非exe格式的程序是不能被Cygwin运行的,所以Cygwin和Unix虚拟机、Wine是完全不同的,叫Unix-like环境,模拟非虚拟,是有限的选择性的模拟,请不要误解。

MingW

MingW(Minimalist GNU on Windows)是一个Linux/Windows下的可以把软件源码中Unix-like OS API调用通过头文件翻译替换成相应的Windows API调用的编译环境,其目的和Cygwin相同。从而把Linux上的软件在不修改源码的情况下编译为可直接在Win下执行的exe。

_和GCC的关系_:MingW是编译环境,不是编译器,GCC是MingW中的核心组成。

_优点_:在Win下可以和Linux一样的方式编译C/C++源码,可以说是Win版的GCC,其生产的Windows PE程序相比Cygwin不依赖任何第三方库,比Cygwin纯粹,理论上也更快速。

_缺点_:编译速度、编译出的程序在算法上可能都比MSVC慢。

_注意_:与Windows下其它编译器不同的是,MinGW与Linux下广泛使用的GNU近乎完全兼容,这意味着,在Linux下如何编译源代码,在MinGW中也可以以完全相同的方式编译。有些Linux下的开发人员(比如开源阵营)发布的源代码通常只提供Linux下的编译方式,而不提供Windows下的编译方式(这可能与其不熟悉windows操作系统有关),但确实有不少用户需要在在Windows下编译使用此源代码。这在种情况下,如果Windows用户想用VC、BC等编译器编译该源代码,必须重写Makefile(各种编译器所支持的Makefile不尽相同),工作量比较大不说,还很难保证不出错。MinGW的出现,提供了两个平台下的“跨平台编译方案”。MinGW与MSYS相配合,连./configure都有了。与GNU不同的是,MinGW编译生成的是Windows下的可执行文件(.exe)或库文件(.dll,.lib)——不过编译过程中的的中间文件仍然是.o文件,而不是.obj文件(这当然无所谓了,中间文件嘛,编译完成后就没有用了)。

在我们对比Cygwin和MingW之前,请先理清一件事,那就是,

如何从Unix-like系统向Windows系统移植软件?

现代操作系统包括Windows和Linux的基本设计概念,像进程线程地址空间虚拟内存这些都大同小异,二者之上的程序之所以不兼容,主要是它们对这些功能具体实现上的差异:

首先,是可执行文件的格式,Window使用PE的格式,并且要求以.EXE为后缀名,Linux则使用Elf。

其次,操作系统API也同,比如,Windows用CreateProcess()创建进程,而Unix-like系统则使用fork(),其他还有很多诸如spawn、signals、select、sockets等。

分析之后可知,要把Unix-like系统上的软件移植到Windows上,有几种思路:

第一种:修改软件源码并重新编译,这个方法最笨,类Unix下大量的软件要修改工作量很大,编译生成目标平台可执行文件格式。

第二种:不修改软件源码但把类Unix接口调用悄悄替换为WinAPI,还是需要重新编译,编译生成目标平台可执行文件格式。

第三种,无缝移植的运行环境,无需重新编译,在一种OS上建立另一中OS的应用软件虚拟环境(和虚拟机不一样),比如Wine(把Windows上的可执行程序直接原样移植到Linux上)。

Cygwin和MingW的对比

作为编译环境时,都依赖于GCC

用它们作编译环境、交叉编译,根本上都是因为GCC编译器的支持,它们做的工作是为GCC的编译扫除Unix-like、Windows间OS API的差异这个障碍。

二者都必须重新编译后实现移植,生成的程序都是PE格式

二者都不能让Linux下的程序直接运行在Windows上(无缝移植),必须通过源代码重新编译。有些人声称cygwin支持rpm的压缩包,注意,rpm压缩包其实是src.rpm,内部还是源码而非elf格式,cygwin不支持常规rpm包的安装。

Cygwin运行在Windows上,MingW运行在Linux或者Windows上

Cygwin是Windows上运行的Unix-like环境,MingW是运行在Linux或者Windows上的Windows PE编译环境。

MingW中的子项目MSys和Cygwin更像

Cygwin除了全面模拟Linux的接口(命令行,OS API),提供给运行在它上面的的Windows程序使用,并提供了大量现成的软件,更像是一个平台。MingW也有一个叫MSys(Minimal SYStem)的子项目,主要是提供了一个模拟Linux的Shell和一些基本的Linux工具。因为编译一个大型程序,光靠一个GCC是不够的,还需要有Autoconf等工具来配置项目,所以一般在Windows下编译ffmpeg等Linux下的大型项目都是通过Msys来完成的,当然Msys只是一个辅助环境,根本的工作还是MingW来做的。

实现思路有同有异

Cygwin和MingW都是第二种软件移植思路,当然,二者还是有区别,区别就在于“替换”方式,Cygwin编译时,程序依然以Linux的方式调用系统API,只不过把Unix-like接口link到自己的cygwin1.dll上,然后在cygwin1.dll中重新调用Windows API,cygwin1.dll再调用Windows对应的实现,来把结果返回给程序。也就是说,他们基于Win32 API中写了一个Unix系统API的重定向层,所以用它移植的软件都依赖于cygwin1.dll,MingW编译时通过特有的WinAPI头文件把类Unix-like调用替换为WinAPI,用它移植的软件无需依赖第三方库,可直接运行在Windows平台。为了达到类Unix软件仅通过重新编译移植到Win的目的,Cygwin在运行时偷梁换柱,MingW在编译时偷梁换柱。

用一个PE格式查看工具检查一下就能发现,Cygwin生成的程序依然有fork()这样的Linux系统调用,但目标库是cygwin1。而MingW生成的程序,则全部使用从KERNEL32导出的标准Windows系统API。

使用方式有同有异

把类Unix上的软件移植到Windows是二者的主要目标,除此之外,顺带的,MingW和Cygwin都可以用来跨平台开发等等其他事情,

Windows + Cygwin:可以在Windows上学习Linux命令,还可以在Windows上做Linux软件的开发,包括用GCC编译elf(交叉编译)。

Linux + MingW:可以在Linux上做Windows软件的开发,包括用GCC编译exe(交叉编译)。

Windows/Linux + MingW:可以摆脱MSVC的“束缚”,用GNU的自由软件来编译生成不依赖第三方库的纯粹Windows PE格式(exe)二进制程序。

Cygwin重量级,MingW轻量级

与MingW思路一致的,两者相比,Cygwin是重量级的(需下载50M以上直至数百兆不等,安装后占用空间可达1G),MinGW是轻量级的(需下载的文件只有20M,安装后70M左右),这是单纯从体积上说的,另外Cygwin现在据说也不是完全免费的了。

网络上的对比列表(UnxUtils自行无视,仅供参考)

功能

UnxUtils

MinGW

Cygwin

设计原理

原生

原生

模拟

运行依赖

无依赖

依赖msys.dll(一定依赖它吗?值得验证)

依赖cygwin.dll

运行性能(比较)

最快

中等

DOS执行

可以

可以

不可以

更新速度

完善停止更新

较慢

基本同步gcc

shell命令

较多

较少

较多

uname

WindowsNT

MINGW32_NT-5.1

CYGWIN_NT-5.1

env

同Windows

同Windows

不完全同Windows

root

C:/

C:/

/

home

C:/Documents and Settings/test

/home/test: No such file or directory

/home/test

pwd

C:/bin

/usr/bin

/home/test

df

cannot read table of mounted filesystems

/cygdrive/c

vi

gcc套件

开发库

WinAPI

POSIX

图形库

GTK/QT

GTK/QT

可移植性

Win32API不可移植

无缝移植

程序运行

原生

模拟

程序依赖

cygwin.dll

程序性能(比较)

较快(慢于VC和Linux下的gcc)

较慢(快于java)

小拓展

一个编译器编译时能否调用编译其他编译器产生的lib、dll?

不可以,name***不同,也就是名字混淆方式不同。

http://rendao.org/blog/1071/

概要:

  众所周知,用C#做界面比C++开发效率要高得多,但在有性能问题的情况下不得不将部分模块使用C++,这时就需要使用C#与C++混合编程。本文给出了两种混合编程的方法以及性能对比。

开发环境:

  ThinkPad T430 i5-3230M 2.6G 8G,Win7 64Bit,VS2013(C++开发设置),C++,C#都采用x64平台,性能验证使用Release版本。

测试纯C++项目性能:

  1. 新建空解决方案:文件|新建|项目|已安装|模板|其他项目类型|Visual Studio解决方案|空白解决方案

  2. 新建PureCpp项目:右击解决方案|添加|新建项目|已安装|Visual C++|Win32控制台程序,按缺省设置生成项目

  3. 在配置管理器中新建x64平台,删除其他平台

  4. 新建CppFunction,并添加测试代码,完整代码如下,程序结果:Result: 1733793664 Elapsed: 109

// CppFunction.h
#pragma once
class CppFunction
{ public:
CppFunction(){} ~CppFunction(){} int TestFunc(int a, int b);
}; // CppFunction.cpp
#include “stdafx.h” #include “CppFunction.h”

class CCalc
{ public:
CCalc(int a, int b)
{
m_a = a;
m_b = b;
} int Calc()
{ if (m_a % 2 == 0){ return m_a + m_b;
} if (m_b % 2 == 0){ return m_a - m_b;
} return m_b - m_a;
} private: int m_a; int m_b;
}; int CppFunction::TestFunc(int a, int b)
{
CCalc calc(a, b); return calc.Calc();
} // PureCpp.cpp : 定义控制台应用程序的入口点。 // #include “stdafx.h” #include #include <windows.h> #include “CppFunction.h”

using namespace std; int _tmain(int argc, _TCHAR* argv[])
{
DWORD start = ::GetTickCount();
CppFunction cppFunction; int result = 0; for (int i = 0; i < 10000; i++){ for (int j = 0; j < 10000; j++){
result += cppFunction.TestFunc(i, j);
}
}
DWORD end = ::GetTickCount();

cout << "Result: " << result << " Elapsed: " << end - start << endl; return 0;

}

View Code

测试纯Csharp项目性能:

  1. 新建PureCsharp项目:右击解决方案|添加|新建项目|已安装|其他语言|Visual C#|控制台应用程序,按缺省设置生成项目

  2. 在配置管理器中新建x64平台,删除其他平台,去掉【创建新的解决方案平台】勾选,否则会报x64平台已经存在

  3. 将C++项目中的代码复制过来稍作改动,完整代码如下,程序结果:Result: 1733793664 Elapsed: 729

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace PureCsharp
{ class CCalc
{ public CCalc(int a, int b)
{
m_a = a;
m_b = b;
} public int Calc()
{ if (m_a % 2 == 0)
{ return m_a + m_b;
} if (m_b % 2 == 0)
{ return m_a - m_b;
} return m_b - m_a;
} private int m_a; private int m_b;
} class CppFunction
{ public int TestFunc(int a, int b)
{
CCalc calc = new CCalc(a, b); return calc.Calc();
}
} class Program
{ static void Main(string[] args)
{
DateTime start = System.DateTime.Now;
CppFunction cppFunction = new CppFunction(); int result = 0; for (int i = 0; i < 10000; i++){ for (int j = 0; j < 10000; j++){
result += cppFunction.TestFunc(i, j);
}
}
DateTime end = System.DateTime.Now;

        System.Console.WriteLine("Result: " + result + " Elapsed: " + (end - start).Milliseconds);
   }
}

}

View Code

性能分析:

  从上面的对比可以看出,同样的功能,C#的耗时几乎是C++的7倍,这个例子里的主要原因是,C++可以使用高效的栈内存对象(CCalc),而C#所有对象只能放在托管堆中。

托管C++混合方式:

  1. 新建C#控制台项目,命名为BenchCsharp,使用它来调用C++项目,修改生成目录为:..\x64\Release\

  2. 新建C++DLL项目,命名为DLLCpp,选择空项目,生成成功,但由于是空项目,不会真正生成dll文件

  3. 在DLLCpp为空项目时,在BenchCsharp中可以成功添加引用,但当DLLCpp中添加类后,就不能成功添加引用了,已经添加的引用也会显示警告

  4. 修改DLLCpp项目属性,右击项目|属性|配置属性|常规|公共语言运行时支持,修改后就可以成功引用了

  5. 在DLLCpp中添加CppFunction类,并复制代码,完整代码如下,程序结果:Result: 1733793664 Elapsed: 405

// CppFunction.h
#pragma once
public ref class CppFunction
{ public:
CppFunction(){} ~CppFunction(){} int TestFunc(int a, int b);
}; // CppFunction.cpp
#include “CppFunction.h”

class CCalc
{ public:
CCalc(int a, int b)
{
m_a = a;
m_b = b;
} int Calc()
{ if (m_a % 2 == 0){ return m_a + m_b;
} if (m_b % 2 == 0){ return m_a - m_b;
} return m_b - m_a;
} private: int m_a; int m_b;
}; int CppFunction::TestFunc(int a, int b)
{
CCalc calc(a, b); return calc.Calc();
}

View Code

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BenchCsharp
{ class Program
{ static void Main(string[] args)
{
DateTime start = System.DateTime.Now;
CppFunction cppFunction = new CppFunction(); int result = 0; for (int i = 0; i < 10000; i++)
{ for (int j = 0; j < 10000; j++)
{
result += cppFunction.TestFunc(i, j);
}
}
DateTime end = System.DateTime.Now;

        System.Console.WriteLine("Result: " + result + " Elapsed: " + (end - start).Milliseconds);
    }
}

}

View Code

性能分析:

  使用混合编程后,性能得到了一定程度的提升,但比起单纯的C++项目,还是差了很多

  将C#主函数中的逻辑转移到DLLCpp项目中,即添加如下的static方法,C#中只要调用该方法,程序结果:Result: 1733793664 Elapsed: 405

int CppFunction::Test()
{
DWORD start = ::GetTickCount();
CppFunction cppFunction; int result = 0; for (int i = 0; i < 10000; i++){ for (int j = 0; j < 10000; j++){
result += cppFunction.TestFunc(i, j);
}
}
DWORD end = ::GetTickCount();

cout << "Result: " << result << " Elapsed: " << end - start << endl; return result;

}

View Code

  并没有变得更快,估计是当使用【公共语言运行时支持】方式编译C++时,不能发挥C++的性能优势

DLLImport混合方式:

  1. 新建非空的C++DLL项目,命名为NativeDLLCpp

  2. 将CppFunction类从PureCpp中复制过来

  3. 代码如下,运行结果:Result: 1733793664 Elapsed: 125

// NativeDLLCpp.cpp : 定义 DLL 应用程序的导出函数。 // #include “stdafx.h” #include #include <windows.h> #include “CppFunction.h”

using namespace std;

#ifdef __cplusplus #define TEXPORT extern “C” _declspec(dllexport)
#else
#define TEXPORT _declspec(dllexport)
#endif TEXPORT int Test()
{
DWORD start = ::GetTickCount();
CppFunction cppFunction; int result = 0; for (int i = 0; i < 10000; i++){ for (int j = 0; j < 10000; j++){
result += cppFunction.TestFunc(i, j);
}
}
DWORD end = ::GetTickCount();

cout << "Result: " << result << " Elapsed: " << end - start << endl; return result;

}

View Code

public class NativeDLLCpp
{
    \[DllImport("NativeDLLCpp.dll")\] public static extern int Test();
} class Program
{ static void Main(string\[\] args)
    {
        DateTime start \= System.DateTime.Now; int result = NativeDLLCpp.Test();
        DateTime end \= System.DateTime.Now;
        System.Console.WriteLine("Result: " + result + " Elapsed: " + (end - start).Milliseconds);
    }
}

View Code

性能分析:

  跟纯C++项目性能几乎一致。

  项目依赖项需要手动设置。

  实现联调的方法:修改C#项目属性|调试|启用本机代码调试

在合作开发时,C#时常需要调用C++DLL,当传递参数时时常遇到问题,尤其是传递和返回字符串是,现总结一下,分享给大家:

VC++中主要字符串类型为:LPSTR,LPCSTR, LPCTSTR, string, CString, LPCWSTR, LPWSTR等
但转为C#类型却不完全相同。

主要有如下几种转换:

将string转为IntPtr:IntPtr System.Runtime.InteropServices.Marshal.StringToCoTaskMemAuto(string)

将IntPtr转为string:string System.Runtime.InteropServices.MarshalPtrToStringAuto(IntPtr)

类型对照:

BSTR ———  StringBuilder

LPCTSTR ——— StringBuilder

LPCWSTR ———  IntPtr

handle———IntPtr

hwnd———–IntPtr

char *———-string

int * ———–ref int

int &———–ref int

void *———-IntPtr

unsigned char *—–ref byte

Struct需要在C#里重新定义一个Struct

CallBack回调函数需要封装在一个委托里,delegate static extern int FunCallBack(string str);

注意在每个函数的前面加上public static extern +返回的数据类型,如果不加public ,函数默认为私有函数,调用就会出错。

在C#调用C++ DLL封装库时会出现两个问题:

1. 数据类型转换问题 
2. 指针或地址参数传送问题

    首先是数据类型转换问题。因为C#是.NET语言,利用的是.NET的基本数据类型,所以实际上是将C++的数据类型与.NET的基本数据类型进行对应。

    例如C++的原有函数是:

int __stdcall FunctionName(unsigned char param1, unsigned short param2)

    其中的参数数据类型在C#中,必须转为对应的数据类型。如:

[DllImport(“ COM DLL path/file ”)] 
extern static int FunctionName(byte param1, ushort param2)

    因为调用的是__stdcall函数,所以使用了P/Invoke的调用方法。其中的方法FunctionName必须声明为静态外部函数,即加上extern static声明头。我们可以看到,在调用的过程中,unsigned char变为了byte,unsigned short变为了ushort。变换后,参数的数据类型不变,只是声明方式必须改为.NET语言的规范。

    我们可以通过下表来进行这种转换:

Win32 Types 
CLR Type

char, INT8, SBYTE, CHAR 
System.SByte

short, short int, INT16, SHORT 
System.Int16

int, long, long int, INT32, LONG32, BOOL , INT 
System.Int32

__int64, INT64, LONGLONG 
System.Int64

unsigned char, UINT8, UCHAR , BYTE 
System.Byte

unsigned short, UINT16, USHORT, WORD, ATOM, WCHAR , __wchar_t 
System.UInt16

unsigned, unsigned int, UINT32, ULONG32, DWORD32, ULONG, DWORD, UINT 
System.UInt32

unsigned __int64, UINT64, DWORDLONG, ULONGLONG 
System.UInt64

float, FLOAT 
System.Single

double, long double, DOUBLE 
System.Double

 之后再将CLR的数据类型表示方式转换为C#的表示方式。这样一来,函数的参数类型问题就可以解决了。

    现在,我们再来考虑下一个问题,如果要调用的函数参数是指针或是地址变量,怎么办?

    对于这种情况可以使用C#提供的非安全代码来进行解决,但是,毕竟是非托管代码,垃圾资源处理不好的话对应用程序是很不利的。所以还是使用C#提供的ref以及out修饰字比较好。

    同上面一样,我们也举一个例子:

int __stdcall FunctionName(unsigned char &param1, unsigned char *param2)

在C#中对其进行调用的方法是:

dllImport(“ file ”)] extern static int FunctionName(ref byte param1, ref byte param2)

看到这,可能有人会问,&是取地址,*是传送指针,为何都只用ref就可以了呢?一种可能的解释是ref是一个具有重载特性的修饰符,会自动识别是取地址还是传送指针。

    在实际的情况中,我们利用参数传递地址更多还是用在传送数组首地址上。 
如:byte[] param1 = new param1(6);

    在这里我们声明了一个数组,现在要将其的首地址传送过去,只要将param1数组的第一个元素用ref修饰。具体如下:

[DllImport(“ file ”)] extern static int FunctionName(ref byte param1[1], ref byte param2)

C# 中调用DLL
为了能用上原来的C++代码,只好研究下从C# 中调用DLL
首先必须要有一个声明,使用的是DllImport关键字:
包含DllImport所在的名字空间

复制代码

using System.Runtime.InteropServices; public class XXXX{
[DllImport(“MyDLL.dll”)]
public static extern int mySum (int a,int b);
}

[DllImport(“MyDLL.dll”)]
public static extern int mySum (int a,int b);

复制代码

代码中DllImport关键字作用是告诉编译器入口点在哪里,并将打包函数捆绑在这个类中
在调用的时候
在类中的时候 直接   mySum(a,b);就可以了
在其他类中调用: XXXX. mySum(a,b);
EntryPoint: 指定要调用的 DLL 入口点。默认入口点名称是托管方法的名称 。
CharSet: 控制名称重整和封送 String 参数的方式 (默认是UNICODE)
CallingConvention指示入口点的函数调用约定(默认WINAPI)(上次报告讲过的)
SetLastError 指示被调用方在从属性化方法返回之前是否调用 SetLastError Win32 API 函数 (C#中默认false )

int 类型

复制代码

[DllImport(“MyDLL.dll”)]
//返回个int 类型
public static extern int mySum (int a1,int b1); //DLL中申明
extern “C” __declspec(dllexport) int WINAPI mySum(int a2,int b2)
{ //a2 b2不能改变a1 b1 //a2=.. //b2=…
return a+b;
} //参数传递int 类型
public static extern int mySum (ref int a1,ref int b1); //DLL中申明
extern “C” __declspec(dllexport) int WINAPI mySum(int *a2,int *b2)
{ //可以改变 a1, b1
*a2=… *b2=… return a+b;
}

DLL 需传入char *类型
[DllImport(“MyDLL.dll”)]
//传入值
public static extern int mySum (string astr1,string bstr1); //DLL中申明
extern “C” __declspec(dllexport) int WINAPI mySum(char * astr2,char * bstr2)
{ //改变astr2 bstr 2 ,astr1 bstr1不会被改变
return a+b;
}

DLL 需传出char *类型
[DllImport(“MyDLL.dll”)]
// 传出值
public static extern int mySum (StringBuilder abuf, StringBuilder bbuf ); //DLL中申明
extern “C” __declspec(dllexport) int WINAPI mySum(char * astr,char * bstr)
{ //传出char * 改变astr bstr –>abuf, bbuf可以被改变
return a+b;
}

复制代码

DLL 回调函数

BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam)

复制代码

using System; using System.Runtime.InteropServices; public delegate bool CallBack(int hwnd, int lParam); //定义委托函数类型
public class EnumReportApp
{
[DllImport(“user32”)] public static extern int EnumWindows(CallBack x, int y); public static void Main() {
CallBack myCallBack = new CallBack(EnumReportApp.Report); EnumWindows(myCallBack, 0);
} public static bool Report(int hwnd, int lParam)
{
Console.Write(“Window handle is “);
Console.WriteLine(hwnd); return true;
}
}

复制代码

DLL  传递结构
BOOL PtInRect(const RECT *lprc, POINT pt);

复制代码

using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)] public struct Point { public int x; public int y;
}
[StructLayout(LayoutKind.Explicit)] public struct Rect
{
[FieldOffset(0)] public int left;
[FieldOffset(4)] public int top;
[FieldOffset(8)] public int right;
[FieldOffset(12)] public int bottom;
}
Class XXXX {
[DllImport(“User32.dll”)] public static extern bool PtInRect(ref Rect r, Point p);
}

复制代码

DLL 回调函数,传递结构 想看的msdn里面都有专题介绍,看的我都是晕晕的:)

其他参考请搜索:

在C#程序设计中使用Win32类库
C#中调用C++托管Dll
如何在C#中加载自己编写的动态链接库

相关文章:Creating a P/Invoke Library

能用上DLL以后感觉还是很好的,原来的C++代码只要修改编译通过就可以了,
高兴没多久,发现.net2005居然可以用VB,VC开发智能设备项目,可以创建MFC智能设备项目
晕晕,难道可以直接用MFC来开发smartphone的程序了,赶紧看看,,,

Visual C++ 使用 __declspec(dllexport) 从 DLL 导出 (到C#) 
由于各种的原因, 如何把unmanaged 的 c++ DLL 转换成 managed C# 是一个问题。

方法有3个.

Ø 使用.def文件

Ø 可以不用.def文件, 使用__declspec(dllexport)关键字, 特别是针对Visual C++编译器的时候

Ø 直接用MC++写

什么时候用.def文件?

.def的意思是module-definition, 这个纯文本的文件定义了模块的信息。 对于编译器来说, 一个方法在编译之后的DLL文件里, 存在的形式名字可能不是作者当时起的那个,例如好好的函数名字function() 变成了?function2@@YAXXZ; 可以用undname查看这个被编译器修饰掉的名字, 原型是”void __cdecl function2(void)”. 大概使用的时候就会遇到类似”链接错误,未决的外部符号…” 的错误.

.def文件主要的作用, 就是”标注” 出这个函数原来的样子, 这样编译器在编译的时候, 规则上就会以C编译器的规则来处理, 修饰被去掉了, 另外同时可以把导出函数的序号值手动的改高一点; 还有一个优点(也是缺点) 就是可以用NONAME来修饰函数, 这样导出函数的序号值就变成了1~N, 即第N个函数. 所以调用GetProcAddress() 的时候, 可以直接用定义的序号值, 而不用写函数的名字(但是名字就完全不可用了), 更好的是, 导出函数的这个DLL会变得比较小, 当然, MSDN强调了一点: 仅你可以并有权更改这个.def文件内容的时候, 你才可以用这个办法.

那么, 什么时候考虑用.def文件呢? 因为编译器不同, 而产生的修饰名不同的话, 这个文件就是必须的.

注意如果文件没有导出函数的话, 这个文件可能降低运行效率。

.def文件的格式

LIBRARY FileNameWithoutExtension

EXPORTS

Function1 @1

Function3 @2

Function2 @3 NONAME

启用 Enable it: Property pages-> Configuration Properties->C/C++ -> Linker -> input -> Module Definition File

那不用.def呢? __declspec(dllexport)的作用

这个东西, 可以给函数用, 也可以给类用. 声明大概这样子:

view source 
< id=”highlighter_930381_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 __declspec(dllexport) int __stdcall GetMid(vector ve); 

2 class __declspec(dllexport) TestClass{ 

3 public : 

4 TestClass(); 

5 }

这牵扯到了一个东西就是__stdcall和__cdecl (还有__fastcall, 不过很少用), 其中__cdecl一般是C或者C++的缺省调用规范, 但是最大的一个区别就是__stdcall在返回前自身清除堆栈, 而__cdecl是调用方来做这个事情(可参考COM中的某些机制), 另一个区别就是__stdcall对于可变参数的函数, 玩不转.

反正今时今日, 大家都在用__stdcall, 所以这么写也没什么问题, 但不是没有. VB里调用标记着__cdecl的方法, 可能会得到一个异常Bad DLL Calling Convention. 解决方法也很简单:

原来的函数

view source 
< id=”highlighter_718220_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 long _cdecl PassStr(LPSTR pStr) 

2 {       return 1;      }

新的函数

view source 
< id=”highlighter_515163_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 long _stdcall PassStrStdCall(LPSTR pStr) 

2 {       return PassStr(pStr);      }

问题是, 如果这个函数原型, 参数是可变的, 那又怎么弄呢?

调用的时候, C#都是这么写的:

view source 
< id=”highlighter_120154_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 [DllImport(“”, EntryPoint = @” ?GetMid@@YAXXZ”, CharSet = CharSet.Auto)] 

2 private static extern void GetMid(…);

这个入口点名字还真别扭, 看来去掉这个修饰还是蛮需要的, 除了用.def文件, 另一个办法就是用 extern “C”.

Extern “C”

一句话总结:这个东西可以去掉修饰名。在不用.def文件的前提下, 这个可以保证你的函数function() 还是这个名字.

但是,这个东西对类不太起作用!

这个东西是这么用的: 放到函数声明的最前面。 就类似这样 extern “C” void __declspec(dllexport) function(void);

对于类, 一般的做法是, 把它的内部方法(特别是实例方法,或变量),wrap出一个方法来。 见下面的实例.

还要做什么?

当一个DLL被初始化的时候, 它需要一个入口点, 一般对于非MFC DLL来说, 这样写就行了:

view source 
< id=”highlighter_702691_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
01 BOOL APIENTRY DllMain(HANDLE hModule,  

02                       DWORD  ul_reason_for_call,  

03                       LPVOID lpReserved 

04                       ) 

05 { 

06     switch( ul_reason_for_call ) 

07     { 

08     case DLL_PROCESS_ATTACH: 

09     case DLL_THREAD_ATTACH: 

10     case DLL_THREAD_DETACH: 

11     case DLL_PROCESS_DETACH: 

12         break; 

13     } 

14     return TRUE; 

15 }

要注意的是这个入口点的名字必须是DllMain, 如果不是需要修改linker的/entry 选项. 否则对于C的话可能会初始化失败.

************************************************************************

开始用导出函数 PInvoke

为了好看一点, 先约定一下:

view source 
< id=”highlighter_95255_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 #define EXT_C extern “C”  

2 #define DLLEXPORT __declspec(dllexport) 

3 #define EXT_C_DLLEXPORT EXT_C DLLEXPORT 

4 #define CALLBACK    __stdcall 

5 #define WINAPI      __stdcall 

6 #define APIENTRY    WINAPI

1. 普通的函数

view source 
< id=”highlighter_171056_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 EXT_C_DLLEXPORT void WINAPI Function(); 

2   

3 [DllImport(“filename.dll”, EntryPoint = “ Function”)] 

4 private static extern void Func();

2. ref或者out

view source 
< id=”highlighter_733498_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 EXT_C_DLLEXPORT void WINAPI Function(Type** ty); 

2   

3 [DllImport(“filename.dll”, EntryPoint = “ Function”)] 

4 private static extern void Func(out Type ty);

3. 指针函数和委托

view source 
< id=”highlighter_987259_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 Typedef void (CALLBACK *pFunc)(int); 

2 EXT_C_DLLEXPORT void WINAPI Compare(int a, int b, pFunc p); 

3 private delegate int CompareCallback(int a, int b); 

4 [DllImport(“filename.dll”,EntryPoint=”Compare”)] 

5 private static extern int Compare(int a, int b, CompareCallback call);

4. 类的处理. 其实不是说不可以把类标记为 DLLEXPORT, 如果可以的话, 当然是wrap比较好

C++里的原型

view source 
< id=”highlighter_909839_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 class DLLEXPORT Test 

2 { 

3 public : 

4     Test(); 

5     ~Test(); 

6     BOOL function(int par) 

7 };

类被export, 函数调用时候注意用CallingConvention.ThisCall.

view source 
< id=”highlighter_447728_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 [DllImport(“filename.dll”, EntryPoint = @”??4Test@@QEAAAEAV0@AEBV0@@Z”, CallingConvention = CallingConvention.ThisCall)] 

2   private static extern int TestFunc(IntPtr hwnd, int par);

采用了”迂回”策略, C++里先这样定义,同理, 添加构造函数等, 函数就变成了这个样子:

view source 
< id=”highlighter_371353_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
01 EXT_C_DLLEXPORT BOOL WINAPI function_wrap(Test* t, int par) 

02 { 

03     return t->function(par); 

04 } 

05 EXT_C_DLLEXPORT Test* Test_ctor() 

06 { 

07     Test* t = new Test(); 

08     return t; 

09 } 

10 EXT_C_DLLEXPORT void Test_dector(Test* t) 

11 { 

12     if(NULL == t) 

13     { 

14         delete t; 

15         t = NULL; 

16     } 

17 }

在C#里这样写, 那么就和平时用没什么区别了.

view source 
< id=”highlighter_413990_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
01 public class Test : IDisposable 

02         { 

03             private IntPtr instance; 

04             public Test() 

05             { 

06                 instance = CreateInstance(); 

07             } 

08   

09              ~Test() 

10             { 

11                 Dispose(false); 

12             } 

13   

14             #region pinvoke 

15             [DllImport(“filename.dll”, EntryPoint = @”Test_ctor”)] 

16             private static extern IntPtr CreateInstance(); 

17             [DllImport(“filename.dll”, EntryPoint = @”Test_dector”)] 

18             private static extern void DestroyInstance(IntPtr hwnd); 

19             [return: MarshalAs(UnmanagedType.Bool)] 

20             [DllImport(“filename.dll”, EntryPoint = @”function_wrap”)] 

21             private static extern bool function_wrap(int par); 

22             #endregion 

23   

24             #region IDisposable Members 

25   

26             public void Dispose() 

27             { 

28                 Dispose(true); 

29             } 

30   

31             private void Dispose(bool bDisposing) 

32             { 

33                 if (instance != IntPtr.Zero) 

34                 { 

35                     DestroyInstance(instance); 

36                 } 

37   

38                 if (bDisposing) 

39                 { 

40                     GC.SuppressFinalize(this); 

41                 } 

42             } 

43   

44             #endregion 

45         }

5. Struct操作.

view source 
< id=”highlighter_777150_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 typedef struct Object_HANDLE { 

2   unsigned long      dbch_size; 

3   HANDLE     dbch_handle; 

4   GUID       dbch_eventguid; 

5   BOOL res_flag; 

6 } Object_Native_HANDLE ;

首先在C#里严格定义这个,LayoutKind.Sequential 用来保证内存分配的正常。

view source 
< id=”highlighter_876084_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 [StructLayout(LayoutKind.Sequential)] 

2 public struct Object_Native_HANDLE 

3 { 

4     public ulong dbch_size; 

5     public IntPtr dbch_handle; 

6     public Guid dbch_eventGuid; 

7     public bool res_flag; 

8 }

Marshal的使用如下:

view source 
< id=”highlighter_487393_clipboard” title=”copy to clipboard” classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000” width=”16” height=”16” codebase=”http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0“ type=”application/x-shockwave-flash”> 
print?
1 Object_Native_HANDLE obj = new Object_Native_HANDLE(); //初始化 

2 int amountToAllocate = Marshal.SizeOf(obj);//获取大小 

3 IntPtr objPtr = Marshal.AllocHGlobal(amountToAllocate); //分配并获取空的空间地址 

4 Marshal.StructureToPtr(obj, objPtr, false); // 值写入分配的空间 

5 //操作… 

6 Marshal.FreeHGlobal(objPtr);//释放空间

最后提一句, unmaged code中的错误, 到managed 以后, 极大可能是捕捉不到的。 所以错误需要分别处理。

花了不少时间, MC++平时用的不多, 不写了。

Castle 开发系列文章 - TerryLee - 博客园

Excerpt

Castle是针对.NET平台的一个开源项目,从数据访问框架ORM到IOC容器,再到WEB层的MVC框架、AOP,基本包括了整个开发过程中的所有东西,为我们快速的构建企业级的应用程序提供了很好的服务。4月份以来,Terrylee写了一系列的Castle的文章,这里做一下总结,后续还有Facility


登录后才能查看或发表评论,立即 登录 或者 逛逛 博客园首页

搜索

积分与排名

  • 积分 - 6319537
  • 排名 - 8

随笔分类

随笔档案

个人站点

我的好友

阅读排行榜

评论排行榜

Copyright © 2024 TerryLee
Powered by .NET 8.0 on Kubernetes

概要

Darwin Streaming Server简称DSS。DSS是Apple公司提供的开源实时流媒体播放服务器程序。整个程序使用C++编写,在设计上遵循高性能,简单,模块化等程序设计原则,务求做到程序高效,可扩充性好。并且DSS是一个开放源代码的,基于标准的流媒体服务器,可以运行在Windows NT和Windows 2000,以及几个UNIX实现上,包括Mac OS X,Linux,FreeBSD,和Solaris操作系统上的。

一.主体框架

  DSS的核心服务器部分是由一个父进程所fork出的一个子进程构成,该父进程就构成了整合流媒体服务器。父进程会等待子进程的退出,如果在运行的时候子进程产生了错误从而退出,那么父进程就会fork出一个新的子进程。可以看出,网络客户和服务器直接的对接是由核心服务器来完成的。网络客户RTSPoverRTP来发送或者接受请求。服务器就通过模块来处理相应的请求并向客户端发送数据包。

  核心流媒体服务通过创建四种类型的线程来完成自己的工作,具体如下:

  服务器自己拥有的主线程。当服务器需要关闭检查,以及在关闭之前记录相关状态打印相关统计信息等任务处理时,一般都是通过这个线程来完成的。

  空闲任务线程。这个任务线程是用来对一个周期任务队列的管理,主要管理两种任务,超时任务和Socket任务。

  事件线程。套接口相关事件由事件线程负责监听,当有RTSP请求或者收到RTP数据包时,事件线程就会把这些实践交给任务线程来处理。

  任务线程。任务线程会把事件从事件线程中取出,并把处理请求传递到对应的服务器模块进行处理,比如把数据包发送给客户端的模块,在默认情况下,核心服务器会为每个处理器核创建一个任务线程。

二.相关协议介绍

  如果要使用QuickTime流媒体服务器的编程接口,您应该熟悉该服务器实现的互联网工程组织(Internet Engineering Task Force,简称IETF)协议,列举如下:

  实时流媒体协议(Real Time Streaming Protocol,简称RTSP)

  实时传输协议(Real Time Transport Protocol,简称RTP)

  实时传输控制协议(Real Time Transport Control Protocol,简称RTCP)

  对话描述协议(Session Description Protocol,简称SDP)

  (1).实时流媒体协议

  当我们需要创建并且对一个或多个时间的同步且连续的音视频的媒体数据流控制的时候,我们需要用到RTSP协议,也就是实时流协议。RTSP并不是通过连续的数据流来创建并控制媒体流数据的,所以不会产生媒体流与控制流的交叉。用另外一种说法就是,RTSP本身是对流媒体服务器的远程控制。为了时间实时音视频数据的受控(快进,暂停)以及按需分配流,这个协议为我们提供了可实现的框架。实时流控制协议可以用在对多个数据发送的会话,通过UDP或者TCP方式,以及基于RTP发送方式来实现。

  (2).实时传输协议

  RTP协议是互联网上进行媒体数据的一种传输协议,为了实现一对一或者一对多的同步传输和提供时间信息,我们就会采用RTP协议。由于其典型应用建立在UDP传输之上,但也能在TCP或者ATM等其他协议上使用这个协议。实时传输协议本身只能对确保数据的实时性以及完整性,但并不对传输的顺序以及传输可靠性提供保障。由于是建立在UDP协议之上,所以RTP协议本身并没有提供流量控制或者阻塞控制,所以在一般情况下我们需要使用RTCP来进行这些帮助。由于DSS本身默认的传输协议就是RTP协议,而RTP协议需要通过RTCP协议进行流量控制,这样很大程度上增加了机顶盒也就是解码端的CPU处理压力,因此本设计采用UDP协议直接对TS包进行发送,不使用RTP协议进行数据封装,由于UDP协议也缺少流量控制机制,我们使用PCR值来对发送流量进行控制以防止接收端出现缓存溢出影响播放质量。

  (3).实时传输控制协议

  实时传输控制协议的作用是管理传输的质量,也就是在进程间传输的同时相互交换信息。在建立RTP会话的时候,参与传输的双方周期性的传输RTCP包,这个数据包中包含了所有相关传输的信息,比如数据包大小,丢失的数据包数量等等。因此通常我们利用RTCP来对传输流量或有效载荷进行动态调整,同时与RTP配合有效的控制传输速率,所以特别适合传送实时数据。

  (4).对话描述协议

  对话描述协议(SDP)就是用来描述多媒体会话通告,多媒体会话邀请和其他形式的多媒体会话初始化的协议。SDP协议对流媒体描述的具体信息如下:会话名和会话目的,会话发起时间,会话中相关的网络信息,会话发起者的相关信息,媒体类型,传输所使用的协议,流媒体编码格式,传输时所使用的端口号,IP网络地址。因此我们可以通过解析SDP协议来获取我们所需要的一些必要的相关信息。

  其中RTSP是非常重要的协议,因此后面会结合原代码做一个详细的分析,这个结果对设计模块有着非常重要的影响,也可以说是本设计的关键。

三.模块分类

  流媒体服务器使用模块来响应各种请求及完成任务。有三种类型的模块:

  (1).内容管理模块

  媒体源相关的RTSP请求与响应,我们通过内容管理模块来管理,每个模块都用来对客户的需求进行解释并做相应处理,例如读取和解析模块支持的文件,或者请求的网络源信息,并通过RTP等方式响应。

  内容管理模块有以下几个:

  QTSSFileModule,

  QTSSReflectorModule,

  QTSSRelayModule,

  QTSSMP3StreamingModule。

  (2).服务器支持模块

  服务器支持模块执行服务器数据的收集和记录功能。

  服务器模块包括:

  QTSSErrorLogModule,

  QTSSAccessLogModule,

  QTSSWebStatsModule,

  QTSSWebDebugModule,

  QTSSAdminModule,

  QTSSPOSIXFileSystemModule。

  (3).访问控制模块

  访问控制模块提供鉴权和授权功能,以及操作URL路径提供支持。

  访问控制模块包括:

  QTSSAccessModule,

  QTSSHomeDirectoryModule,

  QTSSHttpFileModule,

  QTSSSpamDefenseModule。

四.DSS服务器调用模块的主要工作流程

  在DSS中的模块分为动态模块和静态模块,动态模块在服务器启动时会首先装载动态模块,之后才会装载一部分静态模块。我们一般建议将我们自己书写的功能模块编译为动态模块来替换或扩展现有的服务器模块,因为它会被优先装载。在QTSS的模块中必须包含Register这个角色,这也是每个模块所必须支持的角色。在模块被装载之后服务器会调用每个模块的Register角色。在这个角色当中,模块会调用QTSS_AddRole函数来记录这个模块所支持的其他角色。然后服务器就将初始化角色来调用每一个拥有这个角色的模块。这个角色主要是做一些初始化的任务,比如说内存的分配或者对数据结构的初始化等等。在关闭服务器的时候,所有模块的Shutdown角色将被调用,这个角色主要是为了结束工作后处理现场,比如释放内存等等。流媒体服务器主要就是通过这种角色来完成相应任务的。

Darwin Streaming Server调研总结

DSS主要几个特性:

支持MP4、3GPP等文件格式;

支持MPEG-4、H.264等视频编解码格式;

支持RTSP流控协议,支持HTTP协议;

支持RTP流媒体传输协议;

支持单播和组播;

支持基于Web的管理;

具有完备的日志功能。

此外,该服务器版本提供了一个基于模块的扩展方法。利用DSS提供的API就可以很方便地编写静态或动态的模块,对DSS进行扩展,使其支持其它文件格式、协议或者功能。

 Darwin Streaming Server(简称DSS)是苹果公司的开源视频服务器版本,最新版本6.0.3

获取包:6.0.3版本wget http://dss.macosforge.org/downloads/DarwinStreamingSrvr6.0.3-Source.tar

linux的补丁

wget http://www.abrahamsson.com/dss-6.0.3.patch

wget http://dss.macosforge.org/trac/attachment/ticket/6/dss-hh-20081021-1.patch?format=raw

1.   安装步骤:

解压:

tar –xvf  DarwinStreamingSrvr6.0.3-Source.tar

2.   打补丁:

patch -p1 < dss-6.0.3.patch

3.    patch -p1 < dss-hh-20081021-1.patch

4.   修改一下Install:

cd DarwinStreamingSrvr6.0.3-Source

vi Install

行255改成 /usr/sbin/useradd -g qtss qtss

5.   编译:

./Buildit install

生成安装目录:

./DSS_MakeRoot -f /tmp/dss

6.   去安装目录&安装:

cd /tmp/dss

./Install安装完以后,一些默认的基本目录

/var/streaming/logs 日志文件目录

/usr/local/movies 影片存放目录和sdp文件存放目录

/usr/local/sbin/DarwinStreamingServer 启动DSS的入口

/etc/streaming/streamingserver.xml 相关的DSS的配置文件,可以配置端口,目录,等信息

调试:

可以在先对DSS的配置文件/etc/streaming/streamingserver.xml中设置日志日志的级别,然后再在DSS安装目录下面使用./DarwinStreamingServer –d –D 等参数的形式进行前端显示调试,详见./DarwinStreamingServer –h

服务器的作用是充当网络客户和服务器模块的接口,其中网络客户使用RTP和RTSP协议来发送请求和接收响应,而服务器模块则负责处理请求和向客户端发送数据包。核心服务器通过创建四种类型的线程来完成自己的工作,具体如下:

  • 服务器自己拥有的主线程(Main Thread)。这个线程负责检查服务器是否需要关闭,记录状态信息,或者打印统计信息。
  • 空闲任务线程(Idle Task Thread)。空闲任务线程管理一个周期性的任务队列。该任务队列有两种类型:超时任务和套接口任务。
  • 事件线程(Event Thread)。事件线程负责侦听套接口事件,比如收到RTSP请求和RTP数据包,然后把事件传递给任务线程。
  • 一个或者多个任务(Task)线程。任务线程从事件线程中接收RTSP和RTP请求,然后把请求传递到恰当的服务器模块进行处理,把数据包发送给客户端。缺省情况下,核心服务器为每一个处理器创建一个任务线程。

媒体服务器使用模块来响应各种请求及完成任务。有三种类型的模块:

1.       内容管理模块

内容管理模块负责管理与媒体源相关的RTSP请求和响应,比如一个文件或者一个广播。每个模块负责解释客户的请求,读取和解析它们的支持文件或者网络源,并且以RTSP和RTP的方式进行响应。在某些情况下,比如流化mp3的模块,使用的则是HTTP。

QTSSFileModule,QTSSReflectorModule,QTSSRelayModule,和QTSSMP3StreamingModule都是内容管理模块。

2.       服务器支持模块

服务器支持模块执行服务器数据的收集和记录功能。服务器模块包括QTSSErrorLogModule, QTSSAccessLogModule,QTSSWebStatsModule,QTSSWebDebugModule, QTSSAdminModule,和QTSSPOSIXFileSystemModule。

3.       访问控制模块

访问控制模块提供鉴权和授权功能,以及操作URL路径提供支持。

访问控制模块包括QTSSAccessModule,QTSSHomeDirectoryModule,QTSSHttpFileModule,和QTSSSpamDefenseModule。

当一个模块需要访问客户请求的RTSP报头时,可以通过QTSS.h这个API头文件中定义的请求对象来访问相应的请求信息。举例来说,RTSPRequestInterface类实现了API字典元素,这些元素可以通过API来进行访问。名称是以“Interface”结尾的对象,比如RTSPRequestInterface,RTSPSessionInterface,和QTSServerInterface,则用于实现模块的API。

下面是重要的接口类:

  • QTSServerInterface — 这是内部数据的存储对象,在API中标识为QTSS_ServerObject。在API中的每一个QTSS_ServerAttributes都在基类中声明和实现。
  • RTSPSessionInterace — 这是内部数据的存储对象,在API中标识为qtssRTSPSessionObjectType。在API中的每一个QTSS_RTSPSessionAttributes都在基类中声明和实现。
  • RTPSessionInterface — 这是内部数据的存储对象,在API中标识为QTSS_ClientSessionObject。在API中的每一个QTSS_ClientSessionAttributes都在基类中声明和实现。
  • RTSPRequestInterface — 这是内部数据的存储对象,在API中标识为QTSS_RTSPRequestObject。在API中的每一个QTSS_RTSPRequestAttributes都在基类中声明和实现。

Server.tproj

这个目录包含核心服务器(core server)的代码,可以分成三个子系统:

  • 服务器内核。这个子系统中的类都有一个QTSS前缀。QTSServer负责处理服务器的启动和关闭。QTSServerInterface负责保存服务器全局变量,以及收集服务器的各种统计信息。QTSSPrefs是存储服务器偏好设定的地方。QTSSModule,QTSSModuleInterface,和QTSSCallbacks类的唯一目的就是支持QTSS的模块API。
  • RTSP子系统。这些类负责解析和处理RTSP请求,以及实现QTSS模块API的RTSP部分。其中的几个类直接对应QTSS API的一些元素(比如,RTSPRequestInterface类就是对应于QTSS_RTSPRequestObject对象)。每个RTSP TCP连接都有一个RTSP会话对象与之相对应。RTSPSession对象是一个Task对象,负责处理与RTSP相关的事件。
  • RTP子系统。这些类处理媒体数据的发送。RTPSession对象包含与所有RTSP会话ID相关联的数据。每个RTPSession都是一个Task对象,可以接受核心服务器的调度来进行RTP数据包的发送。RTPStream对象代表一个单独的RTP流,一个RTPSession对象可以和任何数目的RTPStream对象相关联。这两个对象实现了QTSS模块API中的针对RTP的部分。

CommonUtilitiesLib

这个目录含有一个工具箱,包括线程管理,数据结构,网络,和文本解析工具。Darwin流媒体服务器及其相关工具通过这些类对类似或者相同的任务进行抽象,以减少重复代码;这些类的封装简化了较高层次的代码;借助这些类还分离了专用于不同平台的代码。下面是对目录下的各个类的简短描述:

  • OS类。这些类在时间,条件变量,互斥锁,和线程方面提供了专用于不同平台的代码抽象。这些类包括OS,OSCond,OSMutex,OSThread,和OSFileSource;数据结构则包括OSQueue,OSHashTable,OSHeap,和OSRef。
  • 套接口类(Sockets)。这些类为TCP和UDP网络通讯方面提供了专用于不同平台的代码抽象。通常情况下,套接口类是异步的(或者说是非阻塞的),可以发送事件给Task对象。这些类有:EventContext,Socket,UDPSocket,UDPDemuxer,UDPSocketPool,TCPSocket,和TCPListenerSocket。
  • 解析工具。这些类负责解析和格式化文本。包括StringParser,StringFormatter,StrPtrLen,和StringTranslator。
  • Task(任务):这些类实现了服务器的异步事件机制。

QTFileLib

流媒体服务器的一个主要特性就是它能够将索引完成(hinted)的QuickTime电影文件通过RTSP和RTP协议提供给客户。这个目录包含QTFile库的源代码,包括负责解析索引完成的QuickTime文件的代码。服务器的RTPFileModule通过调用QTFile库来从索引过的QuickTime文件中取得数据包和元数据。QTFile库可以解析下面几种文件类型:.mov,.mp4(.mov的一种修改版本),和.3gpp(.mov的一种修改版本)。

APICommonCode

这个目录包含与API相关的类的源代码,比如moduletils,或者诸如记录文件的管理这样的公共模块函数。

APIModules

这个目录包含流媒体服务器模块目录,每个模块都有一个目录。

RTSPClientLib

这个目录包含实现RTSP客户端的源代码,这些代码可以用于连接服务器,只要该连接协议被支持。

RTCPUtilitiesLib

这个目录包含解析RTCP请求的源代码。

APIStubLib

这个目录包含API的定义和支持文件。

HTTPUtilitiesLib

这个目录包含解析HTTP请求的源代码。

  每个DSS模块必须实现两个函数:一个是Main函数,服务器在启动时将调用这个函数进行必要的初始化。另一个是Dispatch函数,通过实现此函数,服务器可调用DSS模块并完成特定处理。对于编译到服务器里面的模块,其主函数的地址必须传递到服务器的模块Main函数中。

  具体实现时,Main函数必须命名为MyModule_Main,其中MyModule是模块的文件名。此函数的实现通常如下所示:

QTSS_Error MyModule_Main(void* inPrivateArgs)

{

  return _stublibrary_main(inPrivateArgs, MyModuleDispatch);

  每个DSS模块都必须提供一个Dispatch函数。服务器为了特定的目的需要使用某个模块时,是通过调用该模块的Dispatch函数来实现的,调用时必须将任务的名称及相应的参数传递给该函数。在DSS中,使用角色(Role)这个术语来描述特定的任务。Dispatch函数的格式如下所示:

  void MyModuleDispatch(QTSS_Role inRole,QTSS_RoleParamPtr inParams);

  其中MyModuleDispatch是Dispatch函数的名称;MyModule是模块的文件名;inRole是角色的名称,只有注册了该角色的模块才会被调用;inParams则是一个结构体,可用于传递相应的参数。

对DSS进行扩展,以实现对TS流的支持,主要涉及三个方面的问题:

首先,RTSP协议需要支持TS over DVB-C;

其次,能够通过UDP协议直接发送TS流;

最后,PTCP的实现,发送的速率需要依据PCR[1](Program ClockReference,即节目时钟参考)实现适当的调节。下面针对这三个方面问题的解决进行简要的说明:

为了让RTSP协议能支持TS传输,需要对标准的RTSP协议做扩展,即在SETUP阶段,终端告诉服务器需要TS传输,服务器会为该终端分配传输资源,并告诉终端相应的参数(包括频点和节目号等)。 当使用扩展后的RTSP协议实现一次TS流点播时,与通常的RTSP交互过程相比,在SETUP阶段有所不同。

为了实现TS流通过Cable下发,关键点是视频服务器能够采用UDP协议将TS流依特定速率发送到播放设备。

采用UDP协议把TS包发送,实现相对比较简单,假定TS包的大小是188字节的,只要遵照一个UDP包不应大于以太网最大传输单元的原则,将7个TS包打包成一个UDP包,发送给播放器设备即可实现。

而依特定的速率发送则要求服务器在发送TS流时,必须保证发送数据的速率与媒体正常播放的速率一致性。考虑到终端会有一个缓冲区来平滑发送数据时可能产生的波动,因此对于发送速率与正常播放速率的一致性的要求并不是绝对的。但发送数据带来的波动要在播放设备许可的范围内,否则无法正常播放。

Apple公司Darwin流式服务器源代码分析

当前,伴随着Internet的飞速发展,计算机网络已经进入到每一个普通人的家庭。在这个过程中,一个值得我们关注的现象是:Internet中存储和传输内容的构成已经发生了本质的改变,从传统的基于文本或少量图像的主页变为大容量、富信息量的流式媒体信息。一份早在1998年提交的研究报告就曾指出,流式媒体统治Internet的潮流是不可抗拒的,该报告估计到2003年,存储在网络服务器上的内容超过50%的将是流式媒体信息。但今天看来,这个估计还是有些保守了。
所谓的流式媒体简单的讲就是指人们通过网络实时的收看多媒体信息:如音频流、视频流等。与流式媒体对应的传统工作方式是下载+播放模式,即用户首先下载多媒体文件,然后再在本地播放,这种方法的一个主要缺点是启动延迟较大,例如一个30分钟长的MPEG-I文件(相当于VCD质量),即使使用1.5Mbps的速率下载,也需要半个小时才能完成,这样一个漫长的等待时间实在是无法忍受。在窄带网络环境中,几乎所有基于Internet的流式媒体产品都有着类似的工作原理:首先需要开发高效的压缩编码技术,并通过一套完整有效的传输体系将其发布到用户的桌面上。目前在流式媒体领域,有三种占有主导地位的产品,它们分别是Apple公司的Quick Time、Microsoft公司的Media Server以及Real公司的Real System。本文将介绍QuickTime技术及其开放源代码的Darwin流化服务器。
1     QuickTime技术介绍
Apple公司近日发布了QuickTime 5及QuickTime Streaming Server 3(简称QTSS)。作为客户端的QuickTime 5是用于在Internet上对高质量音频和视频内容进行创建、播放及提供数字流的软件,目前QuickTime在全世界的使用量已经超过1亿5千万份。QuickTime Streaming Server 3是Apple基于标准的、开放式源代码的流式服务器软件的新版本,它包括以下新功能:跳读保护(Skip Protection),一项获得专利的特性组合,它可以保证Internet上数字流的质量,防止中断;全新的易于使用、基于Web的界面,用户可以在本地或远程进行管理,实现服务器配置。作为Internet流媒体联盟(ISMA)的创建者之一,Apple不断致力于开⒎弦到绫曜嫉牟泛图际酰ü岣呋ゲ僮餍岳从呕没У氖褂锰逖椋壳癚uickTime已被国际标准组织(ISO)选为MPEG-4的基本文件格式,可预见Apple将有更多MPEG-4 产品和技术的推出。
QuickTime正迅速成为世界领先的跨平台多媒体技术,而且是迄今为止唯一的开放式源代码、基于标准的数字流解决方案。ZDNet在2000年9月对于三种流式媒体服务器的特征比较说明了QTSS不仅仅被技术开发者关注,而且可以通过简单的定制成为成熟强大的产品,评测结果可见表1。
表1  ZDNet对三类产品的评测结果
服务器模块    QTSS 2.01     Media Server 7       RealServer Basic 7
操作系统支持       Windows NT, 2000; FreeBSD; Linux; Mac OS; Solaris       Windows NT, 2000       Windows NT, 2000
并发流个数    2,000      2,000      25 free/3000 pro
现场直播和广播    Yes  Yes  Yes
在线广告支持      Yes  Yes  Yes
PPV/流加密   No / No   Yes / Yes Yes / Yes
分配流能力    No   Yes  Yes
SMIL标准支持     Yes  No   Yes
RTSP标准支持     Yes        No   Yes
多播支持       Yes  Yes  Yes
状态报告       Yes  Yes  Yes
服务器日志    Yes  Yes  Yes
防火墙和代理支持       Yes  Yes  Yes
远程监控       Yes  Yes  Yes
客户可以使用QuickTime Player或其他支持QuickTime的应用程序在Windows或Macintosh平台上接收视频流,而且QuickTime Player可以从苹果公司的网站上下载免费使用。如果安装了QuickTime的插件,客户还可以直接通过浏览器收看。
客户希望点播一个节目时,QuickTime Player或插件将向QTSS发送请求,指明要点播的节目名。如果该节目存在,QTSS将向客户发送相应的视频流。当客户希望收看现场直播(或实时广播)时,它首先从QTSS获得关于当前频道的编码格式、地址等相关信息,然后再接受该频道的媒体流。
对于那些希望在Internet上实时流化视频或音频信息的用户,QTSS服务器将是一个很好的选择,通过它可实现多项任务,例如:
创建一个24小时在线的Internet广播电台;
现场实况转播:如公司会议、体育节目等;
创建远程学习站点:如能够点播视频和演讲; 
       图1是一个利用QTSS服务器建立的现场直播场景。
2     Darwin流化服务器介绍
Darwin Streaming Server(简称DSS)是QuickTime Streaming Server开放式源代码的版本,同时支持FreeBSD、Linux、Solaris、Windows NT和Windows 2000等多个操作系统,是当前所有同类产品中支持平台最多的一个。DSS的源代码和相关文档可从以下站点获得:http://www.apple.com
DSS源代码完全采用标准C++语言写成,编程风格非常优秀,每个C++类都对应着一对和类同名的.h/.cpp文件。但是由于大量采用了面向对象的概念,如继承、多态等等;而且源文件和类相当多,所以不太容易讲清楚。因此,读者最好事先把代码完整的过滤一两遍,再配合本文,就能看得更清楚点。
整个服务器包括多个子系统,分别存放在独立的工程内,如图2所示。
其中,最为重要的是基础功能类库(CommonUtilitiesLib)和流化服务器(StreamingServer)两个工程,前者是整个系统的通用代码工具箱,包括了线程管理、数据结构、网络和文本分析等多个功能模块。DSS和其他相关的工具使用基础功能类库工程中定义的功能类实现以下三个目标:
(1)抽象出系统中相同或类似的功能,用于降低代码的冗余度;
(2)封装基本功能,简化高层编码的复杂度;
(3)隔离开操作系统平台相关的代码。
而流化服务器工程中包含了DSS对多个国际标准的实现,是整个服务器的主工程。在本文中,我们将重点分析这两个工程中的核心代码和模块。另外,我们还将简单介绍利用DSS提供的开发接口(Module)扩展和定制服务器的方法。
DSS实现了四种IETF制定的国际标准,分别是:实时流传输协议RTSP(Real-time Streaming Protocol, RFC 2326)、实时传输协议(RTP Real-time Transfer Protocol,RFC 1889)、实时传输控制协议RTCP(Real-time Transport Control Protocol,RFC 1889)、会话描述协议SDP(Session Description Protocol,RFC 2327)。这四个标准是开发所有流式媒体产品都必须掌握的,因此在对相关代码进行分析和二次开发之前,希望读者了解上述四种协议的基本思想,上述协议样本可从以下网站获得:http://www.ietf.org
3     基础功能类库(Common Utilities)
3.1   OS类
Darwin Streaming Server支持包括Windows,Linux以及Solaris在内的多种操作系统平台。我们知道,Windows和Unix(或Unix-like)操作系统之间无论从内核还是编程接口上都有着本质的区别,即使是Linux和Solaris,在编程接口上也大为不同。为此,DSS开发了多个用于处理时间、临界区、信号量、事件、互斥量和线程等操作系统相关的类,这些类为上层提供了统一的使用接口,但在内部却需要针对不同的操作系统采用不同的方法实现。表2罗列出了DSS中的主要OS类和数据结构。
表2  DSS中的主要OS类和数据结构
类(数据结构)名       主要功能
OS   平台相关的功能类,如内存分配、时间等
OSCond  状态变量的基本功能和操作
OSMutex 互斥量的基本功能和操作
OSThread       线程类
OSFileSource  简单文件类
OSQueue 队列类
OSHashTable  哈希表类
OSHeap   堆类
OSRef     参考引用类
3.1.1       OSMutex/OSCond Class
在有多个线程并发运行的环境中,能同步不同线程的活动是很重要的,DSS开发了OSMutex和OSCond两个类用以封装不同操作系统对线程同步支持的差异。
我们首先分析OSMutex类,这个类定义了广义互斥量的基本操作,类定义如下:
class OSMutex
{
1     public:
2            OSMutex();         //构造函数
3            ~OSMutex();              //析构函数
4            inline void Lock();              //加锁
5            inline void Unlock();    //解锁
6            inline Bool16 TryLock();     //异步锁,无论是否成功立即返回
7     private:
8     #ifdef __Win32__
9            CRITICAL_SECTION fMutex;  //临界区
10           DWORD        fHolder;                     //拥有临界区的线程id
11           UInt32           fHolderCount;             //进入临界区线程数
              //其他略…
}
在Windows平台上,OSMutex类是通过临界区(CRITICAL_SECTION)来实现的,第10行定义了临界区变量fMutex。类实例化时构造函数调用InitializeCriticalSection(&fMutex)初始化临界区变量,对应的在析构函数中调用DeleteCriticalSection(&fMutex)清除。
Lock()函数用于对互斥量加锁,它调用私有方法RecursiveLock实现:
void        OSMutex::RecursiveLock()
{
       // 当前线程已经拥有互斥量,只需增加引用计数
1     if (OSThread::GetCurrentThreadID() == fHolder)
2     {
3            fHolderCount++;  //增加引用计数
4            return;
5     }
6     #ifdef __Win32__
7            ::EnterCriticalSection(&fMutex); //申请进入临界区
8     #else
9            (void)pthread_mutex_lock(&fMutex);
10    #endif
11    Assert(fHolder == 0);
12    fHolder = OSThread::GetCurrentThreadID();    //更新临界区拥有者标志
13    fHolderCount++;  
14    Assert(fHolderCount == 1);
}
       第1行检测如果当前线程已经拥有互斥量,就只需将内部计数fHolderCount加1,以便纪录正在使用互斥量的方法数。如果当前线程还没有得到互斥量,第7行调用EnterCriticalSection()函数申请进入临界区;如果当前已经有其他线程进入临界区,该函数就会阻塞,使得当前线程进入睡眠状态,直到占用临界区的线程调用LeaveCriticalSection(&fMutex)离开临界区后才可能被唤醒。一旦线程进入临界区后,它将首先更新临界区持有者标志(第12行),同时将临界区引用计数加1。
       注意到另外一个函数TryLock(),该函数也是用于为互斥量加锁,但与Lock()不同的是,TryLock()函数为用户提供了异步调用互斥量的功能,这是因为它调用::TryEnterCriticalSection(&fMutex)函数申请进入缓冲区:如果临界区没有被任何线程拥有,该函数将临界区的访问区给予调用的线程,并返回TRUE,否则它将立刻返回FALSE。TryEnterCriticalSection()和EnterCriticalSection()函数的本质区别在于前者从不挂起线程。
接着分析OSCond类,该类定义了状态变量(Condition Variable)的基本操作,类定义如下:
class OSCond 
{
1     public:
2            OSCond();   //构造函数
3            ~OSCond(); //析构函数

              4            inline void      Signal();       //传信函数
5            inline void      Wait(OSMutex* inMutex, SInt32 inTimeoutInMilSecs = 0);   
//等待传信函数
6            inline void       Broadcast(); //广播传信函数
7     private:
8     #ifdef __Win32__
9            HANDLE                     fCondition;   //事件句柄
10           UInt32                         fWaitCount;  //等待传信用户数
//其他略…
       }
       虽然同是用于线程同步,但OSCond类与OSMutex大不相同,后者用来控制对关键数据的访问,而前者则通过发信号表示某一操作已经完成。在Windows平台中,OSCond是通过事件(event)来实现的;构造函数调用CreateEvent()函数初始化事件句柄fCondition,而析构函数则调用CloseHandle()关闭句柄。
       OSCond的使用流程是这样的:线程调用Wait(OSMutex* inMutex, SInt32 inTimeoutInMilSecs = 0)函数等待某个事件的发生,其中inTimeoutInMilSecs是最长等待时间,0代表无限长。Wait()函数内部调用了WaitForSingleObject (fCondition, theTimeout)函数,该函数告诉系统线程在等待由事件句柄fCondition标识的内核对象变为有信号,参数theTimeout告诉系统线程最长愿意等待多少毫秒。如果指定的内核对象在规定时间内没有变为有信号,系统就会唤醒该线程,让它继续执行。而函数Signal()正是用来使事件句柄fCondition有信号的。Signal()函数内部实现很简单,只是简单调用SetEvent函数将事件句柄设置为有信号状态。
       使用OSCond的过程中存在一种需求,就是希望通知所有正在等待的用户事件已经完成,而Signal()函数每次只能通知一个用户,因此又开发了另外一个广播传信函数如下:
inline void OSCond::Broadcast()
{     //提示:本函数相当循环调用Signal()函数
1     #ifdef __Win32__
2     UInt32 waitCount = fWaitCount;       //等待传信的用户数
3     for (UInt32 x = 0; x ; x++)        //循环为每个用户传信
4     {
5            BOOL theErr = ::SetEvent(fCondition);             //设置事件句柄为有信号状态
6            Assert(theErr == TRUE);
7     }
//此处略…
       }
       Broadcast首先统计所有等待传信的用户数(第2行),然后用一个循环为每个用户传信(第3~7)行。这种编程方法虽然不是很优雅(elegant),但是由于Windows平台上不支持广播传信功能(Linux和Solaris均支持),也只好如此。
3.1.2       OSThread Class
OSThread是DSS中最重要的类之一,它封装并且定义了使用线程的方式,因此需要重点讨论。OSThread类的定义如下:
class OSThread
{
1     public:
       // 必须在使用其他OSThread函数前调用该初始化函数
2     static void              Initialize();

                            3     OSThread(); //构造函数
4     virtual                                 ~OSThread();      //析构函数

              //子类继承该纯虚函数完成自己的工作
5     virtual     void                     Entry() = 0;  

       6     void                     Start(); //启动线程
7     void                     Join();  //等待线程运行完成后删除
8     void                     Detach();     //使线程处于fDetached状态
9     static void              ThreadYield();     //Windows平台不用
10    static void              Sleep(UInt32 inMsec); //让线程睡眠
       …
11    private:
       //标识线程的状态
12    Bool16 fStopRequested:1;
13    Bool16 fRunning:1;
14    Bool16 fCancelThrown:1;
15    Bool16 fDetached:1;
16    Bool16 fJoined:1;
       …
17    static void              CallEntry(OSThread* thread);//调用子类重载的虚函数
18    #ifdef __Win32__
//使用_beginghreadex创建线程时的标准入口函数
19    static unsigned int WINAPI _Entry(LPVOID inThread); 
20    #else
21    static void*    _Entry(void* inThread); //unix下的入口函数
22    #endif
}
OSThread封装了线程的基本功能,一个OSThread的实例代表一个线程。用户通过继承OSThread,并且重载其中的纯虚函数Entry(第5行),从而将自己的任务交给该线程运行。OSThread内部运行机制比较复杂,为此我们用图3所示的流程来描述其运行过程。
      另外,OSThread对于线程的状态定义了一套完整的控制方法。当用户调用start()函数后,按照上图,最终将调用CallEntry()函数,而该函数在调用Entry()之前将线程设定为运行状态(thread->fRunning = true),当Entry()函数运行完后再设为非运行状态;在运行过程中,用户可以通过StopAndWaitForThread()、join()、Detach()以及ThrowStopRequest()等函数改变线程其他状态变量。
3.1.3       OSHashTable/OSQueue/OSHeap/OSRef Class
DSS定义了几个通用的较为复杂的数据结构,它们都以类的方式封装。这些数据结构不但贯穿于DSS的所有源代码,而且由于其封装的十分好,读者可以在看懂源代码的基础上很容易的将它们从DSS的工程中抽取出来,构建自己的基础类库,为将来的开发工作打下良好的基础。另外,对这些基础数据结构源代码的研究将提高我们对于面向对象技术的掌握和领会。
       最主要的数据结构有四种:哈希表(OSHashTable)、队列(OSQueue)、堆(OSHeap)和对象引用表(OSRef)。前三种是我们在编程中大量使用的数据结构,而对象引用表则是类似于COM/DCOM组件编程中IUNKOWN接口功能的数据结构,它首先为每个对象建立了一个字符串形式的ID,以便于通过这个ID找到对象(类似于QueryInterface);另外OSRef类还为每个对象实例建立了引用计数,只有一个对象不再被任何人引用,才可能被释放(类似于AddRef和Release)。
       鉴于这几个类在结构上有相似之处,下面我们将分析OSHashTable的源代码,以便能够帮助读者更好的理解其他几个类。OSHashTable的代码如下:
       template
       class OSHashTable {
       /*提示:OSHashTable被设计成为一个类模版,两个输入参数分别为:class T:实际的对象类;class K:用于为class T计算哈希表键值的功能类。*/
1     public:
2     OSHashTable( UInt32 size )   //构造函数,入参是哈希表中对象的最大个数
3     {
4            fHashTable = new ( T*[size] );   //申请分配size个哈希对象class T的空间
5            Assert( fHashTable );
6            memset( fHashTable, 0, sizeof(T*) * size );      //初始化
7            fSize = size;
/*下面的代码决定用哪种方式为哈希表的键值计算索引;
如果哈希表的大小不是2的幂,只好采用对fSize求余的方法;
否则可以直接用掩码的方式,这种方式相对速度更快*/
8            fMask = fSize - 1;
9            if ((fMask & fSize) != 0)             //fSize不是2的幂
10                  fMask = 0;
11           fNumEntries = 0; //当前对象数
12    }
13    ~OSHashTable()     //析构函数
14    {
15           delete [] fHashTable;   //释放空间
16    }
//下面介绍向哈希表中添加一个class T对象的源代码
17    void Add( T* entry ) {
18           Assert( entry->fNextHashEntry == NULL );
                     /*利用功能类class K,计算class T对象的哈希键值,其计算方法由用户在class K中定义*/
       19           K key( entry );     
20           UInt32 theIndex = ComputeIndex( key.GetHashKey() );//利用键值计算索引
21           entry->fNextHashEntry = fHashTable[ theIndex ]; //在新加对象中存储索引值
22           fHashTable[ theIndex ] = entry; //将该对象插入到索引指定的位置
23           fNumEntries++;   /
24    }
//下面介绍从哈希表中删除一个class T对象的源代码
25    void Remove( T* entry )
26    {
//首先从哈希表中找到待删除的对象
//1、计算哈希键值和其对应的对象索引
27           key( entry );        
28           UInt32 theIndex = ComputeIndex( key.GetHashKey() );   
29           T* elem = fHashTable[ theIndex ];
30           T* last = NULL;
/*2、通过对象索引查找对象,如果不是要找的对象,接着找下一个,直到找到为止。这是因为,存放的时候就是按照这种模式计算索引的。*/
31           while (elem && elem != entry) { 
32                  last = elem;
33                  elem = elem->fNextHashEntry;
34           }
              //找到该对象,将其删除
35           if ( elem )       
36           {
37                  Assert(elem);
38                  if (last)    
39                         last->fNextHashEntry = elem->fNextHashEntry;
40                  else //elem在头部
41                         fHashTable[ theIndex ] = elem->fNextHashEntry;
42                  elem->fNextHashEntry = NULL;
43                  fNumEntries–;
44           }
45    }
//下面介绍从哈希表中查找一个class T对象的方法
46    T* Map( K* key )  //入参为哈希键值
47    {
48                  UInt32 theIndex = ComputeIndex( key->GetHashKey() ); //计算索引
49                  T* elem = fHashTable[ theIndex ];     //找到索引对应的对象
50                  while (elem) {
51                         K elemKey( elem );
52                         if (elemKey =*key) //检查是否找对
53                                break;
54                         elem = elem->fNextHashEntry;   //如果不是,继续找下一个
55                  }
56                  return elem;
57           }
//以下略…
}
       以上介绍了哈希表的构造以及三种基本操作:添加、删除和查询。另外,DSS还定义了OSHashTableIter类用于枚举OSHashTable中的class T对象;其中主要的操作有First和Next等,限于篇幅,此处就不再详述。
3.2   Tasks类
因为服务器从整体上采用了异步的运行模式,这就需要一种用于事件通信的机制。举例来说:一个RTSP连接对应的Socket端口监测到网络上有数据到达,此时必须有一个模块(或代码)被通知(notify)去处理这些数据。为此,DSS定义了Task及其相关类作为实现这一通信机制的核心。
在Task.h/cpp文件中,定义了三个主要的类,分别是:任务线程池类(TaskThreadPool Class)、任务线程类(TaskThread Class)以及任务类(Task Class)。
每个Task对象有两个主要的方法:Signal和Run。当服务器希望发送一个事件给某个Task对象时,就会调用Signal()方法;而Run()方法是在Task对象获得处理该事件的时间片后运行的,服务器中的大部分工作都是在不同Task对象的Run()函数中进行的。每个Task对象的目标就是利用很小的且不会阻塞的时间片完成服务器指定某个工作。
任务线程类是上文介绍的OSThread类的一个子类,代表专门用于运行任务类的一个线程。在每个任务线程对象内部都有一个OSQueue_Blocking类型的任务队列,存储该线程需要执行的任务。后面的分析可以看到,服务器调用一个任务的Signal函数,实际上就是将该任务加入到某个任务线程类的任务队列中去。另外,为了统一管理这些任务线程,DSS还开发了任务线程池类,该类负责生成、删除以及维护内部的任务线程列表。图4描述了任务类的运行。
       下面我们首先分析TashThread类,该类的定义如下:
class TaskThread : public OSThread     //OSThread的子类
{     //提示:所有的Task对象都将在TaskThread中运行
       1     public:
       2     TaskThread() :       OSThread(), fTaskThreadPoolElem(this){}  //构造函数
3     virtual                   ~TaskThread() { this->StopAndWaitForThread(); } //析构函数
       4     private:
              …
       5     virtual void     Entry();       //从OSThread重载的执行函数,仍然能够被子类重载
       6     Task*                   WaitForTask();    //检测是否有该执行的任务

                     7     OSQueueElem        fTaskThreadPoolElem;       //对应的线程池对象
       8     OSHeap                        fHeap; //纪录任务运行时间的堆,用于WaitForTask函数
              /*关键数据结构:任务队列;在Task的Signal函数中直接调用fTaskQueue对象的EnQueue函数将自己加入任务队列*/
       9     OSQueue_Blocking fTaskQueue; 
              //此处略…
       }
       作为OSThread的子类,TaskThread重载了Entry函数,一旦TaskThread的对象被实例化,便运行该函数。Entry()函数的主要任务就是调用WaitForTask()函数监测任务队列,如果发现新任务,就在规定时间内执行;否则,就被阻塞。下面我们简要分析Entry()函数的流程:
       void TaskThread::Entry()
{
       1     Task* theTask = NULL; //空任务

              2     while (true) //线程循环执行
       3     {     //监测是否有需要执行的任务,如果有就返回该任务;否则阻塞;
       4            theTask = this->WaitForTask(); 
       5            Assert(theTask != NULL);

              6            Bool16 doneProcessingEvent = false; //尚未处理事件

              7            while (!doneProcessingEvent)
       8            {
       9            theTask->fUseThisThread = NULL; // 对任务的调度独立于线程
       10           SInt64 theTimeout = 0;      //Task中Run函数的返回值,重要
                     //核心部分:运行任务,根据返回值判断任务进度
       11           if (theTask->fWriteLock)
       12           {     //如果任务中有写锁,需要使用写互斥量,否则可能造成死锁
       13                  OSMutexWriteLocker mutexLocker(&TaskThreadPool::sMutexRW);
       14                  theTimeout = theTask->Run();   //运行任务,得到返回值
       15                  theTask->fWriteLock = false;
       16           }
       17           else
       18           {     //使用读互斥量
       19                  OSMutexReadLocker mutexLocker(&TaskThreadPool::sMutexRW);
       20                  theTimeout = theTask->Run();   //运行任务,得到返回值
       21           }
       22           //监测Task中Run()函数的返回值,共有三种情况
       23           //1、返回负数,表明任务已经完全结束
       24           if (theTimeout        25           {
       26                  delete theTask;     //删除Task对象
       27                  theTask = NULL;
       28                  doneProcessingEvent = true;
       19           }
       30           //2、返回0,表明任务希望在下次传信时被再次立即执行
       31           else if (theTimeout=0)
       32           {
       33                  doneProcessingEvent = compare_and_store(Task::kAlive, 0, &theTask->fEvents);
       34                  if (doneProcessingEvent)
       35                         theTask = NULL; 
       36           }
                     //3、返回正数,表明任务希望在等待theTimeout时间后再次执行
       37           else
       38           {
                     /*将该任务加入到Heap中,并且纪录它希望等待的时间。Entry()函数将通过waitfortask()函数进行检测,如果等待的时间到了,就再次运行该任务*/
       39                  theTask->fTimerHeapElem.SetValue(OS::Milliseconds() + theTimeout);
       40                  fHeap.Insert(&theTask->fTimerHeapElem);
       41                  (void)atomic_or(&theTask->fEvents, Task::kIdleEvent);//设置Idle事件
       42                  doneProcessingEvent = true;
       43           }
              //此处略…
       }
       注意,如果Task的Run()函数返回值TimeOut为正数,意味着该任务是一个周期性的工作,例如发送数据的视频泵(pump),需要每隔一定时间就发出一定量的视频数据,直至整个节目结束。为此,在第38~43行,将该任务加入到堆fHeap中去,并且标记该任务下次运行的时间为TimeOut毫秒之后。将来通过调用WaitForTask()函数就能检测到该任务是否到达规定的运行时间,WaitForTask()函数的代码如下:
       Task* TaskThread::WaitForTask()
{
       1     while (true)
       2     {     //得到当前时间,该函数为静态函数,定义见OS.h
       3            SInt64 theCurrentTime = OS::Milliseconds(); 
                     /*如果堆中有任务,且任务已经到执行时间,返回该任务。 PeekMin函数见OSHeap.h,窃听堆中第一个元素(但不取出)*/
4     if ((fHeap.PeekMin() != NULL) && (fHeap.PeekMin()->GetValue() 从堆中取出第一个任务返回
5                   return (Task*)fHeap.ExtractMin()->GetEnclosingObject();
              //如果堆中有任务,但是尚未到执行时间,计算需要等待的时间
       6            SInt32 theTimeout = 0;
       7            if (fHeap.PeekMin() != NULL)      //计算还需等待的时间
       8                   theTimeout = fHeap.PeekMin()->GetValue() - theCurrentTime;
       9            Assert(theTimeout >= 0);

                            //等待theTimeout时间后从堆中取出任务返回
       10           OSQueueElem* theElem = fTaskQueue.DeQueueBlocking(this, theTimeout);
       11           if (theElem != NULL)
       12                  return (Task*)theElem->GetEnclosingObject();
       13    }     
}
       上文曾经提到,Task对象内有两个方法:Signal和Run。Run函数是一个虚函数,由Task的子类重载,它的用法我们在分析TaskThread的Entry()函数和WaitForTask()函数中已经讨论了。而另一个Signal()函数也十分重要:服务器通过调用该函数将Task加入TaskThread,并且执行Run()函数。Signal()函数的核心部分如下:
       void Task::Signal(EventFlags events)
{
              …
              // fUseThisThread用于指定该任务运行的任务线程
       1     if (fUseThisThread != NULL)       //存在指定任务线程
                     //将该任务加入到指定任务线程的任务队列中
       2            fUseThisThread->fTaskQueue.EnQueue(&fTaskQueueElem);
              //不存在指定的任务线程,随机选择一个任务线程运行该任务
3     else
       4     {
                     //从线程池中随机选择一个任务线程
       5            unsigned int theThread = atomic_add(&sThreadPicker, 1);
       6            theThread %= TaskThreadPool::sNumTaskThreads;
                     //将该任务加入到上面选择的任务线程的任务队列中
       7            TaskThreadPool::sTaskThreadArray[theThread]-> fTaskQueue.EnQueue (&fTaskQueueElem);
       8            }
       }
       至此我们已经将DSS的线程和任务运行机制分析完了,这种由事件去触发任务的概念已经被集成到了DSS的各个子系统中。例如,在DSS中经常将一个Task对象和一个Socket对象关联在一起,当Socket对象收到事件(通过select()函数),相对应的Task对象就会被传信(通过Signal()函数);而包含着处理代码的Run()函数就将在某个任务线程中运行。
       因此,通过使用这些Task对象,我们就可以让所有连接都使用一个线程来处理,这也是DSS的缺省配置方法。
3.3   Socket类
作为一个典型的网络服务器,DSS源代码中的Socket编程部分是其精华之一。DSS定义了一系列Socket类用于屏蔽不同平台在TCP/UDP编程接口和使用方法上的差异。DSS中的Socket类一般都采用异步模式的(即非阻塞的),而且能够向对应的Task对象传信(Signal),这点我们在上一节介绍过。Socket类中具有代表性的类是:EventContext、EventThread、Socket、UDPSocket、TCPSocket以及TCPListenerSocket等等,它们之间的继承关系见图5。
       在eventcontext.h/.cpp文件中,定义了两个类:EventContext类和EventThread类。 Event Context提供了检测Unix式的文件描述符(Socket就是一种文件描述符)产生的事件(通常是EV_RE 或 EV_WR)的能力,同时还可以传信指定的任务。EventThread类是OSThread类的子类,它本身很简单,只是重载了OSThread的纯虚函数Entry(),用以监控所有的Socket端口是否有数据到来,其代码分析如下:
       void EventThread::Entry()
{
/*该结构定义在ev.h中,记录Socket描述符和在该描述符上发生的事件*/
       1     struct eventreq theCurrentEvent;       
       2     ::memset( &theCurrentEvent, ‘\0’, sizeof(theCurrentEvent) );   //初始化该结构

              3     while (true)
4     {
//首先监听Socket端口的事件
       5            int theErrno = EINTR;
       6            while (theErrno=EINTR)
       7            {
8     #if MACOSXEVENTQUEUE //Macos平台
       9                   int theReturnValue = waitevent(&theCurrentEvent, NULL);
10    #else       //其他平台
              /*调用select_waitevent函数监听所有的Socket端口,直到有事件发生为止*/
       11                  int theReturnValue = select_waitevent(&theCurrentEvent, NULL);
12    #endif     
              …
              //有事件发生,唤醒相应的Socket端口
13    if (theCurrentEvent.er_data != NULL)
       14    {
                     //通过事件中的标识找到相应的对象参考指针
       15           StrPtrLen idStr((char*)&theCurrentEvent.er_data, sizeof(theCurrentEvent.er_data));
       16           OSRef* ref = fRefTable.Resolve(&idStr);
       17           if (ref != NULL)
       18           {     //通过参考指针得到EventContext对象
       19                  EventContext* theContext = (EventContext*)ref->GetObject();
                            //利用EventContext对象的ProcessEvent方法传信对应的Task
       20                  theContext->ProcessEvent(theCurrentEvent.er_eventbits);
       21                  fRefTable.Release(ref);       //减少引用计数
       22           }
//此处略…
}
       上述代码有两点需要注意:首先在第11行,调用select_waitevent函数监听所有Socket端口的事件。该函数在Windows平台上是采用WSAAsyncSelect(异步选择)模型实现的。具体实现是:系统首先创建一个窗口类,该类专门用于接受消息;在每个Socket端口创建后,调用WSAsyncSelect函数,同时将上述窗口类的句柄作为参数传入;将来这些Socket端口有事件发生时,Windows就会自动将这些事件映射为标准的Windows消息发送给窗口类,此时select_waitevent函数通过检查消息就能够获得对应Socket端口发生的事件。对于Windows平台下Socket的异步编程技术细节请参阅《Windows网络编程技术》一书。
       另外,在第20行调用的EventContext对象的ProcessEvent函数实现上很简单,只有一行代码:fTask->Signal(Task::kReadEvent);其中fTask为该EventContext对象对应的Task对象;ProcessEvent函数向Task对象传信,以便及时处理刚刚发生的Socket事件。
       与EventThread对应的EventContext对象负责维护指定的描述符,其主要函数包括InitNonBlocking、CleanUp和RequestEvent等。其中InitNonBlocking函数调用Socket API ioctlsocket将用户指定的描述符设置为异步,CleanUp函数用于关闭该描述符;另外,用户通过RequestEvent函数申请对该描述符中某些事件的监听,如前所述,该函数内部调用了WSAsyncSelect来实现这一功能。
       Socket Class、UDPSocket Class和TCPSocketClass三个类都是EventContext的子类,它们封装了TCP和UDP的部分实现,同时扩展了EventContext中的事件,但都没有改变其运行机制,因此此处不再详述,留给读者自行分析。我们要为大家分析的是另外一个比较复杂的Socket类TCPListenerSocket类。TCPListenerSocket用于监听TCP端口,当一个新连接请求到达后,该类将赋予这个新连接一个Socket对象和一个Task对象的配对。首先分析TCPListenerSocket类的主要定义如下:
       class TCPListenerSocket : public TCPSocket, public IdleTask
{
/*提示:该类从有两个基类,所以它既是一个事件监听者,同时也是一个任务Task。作为一个任务,给TCPListenerObject发送Kill事件就可以删除它*/
       1     public:
       2            TCPListenerSocket() :   TCPSocket(NULL, Socket::kNonBlockingSocketType), IdleTask(), fAddr(0), fPort(0), fOutOfDescriptors(false) {}  //构造函数
       3            virtual ~TCPListenerSocket() {}   //析构函数

                                   //addr为地址,port为端口号,初始化函数自动监听TCP端口
       4            OS_Error              Initialize(UInt32 addr, UInt16 port);
                     //子类必须重载该纯虚函数,用于建立新连接时生成任务对象
       5            virtual Task*   GetSessionTask(TCPSocket** outSocket) = 0;
       6            virtual SInt64  Run();  //重载Task的Run函数,子类仍可重载

                            7     private:
                     //重载EventContext的ProcessEvent函数,用于产生Socket和Task对象配对
8            virtual void ProcessEvent(int eventBits);
       9            OS_Error       Listen(UInt32 queueLength);
//其他略…
}
       前面我们分析得知,EventContext类通过ProcessEvent函数来实现对任务的传信工作,但在TCPListenerSocket 中,ProcessEvent函数被重载用来创建Socket和Task对象得配对,该函数的实现如下:
       void TCPListenerSocket::ProcessEvent(int /*eventBits*/)
{     /*提示:该函数运行于系统唯一的EventThread线程中,所以要尽量快速,以免占用过多的系统资源*/
              //此处略去部分定义…
       1     Task* theTask = NULL;     //Task对象
       2     TCPSocket* theSocket = NULL;       //Socket对象

                     //创建对象配对
       3     while (true)
       4     {     //accept连接
       5            int osSocket = accept(fFileDesc, (struct sockaddr*)&addr, &size);
       6            if (osSocket == -1) //监听端口出错
       7            {     //此处略去出错处理     }
                     //用子类重载的GetSessionTask函数创建Task对象
       8            if ((theTask = this->GetSessionTask(&theSocket))=NULL) //创建出错
       9                   close(osSocket);
       10           else  //创建成功,接着创建Socket对象
       11           {     
       12                  Assert(osSocket != EventContext::kInvalidFileDesc);
                            //此处略去部分对新建连接端口的设置(setsockopt函数)
                            //创建新的Socket对象
       13                  theSocket->Set(osSocket, &addr);
       14                  theSocket->InitNonBlocking(osSocket); //初始化
       15                  theSocket->SetTask(theTask); //设置对应的任务
       16           theSocket->RequestEvent(EV_RE); //新对象监听读事件
       17           }
       18    }
              //处理完一次连接请求后,TCPListenerSocket对象还要接着监听
       19    this->RequestEvent(EV_RE);
}
       对Socket类的分析基本完成了,从中我们可以发现,DSS对于网络传信和任务调度之间的处理非常精密,环环相扣,在某种程度上甚至是有些过a于花哨。但是这些基本类是上层RTSP/RTP等服务器子系统编码的基础,因此希望读者能够从本质上掌握这些代码。
4     核心功能库(Server Core)
4.1 RTSP 子系统
       RTSP标准是实时流控制协议(Real-Time Streaming Protocol RFC2326)的简称,它被客户和流式媒体服务器用来交换对媒体的控制信息。图6是RTSP基本操作的描述。
再给出一个RTSP协议的例子如下:
       DSS开发了一个RTSP子系统来支持标准的RTSP协议,本节将分析这些源代码。
       首先,DSS定义了一个TCPListenerSocket类的子类RTSPListenerSocket,用于监听RTSP连接请求。RTSPListenerSocket类做的唯一一件事就是重载了GetSessionTask函数,当客户的连接请求到达后,它创建了一个Socket对象和RTSPSession对象的配对。RTSPSession对象是Task类的子类,是专门用于处理RTSP请求的任务类。
       如图7所示,RTSP连接建立后,服务器会为每个客户维护一个Socket对象和RTSPSession对象的配对;当客户的RTSP请求到达时,Socket对象就会调用RTSPSession对象的Signal方法传信,即将RTSPSession对象加入到TaskThread对象的任务队列中去;而当时间片到来,TaskThread线程就会调用RTSPSession对象的Run方法,这个方法就会处理客户发送过来的RTSP请求。因此,下面我们将主要分析RTSPSession的Run方法。
       为了跟踪当前处理的情况,RTSPSession类内部定义了多个状态,而Run方法其实就是通过在这些状态之间不断切换,同时对客户的RTSP请求做出不同的处理。
                     enum
                     {
                     //RTSPSession的基本状态
                     kReadingRequest= 0,
                     kFilteringRequest= 1,
                     kRoutingRequest= 2,
                     kAuthenticatingRequest= 3,
                     kPreprocessingRequest= 4,
                     kProcessingRequest= 5,
                     kSendingResponse= 6,
                     kPostProcessingRequest       = 7,
                     kCleaningUp= 8,

                                   //当RTSP协议通过HTTP隧道实现时将用到下面的状态
       kWaitingToBindHTTPTunnel = 9,         
kSocketHasBeenBoundIntoHTTPTunnel = 10,
kHTTPFilteringRequest = 11,               
                     kReadingFirstRequest = 12,                 
                     kHaveNonTunnelMessage = 13                          
              }
       另外,值得注意的是,DSS提供一种称为Module的二次开发模式,开发人员可以编写新的Module并且注册其希望运行的状态,系统就会在相应的状态下调用该Module,从而将控制权暂时交给二次开发的代码,以便增强系统的功能。简单起见,下面我们将分析不存在客户模块的Run()函数源代码。首先分析其主框架如下:
       SInt64 RTSPSession::Run()
{
       1     EventFlags events = this->GetEvents();     //取出事件
       2     QTSS_Error err = QTSS_NoErr;
       3     QTSSModule* theModule = NULL;
       4     UInt32 numModules = 0;

              // 设定当前的Module状态
       5     OSThread::GetCurrent()->SetThreadData(&fModuleState);

              //检查该连接是否超时,如果是就设定状态断掉该连接
       6     if ((events & Task::kTimeoutEvent) || (events & Task::kKillEvent))
       7            fLiveSession = false;

                     8     while (this->IsLiveSession()) //如果连接尚未拆除,执行状态机
9     {
              /* 提示:下面是RTSPSession的状态机。因为在处理RTSP请求过程中,有多个地方需要Run方法返回以便继续监听新的事件。为此,我们需要跟踪当前的运行状态,以便在被打断后还能回到原状态*/
       10           switch (fState)
       11           {
       12                  case 状态1: //处理略
13    case 状态2: //处理略…
14    case 状态n: //处理略
       15           }     //此处略…
       }
       Run函数的主框架比较简单,其核心就在于10~15的状态机,因此我们希望按照客户请求到达并且被处理的主要流程为读者描述该状态机的运转。
       1第一次请求到达进入kReadingFirstRequest状态,该状态主要负责从RTSPRequestStream类的对象fInputStream中读出客户的RTSP请求,其处理如下:
              case kReadingFirstRequest:
              {
              1     if ((err = fInputStream.ReadRequest())=QTSS_NoErr)
              2     {/* RequestStream返回QTSS_NoErr意味着所有数据已经从Socket中读出,但尚不能构成一个完整的请求,因此必须等待更多的数据到达*/
              3            fInputSocketP->RequestEvent(EV_RE); //接着请求监听读事件
              4            return 0;      //Run函数返回,等待下一个事件发生
              5     }
              6     if ((err != QTSS_RequestArrived) && (err != E2BIG))
              7     {//出错,停止处理
              8            Assert(err > 0); 
              9            Assert(!this->IsLiveSession());
              10           break;
              11    }
                     //请求已经完全到达,转入kHTTPFilteringRequest状态
              12    if (err = QTSS_RequestArrived)
              13           fState = kHTTPFilteringRequest;
                     //接收缓冲区溢出,转入kHaveNonTunnelMessage状态
       14    if (err=E2BIG)
              15           fState = kHaveNonTunnelMessage;
              }
              continue;
       2正常情况下,在获得一个完整的RTSP请求后(上第12行),系统将进入kHTTPFilteringRequest状态该状态检查RTSP连接是否需要经过HTTP代理实现;如不需要,转入kHaveNonTunnelMessage状态。
       3进入kHaveNonTunnelMessage状态后,系统创建了RTSPRequest类的对象fRequest,该对象解析客户的RTSP请求,并保存各种属性。fRequest对象被传递给其他状态处理。
       4接着进入kFilteringRequest状态,二次开发人员可以通过编写Module对客户的请求做出特殊处理。如果客户的请求为正常的RTSP请求,系统调用SetupRequest函数建立用于管理数据传输的RTPSession类对象,其源代码分析如下:
       void RTSPSession::SetupRequest()
{
       // 首先分析RTSP请求,细节见RTSPRequest.h/.cpp
       1     QTSS_Error theErr = fRequest->Parse();
2     if (theErr != QTSS_NoErr)   
       3            return;

                     //OPTIONS请求,简单发回标准OPTIONS响应即可
4     if (fRequest->GetMethod() = qtssOptionsMethod)
       5     {//此处略去部分处理代码…
6     }

                     //DESCRIBE请求,必须保证已经有了SessionID
       7     if (fRequest->GetMethod() = qtssDescribeMethod)
       8     {
       9            if (fRequest->GetHeaderDictionary()->GetValue(qtssSessionHeader)->Len > 0)
       10           {
       11                  (void)QTSSModuleUtils::SendErrorResponse(fRequest, qtssClientHeaderFieldNotValid, qtssMsgNoSesIDOnDescribe);
12                  return;
       13           }
14    }

                     //查找该请求的RTPSession
       15    OSRefTable* theMap = QTSServerInterface::GetServer()->GetRTPSessionMap();
       16    theErr = this->FindRTPSession(theMap);
       17    if (theErr != QTSS_NoErr)
       18           return;

              //如果未查找到,建立一个新的RTPSession
       19    if (fRTPSession= NULL)
       20    {
       21           theErr = this->CreateNewRTPSession(theMap);
       22           if (theErr != QTSS_NoErr)
       23                  return;
       24    }
              //此处略…
}
       5进入kRoutingRequest状态,调用二次开发人员加入的Module,用于将该请求路由(Routing)出去。缺省情况下,系统本身对此状态不做处理。
       6进入kAuthenticatingRequest状态,调用二次开发人员加入的安全模块,主要用于客户身份验证以及其他如规则的处理。读者如果希望开发具有商业用途的流式媒体服务器,该模块必须进行二次开发。
       7进入kPreprocessingRequest和kProcessingRequest及kPostProcessingRequest状态,这三种状态都是通过调用系统自带或二次开发人员添加的Module来处理RTSP请求,例如系统提供了QTSSReflector Module、QTSSSplitter Module以及QTSSFile Module等模块。其中比较重要的QTSSFile Module属于QTLib库的部分,此处不再详述。
       8进入kSendingResponse状态,用于发送对客户RTSP请求处理完成之后的响应。系统在该状态调用了fOutputStream.Flush()函数将在fOutputStream中尚未发出的请求响应通过Socket端口完全发送出去。
       9进入kCleaningUp状态,清除所有上次处理的数据,并将状态设置为kReadingRequest等待下次请求到达。
       RTSPSession的主流程分析完了,但辅助其操作的多个RTSP类还需要读者自行分析,它们分别是:RTSPSessionInterface Class、RTSPRequest Class、RTSPRequestInterface Class、RTSPRequestStream Class以及RTSPResponseStream Class等等。
4.2 RTP子系统
       RTP标准是实时传输协议(Real-Time Transfer Protocol)的简称,它被客户和流式媒体服务器用来处理流式媒体数据的传输。在介绍RTSP的运行流程时,我们发现RTSPSession对象通过调用SetupRequest函数为客户建立RTPSession对象。RTPSession类是Task类的子类,因此它重载了Task类的Run函数,该函数通过调用FileModule.cpp文件中的SendPacket()函数向客户发送RTP协议打包的流式媒体数据。当客户通过利用RTSP向RTSPSession对象发出PLAY命令后,RTSPSession对象将调用RTPSession对象的Play()函数。Play函数准备好需要打包发送的数据后,利用Task类的Signal函数传信RTPSession对象,使其被加入某个TaskThread的任务队列,从而运行其Run函数。
另外,对同一个节目中的每一个独立的RTP流(如音频流或视频流等),DSS都定义了一个RTPStream类与之对应;显然一个RTPSession对象可能包含多个RTPStream对象。整个RTP子系统的核心运行流程见图8。
       下面,我们首先分析RTPSession中Run()函数的用法:
       SInt64 RTPSession::Run()
{ //提示:该函数代码在TaskThread内运行
1     EventFlags events = this->GetEvents(); //取出事件
2     QTSS_RoleParams theParams;
       //提供给其他Module运行的参数,第一个成员是对象本身
       3     theParams.clientSessionClosingParams.inClientSession = this;        
       //设定自己为当前运行的线程
       4     OSThread::GetCurrent()->SetThreadData(&fModuleState);
              /*如果事件是通知RTPSession对象死亡,就准备自杀。可能导致这种情况的有两种事件:自杀kKillEvent;超时kTimeoutEvent*/
       5     if ((events & Task::kKillEvent) || (events & Task::kTimeoutEvent) || (fModuleDoingAsyncStuff))
       6     {     //处理对象自杀代码,此处略…
       7            return –1;     //返回出错信息,这样析构函数就会被调用,从而让对象完全死亡
       8     }
              //如果正处于暂停(PAUSE)状态,什么都不做就返回,等待PLAY命令
       9     if ((fState == qtssPausedState) || (fModule == NULL))
       10           return 0;

                     //下面代码负责发送数据
       11    {     //对Session互斥量加锁,防止发送数据过程中RTSP请求到来
       12           OSMutexLocker locker(&fSessionMutex);
                     //设定数据包发送时间,防止被提前发送
       13           theParams.rtpSendPacketsParams.inCurrentTime = OS::Milliseconds();
       14           if (fPlayTime > theParams.rtpSendPacketsParams.inCurrentTime) //未到发送时间
       15                  theParams.rtpSendPacketsParams.outNextPacketTime=fPlayTime- theParams.rtpSendPacketsParams.inCurrentTime; //计算还需多长时间才可运行
       16           else
       17           {     //下次运行时间的缺缺省值为0
       18                  theParams.rtpSendPacketsParams.outNextPacketTime = 0;
                     // 设置Module状态
       19                  fModuleState.eventRequested = false;
       20                  Assert(fModule != NULL);
                            //调用QTSS_RTPSendPackets_Role内的函数发送数据,见FileModule.cpp
       21                  (void)fModule->CallDispatch(QTSS_RTPSendPackets_Role, &theParams);
                            //将返回值从负数改为0,否则任务对象就会被TaskThread删除
       22                  if (theParams.rtpSendPacketsParams.outNextPacketTime        23                         theParams.rtpSendPacketsParams.outNextPacketTime = 0;
       24           }
       25    }
              //返回下一次希望被运行的时间;返回值含义见前文的分析
       26    return theParams.rtpSendPacketsParams.outNextPacketTime;
}
       从上面分析可见,正常状态下Run函数的返回值有两种:如果返回值为正数,代表下一次发送数据包的时间,规定时间到来的时候,TaskThread线程会自动调用Run函数;如果返回值等于0,在下次任何事件发生时,Run函数就会被调用,这种情况往往发生在所有数据都已经发送完成或者该RTPSession对象将要被杀死的时候。
       在第21行我们看到,Run函数调用了QTSSFileModule中的QTSS_RTPSendPackets_Role发送数据。在QTSSFileModule.cpp文件的QTSSFileModule_Main函数内,系统又调用了SendPackets函数,这才是真正发送RTP数据包的函数,我们对其代码分析如下:
       QTSS_Error SendPackets(QTSS_RTPSendPackets_Params* inParams)
{
              //此处略去部分定义…
       //得到要发送数据的FileSession对象,其定义见QTSSFileModule.cpp文件
       1     FileSession** theFile = NULL;
       2     UInt32 theLen = 0;
       3     QTSS_Error theErr = QTSS_GetValuePtr(inParams->inClientSession, sFileSessionAttr, 0, (void**)&theFile, &theLen);
       4     if ((theErr != QTSS_NoErr) || (theLen != sizeof(FileSession*))) //出错
       5     {     //设定出错原因,然后断掉连接,并返回
       6            QTSS_CliSesTeardownReason reason = qtssCliSesTearDownServerInternalErr;
       7            (void) QTSS_SetValue(inParams->inClientSession, qtssCliTeardownReason, 0, &reason, sizeof(reason));
       8            (void)QTSS_Teardown(inParams->inClientSession);
       9            return QTSS_RequestFailed;
       10    }
       //该节目文件中音频所能忍受的最大延迟
       11    maxDelayToleranceForStream = (*theFile)->fMaxAudioDelayTolerance;

              12    while (true)
       13    {     
                     //不存在待发送数据包,可能是文件尚未打开
       14           if ((*theFile)->fNextPacket == NULL)
       15           {
       16                  void* theCookie = NULL;
                            //获得第一个数据包,theTransmitTime为传输数据花费的时间
       17                  Float64 theTransmitTime = (*theFile)->fFile.GetNextPacket(&(*theFile)->fNextPacket, &(*theFile)->fNextPacketLen, &theCookie);
       18                  if ( QTRTPFile::errNoError != (*theFile)->fFile.Error() )
                            {//读数据出错,断掉连接,返回。此处略 }
                            …
       19                  (*theFile)->fStream = (QTSS_RTPStreamObject)theCookie; //得到RTPStream对象
       20                  (*theFile)->fPacketPlayTime = (*theFile)->fAdjustedPlayTime + ((SInt64)(theTransmitTime * 1000)); //推迟theTransmitTime长度的播放时间
       21                  (*theFile)->fPacketWasJustFetched = true;       
       22                  if ((*theFile)->fNextPacket != NULL)
       23                  {     // 判断流格式
       24                         QTSS_RTPPayloadType* thePayloadType = NULL;
       25                         QTSS_Error theErr = QTSS_GetValuePtr( (*theFile)->fStream, qtssRTPStrPayloadType, 0, (void**)&thePayloadType, &theLen );
                                   //设定视频流可忍受的最大延迟时间
       26                         if (*thePayloadType == qtssVideoPayloadType)
       27                         maxDelayToleranceForStream = (*theFile)->fMaxVideoDelayTolerance;
       28                  }
       29           }

                                   //仍无数据,说明所有数据已经传输完成了
       30           if ((*theFile)->fNextPacket = NULL)
       31           {     //向fStream中写入长度为0的空数据,以便强制缓冲区刷新
       32                  (void)QTSS_Write((*theFile)->fStream, NULL, 0, NULL, qtssWriteFlagsIsRTP);
       33                  inParams->outNextPacketTime = qtssDontCallSendPacketsAgain;
       34                  return QTSS_NoErr; //完成任务返回
       35           }
                     //提示:开始发送RTP数据包
                     //计算当前时间和该段数据应该发送的时间之间的相对间隔
       36           SInt64 theRelativePacketTime = (*theFile)->fPacketPlayTime - inParams->inCurrentTime;  // inCurrentTime = OS::Milliseconds();

                     37           SInt32 currentDelay = theRelativePacketTime * -1L; //计算传输延迟
       38           theErr =  QTSS_SetValue( (*theFile)->fStream, qtssRTPStrCurrentPacketDelay, 0, ¤tDelay, sizeof(currentDelay) ); //保存该延迟
                     //如果延迟过大,就丢弃该包,等待发送下一个数据包
       39           if (theRelativePacketTime > sMaxAdvSendTimeInMsec)
       40           {
       41                  Assert( theRelativePacketTime > 0 );
       42                  inParams->outNextPacketTime = theRelativePacketTime;
       43                  return QTSS_NoErr;
       44           }
                     //此处略去部分处理视频质量的代码…
                     // 发送当前数据包
       45           QTSS_Error writeErr = QTSS_Write((*theFile)->fStream, (*theFile)->fNextPacket, (*theFile)->fNextPacketLen, NULL, qtssWriteFlagsIsRTP);

                     //其余代码略…
}
       RTP子系统是DSS中最为复杂的部分之一,这是因为发送RTP数据包的过程不但涉及到网络接口,而且和文件系统有着密切的关系。DSS的一个重要特征就是能够将线索化(Hinted)过的QuickTime文件通过RTSP和RTP协议流化出去。所有分析这些文件的代码都被提取出来并且封装在QTFile库中。这种封装方式使得系统的各个部分都变得简单:QTFile负责处理文件的分析;而DSS其他部分负责处理网络和协议。服务器中的RTPFileModule调用QTFile库检索索引过的QuickTime文件的数据包和元数据。QTFile库的讲解超出了本文的范围,但是希望让DSS支持其他媒体格式的读者能够掌握它的实现机制。
5  DSS二次开发接口:Module开发流程
       作为一个运行于多个操作系统平台的开发源代码的服务器,DSS提供了一种称为Module的二次开发接口。使用这个开发接口,我们可以充分利用服务器的可扩展性及其实现的多种协议,并且能够保证和将来版本兼容。DSS中的许多核心功能也是以Module的方式预先实现并且编译的,因此可以说对Module的支持已经被设计到DSS的内核中去了。
       下面我们将分析DSS的一个内嵌Module:QTSSFileModule的源代码来说明Module的编程方式,QTSSFileModule的实现在QTSSFileModule.cpp文件中。
       每个QTSS Module必须实现两个函数:
首先,每个QTSS Module必须实现一个主函数,服务器调用该函数用于启动和初始化模块中的QTSS函数;QTSSFileModule主函数的实现如下:
QTSS_Error QTSSFileModule_Main(void* inPrivateArgs)
{
       return _stublibrary_main(inPrivateArgs, QTSSFileModuleDispatch);
}
其中QTSSFileModuleDispatch是Module必须实现的分发函数名。
另一个需要实现的是分发函数,服务器调用该函数实现某个特殊任务。此时,服务器将向分发函数传入任务的名字和一个任务相关的参数块。QTSSFileModule分发函数的实现如下:
QTSS_Error QTSSFileModuleDispatch(QTSS_Role inRole, QTSS_RoleParamPtr inParamBlock)
{     //根据传入的任务名称和入参执行相应的处理函数
       switch (inRole)      //任务名称
       {
              case QTSS_Register_Role:
                     return Register(&inParamBlock->regParams);
              case QTSS_Initialize_Role:
                     return Initialize(&inParamBlock->initParams);
              case QTSS_RereadPrefs_Role:
                     return RereadPrefs();
              case QTSS_RTSPRequest_Role:
                     return ProcessRTSPRequest(&inParamBlock->rtspRequestParams);
              case QTSS_RTPSendPackets_Role:
                     return SendPackets(&inParamBlock->rtpSendPacketsParams);
              case QTSS_ClientSessionClosing_Role:
                     return DestroySession(&inParamBlock->clientSessionClosingParams);
       }
       return QTSS_NoErr;
}
       其中,分发函数的入参是一个联合,它根据任务名称的不同,具体的数据结构也不同,下面是该数据结构的定义:
       typedef union
{
              QTSS_Register_Params                             regParams;
              QTSS_Initialize_Params                            initParams;
              QTSS_ErrorLog_Params                           errorParams;
              //此处略去其他多个数据结构…
} QTSS_RoleParams, *QTSS_RoleParamPtr;
       DSS提供了两种方式把我们自己开发的Module添加到服务器中:一种称为静态模块(Static Module),该方式将我们开发的Module代码直接编译到内核中去;另一种称为动态模块(Dynamic Module),该方式将我们开发的Module单独编译称为一个动态库,然后修改配置,使服务器在启动时将其加载。图9描述了DSS启动和关闭时模块调用流程。
       当服务器启动时,它首先装载没有被编译进内核的动态模块,然后才装载被编译进内核的静态模块;由于现有的大部分系统功能都是以静态模块的方式存在的,如果你希望用自己的模块替换某个系统功能,最好是编写一个动态模块,因为它们将早于静态模块被装载。
       无论是静态模块还是动态模块,它们的代码都是相同的,唯一的不同就是它们的编译方式。首先为了将静态模块编译到服务器中,我们必须修改QTSServer.cpp文件中的QTSServer::LoadCompiledInModules,并向其中加入以下代码:
       QTSSModule*       myModule=new QTSSModule(*_XYZ_*);
       (void)myModule->Initialize(&sCallbacks,&_XYZMAIN_);
       (void)AddModule(MyModule);
       其中,XYZ是静态模块的名字,而XYZMAIN则是其主函数入口。
       动态模块的编译方法如下:首先单独编译动态模块为一个动态共享库;将该共享库与QTSS API stub library链接到一起;最后将结果文件放置到/usr/sbin/QTSSModules目录中去。此后,服务器在启动时就将自动调用该动态模块。
6  结束语
DSS是一项十分庞大的工程,而且随着新版本的不断推出和功能的增强,其内容也越来越丰富。限于篇幅,本文只是介绍了一些笔者认为比较重要的模块或类,希望能够配合读者更好的掌握DSS的精髓。
我们之所以研究DSS的源代码,基本上有两个目标:一是希望利用DSS作为平台进行二次开发,如增加对媒体格式的支持,增加客户身份认证,增加对媒体内容的管理等模块,使DSS成为一个符合实际需求的实用系统。抱此目的的读者在掌握DSS整体流程的基础上,应着重于其二次开发平台(如Module)以及底层文件和媒体格式支持库的研究。另一类读者可能希望通过研究DSS源代码,掌握在Internet环境中处理流式媒体的关键技术,以便为将来开发相关底层应用做准备。对于这些读者,笔者认为需要下更多的功夫去研究DSS源代码中的许多细节部分:例如高级网络编程(Socket)、多线程之间的通信、任务调度、系统资源(CPU、磁盘等)的合理利用以及用于流式媒体的多个标准协议(RTP/RTCP、RTSP、SDP)的具体实现等等。
作为三大主要流式媒体应用中唯一一个开放源代码的产品,DSS让开发人员能够从最底层研究流式媒体技术,事实上,当前国内外许多公司正是在DSS的基础上开发了自己的流式媒体相关产品。但是需要指出,作为一个开放源代码的工程,DSS的分发和开发须遵循苹果公司给出的一份版权文件(Apple Public Source License),希望进行商业化开发的读者应该仔细研读,该文件可从以下网址获得:http://www.publicsource.apple.com。
最后,如果读者希望跟踪DSS的最新进展,可以申请加入其邮件列表。通过该邮件列表,读者可以和全球众多的DSS开发人员交流经验,而且苹果公司的技术人员将会定期的解答各种问题。该邮件列表的地址为:http://www.lists.apple.com。

docker-compose技术,就是通过一个 .yml 配置文件,将所有的容器的部署方法、文件映射、容器连接等等一系列的配置写在一个配置文件里,最后只需要执行docker-compose up命令就会像执行脚本一样的去一个个安装容器并自动部署他们,极大的便利了复杂服务的部署。

查看docker-compose版本

1
2
3
4
[root@lucky ~]# which docker-compose
/usr/local/bin/docker-compose
[root@lucky ~]# docker-compose -version
docker-compose version 1.25.5, build unknown

2.1:yml文件简介

  • yml文件默认的名字为:docker-compose.yml
  • yml包含三大概念:Services、Networks、Volumes
    一个service代表一个container(这个container可以从docker hub上拉取的image创建也可以用Dockerfile build出来的image创建)
  • service的启动类似docker run,我们可以给其指定network和volume

2.2:docker-compose.yml配置文件实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
version: "3"
services:
nginx:
container_name: web-nginx
image: nginx:latest
restart: always
ports:
- 80:80
volumes:
- ./webserver:/webserver
- ./nginx/nginx.conf:/etc/nginx/nginx.conf


#下面将配置文件做个简单的解释说明
docker-compose的配置文件是一个.yml格式的文件
第一部分
version: "3" #指定语法的版本

第二部分
services: #定义服务
nginx: #服务的名称,-p参数后接服务名称
container_name: web-nginx #容器的名称
image: nginx:latest #镜像
restart: always
ports: #端口映射
- 80:80

第三部分
volumes: #物理机与容器的磁盘映射关系
- ./webserver:/webserver
- ./nginx/nginx.conf:/etc/nginx/nginx.conf

3.1 Docker-Compose的安装

1
2
3
[root@lucky ~]# pip install docker-compose
[root@lucky ~]# docker-compose -v
docker-compose version 1.25.5, build unknown

3.2 Docker-Compose启动容器

docker-compose up需要有docker-compose.yml文件,若为其他 xxx.yml,启动命令则需要改成

1
docker-compose -f xxx.yml up

3.3 Docker-Compose的基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#帮助信息
[root@localhost ~]# docker-compose --help


#用来创建或重新创建服务使用的镜像
[root@localhost ~]# docker-compose build
例如:docker-compose build service_a #创建一个镜像名叫service_a

#用于通过容器发送SIGKILL信号强行停止服务
[root@localhost ~]# docker-compose kill

#显示service的日志信息
[root@localhost ~]# docker-compose logs


#暂停和恢复服务
[root@localhost ~]# docker-compose pause/unpause
docker-compose pause #暂停服务
docker-compose unpause #恢复被暂停的服务

#用于查看服务中的端口与物理机的映射关系
[root@localhost ~]# docker-compose port
例如:docker-compose port nginx_web 80 #查看服务中80端口映射到物理机上的那个端口

#用于显示当前项目下的容器
[root@localhost ~]# dokcer-compose ps
注意,此命令与docker ps不同作用,此命令会显示停止后的容器(状态为Exited),只针对某个项目。

#用于拉取服务依赖的镜像
[root@localhost ~]# docker-compose pull


#用于重启某个服务中的所有容器
[root@localhost ~]# docker-compose restart
例如:docker-compose restart service_name #只有正在运行的服务可以使用重启命令,停止的服务是不可以重启

#删除停止的服务(服务里的容器)
[root@localhost ~]# docker-compose rm
-f #强制删除
-v #删除与容器相关的卷(volumes)


#用于在服务中运行一个一次性的命令
[root@localhost ~]# docker-compose run

这个命令会新建一个容器,它的配置和srvice的配置相同。但两者之间还是有两点不同之处
1、run指定的命令会直接覆盖掉service配置中指定的命令
2、run命令启动的容器不会创建在service配置中指定的端口,如果需要指定使用--service-ports指定


#启动/停止运行某个服务的所有容器
[root@localhost ~]# docker-compose start/stop
docker-compose start 启动运行某个服务的所有容器
docker-compose stop 停止运行某个服务的所有容器

#指定某个服务启动的容器个数
[root@localhost ~]# docker-compose scale
[root@localhost ~]# docker-compose scale --help


备注:
docker-compose 运行时是需要指定service名称,可以同时指定多个,也可以不指定。不指定时默认就是对配置文件中所有的service执行命令。
-f #用于指定配置文件
-p #用于指定项目名称

flask demo 代码

1
2
3
4
5
6
7
[root@lucky test_flask]# tree
.
├── app.py
├── docker-compose.yml
└── Dockerfile

0 directories, 3 files

生成镜像并启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@lucky test_flask]# docker-compose up -d
Building web
Step 1/6 : FRoM python:3.6
---> 13efce2de907
Step 2/6 : COPY . /app
---> 6956a9d2c2e4
Step 3/6 : WORKDIR /app
---> Running in 5486d4108d76
Removing intermediate container 5486d4108d76
---> bbcda2fa513d
Step 4/6 : RUN pip install flask redis
---> Running in b36e39e7cba5
Collecting flask
Downloading Flask-1.1.2-py2.py3-none-any.whl (94 kB)
Collecting redis
Downloading redis-3.5.3-py2.py3-none-any.whl (72 kB)

查看运行的容器

1
2
3
4
5
[root@lucky test_flask]# docker-compose ps
Name Command State Ports
------------------------------------------------------------------------------------
test_flask_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp
test_flask_web_1 python app.py Up 0.0.0.0:8008->5000/tcp

演示结果

1
2
3
4
5
6
[root@lucky test_flask]# curl 0.0.0.0:8008
Hello Container World! I have been seen b'1' times and my hostname is 48a2e539fe61.
[root@lucky test_flask]# curl 0.0.0.0:8008
Hello Container World! I have been seen b'2' times and my hostname is 48a2e539fe61.
[root@lucky test_flask]# curl 0.0.0.0:8008
Hello Container World! I have been seen b'3' times and my hostname is 48a2e539fe61.

EF Code First 一对多、多对多关联,如何加载子集合? - 田园里的蟋蟀 - 博客园

Excerpt

##应用场景先简单描述一下标题的意思:使用 EF Code First 映射配置 Entity 之间的关系,可能是一对多关系,也可能是多对多关系,那如何加载 Entity 下关联的 ICollection 集合对象呢?上面的这个问题,我觉得大家应该都遇到过,当然前提是使用 EF Code First


应用场景

先简单描述一下标题的意思:使用 EF Code First 映射配置 Entity 之间的关系,可能是一对多关系,也可能是多对多关系,那如何加载 Entity 下关联的 ICollection 集合对象呢?

上面的这个问题,我觉得大家应该都遇到过,当然前提是使用 EF Code First,有人会说,在 ICollection 集合对象前加 virtual 导航属性,比如:

1
public virtual ICollection<Role> Roles { get; set; }

然后在 DbContext 初始化的时候,增加懒加载(或延迟加载)配置:

1
public UserDbContext() : base("name=UserDbContext") { this.Configuration.LazyLoadingEnabled = false; }

这种方式当然可以,也是我们常用的一种方式,但这种方式在一种场景中无法使用,就是对关联 ICollection 集合增加 Where 条件,什么意思呢?我下描述一下用户-角色应用场景,一个用户有多个权限,一个权限也可能对应多个用户,所以用户和角色之间的关系是多对多,我们用 EF Code First 进行实现一下:

User(用户)和 Role(角色)实体类:

1
namespace UserRoleDemo.Entities { public class User { public int Id { get; set; } public string Name { get; set; } public string Age { get; set; } public string Address { get; set; } public DateTime DateAdded { get; set; } public virtual ICollection<Role> Roles { get; set; } } public class Role { public int Id { get; set; } public string Name { get; set; } public DateTime DateAdded { get; set; } public virtual ICollection<User> Users { get; set; } } }

UserRoleDbContext 映射配置:

1
public class UserRoleDbContext : DbContext { public UserRoleDbContext() : base("name=UserRoleDb") { //this.Configuration.LazyLoadingEnabled = false; } public virtual DbSet<User> Users { get; set; } public virtual DbSet<Role> Role { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder .Configurations .Add(new UserConfiguration()) .Add(new RoleConfiguration()); base.OnModelCreating(modelBuilder); } public class UserConfiguration : EntityTypeConfiguration<User> { public UserConfiguration() { HasKey(c => c.Id); Property(c => c.Id) .IsRequired() .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); HasMany(t => t.Roles) .WithMany(t => t.Users) .Map(m => { m.ToTable("UserRole"); m.MapLeftKey("UserId"); m.MapRightKey("RoleId"); }); } } public class RoleConfiguration : EntityTypeConfiguration<Role> { public RoleConfiguration() { HasKey(c => c.Id); Property(c => c.Id) .IsRequired() .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); } } }

生成对应数据库:

可以看到,我们项目中只有 User 和 Role 两个实体对象,但是生成数据库多了一个 UserRole 表,这个是我们在 UserConfiguration 进行映射配置的结果,当然你不配置也可以,EF Code First 会自动帮你映射,但映射关联表的名字和字段就不能自定义了,如果你深入使用 EF Code First 你会越发觉得它的强大之处,因为它会让你感受不到数据库的“存在”,在应用程序中,所有都是对象之间的操作,没有了事务脚本模式的代码,你可以专注于应用对象的“研究”,即使再复杂的映射配置,EF Code First 也会帮你完成。比如这样一段代码:user.Roles,如果常规的方式(SQL),你会去在应用程序中编写“User join UserRole”的 SQL 代码,但是如果使用 EF Code First,只要映射配置正确,直接 user.Roles 就可以了,当然它不仅如此。

咳咳,扯的有点远了,有点像为微软打广告的意思,呵呵。

言归正传,用户角色的场景就这么简单,上面我说过不能使用懒加载方式解决的问题,比如我要获取一个 User 对象,但在访问 user.Roles 集合的时候,Roles 集合中 Role 对象的 DateAdded 必须大于昨天。这个就不能使用懒加载方式了,因为必须要在 user.Roles 去编写 Where 条件,而懒加载方式是获取所有关联对象的集合,怎么解决这个实际问题呢?请看下面。

问题分析

查询场景:获取 Id 为 1 的 User 对象,并且 User 下的 Roles 集合的 DateAdded 大于昨天。

问题很简单,就是这段话怎么翻译成代码?或者怎么用 Linq 的方式写出来?

有人可能会想到 Include,但使用这种方式就没必要 user.Roles 了,这种方式不可取,然后我再网上找了另一种方式,使用 Any 或 All,大致代码如下:

1
using (var context = new UserRoleDbContext()) { var user = context.Users .Where(u => u.Id == 1) .Where(u => u.Roles.All(r => r.DateAdded > DateTime.Now.AddDays(-1))) .FirstOrDefault(); foreach (var role in user.Roles) { Console.WriteLine(role.DateAdded); } }

使用 Sql Server Profiler 跟踪生成的 SQL 代码,就会发现,我们写的 DateAdded > DateTime.Now.AddDays(-1) 条件会出现在 User 获取中,下面 user.Roles 遍历的时候,还是会加载关联下的所有集合对象,当然这种方式使用必须要开启懒加载。

我个人觉得,这个问题应该在很多应用场景中都会出现,但遗憾的是网上实在找不到响应的解决方案(映射配置的比较多,但获取方式的基本上没有),当然不是说没有方式解决,最简单的就是把集合全部加载出来,然后在内存中进行过滤,项目简单的还好,如果数据量非常大,这种方式也是不可取的,最后在 MSDN 上找到一篇很多年的博客:Using DbContext in EF 4.1 Part 6: Loading Related Entities,注意 EF 版本是 4.1,现在 7.0 都快出来了,哎!

看到“Loading Related Entities”这个标题,我就知道这篇博客就是我想要的,然后按照它描述的,配置如下:

首先,禁止懒加载:

1
this.Configuration.LazyLoadingEnabled = false;

Linq 查询代码:

1
using (var context = new UserRoleDbContext()) { var user = context.Users .Where(u => u.Id == 1) .FirstOrDefault(); context.Entry(user) .Collection(u => u.Roles) .Query() .Where(r => r.DateAdded > DateTime.Now.AddDays(-1)) .Load(); foreach (var role in user.Roles) { Console.WriteLine(role.DateAdded); } }

先说明一下,这段代码是不能运行的,因为 user.Roles 集合的值为 null,至于原因,我是后来才知道的,这种方式只适用于“一对多”的关系,哪篇博客中的演示场景也是“一对多”,如果我们把 Query() 和后面的 Where 代码去掉,没有了条件查询,这段代码时可以运行的,至于原因,我觉得没有了 where,那和懒加载又有什么区别呢。

“一对多”的方式是这种,那“多对多”的呢?答案是在 Collection 后加 Include,示例代码:

1
using (var context = new UserRoleDbContext()) { var user = context.Users .Where(u => u.Id == 1) .FirstOrDefault(); context.Entry(user) .Collection(u => u.Roles) .Query() .Include(r => r.Users) .Where(r => r.DateAdded > DateTime.Now.AddDays(-1)) .Load(); foreach (var role in user.Roles) { Console.WriteLine(role.DateAdded); } }

这种方式确实是可以运行成功的,也是我们想要的效果,但如果你看一下跟踪生成的 SQL 代码,你就不想使用它了,为什么?我们看一下生成的 SQL 代码:

1
SELECT [Project1].[UserId] AS [UserId], [Project1].[RoleId] AS [RoleId], [Project1].[Id] AS [Id], [Project1].[Name] AS [Name], [Project1].[DateAdded] AS [DateAdded], [Project1].[C1] AS [C1], [Project1].[Id1] AS [Id1], [Project1].[Name1] AS [Name1], [Project1].[Age] AS [Age], [Project1].[Address] AS [Address], [Project1].[DateAdded1] AS [DateAdded1] FROM ( SELECT [Extent1].[UserId] AS [UserId], [Extent1].[RoleId] AS [RoleId], [Extent2].[Id] AS [Id], [Extent2].[Name] AS [Name], [Extent2].[DateAdded] AS [DateAdded], [Join2].[Id] AS [Id1], [Join2].[Name] AS [Name1], [Join2].[Age] AS [Age], [Join2].[Address] AS [Address], [Join2].[DateAdded] AS [DateAdded1], CASE WHEN ([Join2].[UserId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1] FROM [dbo].[UserRole] AS [Extent1] INNER JOIN [dbo].[Roles] AS [Extent2] ON [Extent1].[RoleId] = [Extent2].[Id] LEFT OUTER JOIN (SELECT [Extent3].[UserId] AS [UserId], [Extent3].[RoleId] AS [RoleId], [Extent4].[Id] AS [Id], [Extent4].[Name] AS [Name], [Extent4].[Age] AS [Age], [Extent4].[Address] AS [Address], [Extent4].[DateAdded] AS [DateAdded] FROM [dbo].[UserRole] AS [Extent3] INNER JOIN [dbo].[Users] AS [Extent4] ON [Extent4].[Id] = [Extent3].[UserId] ) AS [Join2] ON [Extent2].[Id] = [Join2].[RoleId] WHERE ([Extent1].[UserId] = @EntityKeyValue1) AND ([Extent2].[DateAdded] > (SysDateTime())) ) AS [Project1] ORDER BY [Project1].[UserId] ASC, [Project1].[RoleId] ASC, [Project1].[Id] ASC, [Project1].[C1] ASC

看见这一坨的代码就心烦,而且这只是两段 SQL 代码的一个,因为上面我们使用:context.Users.FirstOrDefault(),也会生成一坨 SQL 代码,只不过没那么复杂而已,其实复杂之处,就是我们使用 Include 方式,把 User、Role 和 UserRole 表关联起来使用了,其实我们只是想获取某个 user 下的 Role 集合而已,在 stackoverflow 中有人也有同样的问题:EF 4.1 loading filtered child collections not working for many-to-many,当然讲的比我详细多了。

其实最后的解决方式有点“无语”,为什么呢?看一下代码就知道了:

1
using (var context = new UserRoleDbContext()) { var user = context.Users .Where(u => u.Id == 1) .FirstOrDefault(); user.Roles = context.Entry(user) .Collection(u => u.Roles) .Query() .Where(r => r.DateAdded > DateTime.Now) .ToList(); foreach (var role in user.Roles) { Console.WriteLine(role.DateAdded); } }

你可能发现了与上面代码的不同,就是我们使用 Entry 获取集合对象,重新给 user.Roles 属性赋值,因为 ToList 了,同样会产生两条 SQL 代码,但这种代码,我们是可以接受的:

1
SELECT [Extent2].[Id] AS [Id], [Extent2].[Name] AS [Name], [Extent2].[DateAdded] AS [DateAdded] FROM [dbo].[UserRole] AS [Extent1] INNER JOIN [dbo].[Roles] AS [Extent2] ON [Extent1].[RoleId] = [Extent2].[Id] WHERE ([Extent1].[UserId] = @EntityKeyValue1) AND ([Extent2].[DateAdded] > (SysDateTime()))

示例 Demo 下载:

非常珍贵的参考资料:

CI 精华文章:

Gitlab 部署 CI 相关资料:

持续集成(Continuous integration - CI)的作用:代码在提交到资源库之前,进行构建、自动化测试和发布等等,我们每天需要提交大量的代码,持续集成可以有效的帮助我们发现代码中的 Bug,并且减少一些反复的工作等等,使团队更加有效的开发协作。

GitLab CI 官方介绍:https://about.gitlab.com/gitlab-ci/

Gitlab 在 8.0 以上版本集成了 CI,所以我们不需要另外配置一个 gitlab-ci-server 服务器,为我们部署减少了很多的工作,点个赞👍!

先吐槽下,Gitlab 部署 CI 我大概花了一周的时间,但也只是进行了一点点,最重要的三点:nuget restore, bulid *.slnrun unit tests现在基本上是可以了,在部署的过程中,深感到问题分享的重要性,遇到的大量问题,Google 基本上搜不到,中文相关资料也就上面的几篇文章,但看过之后发现都是简简单单的介绍而已,并没有记录详细的部署过程,所以,我基本上都是看的 Gitlab 官方帮助文档,但 Gitlab 的更新很频繁,所以有些帮助文档都没进行更新,避免不了踩进一些坑,那怎么办呢?解决方式就是不断的进行尝试,比如我配置.gitlab-ci.yml文件的时候,就不断的进行code commit测试(一百多个提交😂):

并且有先见之明的把问题解决过程,都用 Issue 进行记录了😏:

下面就从上面这几个 Issue 进行展开,把每个问题和解决过程都分享出来,希望可以帮助到遇到相同问题的园友。

1. install configue gitlab-ci-multi-runner

GitLab 部署 CI 的第一步就是安装 gitlab-ci-multi-runner,你可以把它理解为:跑 CI 的服务。

windows 安装教程:https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/install/windows.md

下载好 gitlab-ci-multi-runner-windows-amd64.exe 安装文件后,将安装文件放在C:\Multi-Runner下,以管理员权限运行命令行,如果gitlab-ci-multi-runner 命令找不到,直接用gitlab-ci-multi-runner-windows-amd64.exe命令运行。

在 Gitlab 项目中打开 Settings > Runners,找到URLtoken,等会安装的时候需要配置。

安装配置步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C:\WINDOWS\system32>cd C:\Multi-Runner

C:\Multi-Runner>gitlab-ci-multi-runner-windows-amd64.exe register
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/ci):
URL
Please enter the gitlab-ci token for this runner:
token
Please enter the gitlab-ci description for this runner:
[DESKTOP-2P9GHDD]: xishuai-ci
Please enter the gitlab-ci tags for this runner (comma separated):
dev
Registering runner... succeeded runner=avuSXASJ
Please enter the executor: docker-ssh, parallels, shell, ssh, virtualbox, docker+machine, docker-ssh+machine, docker:
shell
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!

上面executor: shell 是默认配置,意思是本地执行,也可以使用sshdocker,不过需要增加一些远端链接配置。

完成后,会在C:\Multi-Runner目录下,生成一个config.toml配置文件,我们上面输入的配置信息也都会在这里面,配置说明:https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md

停止,运行和验证命令:

1
2
3
4
5
6
C:\Multi-Runner>gitlab-ci-multi-runner-windows-amd64.exe stop

C:\Multi-Runner>gitlab-ci-multi-runner-windows-amd64.exe start

C:\Multi-Runner>gitlab-ci-multi-runner-windows-amd64.exe verify
Verifying runner... is alive runner=5ae63365

如果运行C:\Multi-Runner>gitlab-ci-multi-runner-windows-amd64.exe start出现错误,则需要将gitlab-ci-multi-runner-windows-amd64.exe拷贝一份,重命名为gitlab-ci-multi-runner.exe

另外, Gitlab 项目 Settings > Project Settings Features > Builds 选项需要打勾。

gitlab-ci-multi-runner 安装配置完之后,我们就可以在 Gitlab 项目 Settings > Runners 中,看到 Runners 的信息了。

2. restore nuget packages

这次任务:使用 CI, nuget 还原解决方案中的程序包。

gitlab-ci-multi-runner 安装配置完之后,我们还需要在 Gitlab 项目中添加一个.gitlab-ci.yml文件,官方介绍:http://doc.gitlab.com/ee/ci/yaml/README.html

因为一开始我对.gitlab-ci.yml配置一点都不了解,所以,我当时按照这个教程 CI Quick Start,添加了如下的.gitlab-ci.yml文件配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
before_script:
- apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
- ruby -v
- which ruby
- gem install bundler --no-ri --no-rdoc
- bundle install --jobs $(nproc) "${FLAGS[@]}"

rspec:
script:
- bundle exec rspec

rubocop:
script:
- bundle exec rubocop

添加好.gitlab-ci.yml文件配置后,我们就可以在项目中的 Builds,看到提交后的构建工作了,随便在 Gitlab 项目中添加一个解决方案,然后再添加一个类库项目,并且使用 nuget 安装一个程序包,最后使用 git 提交到 Gitlab 中,就可以看到 Builds 的过程和结果了,首次提交结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
gitlab-ci-multi-runner 1.1.3 (a470667)
Using Shell executor...
Running on DESKTOP-2P9GHDD...
Cloning repository...
'"git"' �����ڲ����ⲿ���Ҳ���ǿ����еij���
���������ļ���
ϵͳ�Ҳ���ָ����·����
Checking out 2f82ccb0 as master...
'"git"' �����ڲ����ⲿ���Ҳ���ǿ����еij���
���������ļ���

ERROR: Build failed: exit status 9009

这个问题搞了我很久,因为错误信息乱码了,根本找不到相关的解决方案,后来无意间搜到 Gitlab 中的一个 Issue,里面提到了一个gitlab-ci-multi-runner --debug run命令,意思是调试运行 CI,这样我们就可以看到详细的错误信息了,debug 的错误信息比较多,并且完全看不懂,不过我们可以通过 Builds 看到简洁的错误日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
gitlab-ci-multi-runner 1.1.3 (a470667)
Using Shell executor...
Running on DESKTOP-2P9GHDD...
Cloning repository...
Cloning into 'C:/Multi-Runner/builds/500c7a25/0/dev/CNBlogsCI-Sample'...
fatal: unable to access 'https://gitlab-ci-token:xxxxxx@gitlab.com/dev/CNBlogsCI-Sample.git/': error setting certificate verify locations:
CAfile: C:\Multi-Runner\builds\500c7a25\0\dev\CNBlogsCI-Sample.tmp\GIT_SSL_CAINFO
CApath: none
The system cannot find the path specified.
Checking out ac05d090 as master...
fatal: Not a git repository (or any of the parent directories): .git

ERROR: Build failed: exit status 128

上面错误日志的意思是,没有git clone repository 成功,并且没有权限访问,后来 Google 到了一个解决方案:error setting certificate verify locations

解决方式:C:\Multi-Runner\config.toml文件添加shell = 'powershell'节点,添加在[[runners]]节点后。

解决完这个问题之后,去研究了下.gitlab-ci.yml中的nuget restore配置(Google 搜的,太坑),将.gitlab-ci.yml文件修改如下:

1
2
3
4
5
6
7
8
9
stages:
- build

job:
stage: build
script:
- ls
- echo "Restoring NuGet Packages..."
- '"C:\Users\xishuai\.dnx\packages\ClassLibrary2\2.0.0\packages\NuGet.CommandLine.2.8.5\tools\NuGet.exe" restore "src/CNBlogsCI-Sample.sln"'

commit提交测试,出现下面的错误信息:

1
2
3
4
5
6
7
8
9
10
11
gitlab-ci-multi-runner 1.1.3 (a470667)
Using Shell executor...
At C:\Users\xishuai\AppData\Local\Temp\build_script140243225\script.ps1:132 char:105
+ ... 0.0\packages\NuGet.CommandLine.2.8.5\tools\NuGet.exe" restore "src/CN ...
+ ~~~~~~~
Unexpected token 'restore' in expression or statement.
+ CategoryInfo : ParserError: (:) [], ParseException
+ FullyQualifiedErrorId : UnexpectedToken


ERROR: Build failed: exit status 1

从错误信息中可以看到,没有识别restore命令,啥意思?这个问题又搞了我好久,Google Unexpected token 'restore' in expression or statement. 关键字,毛都搜不到,没办法,后来只能更换关键字搜,但搜到的信息凤毛麟角,后来参考搜来的资料,将.gitlab-ci.yml改为:

1
2
3
4
5
6
7
8
9
10
stages:
- build

job:
stage: build
script:
- ls
- echo "Restoring NuGet Packages..."
- 'call "%VS140COMNTOOLS%\vsvars32.bat"'

1
2
3
before_script:
- echo "Restoring NuGet Packages..."
- 'call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\vsvars32.bat"'

%VS140COMNTOOLS%\vsvars32.bat 是什么鬼?不太清楚,毫无疑问,又出现了错误,信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
gitlab-ci-multi-runner 1.1.3 (a470667)
Using Shell executor...
Running on DESKTOP-2P9GHDD...
Fetching changes...
HEAD is now at 3926803 Update .gitlab-ci.yml
From https://gitlab.com/dev/CNBlogsCI-Sample
3926803..d8f10a7 master -> origin/master
Checking out d8f10a7c as master...
Previous HEAD position was 3926803... Update .gitlab-ci.yml
HEAD is now at d8f10a7... Update .gitlab-ci.yml
$ ls


Directory: C:\Multi-Runner\builds\5ae63365\0\dev\CNBlogsCI-Sample


Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 5/4/2016 02:45 PM src
-a---- 5/4/2016 02:45 PM 89 .gitignore
-a---- 5/4/2016 02:49 PM 527 .gitlab-ci.yml
$ echo "Restoring NuGet Packages..."
Restoring NuGet Packages...
$ call "%VS140COMNTOOLS%\vsvars32.bat"
call : The term 'call' is not recognized as the name of a cmdlet, function, script file, or operable program. Check
the spelling of the name, or if a path was included, verify that the path is correct and try again.
At C:\Users\xishuai\AppData\Local\Temp\build_script250102679\script.ps1:132 char:3
+ call "%VS140COMNTOOLS%\vsvars32.bat"
+ ~~~~
+ CategoryInfo : ObjectNotFound: (call:String) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : CommandNotFoundException


ERROR: Build failed: exit status 1

也是毫无头绪的错误,这么办呢?后来想想nuget restore始终不成功,能不能换个命令呢?突然想到了 ASP.NET 5,还原程序包使用的是dnu restore命令,那就尝试下吧,将解决方案中的项目删掉,然后添加 ASP.NET 5 项目,.gitlab-ci.yml改为:

1
2
3
4
before_script:
- echo "Restoring NuGet Packages..."
- dnvm use 1.0.0-beta5 -r coreclr -a x64
- dnu restore

哇塞,这次终于成功了(突然有种想哭的冲动😭),日志信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Running on DESKTOP-2P9GHDD...
Fetching changes...
HEAD is now at 33436d8 test commit
From https://gitlab.com/dev/CNBlogsCI-Sample
33436d8..c80b2d5 master -> origin/master
Checking out c80b2d5d as master...
Previous HEAD position was 33436d8... test commit
HEAD is now at c80b2d5... test commit
$ echo "Release build..."
Release build...
$ dnvm use 1.0.0-beta5 -r coreclr -a x64
Adding C:\Users\xishuai\.dnx\runtimes\dnx-coreclr-win-x64.1.0.0-beta5\bin to process PATH
$ dnu restore
Microsoft .NET Development Utility CoreCLR-x64-1.0.0-beta5-12103

Restoring packages for C:\Multi-Runner\builds\5ae63365\0\dev\CNBlogsCI-Sample\src\CNBlogsCI-Sample.ClassLibrary\project.json
GET https://www.nuget.org/api/v2/
OK https://www.nuget.org/api/v2/ 5524ms
GET http://nuget.cnitblog.com/nuget/Default/
OK http://nuget.cnitblog.com/nuget/Default/ 2406ms
GET https://www.myget.org/F/aspnetvnext/api/v2/
OK https://www.myget.org/F/aspnetvnext/api/v2/ 5225ms
CACHE https://www.nuget.org/api/v2/
GET https://www.myget.org/F/aspnetmaster/api/v3/index.json
OK https://www.myget.org/F/aspnetmaster/api/v3/index.json 2938ms
GET https://www.myget.org/F/xunit/api/v3/index.json
OK https://www.myget.org/F/xunit/api/v3/index.json 1976ms
Writing lock file C:\Multi-Runner\builds\5ae63365\0\dev\CNBlogsCI-Sample\src\CNBlogsCI-Sample.ClassLibrary\project.lock.json
Restore complete, 18775ms elapsed

Build succeeded

虽然 ASP.NET 5 还原程序包成功了,但依旧解决不了问题啊,因为必须得解决nuget restore的问题,因为很多项目都没用 ASP.NET 5,怎么办呢?又回到了出发点,问题能磨死人啊,过程就不叙述了,后来无意间将.gitlab-ci.yml改为:

1
2
3
before_script:
- echo "Restoring NuGet Packages..."
- C:\Program Files (x86)\NuGet\nuget.exe restore src/CNBlogsCI-Sample.sln

仔细看看和上面的配置有什么不同,我把'"去掉了,commit代码测试,出现了下面和一开始不一样的错误(有戏了😏):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
gitlab-ci-multi-runner 1.1.3 (a470667)
Using Shell executor...
Running on DESKTOP-2P9GHDD...
Fetching changes...
Removing src/ClassLibrary1/bin/
Removing src/ClassLibrary1/obj/
HEAD is now at 191e7e0 test commit
From https://gitlab.com/dev/CNBlogsCI-Sample
191e7e0..feebdef master -> origin/master
Checking out feebdefb as master...
Previous HEAD position was 191e7e0... test commit
HEAD is now at feebdef... test commit
$ echo "Restoring NuGet Packages..."
Restoring NuGet Packages...
$ C:\Program Files (x86)\NuGet\nuget.exe restore src/CNBlogsCI-Sample.sln
x86 : The term 'x86' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the
spelling of the name, or if a path was included, verify that the path is correct and try again.
At C:\Users\xishuai\AppData\Local\Temp\build_script166211738\script.ps1:128 char:21
+ C:\Program Files (x86)\NuGet\nuget.exe restore src/CNBlogsCI-Sample ...
+ ~~~
+ CategoryInfo : ObjectNotFound: (x86:String) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : CommandNotFoundException


ERROR: Build failed: exit status 1

根据上面的错误日志,可以看到,就是目录中的x86问题,然后我把目录改为C:\Program Files\NuGet\nuget.exe之后(nuget.exe拷贝到相应目录下),还是有问题,然后就直接放在C盘目录下,终于build成功(眼泪夺眶而出😂)。

.gitlab-ci.yml配置:

1
2
3
before_script:
- echo "Restoring NuGet Packages..."
- C:\NuGet\nuget.exe restore "src\CNBlogsCI-Sample.sln"

build成功日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gitlab-ci-multi-runner 1.1.3 (a470667)
Using Shell executor...
Running on DESKTOP-2P9GHDD...
Fetching changes...
HEAD is now at 1ac80d7 test commit
From https://gitlab.com/dev/CNBlogsCI-Sample
1ac80d7..683a8bc master -> origin/master
Checking out 683a8bcb as master...
Previous HEAD position was 1ac80d7... test commit
HEAD is now at 683a8bc... test commit
$ echo "Restoring NuGet Packages..."
Restoring NuGet Packages...
$ C:\NuGet\nuget.exe restore "src\CNBlogsCI-Sample.sln"
Installing 'AutoMapper 4.2.1'.
Successfully installed 'AutoMapper 4.2.1'.

Build succeeded

看似简单的结果,但过程真是太扯蛋了,如果我当时看到类似这篇博文分享,也不至于如此,还没完,继续。。。

3. build *.sln

这次任务:使用 CI, build 生成解决方案中的项目。

生成解决方案的问题解决过程相对简单些,不过上面漏掉了一处,这边再补充下,.gitlab-ci.yml配置:

1
2
3
4
5
6
7
8
9
10
11
stages:
- build

job:
stage: build
script:
- echo "Release build..."
- C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild.exe /consoleloggerparameters:ErrorsOnly /maxcpucount /nologo /property:Configuration=Release /verbosity:quiet "CNBlogsCI-Sample.sln"
tags:
except:
- tags

错误日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gitlab-ci-multi-runner 1.1.3 (a470667)
Using Shell executor...
Running on DESKTOP-2P9GHDD...
Fetching changes...
HEAD is now at 07a6ffd Merge branch 'master' of gitlab.com:dev/CNBlogsCI-Sample
From https://gitlab.com/dev/CNBlogsCI-Sample
07a6ffd..73bd820 master -> origin/master
Checking out 73bd8207 as master...
Previous HEAD position was 07a6ffd... Merge branch 'master' of gitlab.com:dev/CNBlogsCI-Sample
HEAD is now at 73bd820... test commit
$ echo "Release build..."
Release build...
$ C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild.exe /consoleloggerparameters:ErrorsOnly /maxcpucount /nologo /property:Configuration=Release /verbosity:quiet "CNBlogsCI-Sample.sln"
MSBUILD : error MSB1009: ��Ŀ�ļ������ڡ�
����: CNBlogsCI-Sample.sln

ERROR: Build failed: exit status 1

这个错误和最开始的乱码错误一样,未知的错误,无从下手,后来,又无意间搜到了一个 Gitlab Issue(好多无意间😄,没办法,Google 只能搜索所有可能的关键字):Question about local project path

.gitlab-ci.yml配置改为:

1
2
3
4
5
6
7
8
9
10
11
12
stages:
- build

job:
stage: build
script:
- ls
- echo "Release build..."
- C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild.exe /consoleloggerparameters:ErrorsOnly /maxcpucount /nologo /property:Configuration=Release /verbosity:quiet "CNBlogsCI-Sample.sln"
tags:
except:
- tags

然后看到了详细错误(又有戏了😏):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
gitlab-ci-multi-runner 1.1.3 (a470667)
Using Shell executor...
Running on DESKTOP-2P9GHDD...
Fetching changes...
HEAD is now at eb2ec26 Update .gitlab-ci.yml
Checking out eb2ec265 as master...
HEAD is now at eb2ec26... Update .gitlab-ci.yml
$ ls


Directory: C:\Multi-Runner\builds\5ae63365\0\dev\CNBlogsCI-Sample


Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 5/4/2016 10:26 AM src
-a---- 5/4/2016 11:19 AM 315 .gitlab-ci.yml
$ echo "Release build..."
Release build...
$ C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild.exe /consoleloggerparameters:ErrorsOnly /maxcpucount /nologo /property:Configuration=Release /verbosity:quiet "CNBlogsCI-Sample.sln"
MSBUILD : error MSB1009: Project file does not exist.
Switch: CNBlogsCI-Sample.sln



ERROR: Build failed: exit status 1

error MSB1009: Project file does not exist.这个错误就很清晰了,项目文件找不到,也就是没有找到CNBlogsCI-Sample.sln,怎么会呢?重新查看了 Gitlab 中的项目文件目录,CNBlogsCI-Sample.sln在根目录下的src目录下,重新修改下.gitlab-ci.yml配置:

1
2
3
4
5
6
7
8
9
10
11
12
stages:
- build

job:
stage: build
script:
- ls
- echo "Release build..."
- C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild.exe /consoleloggerparameters:ErrorsOnly /maxcpucount /nologo /property:Configuration=Release /verbosity:quiet "src/CNBlogsCI-Sample.sln"
tags:
except:
- tags

build成功,日志详情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
gitlab-ci-multi-runner 1.1.3 (a470667)
Using Shell executor...
Running on DESKTOP-2P9GHDD...
Fetching changes...
HEAD is now at a51aeea test commit
From https://gitlab.com/dev/CNBlogsCI-Sample
a51aeea..170fbc4 master -> origin/master
Checking out 170fbc4a as master...
Previous HEAD position was a51aeea... test commit
HEAD is now at 170fbc4... test commit
$ ls


Directory: C:\Multi-Runner\builds\5ae63365\0\dev\CNBlogsCI-Sample


Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 5/4/2016 11:38 AM src
-a---- 5/4/2016 11:38 AM 319 .gitlab-ci.yml
$ echo "Release build..."
Release build...
$ C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild.exe /consoleloggerparameters:ErrorsOnly /maxcpucount /nologo /property:Configuration=Release /verbosity:quiet "src/CNBlogsCI-Sample.sln"



Build succeeded

4. run unit tests

这次任务:使用 CI, run 跑解决方案中的单元测试,可以成为自动化测试。

这次基本上没有什么问题解决过程,因为 Google 完全搜不到相关资料,所以,我最后是按照我的想法实现的,xUnit 除了用 VS2015 进行跑单元测试外,我们还可以用命令行的方式,打开 cmd 输入:C:\xunit.runner.console\tools\xunit.console.exe "src\ClassLibrary2\bin\debug\ClassLibrary2.dll",结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
C:\Users\xishuai\Desktop\CNBlogs\CNBlogsCI-Sample\src> C:\xunit.runner.console\tools\xunit.console.exe "src\ClassLibrary2\bin\debug\ClassLibrary2.dll"
xUnit.net Console Runner (64-bit .NET 4.0.30319.42000)
Discovering: ClassLibrary2
Discovered: ClassLibrary2
Starting: ClassLibrary2
ClassLibrary2.Class1.Test2 [FAIL]
Assert.True() Failure
Expected: True
Actual: False
Stack Trace:
ClassLibrary2\Class1.cs(21,0): at ClassLibrary2.Class1.Test2()
Finished: ClassLibrary2
=== TEST EXECUTION SUMMARY ===
ClassLibrary2 Total: 2, Errors: 0, Failed: 1, Skipped: 0, Time: 0.224s

好,既然命令行可以跑单元测试,那么我们就可以在.gitlab-ci.yml中添加脚本配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
stages:
- build
- test

before_script:
- echo "Restoring NuGet Packages..."
- C:\NuGet\nuget.exe restore "src\CNBlogsCI-Sample.sln"

build_job:
stage: build
script:
- echo "Release build..."
- C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild.exe /consoleloggerparameters:ErrorsOnly /maxcpucount /nologo /property:Configuration=Release /verbosity:quiet "src\CNBlogsCI-Sample.sln"
except:
- tags

test_job:
stage: test
script:
- echo "Tests run..."
- C:\xunit.runner.console\tools\xunit.console.exe "src\ClassLibrary2\bin\debug\ClassLibrary2.dll"
- C:\xunit.runner.console\tools\xunit.console.exe "src\ClassLibrary3\bin\debug\ClassLibrary3.dll"

xUnit 单元测试不通过日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
gitlab-ci-multi-runner 1.1.3 (a470667)
Using Shell executor...
Running on DESKTOP-2P9GHDD...
Fetching changes...
Removing src/ClassLibrary1/bin/
Removing src/ClassLibrary1/obj/
Removing src/ClassLibrary2/bin/Release/
Removing src/ClassLibrary2/obj/
Removing src/ClassLibrary3/bin/
Removing src/ClassLibrary3/obj/
Removing src/packages/
HEAD is now at d176025 test commit
Checking out d1760259 as master...
HEAD is now at d176025... test commit
$ echo "Restoring NuGet Packages..."
Restoring NuGet Packages...
$ C:\NuGet\nuget.exe restore "src\CNBlogsCI-Sample.sln"
Installing 'xunit.abstractions 2.0.0'.
Installing 'xunit.assert 2.1.0'.
Installing 'xunit 2.1.0'.
Installing 'AutoMapper 4.2.1'.
Successfully installed 'xunit 2.1.0'.
Installing 'xunit.core 2.1.0'.
Successfully installed 'xunit.abstractions 2.0.0'.
Successfully installed 'xunit.core 2.1.0'.
Installing 'xunit.extensibility.core 2.1.0'.
Successfully installed 'xunit.assert 2.1.0'.
Installing 'xunit.extensibility.execution 2.1.0'.
Installing 'xunit.runner.console 2.1.0'.
Successfully installed 'AutoMapper 4.2.1'.
Successfully installed 'xunit.runner.console 2.1.0'.
Successfully installed 'xunit.extensibility.core 2.1.0'.
Successfully installed 'xunit.extensibility.execution 2.1.0'.
$ echo "Tests run..."
Tests run...
$ C:\xunit.runner.console\tools\xunit.console.exe "src\ClassLibrary2\bin\debug\ClassLibrary2.dll"
xUnit.net Console Runner (64-bit .NET 4.0.30319.42000)
Discovering: ClassLibrary2
Discovered: ClassLibrary2
Starting: ClassLibrary2
Finished: ClassLibrary2
=== TEST EXECUTION SUMMARY ===
ClassLibrary2 Total: 2, Errors: 0, Failed: 0, Skipped: 0, Time: 0.179s
$ C:\xunit.runner.console\tools\xunit.console.exe "src\ClassLibrary3\bin\debug\ClassLibrary3.dll"
error: file not found: src\ClassLibrary3\bin\debug\ClassLibrary3.dll

ERROR: Build failed: exit status 1

xUnit 单元测试通过日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
gitlab-ci-multi-runner 1.1.3 (a470667)
Using Shell executor...
Running on DESKTOP-2P9GHDD...
Fetching changes...
Removing src/ClassLibrary1/bin/
Removing src/ClassLibrary1/obj/
Removing src/ClassLibrary2/bin/Release/
Removing src/ClassLibrary2/obj/
Removing src/ClassLibrary3/bin/Release/
Removing src/ClassLibrary3/obj/
Removing src/packages/
HEAD is now at 2467772 test commit
Checking out 2467772f as master...
HEAD is now at 2467772... test commit
$ echo "Restoring NuGet Packages..."
Restoring NuGet Packages...
$ C:\NuGet\nuget.exe restore "src\CNBlogsCI-Sample.sln"
Installing 'AutoMapper 4.2.1'.
Installing 'xunit.abstractions 2.0.0'.
Installing 'xunit.assert 2.1.0'.
Installing 'xunit 2.1.0'.
Successfully installed 'xunit 2.1.0'.
Installing 'xunit.core 2.1.0'.
Successfully installed 'xunit.abstractions 2.0.0'.
Successfully installed 'xunit.core 2.1.0'.
Installing 'xunit.extensibility.execution 2.1.0'.
Installing 'xunit.extensibility.core 2.1.0'.
Successfully installed 'xunit.assert 2.1.0'.
Installing 'xunit.runner.console 2.1.0'.
Successfully installed 'AutoMapper 4.2.1'.
Successfully installed 'xunit.runner.console 2.1.0'.
Successfully installed 'xunit.extensibility.core 2.1.0'.
Successfully installed 'xunit.extensibility.execution 2.1.0'.
$ echo "Tests run..."
Tests run...
$ C:\xunit.runner.console\tools\xunit.console.exe "src\ClassLibrary2\bin\debug\ClassLibrary2.dll"
xUnit.net Console Runner (64-bit .NET 4.0.30319.42000)
Discovering: ClassLibrary2
Discovered: ClassLibrary2
Starting: ClassLibrary2
Finished: ClassLibrary2
=== TEST EXECUTION SUMMARY ===
ClassLibrary2 Total: 2, Errors: 0, Failed: 0, Skipped: 0, Time: 0.194s
$ C:\xunit.runner.console\tools\xunit.console.exe "src\ClassLibrary3\bin\debug\ClassLibrary3.dll"
xUnit.net Console Runner (64-bit .NET 4.0.30319.42000)
Discovering: ClassLibrary3
Discovered: ClassLibrary3
Starting: ClassLibrary3
Finished: ClassLibrary3
=== TEST EXECUTION SUMMARY ===
ClassLibrary3 Total: 1, Errors: 0, Failed: 0, Skipped: 0, Time: 0.184s

Build succeeded

基本上实现了我们想要的效果,但这种实现方式有两个不好的地方:

  • 需要将单元测试的 *.dll 文件上传到 git 资源库。
  • 每增加一个单元测试项目,就必须在.gitlab-ci.yml中添加一段脚本。

我个人觉得 CI 中的自动化测试,肯定不是像我这样搞的,但实在找不到相关资料,如果大家知悉,还请告知,感谢~

另外,如果是 ASP.NET 5 项目,进行自动化测试配置,会非常简单,配置如下:

1
2
3
4
5
test:
stage: test
script:
- echo "Tests run..."
- dnx test

5. configue .gitlab-ci.yml

.gitlab-ci.yml官方资料:http://doc.gitlab.com/ee/ci/yaml/README.html

其他示例:

.gitlab-ci.yml中的配置说明,上面的官方资料介绍的非常详细,下面我再简单介绍下,就用我这次部署 CI 完善后的.gitlab-ci.yml配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
stages:
- build
- test

before_script:
- echo "Restoring NuGet Packages..."
- C:\NuGet\nuget.exe restore "src\CNBlogsCI-Sample.sln"
only:
- master

build_job:
stage: build
script:
- echo "Release build..."
- C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild.exe /consoleloggerparameters:ErrorsOnly /maxcpucount /nologo /property:Configuration=Release /verbosity:quiet "src\CNBlogsCI-Sample.sln"
except:
- tags
only:
- master

test_job:
stage: test
script:
- echo "Tests run..."
- C:\xunit.runner.console\tools\xunit.console.exe "src\ClassLibrary2\bin\debug\ClassLibrary2.dll"
- C:\xunit.runner.console\tools\xunit.console.exe "src\ClassLibrary3\bin\debug\ClassLibrary3.dll"
only:
- master

stage翻译为阶段的意思,在构建的过程中,必须要有一个先后顺序,最上面的stages配置意思是,先构建阶段为buildjob,然后再构建阶段为testjob,下面build_jobtest_job都是job,如果不配置stages,默认为:

1
2
3
4
stages:
- build
- test
- deploy

before_script的意思是,执行在所有的job之前的脚本,比如构建build_jobtest_job都先执行before_scriptbuild_jobtest_job中的stage配置,意思是此job属于哪个stage,这个stage就是最上面的stages配置,除了默认的build,testdeploy,你也可以添加自定义的stage,另外,如果job不添加stage配置,默认配置为test,比如上面的test_job,就可以省略stage: test配置。

另外,job还有一个when: on_failure/on_success /always 配置,如果我们对job进行了stage配置,默认都会是when: on_success

only - master的意思是,只有``master分支才会进行构建,script`的意思很明了,就是要执行的脚本命名。

6. configue build status badge image

构建状态徽章,就是我们平常在 Github 项目中看到构建图标,有passfailing等等。

Gitlab CI 中的教程 builds-badge 真的很坑爹,怎么试都不行,后来无意间看到 Gitlab 项目的一个选项 Settings > Badges

复制上面的代码,然后添加在README.md文件中:

1
[![build status](https://gitlab.com/dev/CNBlogsCI-Sample/badges/master/build.svg)](https://gitlab.com/dev/CNBlogsCI-Sample/commits/master)

这样在commit``bulid的时候,就会动态的显示bulid的过程和结果,并且是图片显示。

Gitlab 部署好 CI 之后,我们会发现,在项目中随处可见这样的图标:


这篇博文没有什么阅读价值,因为都是零零碎碎的问题和解决纪录,没有什么可读性,如果你能阅读到这,我真的会很感动。

分享是有价值的一件事,如果园友在遇到相同问题的时候,可以 Google 到这篇博文,那写这篇博文也就值了😏。