说实在的,也怪你前面有个美元符号,估计很多人都搜不到想要的东西的~
来源: MongoDB查询两个属性不相等 – 谢彬のCSDN专栏 – CSDN博客
执行前:
执行后:
关键代码:{$where:”this.mp!=this.sp”}
- {$where:“this.mp!=this.sp”}
后续将这种方法转成java代码,敬请期待~
说实在的,也怪你前面有个美元符号,估计很多人都搜不到想要的东西的~
来源: MongoDB查询两个属性不相等 – 谢彬のCSDN专栏 – CSDN博客
执行前:
执行后:
关键代码:{$where:”this.mp!=this.sp”}
后续将这种方法转成java代码,敬请期待~
From:http://segmentfault.com/a/1190000002519500我们知道,在 laravel 中使用 resource 的话,只需要绑定模型,在创建表单,链接时,直接可以拿来用,不需要单独的去给路由 as 别名如Route::resource(‘main’,’MainController’);// 创建链接URL::route(‘main.i
来源: laravel Route::controller 使用路由命名 – kendyhj9999的专栏 – CSDN博客
From:http://segmentfault.com/a/1190000002519500
我们知道,在 laravel 中使用 resource 的话,只需要绑定模型,在创建表单,链接时,直接可以拿来用,不需要单独的去给路由 as 别名
如
Route::resource('main','MainController');
// 创建链接
URL::route('main.index')
但是我们使用 Route::controller 时,在创建链接,尝试用以上方法访问时,就会报错
如
Route::controller('main','MainController');
// 创建链接
URL::route('main.index') // 抛出路由不存在的错误
那我们如何像使用 resource 一样方便的来使用 controller 呢?
很简单,我们打开 controller 的源码一看就知道了
// 源码路径:vendor/laravel/framework/src/Illuminate/Routing/Router.php :257 行
看到如下方法
/**
* Route a controller to a URI with wildcard routing.
*
* @param string $uri
* @param string $controller
* @param array $names
* @return void
*/
public function controller($uri, $controller, $names = array())
{
$prepended = $controller;
// First, we will check to see if a controller prefix has been registered in
// the route group. If it has, we will need to prefix it before trying to
// reflect into the class instance and pull out the method for routing.
if ( ! empty($this->groupStack))
{
$prepended = $this->prependGroupUses($controller);
}
$routable = $this->getInspector()->getRoutable($prepended, $uri);
// When a controller is routed using this method, we use Reflection to parse
// out all of the routable methods for the controller, then register each
// route explicitly for the developers, so reverse routing is possible.
foreach ($routable as $method => $routes)
{
foreach ($routes as $route)
{
$this->registerInspected($route, $controller, $method, $names);
}
}
$this->addFallthroughRoute($controller, $uri);
}
// 我们看到可以传递第三个参数,是一个数组,那么数组的内容是什么呢?此方法里面没有处理 name,我们注意看这一行
$this->registerInspected($route, $controller, $method, $names);
//好了,我们找到 registerInspected 这个方法,看他如何处理 name
protected function registerInspected($route, $controller, $method, &$names)
{
$action = array('uses' => $controller.'@'.$method);
// If a given controller method has been named, we will assign the name to the
// controller action array, which provides for a short-cut to method naming
// so you don't have to define an individual route for these controllers.
$action['as'] = array_get($names, $method);
$this->{$route['verb']}($route['uri'], $action);
}
我们看到他以 . 去切割了 name ,然后加入了进去,这样我们就清楚很多啦
路由这样写
Route::controller(
'options',
'OptionsController',
[
'getSite'=>'options.site'
]
);
// 现在就可以使用创建链接啦
URL::route('options.site')
这些东西找了下 laravel 文档没找着,所以自己直接看的源码
本文发布源在:laravel Route::controller 使用路由命名
欢迎大家加入 laravel 交流群一起讨论:365969825
Laravel学院致力于提供优质Laravel中文学习资源
来源: 现代 PHP 新特性系列(一) —— 命名空间 – Laravel学院
如果你只需要知道现代PHP特性中的一个,那就应该是命名空间。命名空间在PHP5.3.0中引入,其作用是按照一种虚拟的层次结构组织PHP代码,这种层次结构类似操作系统中文件系统的目录结构。命名空间是现代PHP组件生态的基础,现代的PHP组件框架代码都是放在各自全局唯一的厂商命名空间中,以免和其他厂商使用的常见类名冲突。
下面我来看看真实的PHP组件是如何使用命名空间的。Laravel框架中的Http组件用于管理HTTP请求和响应,这个组件用到了常见的类名,例如Request、Response,很多其他PHP组件也用到了这样的类名,既然其他PHP代码也用到了相同的类名,那怎么使用这个组件呢?其实我们可以放心使用,因为这个组件的代码放在了唯一的厂商命名空间Illuminate中。打开这个组件在GitHub中的仓库(https://github.com/laravel/framework/blob/master/src/Illuminate/Http/Response.php),找到Response.php文件:

第3行代码如下:
namespace Illuminate\Http;
这一行是PHP命名空间声明语句,声明命名空间的代码始终应该放在<?php标签后的第一行。通过这个命名空间的声明语句我们可以看到Response位于厂商命名空间Illuminate中(最顶层命名空间),我们还看到Response类在子命名空间Http中,你可以看下和Response.php文件在同一层级的其他文件,会发现它们都使用相同的命名空间声明语句。
命名空间的作用是封装和组织相关的PHP类,就像在文件系统中把相关的文件放在同一个目录中一样。PHP命名空间和操作系统的物理文件系统不同,这是一个虚拟概念,没必要和文件系统中的目录结构完全相同,虽然如此,但是大多数PHP组件为了兼容广泛使用的PSR-4自动加载标准,会把命名空间放到对应文件系统的子目录中。
前面已经提到过,我们的代码可能和其他开发者的代码使用相同的类名、接口名、函数或常量名,如果不使用命名空间,名称会起冲突,导致PHP执行出错。而使用命名空间将代码放到唯一的厂商命名空间,我们的代码就可以和其他开发者使用相同的类名、接口名、函数或常量名。
当然如果你开发的是小型个人项目,只有少量的依赖,类名冲突可能不是问题,但是如果在团队中工作,开发用到许多第三方依赖的大型项目,就要认真对待命名冲突问题,因为你无法控制项目依赖在全局命名空间中引入的类、接口、函数和常量,这也是为什么要使用命名空间的原因。
每个PHP类、接口、函数和常量都在命名空间中,声明命名空间很简单,在<?php标签后的第一行声明,声明语句以namespace开头,随后是一个空格,然后是命名空间的名称,最后以;结尾。
命名空间经常用于设置顶层厂商名,比如我们设置厂商名为LaravelAcademy:
<?php namespace LaravelAcademy;
在这个命名空间声明语句后声明的所有PHP类、接口、函数和常量都在LaravelAcademy命名空间中,而且和Laravel学院有某种关系。如果我们想组织学院用到的代码该怎么做呢?答案是使用子命名空间。
子命名空间的声明方式和前面的示例完全一样,唯一的区别是我们要使用\符号把命名空间和子命名空间分开,例如:
<?php namespace LaravelAcademy\ModernPHP;
这个命名空间后的所有类、接口、函数和常量都位于LaravelAcademy\ModernPHP中。
在同一个命名空间中的类没必要在同一个PHP文件中声明,可以在PHP文件的顶部指定一个命名空间或子命名空间,此时,这个文件的代码就是该命名空间或子命名空间的一部分。因此我们可以在不同文件中编写属于同一个命名空间的多个类。
注:厂商命名空间是最顶层的命名空间,也是最重要的命名空间,用于识别品牌或组织,必须具有全局唯一性。子命名空间相对而言没那么重要,但是可以用于组织项目的代码。
在命名空间出现之前,PHP开发者使用Zend风格的类名解决命名冲突问题,这是一种类的命名方案,因Zend框架而流行,这种命名方案在PHP类名中使用下划线的方式表示文件系统的目录分隔符。这种约定有两个作用:其一,确保类名是唯一的;其二,原生的自动加载器会把类名中的下划线替换成文件系统的目录分隔符,从而确定文件的路径。例如,Zend_Cloud_DocumentService_Adapter_WindowsAzure_Query类对应的文件是Zend/Cloud/DocumentService/Adapter/WindowsAzure/Query.php。可以看出,这种命名有个缺点:类名特别长。
现代的PHP命名空间也有这个问题,例如上述Response类完整的全名是Illuminate\Http\Response,幸好,我们可以通过导入以及创建别名的方式来改变这一状况。
导入的意思是指,在每个PHP文件中告诉PHP想使用哪个命名空间、类、接口、函数和常量,导入后就不用使用全名了:
<?php use Illuminate\Http\Response; $response = new Response(‘Oops’, 400); $response->send();
我们通过use关键字告诉PHP,我们想使用Illuminate\Http\Response类,我们只需要输入一次完全限定的类名,随后实例化Response的时候,无需使用完整的类名。
如果觉得这样的类名还是长,可以创建别名。创建别名指的是告诉PHP我要使用简单的名称引用导入的类、接口、函数或常量:
<?php use Illuminate\Http\Response as Res; $res = new Res(‘Oops’, 400); $res->send();
从PHP 5.6开始还可以导入函数和常量,不过要调整use关键字的句法,如果要导入函数,需要使用use func:
<?php use func Namespace\functionName functionName();
如果想导入常量,可以使用use constant:
<?php use constant Namespace\CONST_NAME; echo CONST_NAME;
当然也支持别名,创建方式和类一样。
如果想要在PHP文件中导入多个类、接口、函数或常量,需要在PHP文件的顶部使用多个use语句,PHP支持用简短的语法把多个use语句写成一行:
<?php use Illuminate\Http\Request, Illuminate\Http\Response;
但是为了可读性,建议不要这么写,还是一行写一个use语句比较好:
<?php use Illuminate\Http\Request; use Illuminate\Http\Response;
PHP允许在一个文件中定义多个命名空间:
<?php
namespace Foo {
//声明类、接口、函数、常量
}
namespace Bar {
//声明类、接口、函数、常量
}
但这么做不好,违背了“一个文件一个类”的良好实践,因此不建议这么做。
如果引用的类、接口、函数和常量没有指定命名空间,PHP假定引用的类、接口、函数和常量在当前的命名空间中。如果要使用其他命名空间的类、接口、函数或常量,需要使用完全限定的PHP类名(命名空间+类名)。
有些代码在全局命名空间中,没有命名空间,比如原生的Exception类就是这样。在命名空间中引用全局的代码时,需要在类、接口、函数或常量前加\符号:
<?php
namespace My\App;
class Foo {
public function doSomething() {
throw new \Exception();
}
}
命名空间还为PHP-FIG制定的PSR-4自动加载标准奠定了坚实的基础,大多数现代的PHP组件都使用了这种自动加载模式,使用依赖管理器Composer可以自动加载项目的依赖,后续我们还会详细介绍Composer和PHP-FIG,现在你只需要知道没有命名空间,就没有现代的PHP生态系统和基于组件的新型架构,由此可见命名空间的重要性。
来源: laravel5集成支付宝alipay支付教程 – 知乎专栏
准备
支付宝账号
支付宝签约
蚂蚁金服开放平台账号
我这里是即时到账演示

开放平台

安装
1.执行命令安装包
>composer require latrell/alipay dev-master
2.执行更新
>composer update
3.找到 config/app.php 配置文件中,key为 providers 的数组,在数组中添加服务提供者。
'providers' => [
// ...
'Latrell\Alipay\AlipayServiceProvider',
]
运行
php artisan vendor:publish
命令,发布配置文件到你的项目中
4.配置说明
说明
配置文件 config/latrell-alipay.php 为公共配置信息文件, config/latrell-alipay-web.php 为Web版支付宝SDK配置, config/latrell-alipay-mobile.php 为手机端支付宝SDK配置。
打开config下
latrell-alipay-web.php
<?php
return [
// 安全检验码,以数字和字母组成的32位字符。
'key' => '',//这个在支付宝里面获得商家服务显示key看下图
//签名方式
'sign_type' => 'MD5',
// 服务器异步通知页面路径。
'notify_url' => 'http://www.cxycdz.cn/alipay/webnotify',
// 页面跳转同步通知页面路径。
'return_url' => 'http://www.cxycdz.cn/alipay/webreturn'
];

路由参考
/*支付*/
Route::Group(['namespace' => 'Moneymember'], function () {
Route::get('/pay', ['as' => 'website.pay', 'uses' => 'PayController@index']);
Route::get('/alipay/webnotify', ['as' => 'website.pay', 'uses' => 'PayController@webnotify']);
Route::get('/alipay/webreturn', ['as' => 'website.pay', 'uses' => 'PayController@webreturn']);
例子
支付申请
网页
// 创建支付单。
$alipay = app('alipay.web');
$alipay->setOutTradeNo('order_id');
$alipay->setTotalFee('order_price');
$alipay->setSubject('goods_name');
$alipay->setBody('goods_description');
$alipay->setQrPayMode('4'); //该设置为可选,添加该参数设置,支持二维码支付。
// 跳转到支付页面。
return redirect()->to($alipay->getPayLink());
手机端
// 创建支付单。
$alipay = app('alipay.mobile');
$alipay->setOutTradeNo('order_id');
$alipay->setTotalFee('order_price');
$alipay->setSubject('goods_name');
$alipay->setBody('goods_description');
// 返回签名后的支付参数给支付宝移动端的SDK。
return $alipay->getPayPara();
结果通知
网页
/**
* 异步通知
*/
public function webNotify()
{
// 验证请求。
if (! app('alipay.web')->verify()) {
Log::notice('Alipay notify post data verification fail.', [
'data' => Request::instance()->getContent()
]);
return 'fail';
}
// 判断通知类型。
switch (Input::get('trade_status')) {
case 'TRADE_SUCCESS':
case 'TRADE_FINISHED':
// TODO: 支付成功,取得订单号进行其它相关操作。
Log::debug('Alipay notify post data verification success.', [
'out_trade_no' => Input::get('out_trade_no'),
'trade_no' => Input::get('trade_no')
]);
break;
}
return 'success';
}
/**
* 同步通知
*/
public function webReturn()
{
// 验证请求。
if (! app('alipay.web')->verify()) {
Log::notice('Alipay return query data verification fail.', [
'data' => Request::getQueryString()
]);
return view('alipay.fail');
}
// 判断通知类型。
switch (Input::get('trade_status')) {
case 'TRADE_SUCCESS':
case 'TRADE_FINISHED':
// TODO: 支付成功,取得订单号进行其它相关操作。
Log::debug('Alipay notify get data verification success.', [
'out_trade_no' => Input::get('out_trade_no'),
'trade_no' => Input::get('trade_no')
]);
break;
}
return view('alipay.success');
}
手机端
/**
* 支付宝异步通知
*/
public function alipayNotify()
{
// 验证请求。
if (! app('alipay.mobile')->verify()) {
Log::notice('Alipay notify post data verification fail.', [
'data' => Request::instance()->getContent()
]);
return 'fail';
}
// 判断通知类型。
switch (Input::get('trade_status')) {
case 'TRADE_SUCCESS':
case 'TRADE_FINISHED':
// TODO: 支付成功,取得订单号进行其它相关操作。
Log::debug('Alipay notify get data verification success.', [
'out_trade_no' => Input::get('out_trade_no'),
'trade_no' => Input::get('trade_no')
]);
break;
}
return 'success';
}
支付宝 SDK 在 Laravel 5 封装包https://github.com/Latrell/AlipayAlipay支付宝SDK在Laravel5封装包。该拓展包想要达到在Laravel5框架下,便捷使用支付宝的目的。安装composer require latrell/alipay dev-master更新你的依赖包 composer update 或者全
来源: 使用Laravel AliPay – will5451的博客 – CSDN博客
Alipay
支付宝SDK在Laravel5封装包。
该拓展包想要达到在Laravel5框架下,便捷使用支付宝的目的。
安装
composer require latrell/alipay dev-master
更新你的依赖包 composer update 或者全新安装 composer install。
使用
要使用支付宝SDK服务提供者,你必须自己注册服务提供者到Laravel服务提供者列表中。 基本上有两种方法可以做到这一点。
找到 config/app.PHP 配置文件中,key为 providers 的数组,在数组中添加服务提供者。
'providers' => [
// ...
'Latrell\Alipay\AlipayServiceProvider',
]
运行 php artisan vendor:publish 命令,发布配置文件到你的项目中。
配置文件 config/latrell-alipay.php 为公共配置信息文件, config/latrell-alipay-web.php 为Web版支付宝SDK配置, config/latrell-alipay-mobile.php 为手机端支付宝SDK配置。
例子
支付申请
网页
// 创建支付单。
$alipay = app('alipay.web');
$alipay->setOutTradeNo('order_id');
$alipay->setTotalFee('order_price');
$alipay->setSubject('goods_name');
$alipay->setBody('goods_description');
// 跳转到支付页面。
return redirect()->to($alipay->getPayLink());
手机端
// 创建支付单。
$alipay = app('alipay.mobile');
$alipay->setOutTradeNo('order_id');
$alipay->setTotalFee('order_price');
$alipay->setSubject('goods_name');
$alipay->setBody('goods_description');
// 返回签名后的支付参数给支付宝移动端的SDK。
return $alipay->getPayPara();
结果通知
网页
/**
* 异步通知
*/
public function webNotify()
{
// 验证请求。
if (! app('alipay.web')->verify()) {
Log::notice('Alipay notify post data verification fail.', [
'data' => Request::instance()->getContent()
]);
return 'fail';
}
// 判断通知类型。
switch (Input::get('trade_status')) {
case 'TRADE_SUCCESS':
case 'TRADE_FINISHED':
// TODO: 支付成功,取得订单号进行其它相关操作。
Log::debug('Alipay notify post data verification success.', [
'out_trade_no' => Input::get('out_trade_no'),
'trade_no' => Input::get('trade_no')
]);
break;
}
return 'success';
}
/**
* 同步通知
*/
public function webReturn()
{
// 验证请求。
if (! app('alipay.web')->verify()) {
Log::notice('Alipay return query data verification fail.', [
'data' => Request::getQueryString()
]);
return view('alipay.fail');
}
// 判断通知类型。
switch (Input::get('trade_status')) {
case 'TRADE_SUCCESS':
case 'TRADE_FINISHED':
// TODO: 支付成功,取得订单号进行其它相关操作。
Log::debug('Alipay notify get data verification success.', [
'out_trade_no' => Input::get('out_trade_no'),
'trade_no' => Input::get('trade_no')
]);
break;
}
return view('alipay.success');
}
手机端
/**
* 支付宝异步通知
*/
public function alipayNotify()
{
// 验证请求。
if (! app('alipay.mobile')->verify()) {
Log::notice('Alipay notify post data verification fail.', [
'data' => Request::instance()->getContent()
]);
return 'fail';
}
// 判断通知类型。
switch (Input::get('trade_status')) {
case 'TRADE_SUCCESS':
case 'TRADE_FINISHED':
// TODO: 支付成功,取得订单号进行其它相关操作。
Log::debug('Alipay notify get data verification success.', [
'out_trade_no' => Input::get('out_trade_no'),
'trade_no' => Input::get('trade_no')
]);
break;
}
return 'success';
}
来源: SQL Server DDL触发器运用 – 听风吹雨 – 博客园
说到触发器,大家都会想到这样的使用场景:当一个表的数据修改了,运用DML触发插入或者更新到其它表中;那DDL触发器(SQL Server 2005引入的新功能)会运用到什么场景中呢?本文将为你讲述4种运用DDL触发器的场景:
1) 禁止用户修改和删除表;
2) 禁止用户删除数据库;
3) 记录和监控某数据库所有的DDL操作;
4) 把DDL操作信息以邮件的形式主动发送通知和预警;
DDL触发器是由修改数据库对象的 DDL 语句(如以 CREATE、ALTER 或 DROP)激发。
DDL触发器支持BEFORE和AFTER事件触发器,并在数据库或模式级运行。通常,DDL触发器用于监控数据库中的重要事件。有时用它们来监控错误代码。错误代码可能会执行破坏数据库或使数据库不稳定的活动。更常见的情况是:在开发、测试和stage系统中用它们来了解和监控数据库活动的动态。
当监控GRANT和REVOKE权限语句时,它们也是有效的安全工具。
(一) 首先我们来看一个简单的例子:创建数据库DDL_DB和一个名为DatabaseLog的表,现在创建一个DDL触发器:禁止用户修改和删除表,并进行提醒。执行下面的SQL脚本进行测试。
--Script1:
--创建测试数据库
USE MASTER
GO
CREATE DATABASE DDL_DB
--创建DDL触发器记录表
USE DDL_DB
GO
CREATE TABLE [dbo].[DatabaseLog](
[DatabaseLogID] [int] IDENTITY(1,1) NOT NULL,
[PostTime] [datetime] NOT NULL,
[ServerName] [sysname] NOT NULL,
[LoginName] [sysname] NOT NULL,
[DatabaseUser] [sysname] NOT NULL,
[DatabaseName] [sysname] NOT NULL,
[Schema] [sysname] NULL,
[Object] [sysname] NULL,
[TSQL] [nvarchar](max) NOT NULL,
[Event] [sysname] NOT NULL,
[XmlEvent] NOT NULL,
CONSTRAINT [PK_DatabaseLog_DatabaseLogID] PRIMARY KEY NONCLUSTERED
(
[DatabaseLogID] ASC
) ON [PRIMARY]
) ON [PRIMARY]
--Script2:
--创建DDL触发器:禁止修改或者删除数据表
CREATE TRIGGER DDL_TableTrigger
ON DATABASE
FOR DROP_TABLE, ALTER_TABLE
AS
PRINT '对不起,您不能对数据表进行操作,请联系DBA'
ROLLBACK ;
--测试删除表
USE DDL_DB
GO
DROP TABLE [DatabaseLog]
(Figure1:创建数据库级别的DDL)
(Figure2:返回的提示信息)
(Figure3:SSMS返回的提示信息)
创建数据库级别的DDL之后会出现在数据库触发器列表中,如Figure1;当执行删除表的Drop等DDL命令的时候,就会出现Figure2的提示信息;如果是在SSMS中删除表则会出现Figure3的提示信息。
(二) 在上面的基础上再进行扩展,创建一个DDL触发器:禁止用户删除数据库,并进行提醒。
--Script3:
--禁止SQL Server服务器里删除数据库
CREATE TRIGGER DDL_DataBaseTrigger
ON ALL SERVER
FOR DROP_DATABASE
AS
PRINT '对不起,您不能删除数据库,请联系DBA'
ROLLBACK;
--测试删除数据库
USE MASTER
GO
DROP DATABASE [DDL_DB]
(Figure4:创建服务器级别的DDL)
(Figure5:返回的提示信息)
(Figure6:SSMS返回的提示信息)
创建服务器级别的DDL之后会出现在服务器对象-触发器的列表中,如Figure4;当执行删除数据库的Drop等DDL命令的时候,就会出现Figure5的提示信息;如果是在SSMS中删除数据库则会出现Figure6的提示信息。
(三) 很多时候在程序开发阶段是不会禁用对数据库的修改的,这些时候我们更希望是记录数据库的修改信息,方便对信息进行跟踪检查。使用 EVENTDATA 函数,可以捕获有关激发 DDL 触发器的事件的信息,此函数返回 xml 值。
前面已经创建了数据表DatabaseLog,创建下面的DDL_DatabaseLog触发器,每当数据库发生DDL事件,DDL触发器就会把相关的DDL信息插入到DatabaseLog表,信息包括操作的时间,操作人,操作的SQL等。
执行Script5测试脚本,返回Figure7的信息,查询DatabaseLog表,返回的记录有2条,一条是创建表信息,一条是删除表信息,如Figure8、Figure9所示。
--Script4:
--创建当前数据库的DDL触发器
USE DDL_DB
GO
-- =============================================
-- Author: <听风吹雨>
-- Create date: <2013.05.03>
-- Description: <记录数据库DDL操作>
-- Blog: <http://www.cnblogs.com/gaizai/>
-- =============================================
CREATE TRIGGER [DDL_DatabaseLog]
ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS AS
BEGIN
SET NOCOUNT ON;
DECLARE @data XML;
DECLARE @schema sysname;
DECLARE @object sysname;
DECLARE @eventType sysname;
SET @data = EVENTDATA();
SET @eventType = @data.value('(/EVENT_INSTANCE/EventType)[1]', 'sysname');
SET @schema = @data.value('(/EVENT_INSTANCE/SchemaName)[1]', 'sysname');
SET @object = @data.value('(/EVENT_INSTANCE/ObjectName)[1]', 'sysname')
IF @object IS NOT NULL
PRINT ' ' + @eventType + ' - ' + @schema + '.' + @object;
ELSE
PRINT ' ' + @eventType + ' - ' + @schema;
IF @eventType IS NULL
PRINT CONVERT(nvarchar(max), @data);
INSERT [DDL_DB].[dbo].[DatabaseLog](
[PostTime],
[ServerName],
[LoginName],
[DatabaseUser],
[DatabaseName],
[Schema],
[Object],
[TSQL],
[Event],
[XmlEvent])
VALUES(
GETDATE(),
@data.value('(/EVENT_INSTANCE/ServerName)[1]', 'sysname'),
@data.value('(/EVENT_INSTANCE/LoginName)[1]', 'sysname'),
CONVERT(sysname, CURRENT_USER),
@data.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'sysname'),
CONVERT(sysname, @schema),
CONVERT(sysname, @object),
@data.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'nvarchar(max)'),
@eventType,
@data
);
END;
--Script5:测试DDL记录
--禁用DDL 触发器
DISABLE TRIGGER DDL_TableTrigger ON DATABASE;
GO
CREATE TABLE TestTable (a int)
GO
DROP TABLE TestTable;
GO
SELECT * FROM [DatabaseLog];
GO
(Figure7:返回的提示信息)
(Figure8:DatabaseLog表前半部分信息)
(Figure9:DatabaseLog表后半部分信息)
(四) 我们可以使用DDL触发器主动监控DDL语句的执行,当有对数据库执行DDL就会触发,我们把这些信息保存到表中,并且把操作用户的HostName和修改的T-SQL以邮件的形式发送到指定的邮件。关于设置数据库邮件可以参考:SQL Server 数据库邮件。发送邮件的效果如Figure10。邮件部分参考:MS SQL监控数据库的DDL操作
--Script5:
--创建当前数据库的DDL触发器
USE DDL_DB
GO
-- =============================================
-- Author: <听风吹雨>
-- Create date: <2013.05.03>
-- Description: <记录数据库DDL操作,发送邮件预警>
-- Blog: <http://www.cnblogs.com/gaizai/>
-- =============================================
CREATE TRIGGER [DDL_DatabaseLog]
ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS AS
BEGIN
SET NOCOUNT ON;
DECLARE @data XML;
DECLARE @schema sysname;
DECLARE @object sysname;
DECLARE @eventType sysname;
DECLARE @databaseName sysname;
DECLARE @tableHTML NVARCHAR(MAX);
SET @data = EVENTDATA();
SET @eventType = @data.value('(/EVENT_INSTANCE/EventType)[1]', 'sysname');
SET @schema = @data.value('(/EVENT_INSTANCE/SchemaName)[1]', 'sysname');
SET @object = @data.value('(/EVENT_INSTANCE/ObjectName)[1]', 'sysname');
SET @databaseName = @data.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'sysname');
IF @object IS NOT NULL
PRINT ' ' + @eventType + ' - ' + @schema + '.' + @object;
ELSE
PRINT ' ' + @eventType + ' - ' + @schema;
IF @eventType IS NULL
PRINT CONVERT(nvarchar(max), @data);
INSERT [DDL_DB].[dbo].[DatabaseLog](
[PostTime],
[ServerName],
[LoginName],
[DatabaseUser],
[DatabaseName],
[Schema],
[Object],
[TSQL],
[Event],
[XmlEvent])
VALUES(
GETDATE(),
@data.value('(/EVENT_INSTANCE/ServerName)[1]', 'sysname'),
@data.value('(/EVENT_INSTANCE/LoginName)[1]', 'sysname'),
CONVERT(sysname, CURRENT_USER),
@databaseName,
CONVERT(sysname, @schema),
CONVERT(sysname, @object),
@data.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'nvarchar(max)'),
@eventType,
@data
);
SET @tableHTML =
N'<H1>DDL Event</H1>' +
N'<table border="0">' +
N'<tr><th>PostTime</th><th>ServerName</th><th>LoginName</th><th>DatabaseUser</th><th>DatabaseName</th><th>Object</th>' +
N'<th>TSQL</th></tr>' +
CAST((SELECT
td = [PostTime],'',
td = [ServerName],'',
td = [LoginName],'',
td = [DatabaseUser],'',
td = [DatabaseName],'',
td = [Object],'',
td = TSQL,''
FROM [DDL_DB].[dbo].[DatabaseLog]
WHERE DatabaseLogID =(SELECT MAX(DatabaseLogID) FROM [DDL_DB].[dbo].[DatabaseLog])
FOR XML PATH('tr'), TYPE) AS NVARCHAR(MAX)) +
N'</table>';
DECLARE @subjectStr NVARCHAR(MAX);
SET @subjectStr = 'DDL Event - DataBaseName: ' + @databaseName;
EXEC msdb.dbo.sp_send_dbmail
@profile_name = 'DataBase_DDL_Event',
@recipients='bbspediy@126.com',
@subject = @subjectStr,
@body = @tableHTML,
@body_format = 'HTML';
END;
(Figure10:邮件收到的预警)
(一) 关于DML、DDL、DCL、TCL的解释:
DML
DML is abbreviation of Data Manipulation Language. It is used to retrieve, store, modify, delete, insert and update data in database.
Examples: SELECT, UPDATE, INSERT statements
DDL
DDL is abbreviation of Data Definition Language. It is used to create and modify the structure of database objects in database.
Examples: CREATE, ALTER, DROP statements
DCL
DCL is abbreviation of Data Control Language. It is used to create roles, permissions, and referential integrity as well it is used to control access to database by securing it.
Examples: GRANT, REVOKE statements
TCL
TCL is abbreviation of Transactional Control Language. It is used to manage different transactions occurring within a database.
Examples: COMMIT, ROLLBACK statements
(二) 关于DML与DDL运用场景的一些区别:
DML 触发器可以看作是一种特殊的存储过程,可以保证系统保持其完整性,在系统中进行级联更新或强行业务规则。通过INSERTED 和 DELETED ,我们可以检索哪些列被更新了。DML触发器的本质就是当这两个发生数据修改时自动运行的存储过程。
DDL 触发器的构建主要是为了安全,或者根据部门的需求对系统所进行的变更进行通报。通过使用 EVENTDATA( ) 函数,可以在触发器中使用XML信息。
(三) 如果是线上的系统,可以考虑做下面的限制:在工作时间,不允许修改任何存储过程,否则回滚,示例代码如下:IF DATEPART(hour, GETDATE()) >=9 AND DATEPART(hour, GETDATE()) <= 17
(四) 一些维护DDL的SQL脚本:
--启用DDL 触发器 ENABLE TRIGGER DDL_TableTrigger ON DATABASE; --禁用DDL 触发器 DISABLE TRIGGER ddlDatabaseTriggerLog ON DATABASE; --删除DDL 触发器 DROP TRIGGER ddlDatabaseTriggerLog ON DATABASE; --禁用当前数据库中所有数据库级别的DDL 触发器 DISABLE TRIGGER ALL ON DATABASE --禁用服务器实例中所有服务器级别的DDL 触发器 DISABLE TRIGGER ALL ON ALL SERVER
(五) 所有的DDL事件可以查看DDL 事件,也可以通过下面的SQL进行查看:
--获取有关DDL 触发器可触发的事件或事件组的信息 SELECT * FROM sys.trigger_event_types --查看触发器的依赖关系 SELECT * FROM sys.sql_expression_dependencies SELECT * FROM sys.dm_sql_referenced_entities SELECT * FROM sys.dm_sql_referencing_entities --获取有关数据库范围内的触发器的信息 SELECT * FROM sys.triggers --获取有关激发触发器的数据库事件的信息 SELECT * FROM sys.trigger_events SELECT * FROM sys.trigger_events AS a LEFT join sys.triggers AS b ON a.object_id=b.object_id WHERE name = 'ddlDatabaseTriggerLog' --获取有关服务器范围内的触发器的信息 SELECT * FROM sys.server_triggers SELECT * FROM sys.server_trigger_events --查看数据库范围内的触发器的定义 SELECT * FROM sys.sql_modules
(六) 在执行Script3的时候如果你正在使用SSMS打开这个数据库(SPID)的话,那有可能不是出现Figure5的错误信息,而是出现Figure11的错误,这是因为你没有关闭SPID这些窗口,我还没有在程序连接的情况测试是否会返回这些信息:
(Figure11:Figure5可能出现的)
(七) 如果你想修改DDL触发器的内容,那么你不能直接Alter DDL,而应该是先执行Drop DDL,之后在Create DDL。
(八) 之前已经创建了DDL_TableTrigger和DDL_DatabaseLog触发器,这两个触发器都是在DDL_DB数据库中创建的,当我们需要修改DDL触发器,应该触发对象从小到大进行修改,即DDL_TableTrigger(表)到DDL_DatabaseLog(数据库)进行修改。
如Figure12所示,如果只修改DDL_TableTrigger(Drop、Create),再执行下面的脚本将会出现Figure13的错误(还没找到官方理论描述)。解决办法就是对DDL_DatabaseLog进行创建创建(Drop、Create)。
--测试删除表 USE MASTER GO DROP DATABASE [DDL_DB]
(Figure12:DDL触发器列表)
(Figure13:错误信息)
(一) 删除DDL触发器是否也可以触发一个事件呢?不然如何防止用户先删除DDL触发器之后再做DDL操作呢?难道是用户权限?
解答:第一种方法,可以对DDL触发器进行权限控制;第二种方式就是在服务器级别加一个DROP的触发器,可以监控各个数据库的DDL触发器;下图Figure14是DDL_DatabaseLog被删除时的预警;
(Figure14:删除DDL触发器)
(二) 能对所有数据库进行DDL监控?一条DDL预警能实现?
解答:可以在DDL_DatabaseLog把 ON DATABASE 设置为ON All SERVER,这样就可以监控整个服务器实例,下图Figure15是Logon_DB的DDL预警;
(Figure15:删除DDL触发器)
SQL Server 2005 – Default Trace (默认跟踪)
MS SQL监控数据库的DDL操作(邮件通知)
SQL SERVER – What is – DML, DDL, DCL and TCL – Introduction and Examples
来源: .NET 即时通信,WebSocket服务端实例 – Tsong Chen – 博客园
即时通信常用手段
1.第三方平台 谷歌、腾讯 环信等多如牛毛,其中谷歌即时通信是免费的,但免费就是免费的并不好用。其他的一些第三方一般收费的,使用要则限流(1s/限制x条消息)要么则限制用户数。
但稳定性什么都还不错,又能将服务压力甩出
2.System.Net.Sockets.Socket,也能写一套较好的服务器端。在.NET 4.5之前用较多,使用起来麻烦。需要对数据包进行解析等操作(但貌似网上有对超长包的处理方法)
3.System.Net.WebSockets.WebSocket,这个,是.NET 4.5出来的东西,对服务器环境也有所要求,IIS8及以上。意味着Windows Server2008R2自带的IIS不支持,Windows8及Server2012以上自带的IIS可以。本文主要将这种方式的实例
完整流程
1).客户端请求连接
|
1
|
ws = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + '/Handler1.ashx?user=' + $("#user").val()); |
2).服务端获取连接对象并存储到连接池中
|
1
|
CONNECT_POOL.Add(user, socket); |
3).连接对象开始监听(每个客户端与服务器保存长链接)
|
1
|
WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None); |
4).客户端A发送消息给B
|
1
|
ws.send($("#to").val() + "|" + $('#content').val()); |
5).服务端A的连接对象监听到来自A的消息
|
1
|
string userMsg = Encoding.UTF8.GetString(buffer.Array, 0, result.Count); |
6).解析消息体(B|你好我是A)得到接收者ID,根据接收者ID到连接池中查找B的服务端连接对象,并通过B的连接对象将消息推送给B客户端
|
1
|
WebSocket destSocket = CONNECT_POOL[descUser];<br>...<br>...<br>await destSocket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None); |
7).服务端A连接对象继续监听
|
1
|
WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None); |
8).B客户端接收到推送过来的消息
|
1
2
3
|
ws.onmessage = function (evt) { $('#msg').append('<p>' + evt.data + '</p>');} |
下面则是完整代码
客户端部分
客户端异常简单,正常情况直接用WebSocket,然后监听WebSocket的几个事件就ok。连接的时候可将当前连接者的ID传入(用户编号),发送消息的时候 采用 “接收者ID|我是消息内容” 这种方式,如“A|A你好,我是B!”
但如用移动端使用还是有一些常见的场景需要处理下的
1:手机关屏幕,IOS关掉屏幕的时候WebSocket会立即失去连接,Android则会等待一段时间才会失去连接。服务器端能检测到失去连接
2:网络不稳定,断网情况WebSocket也不会立即失去连接,服务器端不能知道。(可以服务端设计心跳机制,定时给连接池中的用户发送消息,来检测用户是否保持连接)
3:其他等等…(突然关机、后台结束应用)
无论哪种,客户端在发送消息(或者网络恢复连接、亮屏)的时候可以先判断ws的状态,如果不是连接状态则需要重连(new下即可)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
<title></title>
<script src="jquery-1.11.3.min.js"></script>
<script>
var ws;
$().ready(function () {
$('#conn').click(function () {
ws = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + '/Handler1.ashx?user=' + $("#user").val());
$('#msg').append('<p>正在连接</p>');
ws.onopen = function () {
$('#msg').append('<p>已经连接</p>');
}
ws.onmessage = function (evt) {
$('#msg').append('<p>' + evt.data + '</p>');
}
ws.onerror = function (evt) {
$('#msg').append('<p>' + JSON.stringify(evt) + '</p>');
}
ws.onclose = function () {
$('#msg').append('<p>已经关闭</p>');
}
});
$('#close').click(function () {
ws.close();
});
$('#send').click(function () {
if (ws.readyState == WebSocket.OPEN) {
ws.send($("#to").val() + "|" + $('#content').val());
}
else {
$('#tips').text('连接已经关闭');
}
});
});
</script>
</head>
<body>
<div>
<input id="user" type="text" />
<input id="conn" type="button" value="连接" />
<input id="close" type="button" value="关闭"/><br />
<span id="tips"></span>
<input id="content" type="text" />
<input id="send" type="button" value="发送"/><br />
<input id="to" type="text" />目的用户
<div id="msg">
</div>
</div>
</body>
</html>
服务器端部分
服务器端使用Handler(也可用WebAPI)来做,主要用WebSocket的类来实现。代码中都有相对详细的注释,这边只说一些需要注意的问题
1:Dictionary<string,WebSocket> CONNECT_POOL:用户连接池。请求Handler的时候会将当前连接者的用户ID传入,服务器端维护着所有已连接的用户ID和当前用户的WebSocket连接对象
2:Dictionary<string,List<MessageInfo>> MESSAGE_POOL:离线消息池。如果A->B发送消息,B当前因为某种原因没在线(突然断网/黑屏等原因),会将这条消息先保存起来(2天),待B连接后立马将B的离线消息推送给他。(2:MessageInfo:离线Entity。记录当前离线消息的时间、内容)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.WebSockets;
namespace WebApplication1
{
/// <summary>
/// 离线消息
/// </summary>
public class MessageInfo
{
public MessageInfo(DateTime _MsgTime, ArraySegment<byte> _MsgContent)
{
MsgTime = _MsgTime;
MsgContent = _MsgContent;
}
public DateTime MsgTime { get; set; }
public ArraySegment<byte> MsgContent { get; set; }
}
/// <summary>
/// Handler1 的摘要说明
/// </summary>
public class Handler1 : IHttpHandler
{
private static Dictionary<string, WebSocket> CONNECT_POOL = new Dictionary<string, WebSocket>();//用户连接池
private static Dictionary<string, List<MessageInfo>> MESSAGE_POOL = new Dictionary<string, List<MessageInfo>>();//离线消息池
public void ProcessRequest(HttpContext context)
{
if (context.IsWebSocketRequest)
{
context.AcceptWebSocketRequest(ProcessChat);
}
}
private async Task ProcessChat(AspNetWebSocketContext context)
{
WebSocket socket = context.WebSocket;
string user = context.QueryString["user"].ToString();
try
{
#region 用户添加连接池
//第一次open时,添加到连接池中
if (!CONNECT_POOL.ContainsKey(user))
CONNECT_POOL.Add(user, socket);//不存在,添加
else
if (socket != CONNECT_POOL[user])//当前对象不一致,更新
CONNECT_POOL[user] = socket;
#endregion
#region 离线消息处理
if (MESSAGE_POOL.ContainsKey(user))
{
List<MessageInfo> msgs = MESSAGE_POOL[user];
foreach (MessageInfo item in msgs)
{
await socket.SendAsync(item.MsgContent, WebSocketMessageType.Text, true, CancellationToken.None);
}
MESSAGE_POOL.Remove(user);//移除离线消息
}
#endregion
string descUser = string.Empty;//目的用户
while (true)
{
if (socket.State == WebSocketState.Open)
{
ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[2048]);
WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None);
#region 消息处理(字符截取、消息转发)
try
{
#region 关闭Socket处理,删除连接池
if (socket.State != WebSocketState.Open)//连接关闭
{
if (CONNECT_POOL.ContainsKey(user)) CONNECT_POOL.Remove(user);//删除连接池
break;
}
#endregion
string userMsg = Encoding.UTF8.GetString(buffer.Array, 0, result.Count);//发送过来的消息
string[] msgList = userMsg.Split('|');
if (msgList.Length == 2)
{
if (msgList[0].Trim().Length > 0)
descUser = msgList[0].Trim();//记录消息目的用户
buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(msgList[1]));
}
else
buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(userMsg));
if (CONNECT_POOL.ContainsKey(descUser))//判断客户端是否在线
{
WebSocket destSocket = CONNECT_POOL[descUser];//目的客户端
if (destSocket != null && destSocket.State == WebSocketState.Open)
await destSocket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
}
else
{
Task.Run(() =>
{
if (!MESSAGE_POOL.ContainsKey(descUser))//将用户添加至离线消息池中
MESSAGE_POOL.Add(descUser, new List<MessageInfo>());
MESSAGE_POOL[descUser].Add(new MessageInfo(DateTime.Now, buffer));//添加离线消息
});
}
}
catch (Exception exs)
{
//消息转发异常处理,本次消息忽略 继续监听接下来的消息
}
#endregion
}
else
{
break;
}
}//while end
}
catch (Exception ex)
{
//整体异常处理
if (CONNECT_POOL.ContainsKey(user)) CONNECT_POOL.Remove(user);
}
}
public bool IsReusable
{
get
{
return false;
}
}
}
}
PS:对Dictionary的操作可以写两个方案Add和Remove,并用lock,这样子并发会稳定点
来源: 基于即时通信和LBS技术的位置感知服务(三):搭建Openfire服务器+测试2款IM客户端 – charley_yang – 博客园
主要包含4个章节:
1. Java 领域的即时通信的解决方案
2. 搭建 Openfire 服务器
3. 使用客户端测试我们搭建的 Openfire 服务器
4. Smack 和 ASmack
一、Java领域的即时通信的解决方案
Java领域的即时通信的解决方案可以考虑openfire+spark+smack。
1. Openfire是基于Jabber协议(XMPP)实现的即时通信服务器端版本,目前最新的版本为3.6.4,网上可以找到下载的源代码。

2. 即时通信客户端可使用spark2.5.8,这个版本是目前最新的release版本,经过测试发现上一版本在视频支持。

3. Smack是即时通信客户端编程库,可以使用smack的API向openfire注册用户发送消息,并且可以通过监听器获得此用户的应答消息,甚至可以做自动应答机器人,后门我们会用到该API。
二、搭建Openfire服务器
2.1 准备工作:配置主机的域名
使用openfire需要配置机器的域名。打开C:\WINDOWS\system32\drivers\etc\hosts文件,增加一新行:127.0.0.1 im.comit.com.cn
其他机器使用域名访问openfire,也需要在C:\WINDOWS\system32\drivers\etc\hosts中指定im.comit.com.cn对应的ip地址,例如我的局域网IP为192.168.0.177,则hosts文件中应增加一新行:
192.168.0.177 im.comit.com.cn
可以ping一下刚才的域名im.comit.com.cn,验证是否配置正确:
2.2 准备工作:创建openfire运行所需的数据库
下载openfire:openfire需要java运行时,由于我的机器已经有了java开发环境,所以下载了免安装版的zip压缩包。
下载完解压缩,我的目录是:D:\Technology\XMPP\openfire。
需要创建openfire运行所需的数据库:我电脑已经安装了的是SQLServer 2005。打开D:\Technology\XMPP\openfire\resources\database目录:
打开Microsoft SQL Server Management Studio创建数据库Openfire并运行openfire_SQLServer.SQL的脚本:
这里截取了一部分表。
由于openfire安装包并没有内置SQLServer的jdbc驱动,所以需要从网上下载sqljdbc.jar并拷贝至以下目录:D:\Technology\XMPP\openfire\lib。
2.3 开始安装openfire
表创建完毕之后、运行bin目录下的openfire.exe。
选择Launch Admin打开管理员界面:
选择简体中文,Continue下一步:
输入2.1中配置的域名:im.comit.com.cn,端口默认。继续:
这一步选择标准数据库,继续:
选择SqlServer
驱动程序类输入:com.microsoft.sqlserver.jdbc.SQLServerDriver
数据库URL:jdbc:sqlserver://127.0.0.1;DatabaseName=Openfire
出入用户名sa,密码xxxxxx,其他默认。点击继续:
选择“初始设置”,继续:
这里顺便输入游戏号以及密码,说明:这一步是创建admin帐号,一定要记住密码。继续:
系统提示openfire已经安装完成了。
在Microsoft SQL Server Management Studio ,打开表ofUser表,会看到刚才创建的admin帐号:
这个时候不要点“登录到管理控制台”,登录肯定失败,需要切换到这个页面:
点”stop”,然后点“start”,等重启动成功后点”Launch Admin”,进入登录页面:
输入admin和刚才创建的密码,登录:
从管理员界面,我们可以查看服务器名称:im.comit.com.cn ;并且可以管理用户组和用户:
这里,通过管理界面插入一个test用户(那么该用户的jid为:teat@im.comit.com.cn,有关jid的定义在第二篇文章介绍XMPP协议时有介绍)。
到目前为止,openfire服务器基本搭建成功。
三、使用客户端测试我们搭建的openfire服务器
主要测试用例包括:连接openfire服务器、在客户端注册用户、在不同的基于XMPP协议的客户端进行添加好友请求以及发送即时消息。
1. Spark 客户端:与openfire同一个开源组织的基于XMPP协议的客户端
2. SparkWeb:基于Web的XMPP客户端
3. 如意通(Rooyee Message,国内公司开发的开源的基于XMPP、支持文件、音频、视频聊天、功能比较强悍的一款软件,下载地址:http://rooyee.biz/a/xiazaizhongxin/ruanjianxiazai/index.html)
开始进行测试:
安装openfire服务器时,我们创建了帐号test,现在我们使用Spark客户端申请一个帐号test2,注意服务器输入im.comit.com.cn。创建成功之后登录。
在Microsoft SQL Server Management Studio ,打开表ofUser表,会看到刚才创建的test2帐号:
Rooyee Message的登录设置与Spark类似,我们使用test帐号登录Rooyee Message。
2款软件都登录成功之后。使用Spark登录的test2 来向Rooyee Message登录的test 发送联系人添加请求。
桌面右下角Rooyee Message会弹出test2的请求对话框
同意请求之后,在联系人列表就可以看到双方在线的情况以及对方的注册信息。
下面测试2款异构的客户端之间的通信(Spark是Java客户端,Rooyee Message是用Delphi开发的客户端,但两者都是基于XMPP协议)。
Spark客户端的消息。
Rooyee Message的消息框。
同时打开Openfire的后台,我们可以看到两者的的回话信息:
到现在服务器和客户端都已经准备测试通过了。
从中我们可以发觉XMPP协议的开放性。
四、Smack 和 ASmack
1. Smack API是一个完整的实现了XMPP协议的开源API库,支持文件、音频、视频等消息。可以使用该API库进行二次开发。
2. 由于Smack官方没有实现移动版本的API,但是第三方提供开源的基于Smack的Anadorid API:ASmack(Android build environment and patches for smack)。在Android上面可以利用该API可以进行基于XMPP协议的即时消息应用程序开发。
来源: 部署一个基于 Meteor 的 Web 和移动聊天服务器
几个月前,我的大家庭中的成员建议我建立一个私人聊天服务器供我们自己的群体使用。我的开发人员好友也提出了类似的请求。但直到最近,我惯常的拖延和对安装和管理前景的争论延误了这一实施工作。但是最近,一个很诱人的机会促使我最终启动了我自己的私人聊天服务器。
作为 developerWorks 的定期撰稿人,我在 developerWorks Premium 推出时就开始了解它。该计划是专为开发人员设计的付费会员计划,这个事实激起了我的好奇心。由于有海量的代码、开发知识和评估软件在网络上自由传播,很难想象 IBM 可将什么值得从开发人员的口袋中掏出的值钱的东西集中在一起。经过一些探索后,我很高兴分享我的发现。会员有许多特别待遇,不仅仅是 Bluemix 上足够开始我的聊天服务器托管尝试的云托管额度。
在本教程中,您将复制我的运行和操作 Web 和移动聊天服务器的成功成果。您将使用 Rocket.Chat— 一个我经常参与的 托管在 GitHub 上(且基于 Meteor) 的 MIT 授权项目。像我和我的朋友一样,您的小组可使用该聊天服务器来讨论问题,共享代码,一起工作和闲聊— 完全没有隐私问题或意料之外的宕机时间。甚至在临时休息和缺席期间,您也可使用 iOS 和 Android 客户端来与小组保持联系。
“使用 Rocket.Chat,只需占用您数分钟时间,即可在 Bluemix 上为您自己的团队或家人运行一个私人聊天服务器。”
运行示例服务器在 GitHub 上获取 Rocket.Chat 代码
单击 Run the example server 按钮试验我托管在 Bluemix 上的一个示例群聊服务器。这个屏幕截图显示了 Web 聊天的实际应用:

在您的工作站上,运行:
git clone https://github.com/rocketchat/Rocket.Chat
将目录更改为 Rocket.Chat,创建一个名为 .cfignore 的文件,然后将下面这行添加到该文件中:
|
1
|
.meteor/local |
这一行告诉 Cloud Foundry CLI 在部署期间不要将 .meteor/local 目录的内容上传到 Bluemix,进而节省大量部署时间和带宽。
在这一步中,通过一个 自定义 buildpack 将 Rocket.Chat 部署到 Bluemix,而无需启动服务器。(服务器需要设置一些环境变量才能运行;您将在 第 4 步 中设置它们。)
运行下面这个命令:
cf push your-app-name -b https://github.com/ind1go/bluemix-buildpack-meteor.git -m 512M --no-start
您必须为 your-app-name 选择一个唯一名称。您的命令输出应该类似于以下内容:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Creating app singdevchat in org developerWorks / space Sing-Li as westmakaha@yahoo.com...OKCreating route singdevchat.mybluemix.net...OKBinding singdevchat.mybluemix.net to singdevchat...OKUploading singdevchat...Uploading app files from: /home/autoqa/github/Rocket.ChatUploading 3.3M, 1554 filesDone uploadingOK |
作为 Meteor 应用程序,Rocket.Chat 需要一个 MongoDB 实例作为它的后备存储。您可从各服务提供程序获得一个实例,或者可以 自行运行一个。
两个具有 MongoDB 计划的流行的提供程序是 compose.io 和 MongoLab。我选择了一个 compose.io 计划并创建了一个名为 bluerocket 的数据库(您可为您自己的数据库使用任何名称)。
对于您创建的数据库,添加一个名为 rocketchat 的数据库用户并设置一个密码。
要将 Rocket.Chat 与 MongoDB 实例挂钩,您需要来自该提供程序的以下信息:
您必须至少设置以下环境变量,服务器才能正确启动:
ROOT_URL,用于访问服务器的 URLMONGO_URL,服务器用于访问您的 MongoDB 实例的 URLcf set-env your-app-name MONGO_URL mongodb://user-name:password@hostname:port/database-name
cf set-env your-app-name ROOT_URL https://your-app-name.mybluemix.net
cf env your-app-name
该命令输出显示了您的环境变量的值:
|
1
2
3
4
5
6
7
8
9
10
|
Getting env variables for app singdevchat in org developerWorks / space Sing-Li as westmakaha@yahoo.com...OKSystem-Provided:...User-Provided:MONGO_URL: mongodb://rocketchat:xxxxx@xxxxxxxxx:88888/bluerocketROOT_URL: https://singdevchat.mybluemix.net |
cf start your-app-name|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
Starting app singdevchat in org developerWorks / space Sing-Li as westmakaha@yahoo.com...-----> Downloaded app package (3.9M)Cloning into '/tmp/buildpacks/bluemix-buildpack-meteor'...-----> Resolving engine versions No version of Node.js specified in nodeversion, using '0.10.*' Using Node.js version: 0.10.40-----> Fetching Node.js binaries - downloading and extracting node from http://nodejs.org/dist/v0.10.40/node-v0.10.40-linux-x64.tar.gz Installing meteor######################################################################## 100.0%Downloading Meteor distribution######################################################################## 100.0%Meteor 1.2.1 has been installed in your home directory (~/.meteor).�Requested state: startedinstances: 1/1usage: 512M x 1 instancesurls: singdevchat.mybluemix.netlast uploaded: Mon Nov 30 04:39:28 UTC 2015stack: cflinuxfs2buildpack: https://github.com/ind1go/bluemix-buildpack-meteor.git state since cpu memory disk details #0 running 2015-11-29 11:55:44 PM 0.0% 255.9M of 512M 284.5M of 1G |
您的群聊服务器现在正在运行。Rocket.Chat 支持最新版本的 Chrome、Safari 和 Firefox,也可以使用 iPhone 和 Android 手机浏览器。
可通过 Administration 菜单执行的任务包括:
向小组的其他成员发送包含服务器的 URL 的电子邮件邀请。他们可在服务器上注册新帐户。然后在您的小组中尝试一些新特性:
全面了解这个功能丰富的聊天服务器。您可能会找到许多适合您的聊天小组的功能。
扩展 Rocket.Chat 的特性的首选方式是通过 hubot,这是 GitHub 开发的一个开源自动化接口。在这里,您可以实时地看到在项目的 GitHub 存储库中发生的,hubot 向 Rocket.Chat 社区服务器公布的活动:

Hubots 是简单的程式脚本,可以使用 CoffeeScript 或 JavaScript 编写。这些脚本通常在一台单独的计算机上运行并以普通用户身份连接到聊天服务器。hubot 以 Node.js 可执行程序的形式运行,可使用任何可用的 Node.js 库。
您不需要知道 Rocket.Chat 的内部接口即可编写 hubot。Hubot 提供了一种简单的编程抽象方法。您可从 Hubot 文档学到需要的所有知识。
GitHub Hubot 脚本存储库 包含许多可立即用于 Rocket.Chat 的示例程式。Rocket.Chat 还拥有与 GitHub(或 GitLab)连接的 可随时运行的程式。这些程式可实时地向聊天室报告代码存储库活动(比如创建和修改 pull 请求、合并、问题或评论)。该程式还能够从 GitHub 获取 pull 请求、问题和要点等数据。随着时间的推移,可以证明这些程式对我的开发人员好友群体具有宝贵的价值。
Rocket.Chat 是一个快速演变的开源项目,有一个庞大的全球贡献者社区(目前超过 100 人)。源代码和关联的工具会在一段时间内发生巨大的变化。
如果您已熟悉 Meteor 编程,您将会熟悉 Rocket.Chat 的代码库的结构。如果不熟悉,可以学习 Meteor 文档 来稍加了解。
为了帮助您开始自行分析源代码,这里是一些主要源代码目录和文件,以及它们当前的内容:
flow-router 处理的应用程序路由。有关 Rocket.Chat 开发和部署问题,随时可以通过社区聊天服务器 与热心的 Rocket.Chat 社区成员联系。这个服务器始终运行 Rocket.Chat 的最新开发构建版,拥有 25,000 多名注册成员,而且是持续测试代码的主要方式。Rocket.Chat 社区还维护着一个 wiki,其中包含对开发人员和部署人员都有帮助的文档。
使用 Rocket.Chat,只需占用您数分钟时间,即可在 Bluemix 上为您自己的团队或家人运行一个私人聊天服务器。developerWorks Premium 包含一些相关的开发人员资源和云计算津贴,方便不断学习和立即动手试验。
来源: 急急如律令!火速搭建一个C#即时通信系统!(附源码分享——高度可移植!) – C#源码小二郎 – 博客园
(2016年3月更:由于后来了解到GGTalk开源即时通讯系统,因此直接采用了该资源用于项目开发,在此对作者表示由衷的感谢!)
——————————————————————————————————
人在外包公司,身不由己!各种杂七杂八的项目都要做,又没有自己的技术沉淀,每次涉足新的项目都倍感吃力,常常现学现卖,却不免处处碰壁!当然,话说回来,也是自己的水平有限在先,一马配一鞍,无奈也只能留在外包公司。
这不,就在上一周,领导下达一个任务:3天内搭建一个C#即时通讯系统,与原有的办公系统集成。
我正心里犯嘀咕;“网络编程自己就只知道一点皮毛啊,还是大学选修课上听老师讲的那一点东西,别说即时通讯了,以前也就只照着书上的例子写过一个抓包工具当作业交过,彻头彻尾的小白啊,何况都毕业几年了,连“套接字”都快忘了!”
领导补充说:“这个即时通讯系统要尽快完成,之后还有别的的项目。”
我:“······好的”
没办法,就像领导常说的“有条件要上,没有条件创造条件也要上!”,临危受命,唯有逆流而上!
想都别想,写即时通讯总不能从socket写起啊,那样写出来的东西只能读书的时候当作业交给老师看下,然后记一个平时成绩,给领导看那就是找抽!
所以,只能“登高而招,顺风而呼”,园子里大神多,资源也多,找找看有没有可以参考的。(这也是我一直以来的工作方法,呵呵)
终于,看到了一篇轻量级通信引擎StriveEngine通信demo源码研究学习了一下,稍加揣摩,很快就完成了领导所交付的重任!在此要鸣谢该文的作者!
言归正传,接下来就把自己的学习所得以及编写过程详尽的分享给大家!



首先,网络中的数据是源源不断的二进制流,有如长江之水连绵不绝,那么,即时通讯系统如何从连绵不绝的数据流中准确的识别出一个消息呢?换言之,在悠远绵长的网络数据流中,一个个具体的消息应该如何被界定出来呢?
这就需要用到通信协议。通信协议,一个大家耳熟能详的术语,什么TCP啊、UDP啊、IP啊、ICMP啊,以前学《计算机网络》时,各种协议充斥寰宇。但是,从教科书上抽象的概念中,你真的了解什么是通信协议吗?
回到开始的问题,我想恐怕可以这样来理解:通信协议就是要让消息遵循一定的格式,而这样的格式是参与通信的各方都知晓且遵守的,依据这样的格式,消息就能从数据流中被完整的识别出来。
通信协议的格式通常分为两类:文本消息、二进制消息。
文本协议相对简单,通常使用一个特殊的标记符作为一个消息的结束。这样一来,根据这个特殊的标志符,每个消息之间就有了明确的界限。
二进制协议,通常是由消息头(Header)和消息体(Body)构成的,消息头的长度固定,而且,通过解析消息头,可以知道消息体的长度。如此,我们便可以从网络流中解析出一个个完整的二进制消息。
两种协议各有优劣,虽然文本协议比较简单方便,但是二进制协议更具有普适性,诸如图片啊、文件啊都可以转化为二进制数组,所以我在写即时通讯时采用的是二进制协议。
我定义的二进制协议是:消息头固定为8个字节:前四个字节为一个int,其值表示消息类型;后四个字节也是一个int,其值表示消息体长度。
先来看消息头的定义
1 public class MsgHead
2 {
3 private int messageType;
4 /// <summary>
5 /// 消息类型
6 /// </summary>
7 public int MessageType
8 {
9 get { return messageType; }
10 set { messageType = value; }
11 }
12
13 private int bodyLength;
14 /// <summary>
15 /// 消息体长度
16 /// </summary>
17 public int BodyLength
18 {
19 get { return bodyLength; }
20 set { bodyLength = value; }
21 }
22
23 public const int HeadLength = 8;
24
25
26 public MsgHead(int msgType,int bodyLen)
27 {
28 this.bodyLength = bodyLen;
29 this.messageType = msgType;
30 }
31
32 public byte[] ToStream()
33 {
34 byte[] buff = new byte[MsgHead.HeadLength];
35 byte[] bodyLenBuff = BitConverter.GetBytes(this.bodyLength);
36 byte[] msgTypeBuff = BitConverter.GetBytes(this.messageType);
37 Buffer.BlockCopy(msgTypeBuff, 0, buff, 0, msgTypeBuff.Length);
38 Buffer.BlockCopy(bodyLenBuff, 0, buff, 4, bodyLenBuff.Length);
39 return buff;
40 }
41 }
然后我们将识别消息的方法封装到一个协议助手类中,即收到消息的时候,明确如下两个问题:1.固定前多少位是消息头。2.如何从消息头中获取消息体长度。
1 public class StreamContractHelper : IStreamContractHelper
2 {
3 /// <summary>
4 /// 消息头长度
5 /// </summary>
6 public int MessageHeaderLength
7 {
8 get { return MsgHead.HeadLength; }
9 }
10 /// <summary>
11 /// 从消息头中解析出消息体长度,从而可以间接取出消息体
12 /// </summary>
13 /// <param name="head"></param>
14 /// <returns></returns>
15 public int ParseMessageBodyLength(byte[] head)
16 {
17 return BitConverter.ToInt32(head,4);
18 }
19 }
20

然后我们来定义满足协议的消息基类,其中重点是要定义ToContractStream()方法,使得消息能够序列化成满足协议的二进制流,从而通过网络进行传输。
1 [Serializable]
2 public class BaseMsg
3 {
4 private int msgType;
5
6 public int MsgType
7 {
8 get { return msgType; }
9 set { msgType = value; }
10 }
11 /// <summary>
12 /// 序列化为本次通信协议所规范的二进制消息流
13 /// </summary>
14 /// <returns></returns>
15 public byte[] ToContractStream()
16 {
17 return MsgHelper.BuildMsg(this.msgType, SerializeHelper.SerializeObject(this));
18 }
19 }
然后我们来看看MsgHelper类的具体实现
1 public static class MsgHelper
2 {
3 /// <summary>
4 /// 构建消息
5 /// </summary>
6 /// <param name="msgType">消息类型</param>
7 /// <param name="msgBody">消息体</param>
8 /// <returns></returns>
9 public static byte[] BuildMsg(int msgType, Byte[] msgBody)
10 {
11 MsgHead msgHead = new MsgHead(msgType, msgBody.Length);
12 //将消息头与消息体拼接起来
13 byte[] msg = BufferJointer.Joint(msgHead.ToStream(), msgBody);
14 return msg;
15 }
16
17 public static byte[] BuildMsg(int msgType, string str)
18 {
19 return MsgHelper.BuildMsg(msgType, Encoding.UTF8.GetBytes(str));
20 }
21 /// <summary>
22 /// 将二进制数组还原成消息对象
23 /// </summary>
24 /// <typeparam name="T">所要还原成的消息类</typeparam>
25 /// <param name="msg">消息数据</param>
26 /// <returns></returns>
27 public static T DeserializeMsg<T>(byte[] msg)
28 {
29 return (T)SerializeHelper.DeserializeBytes(msg, 8, msg.Length - 8);
30 }
31 }
然后我们再看一个具体的消息类ChatMsg的定义
1 [Serializable]
2 public class ChatMsg:BaseMsg
3 {
4 private string sourceUserID;
5 /// <summary>
6 /// 发送该消息的用户ID
7 /// </summary>
8 public string SourceUserID
9 {
10 get { return sourceUserID; }
11 set { sourceUserID = value; }
12 }
13 private string targetUserID;
14 /// <summary>
15 /// 该消息所发往的用户ID
16 /// </summary>
17 public string TargetUserID
18 {
19 get { return targetUserID; }
20 set { targetUserID = value; }
21 }
22 private DateTime timeSent;
23 /// <summary>
24 /// 该消息的发送时间
25 /// </summary>
26 public DateTime TimeSent
27 {
28 get { return timeSent; }
29 set { timeSent = value; }
30 }
31 private string msgText;
32 /// <summary>
33 /// 该消息的文本内容 ///
34 /// </summary>
35 public string MsgText
36 {
37 get { return msgText; }
38 set { msgText = value; }
39 }
40 /// <summary>
41 /// 构造一个ChatMsg实例
42 /// </summary>
43 /// <param name="_sourceUserID">该消息源用户ID</param>
44 /// <param name="_targetUserID">该消息目标用户ID</param>
45 /// <param name="_MsgText">该消息的文本内容 </param>
46 public ChatMsg(string _sourceUserID, string _targetUserID, string _MsgText)
47 {
48 base.MsgType = Core.MsgType.Chatting;
49 this.sourceUserID = _sourceUserID;
50 this.targetUserID = _targetUserID;
51 this.timeSent = DateTime.Now;
52 this.msgText = _MsgText;
53 }
54 }

1.客户端发送登陆消息
private void button_login_Click(object sender, EventArgs e)
{
this.selfID = this.textBox_ID.Text.Trim();
LoginMsg loginMsg = new LoginMsg(this.selfID);
this.tcpPassiveEngine.PostMessageToServer(loginMsg.ToContractStream());
}
2.服务端回复登陆消息
1 if (msgType == MsgType.Logining)
2 {
3 LoginMsg loginMsg = MsgHelper.DeserializeMsg<LoginMsg>(msg);
4 this.ReplyLogining(loginMsg, userAddress);
5 //将在线用户告知其他客户端
6 this.TellOtherUser(MsgType.NewOnlineFriend, loginMsg.SrcUserID);
7 }
8
9 /// <summary>
10 /// 回复登陆消息
11 /// </summary>
12 /// <param name="loginMsg"></param>
13 /// <param name="userAddress"></param>
14 private void ReplyLogining(LoginMsg loginMsg, IPEndPoint userAddress)
15 {
16 if (this.onlineManager.Contains(loginMsg.SrcUserID))//重复登录
17 {
18 loginMsg.LogonResult = LogonResult.Repetition;
19 this.tcpServerEngine.SendMessageToClient(userAddress, loginMsg.ToContractStream());
20 }
21 else//此demo简化处理回复成功,其他验证未处理
22 {
23 this.AddUser(loginMsg.SrcUserID, userAddress);
24 this.ShowOnlineUserCount();
25 loginMsg.LogonResult = LogonResult.Succeed;
26 this.tcpServerEngine.SendMessageToClient(userAddress, loginMsg.ToContractStream());
27 }
28 }
3.客户端处理登陆结果
1 private void tcpPassiveEngine_MessageReceived(IPEndPoint userAddress, byte[] msg)
2 {
3 //取出消息类型
4 int msgType = BitConverter.ToInt32(msg, 0);
5 //验证消息类型
6 if (msgType == MsgType.Logining)
7 {
8 LoginMsg loginMsg = MsgHelper.DeserializeMsg<LoginMsg>(msg);
9 if (loginMsg.LogonResult == LogonResult.Succeed)
10 {
11 this.DialogResult = DialogResult.OK;
12 this.tcpPassiveEngine.MessageReceived -= new StriveEngine.CbDelegate<IPEndPoint, byte[]>(tcpPassiveEngine_MessageReceived);
13 }
14 if (loginMsg.LogonResult == LogonResult.Repetition)
15 {
16 MessageBox.Show("登录失败,该账号已经登录!");
17 }
18 }
19 }

1.客户端A发送聊天消息给服务器
1 /// <summary>
2 /// 发送聊天消息
3 /// </summary>
4 /// <param name="sender"></param>
5 /// <param name="e"></param>
6 private void button_send_Click(object sender, EventArgs e)
7 {
8 string chatText = this.richTextBox_Write.Text;
9 if (string.IsNullOrEmpty(chatText))
10 {
11 MessageBox.Show("消息不能为空");
12 return;
13 }
14 ChatMsg chatMsg = new ChatMsg(this.selfUserID, this.friendID, chatText);
15 this.tcpPassiveEngine.SendMessageToServer(chatMsg.ToContractStream());
16 this.ShowChatMsg(chatMsg);
17 }
2.服务端转发聊天消息
1 if (msgType == MsgType.Chatting)
2 {
3 ChatMsg chatMsg = MsgHelper.DeserializeMsg<ChatMsg>(msg);
4 if (this.onlineManager.GetKeyList().Contains(chatMsg.TargetUserID))
5 {
6 IPEndPoint targetUserAddress = this.onlineManager.Get(chatMsg.TargetUserID).Address;
7 this.tcpServerEngine.SendMessageToClient(targetUserAddress, msg);
8 }
9 }
3.客户端B接收并显示聊天消息
1 void tcpPassiveEngine_MessageReceived(IPEndPoint userAddress, byte[] msg)
2 {
3 //取出消息类型
4 int msgType = BitConverter.ToInt32(msg, 0);
5 //验证消息类型
6 if (msgType == MsgType.Chatting)
7 {
8 ChatMsg chatMsg = MsgHelper.DeserializeMsg<ChatMsg>(msg);
9 this.ShowChatForm(chatMsg.SourceUserID);
10 this.ChatMsgReceived(chatMsg);
11 }
12 }
13
14 /// <summary>
15 /// 显示聊天窗
16 /// </summary>
17 /// <param name="friendUserID">聊天对方用户ID</param>
18 private void ShowChatForm(string friendUserID)
19 {
20 if (this.InvokeRequired)
21 {
22 this.Invoke(new CbGeneric<string>(this.ShowChatForm), friendUserID);
23 }
24 else
25 {
26 ChatForm form = this.chatFormManager.GetForm(friendUserID);
27 if (form == null)
28 {
29 form = new ChatForm(this.selfID, friendUserID, this, this.tcpPassiveEngine);
30 form.Text = string.Format("与{0}对话中···", friendUserID);
31 this.chatFormManager.Add(form);
32 form.Show();
33 }
34 form.Focus();
35 }
36 }
37
38
39 /// <summary>
40 /// 显示聊天消息
41 /// </summary>
42 /// <param name="chatMsg"></param>
43 private void ShowChatMsg(ChatMsg chatMsg)
44 {
45 if (this.InvokeRequired)
46 {
47 this.Invoke(new CbGeneric<ChatMsg>(this.formMain_chatMsgReceived), chatMsg);
48 }
49 else
50 {
51 this.richTextBox_display.AppendText(chatMsg.SourceUserID + " " + chatMsg.TimeSent.ToString() + "\r\n");
52 this.richTextBox_display.AppendText(chatMsg.MsgText + "\r\n");
53 this.richTextBox_Write.Clear();
54 }
55 }
源码说明:1.客户端与服务端均含有配置文件,可配置进程的IP与端口号。
2.代码均含有详细注释。
3.调试时确保客户端的配置文件相关信息无误,先启动服务端再启动客户端。
4.登录账号与密码均为任意。
5.点击好友头像即可聊天。
下载:Chat.Demo
版权声明:本文为博主原创文章,未经博主允许不得转载。