现代 PHP 新特性系列(一) —— 命名空间 – Laravel学院

mikel阅读(1270)

Laravel学院致力于提供优质Laravel中文学习资源

来源: 现代 PHP 新特性系列(一) —— 命名空间 – Laravel学院

1、什么是命名空间

如果你只需要知道现代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文件:

laravel-response

第3行代码如下:

namespace Illuminate\Http;

这一行是PHP命名空间声明语句,声明命名空间的代码始终应该放在<?php标签后的第一行。通过这个命名空间的声明语句我们可以看到Response位于厂商命名空间Illuminate中(最顶层命名空间),我们还看到Response类在子命名空间Http中,你可以看下和Response.php文件在同一层级的其他文件,会发现它们都使用相同的命名空间声明语句。

命名空间的作用是封装和组织相关的PHP类,就像在文件系统中把相关的文件放在同一个目录中一样。PHP命名空间和操作系统的物理文件系统不同,这是一个虚拟概念,没必要和文件系统中的目录结构完全相同,虽然如此,但是大多数PHP组件为了兼容广泛使用的PSR-4自动加载标准,会把命名空间放到对应文件系统的子目录中。

2、为什么使用命名空间

前面已经提到过,我们的代码可能和其他开发者的代码使用相同的类名、接口名、函数或常量名,如果不使用命名空间,名称会起冲突,导致PHP执行出错。而使用命名空间将代码放到唯一的厂商命名空间,我们的代码就可以和其他开发者使用相同的类名、接口名、函数或常量名。

当然如果你开发的是小型个人项目,只有少量的依赖,类名冲突可能不是问题,但是如果在团队中工作,开发用到许多第三方依赖的大型项目,就要认真对待命名冲突问题,因为你无法控制项目依赖在全局命名空间中引入的类、接口、函数和常量,这也是为什么要使用命名空间的原因。

3、声明命名空间

每个PHP类、接口、函数和常量都在命名空间中,声明命名空间很简单,在<?php标签后的第一行声明,声明语句以namespace开头,随后是一个空格,然后是命名空间的名称,最后以;结尾。

命名空间经常用于设置顶层厂商名,比如我们设置厂商名为LaravelAcademy

<?php
namespace LaravelAcademy;

在这个命名空间声明语句后声明的所有PHP类、接口、函数和常量都在LaravelAcademy命名空间中,而且和Laravel学院有某种关系。如果我们想组织学院用到的代码该怎么做呢?答案是使用子命名空间。

子命名空间的声明方式和前面的示例完全一样,唯一的区别是我们要使用\符号把命名空间和子命名空间分开,例如:

<?php
namespace LaravelAcademy\ModernPHP;

这个命名空间后的所有类、接口、函数和常量都位于LaravelAcademy\ModernPHP中。

在同一个命名空间中的类没必要在同一个PHP文件中声明,可以在PHP文件的顶部指定一个命名空间或子命名空间,此时,这个文件的代码就是该命名空间或子命名空间的一部分。因此我们可以在不同文件中编写属于同一个命名空间的多个类。

注:厂商命名空间是最顶层的命名空间,也是最重要的命名空间,用于识别品牌或组织,必须具有全局唯一性。子命名空间相对而言没那么重要,但是可以用于组织项目的代码。

4、导入和别名

在命名空间出现之前,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;

当然也支持别名,创建方式和类一样。

5、实用技巧

多重导入

如果想要在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支付教程 - 知乎专栏

mikel阅读(1341)

来源: 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';
    }

使用Laravel AliPay - will5451的博客 - CSDN博客

mikel阅读(1020)

支付宝 SDK 在 Laravel 5 封装包https://github.com/Latrell/AlipayAlipay支付宝SDK在Laravel5封装包。该拓展包想要达到在Laravel5框架下,便捷使用支付宝的目的。安装composer require latrell/alipay dev-master更新你的依赖包 composer update 或者全

来源: 使用Laravel AliPay – will5451的博客 – CSDN博客

支付宝 SDK 在 Laravel 5 封装包

https://github.com/Latrell/Alipay

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触发器运用 - 听风吹雨 - 博客园

mikel阅读(946)

来源: SQL Server DDL触发器运用 – 听风吹雨 – 博客园

一.本文所涉及的内容(Contents)

  1. 本文所涉及的内容(Contents)
  2. 背景(Contexts)
  3. 基础知识(Rudimentary Knowledge)
  4. DDL运用场景(DDL Scene)
  5. 补充说明(Addon)
  6. 疑问(Questions)
  7. 参考文献(References)

二.背景(Contexts)

说到触发器,大家都会想到这样的使用场景:当一个表的数据修改了,运用DML触发插入或者更新到其它表中;那DDL触发器(SQL Server 2005引入的新功能)会运用到什么场景中呢?本文将为你讲述4种运用DDL触发器的场景:

1) 禁止用户修改和删除表;

2) 禁止用户删除数据库;

3) 记录和监控某数据库所有的DDL操作;

4) 把DDL操作信息以邮件的形式主动发送通知和预警;

三.基础知识(Rudimentary Knowledge)

DDL触发器是由修改数据库对象的 DDL 语句(如以 CREATE、ALTER 或 DROP)激发。

DDL触发器支持BEFORE和AFTER事件触发器,并在数据库或模式级运行。通常,DDL触发器用于监控数据库中的重要事件。有时用它们来监控错误代码。错误代码可能会执行破坏数据库或使数据库不稳定的活动。更常见的情况是:在开发、测试和stage系统中用它们来了解和监控数据库活动的动态。

当监控GRANT和REVOKE权限语句时,它们也是有效的安全工具。

四.DDL运用场景(DDL Scene)

(一) 首先我们来看一个简单的例子:创建数据库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]
复制代码

wps_clip_image-14403_thumb

(Figure1:创建数据库级别的DDL)

wps_clip_image-32149_thumb

(Figure2:返回的提示信息)

wps_clip_image-7628_thumb

(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]
复制代码

wps_clip_image-11348_thumb

(Figure4:创建服务器级别的DDL)

wps_clip_image-13353_thumb

(Figure5:返回的提示信息)

wps_clip_image-16272_thumb

(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
复制代码

wps_clip_image-15637_thumb

(Figure7:返回的提示信息)

wps_clip_image-3080_thumb

(Figure8:DatabaseLog表前半部分信息)

wps_clip_image-5273_thumb

(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;
复制代码

wps_clip_image-30426_thumb

(Figure10:邮件收到的预警)

五.补充说明(Addon)

(一) 关于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这些窗口,我还没有在程序连接的情况测试是否会返回这些信息:

wps_clip_image-1961_thumb

(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]

wps_clip_image-27698_thumb

(Figure12:DDL触发器列表)

wps_clip_image-170_thumb

(Figure13:错误信息)

七.疑问(Questions)

(一) 删除DDL触发器是否也可以触发一个事件呢?不然如何防止用户先删除DDL触发器之后再做DDL操作呢?难道是用户权限?

解答:第一种方法,可以对DDL触发器进行权限控制;第二种方式就是在服务器级别加一个DROP的触发器,可以监控各个数据库的DDL触发器;下图Figure14是DDL_DatabaseLog被删除时的预警;

wps_clip_image-10302_thumb

(Figure14:删除DDL触发器)

(二) 能对所有数据库进行DDL监控?一条DDL预警能实现?

解答:可以在DDL_DatabaseLog把 ON DATABASE 设置为ON All SERVER,这样就可以监控整个服务器实例,下图Figure15是Logon_DB的DDL预警;

wps_clip_image-16633_thumb

(Figure15:删除DDL触发器)

八.参考文献(References)

MS SQL Server:DDL 触发器

DDL 触发器(msdn)

DDL 触发器(technet)

DML 触发器(technet)

登录触发器(technet)

SQL Server 2005 – Default Trace (默认跟踪)

MS SQL监控数据库的DDL操作(邮件通知)

DDL触发器分为

引用 DDL触发器介绍(Oracle)

DDL触发器

SQL SERVER – What is – DML, DDL, DCL and TCL – Introduction and Examples

管理触发器安全性

使用 EVENTDATA 函数

CREATE TRIGGER (Transact-SQL)

sp_send_dbmail (Transact-SQL)

.NET 即时通信,WebSocket服务端实例 - Tsong Chen - 博客园

mikel阅读(1064)

来源: .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 - 博客园

mikel阅读(1182)

来源: 基于即时通信和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,网上可以找到下载的源代码。

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

      Spark
      3. Smack是即时通信客户端编程库,可以使用smack的API向openfire注册用户发送消息,并且可以通过监听器获得此用户的应答消息,甚至可以做自动应答机器人,后门我们会用到该API。

      Smack

     

二、搭建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

      DNS2

     

      可以ping一下刚才的域名im.comit.com.cn,验证是否配置正确:

      DNS3

 

2.2 准备工作:创建openfire运行所需的数据库   

      下载openfire:openfire需要java运行时,由于我的机器已经有了java开发环境,所以下载了免安装版的zip压缩包。

      Openfire 下载

     

      下载完解压缩,我的目录是:D:\Technology\XMPP\openfire。

      需要创建openfire运行所需的数据库:我电脑已经安装了的是SQLServer 2005。打开D:\Technology\XMPP\openfire\resources\database目录:

      db

      打开Microsoft SQL Server Management Studio创建数据库Openfire并运行openfire_SQLServer.SQL的脚本:

      table

      这里截取了一部分表。

      由于openfire安装包并没有内置SQLServer的jdbc驱动,所以需要从网上下载SQLjdbc.jar并拷贝至以下目录:D:\Technology\XMPP\openfire\lib。

     

2.3 开始安装openfire

      表创建完毕之后、运行bin目录下的openfire.exe。

      run

      选择Launch Admin打开管理员界面:

      step_1

      选择简体中文,Continue下一步:

      step_2_2

      输入2.1中配置的域名:im.comit.com.cn,端口默认。继续:

      step_3

      这一步选择标准数据库,继续:

      step_4

      选择SqlServer

      驱动程序类输入:com.microsoft.sqlserver.jdbc.SQLServerDriver

      数据库URL:jdbc:sqlserver://127.0.0.1;DatabaseName=Openfire

      出入用户名sa,密码xxxxxx,其他默认。点击继续:

      step_5

      选择“初始设置”,继续:

      step_6

      这里顺便输入游戏号以及密码,说明:这一步是创建admin帐号,一定要记住密码。继续:

      step_7

      系统提示openfire已经安装完成了。

      在Microsoft SQL Server Management Studio ,打开表ofUser表,会看到刚才创建的admin帐号:

      step_11

      这个时候不要点“登录到管理控制台”,登录肯定失败,需要切换到这个页面:

      step_8

      点”stop”,然后点“start”,等重启动成功后点”Launch Admin”,进入登录页面:

      step_9

      输入admin和刚才创建的密码,登录:

      step_10

      从管理员界面,我们可以查看服务器名称:im.comit.com.cn ;并且可以管理用户组和用户:

     step_13

      这里,通过管理界面插入一个test用户(那么该用户的jid为:teat@im.comit.com.cn,有关jid的定义在第二篇文章介绍XMPP协议时有介绍)。

      到目前为止,openfire服务器基本搭建成功。

 

三、使用客户端测试我们搭建的openfire服务器

 

      主要测试用例包括:连接openfire服务器、在客户端注册用户、在不同的基于XMPP协议的客户端进行添加好友请求以及发送即时消息。

      1. Spark 客户端:与openfire同一个开源组织的基于XMPP协议的客户端

      Spark

      2. SparkWeb:基于Web的XMPP客户端

      SparkWeb

      3. 如意通(Rooyee Message,国内公司开发的开源的基于XMPP、支持文件、音频、视频聊天、功能比较强悍的一款软件,下载地址:http://rooyee.biz/a/xiazaizhongxin/ruanjianxiazai/index.html

      rooyee

     

      开始进行测试:

 

      step_15

            step_17    

      安装openfire服务器时,我们创建了帐号test,现在我们使用Spark客户端申请一个帐号test2,注意服务器输入im.comit.com.cn。创建成功之后登录。

      在Microsoft SQL Server Management Studio ,打开表ofUser表,会看到刚才创建的test2帐号:

      step_18

      Rooyee Message的登录设置与Spark类似,我们使用test帐号登录Rooyee Message。

      step_19

     

      2款软件都登录成功之后。使用Spark登录的test2 来向Rooyee Message登录的test 发送联系人添加请求。

      step_20

      桌面右下角Rooyee Message会弹出test2的请求对话框

      step_23

      同意请求之后,在联系人列表就可以看到双方在线的情况以及对方的注册信息。

      step_25

 

      下面测试2款异构的客户端之间的通信(Spark是Java客户端,Rooyee Message是用Delphi开发的客户端,但两者都是基于XMPP协议)。

      step_26

      Spark客户端的消息。

      step_27

      Rooyee Message的消息框。

      同时打开Openfire的后台,我们可以看到两者的的回话信息:

      step_28

      到现在服务器和客户端都已经准备测试通过了。

      从中我们可以发觉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协议的即时消息应用程序开发

     

微软企业开发技术 | 移动开发(Google Android、Windows Mobile)技术| 嵌入式系统设计与开发 | JAVA开发

部署一个基于 Meteor 的 Web 和移动聊天服务器

mikel阅读(1239)

来源: 部署一个基于 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 聊天的实际应用:

运行中的聊天程序的屏幕截图

第 1 步. 获取源代码

在您的工作站上,运行:

git clone https://github.com/rocketchat/Rocket.Chat

将目录更改为 Rocket.Chat,创建一个名为 .cfignore 的文件,然后将下面这行添加到该文件中:

1
.meteor/local

这一行告诉 Cloud Foundry CLI 在部署期间不要将 .meteor/local 目录的内容上传到 Bluemix,进而节省大量部署时间和带宽。

第 2 步. 构建服务器并将其推送到 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...
OK
Creating route singdevchat.mybluemix.net...
OK
Binding singdevchat.mybluemix.net to singdevchat...
OK
Uploading singdevchat...
Uploading app files from: /home/autoqa/github/Rocket.Chat
Uploading 3.3M, 1554 files
Done uploading
OK

第 3 步. 为服务器创建一个 MongoDB 实例

作为 Meteor 应用程序,Rocket.Chat 需要一个 MongoDB 实例作为它的后备存储。您可从各服务提供程序获得一个实例,或者可以 自行运行一个

两个具有 MongoDB 计划的流行的提供程序是 compose.io 和 MongoLab。我选择了一个 compose.io 计划并创建了一个名为 bluerocket 的数据库(您可为您自己的数据库使用任何名称)。

对于您创建的数据库,添加一个名为 rocketchat 的数据库用户并设置一个密码。

要将 Rocket.Chat 与 MongoDB 实例挂钩,您需要来自该提供程序的以下信息:

  • 主机名
  • 端口号
  • 数据库名
  • 用户名
  • 密码

第 4 步. 设置需要的应用程序环境变量并启动服务器

您必须至少设置以下环境变量,服务器才能正确启动:

  • ROOT_URL,用于访问服务器的 URL
  • MONGO_URL,服务器用于访问您的 MongoDB 实例的 URL
  1. 运行以下命令,替换来自您的提供程序的 MongoDB 信息:cf 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

  2. 确认这些环境变量已设置: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...
    OK
    System-Provided:
    ...
    User-Provided:
    MONGO_URL: mongodb://rocketchat:xxxxx@xxxxxxxxx:88888/bluerocket
    ROOT_URL: https://singdevchat.mybluemix.net
  3. 启动服务器:cf start your-app-name
  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
    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: started
    instances: 1/1
    usage: 512M x 1 instances
    urls: singdevchat.mybluemix.net
    last uploaded: Mon Nov 30 04:39:28 UTC 2015
    stack: cflinuxfs2
    buildpack: 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

第 5 步. 创建一个管理用户

您的群聊服务器现在正在运行。Rocket.Chat 支持最新版本的 Chrome、Safari 和 Firefox,也可以使用 iPhone 和 Android 手机浏览器。

  1. 在浏览器中,在 https://your-app-name.mybluemix.net 上访问您的服务器。这是默认登录页面:默认 Rocket.Chat 登录页面的屏幕截图
  2. 单击 Register a new account 并输入需要的信息来创建一个新用户。第一个用户会成为管理用户,可为其他用户提供管理特权。
  3. 以管理用户身份登录,单击左上角的面板并从菜单中选择 Administration 来核对管理选项:Administration 菜单的屏幕截图

可通过 Administration 菜单执行的任务包括:

  • 查看服务器统计数据
  • 管理服务器上的用户和聊天室
  • 配置 GitHub、GitLab、Facebook、Twitter、LinkedIn、WordPress、LDAP、SAML 等的单一登录
  • 设置主题中使用的颜色和登录页上的页眉徽标
  • 设置用于传出电子邮件通知的 SMTP 服务器
  • 配置一般的聊天 UI 行为

第 6 步. 添加更多用户并探索特性

向小组的其他成员发送包含服务器的 URL 的电子邮件邀请。他们可在服务器上注册新帐户。然后在您的小组中尝试一些新特性:

  • 设置您的头像:单击左上角的面板并选择 My Account > Change avatar
  • 在 #general 室内彼此发送消息。
  • 创建新的公共聊天室。
  • 彼此发送私聊消息。
  • 创建一个私人聊天室并邀请其他用户。
  • 将文件拖到聊天窗口中来将其上传到服务器。
  • 下载上传的文件。
  • 通过发送其 URL 来共享多媒体链接,比如 YouTube 电影或 PNG 文件。
  • 更改 UI 使用的语言;Rocket.Chat 开箱即用地支持超过 22 种语言。
  • 与其他用户开始视频或语音聊天(这仅适用于 兼容 webrtc 的浏览器)。

全面了解这个功能丰富的聊天服务器。您可能会找到许多适合您的聊天小组的功能。

第 7 步. 扩展 Rocket.Chat(可选)

扩展 Rocket.Chat 的特性的首选方式是通过 hubot,这是 GitHub 开发的一个开源自动化接口。在这里,您可以实时地看到在项目的 GitHub 存储库中发生的,hubot 向 Rocket.Chat 社区服务器公布的活动:

hubot 公告活动的屏幕截图

Hubots 是简单的程式脚本,可以使用 CoffeeScript 或 JavaScript 编写。这些脚本通常在一台单独的计算机上运行并以普通用户身份连接到聊天服务器。hubot 以 Node.js 可执行程序的形式运行,可使用任何可用的 Node.js 库。

您不需要知道 Rocket.Chat 的内部接口即可编写 hubot。Hubot 提供了一种简单的编程抽象方法。您可从 Hubot 文档学到需要的所有知识。

GitHub Hubot 脚本存储库 包含许多可立即用于 Rocket.Chat 的示例程式。Rocket.Chat 还拥有与 GitHub(或 GitLab)连接的 可随时运行的程式。这些程式可实时地向聊天室报告代码存储库活动(比如创建和修改 pull 请求、合并、问题或评论)。该程式还能够从 GitHub 获取 pull 请求、问题和要点等数据。随着时间的推移,可以证明这些程式对我的开发人员好友群体具有宝贵的价值。

第 8 步. 探究 Rocket.Chat 源代码(可选)

Rocket.Chat 是一个快速演变的开源项目,有一个庞大的全球贡献者社区(目前超过 100 人)。源代码和关联的工具会在一段时间内发生巨大的变化。

如果您已熟悉 Meteor 编程,您将会熟悉 Rocket.Chat 的代码库的结构。如果不熟悉,可以学习 Meteor 文档 来稍加了解。

为了帮助您开始自行分析源代码,这里是一些主要源代码目录和文件,以及它们当前的内容:

  • Rocket.Chat/Dockerfile 为服务器构建可部署的 Docker 映像
  • Rocket.Chat/docker-compose.yml 使用 Docker Compose 运行服务器和 MongoDB 容器。
  • Rocket.Chat/server 包含服务器端代码。
  • Rocket.Chat/server/methods 包含可从客户端调用的 Meteor 远程方法。
  • Rocket.Chat/server/publications 包含 Meteor 发布实现。
  • Rocket.Chat/server/startup 包含在启动时运行的服务器逻辑。
  • Rocket.Chat/client 包含客户端代码。
  • Rocket.Chat/client/routes 包含由 flow-router 处理的应用程序路由。
  • Rocket.Chat/client/startup/startup.coffee 是客户端在启动时运行的内容。
  • Rocket.Chat/public 包含静态资源,比如字体和图像,以及一些通用代码。
  • Rocket.Chat/packages 包含模块化代码,这些代码采用 Meteor 包的形式,构成了服务器逻辑的很大一部分。
  • Rocket.Chat/packages/rocketchat-ui 是包含核心 UI 的包。
  • Rocket.Chat/packages/rocketchat-ui/views/app 包含核心 UI 视图和关联的 Meteor 模板。当前的 UI 是使用 Meteor Blaze 构造的。
  • Rocket.Chat/packages/rocketchat-ui-XXX 是包含各种 UI 组件的实现的包。
  • Rocket.Chat/packages/rocketchat-lib/server/models 包含聊天服务器逻辑所使用的数据模型定义,目前在 MongoDB 中实现。

结束语

有关 Rocket.Chat 开发和部署问题,随时可以通过社区聊天服务器 与热心的 Rocket.Chat 社区成员联系。这个服务器始终运行 Rocket.Chat 的最新开发构建版,拥有 25,000 多名注册成员,而且是持续测试代码的主要方式。Rocket.Chat 社区还维护着一个 wiki,其中包含对开发人员和部署人员都有帮助的文档。

使用 Rocket.Chat,只需占用您数分钟时间,即可在 Bluemix 上为您自己的团队或家人运行一个私人聊天服务器。developerWorks Premium 包含一些相关的开发人员资源和云计算津贴,方便不断学习和立即动手试验。

相关主题

急急如律令!火速搭建一个C#即时通信系统!(附源码分享——高度可移植!) - C#源码小二郎 - 博客园

mikel阅读(1228)

来源: 急急如律令!火速搭建一个C#即时通信系统!(附源码分享——高度可移植!) – C#源码小二郎 – 博客园

2016年3月更:由于后来了解到GGTalk开源即时通讯系统,因此直接采用了该资源用于项目开发,在此对作者表示由衷的感谢!)

——————————————————————————————————

人在外包公司,身不由己!各种杂七杂八的项目都要做,又没有自己的技术沉淀,每次涉足新的项目都倍感吃力,常常现学现卖,却不免处处碰壁!当然,话说回来,也是自己的水平有限在先,一马配一鞍,无奈也只能留在外包公司。

这不,就在上一周,领导下达一个任务:3天内搭建一个C#即时通讯系统,与原有的办公系统集成。

我正心里犯嘀咕;“网络编程自己就只知道一点皮毛啊,还是大学选修课上听老师讲的那一点东西,别说即时通讯了,以前也就只照着书上的例子写过一个抓包工具当作业交过,彻头彻尾的小白啊,何况都毕业几年了,连“套接字”都快忘了!”

领导补充说:“这个即时通讯系统要尽快完成,之后还有别的的项目。”

我:“······好的”

没办法,就像领导常说的“有条件要上,没有条件创造条件也要上!”,临危受命,唯有逆流而上!

想都别想,写即时通讯总不能从socket写起啊,那样写出来的东西只能读书的时候当作业交给老师看下,然后记一个平时成绩,给领导看那就是找抽!

所以,只能“登高而招,顺风而呼”,园子里大神多,资源也多,找找看有没有可以参考的。(这也是我一直以来的工作方法,呵呵)

终于,看到了一篇轻量级通信引擎StriveEngine通信demo源码研究学习了一下,稍加揣摩,很快就完成了领导所交付的重任!在此要鸣谢该文的作者!

言归正传,接下来就把自己的学习所得以及编写过程详尽的分享给大家!

 

一·C#即时通信系统界面快照

   

 

二·网络消息流与通信协议

首先,网络中的数据是源源不断的二进制流,有如长江之水连绵不绝,那么,即时通讯系统如何从连绵不绝的数据流中准确的识别出一个消息呢?换言之,在悠远绵长的网络数据流中,一个个具体的消息应该如何被界定出来呢?

这就需要用到通信协议。通信协议,一个大家耳熟能详的术语,什么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         }
复制代码

 

五·C#即时通信源码下载

    源码说明:1.客户端与服务端均含有配置文件,可配置进程的IP与端口号。
                     2.代码均含有详细注释。
                     3.调试时确保客户端的配置文件相关信息无误,先启动服务端再启动客户端。
                     4.登录账号与密码均为任意。
                     5.点击好友头像即可聊天。
    下载:Chat.Demo 

 


  附相关系列源码: 

 文本协议通信demo源码

二进制通信demo源码及说明文档

打通B/S与C/S通信demo源码与说明文档


版权声明:本文为博主原创文章,未经博主允许不得转载。

下载中心 - C#开源即时通讯GGTalk - 博客园

mikel阅读(1637)

来源: 下载中心 – C#开源即时通讯GGTalk – 博客园

GGTalk(简称GG)是可在广域网部署运行的QQ高仿版,2013.8.7发布GG V1.0版本,至今最新是5.5版本,关于GG更详细的介绍,可以查看 可在广域网部署运行的QQ高仿版 — GGTalk总览。

GGMeeting是可在广域网部署运行的视频会议系统Demo,2015.05.11发布V1.0版本,关于GGMeeting更详细的介绍,可以查看 打造自己的视频会议系统 GGMeeting

1.GGTalk服务端和PC端源码

源码下载:GGTalk-V5.5.rar     网盘下载更快

部署下载:GGTalk V5.5 可直接部署版本    网盘下载更快

(压缩包中有 《部署说明.txt》 和 创建数据库的脚本SQLServer.SQL 、MySQL.sql)

部署说明:

1.当前版本服务端默认配置为内存虚拟数据库版本,不需要安装数据库。

2.将GGTalk.Server文件夹拷贝到服务器上,运行GGTalk.Server.exe。

3.修改客户端配置文件GGTalk.exe.config中ServerIP配置项的值为服务器的IP。

4.运行客户端,注册帐号登录试用。

5.内置测试帐号为 10000,10001,10002,10003,10004;密码都是 1。

6.若要测试Android移动端,请先修改安卓源码中服务器的IP和端口,然后重新编译生成apk。

    如果需要使用真实的物理数据库,则需按下列步骤进行:

1. 在SQLServer 2000/2005/2008 中新建数据库GGTalk,然后在该库中执行 SqlServer.sql 文件中的脚本以创建所需表。

(如果要使用MySQL数据库,则使用MySQL.sql脚本)

2. 打开服务端的配置文件GGTalk.Server.exe.config

(1)修改 UseVirtualDB 配置项的值为false。

(2)修改 DBType 为 SqlServer 或 MySQL。

(3)修改 DBIP 配置项的值为数据库的IP地址。

(4)修改 SaPwd 配置项的值为数据库管理员sa的密码。

3.修改客户端配置文件GGTalk.exe.config中ServerIP配置项的值为服务器的IP。

4.运行客户端,注册帐号登录试用。

 

2.GG安卓版源码

最后更新:2016.11.25

源码下载:GG-Android.rar     网盘下载更快  (若要和PC端联合测试,请关闭PC端那边的聊天消息加密功能:将PC客户端项目的GlobalResourceManager类的 des3Encryption 成员赋值为 null 即可!)

       说明:本安卓demo属于入门级水平,目的是为了展示与PC打通的基本实现。若要将GG安卓版本的源码用于正式项目中,建议先对其进行重构,或者敬请等候后续更完善的版本分享给大家!

 

3.GGMeeting 源码

GGMeeting的当前版本为2.0,大家可以下载源码研究下。

 GGMeeting-V2.0 源码

GGMeeting-V2.0 可直接部署版本

 

————————————————————————————————————————————

联系方式:

QQ:2027224508 

邮箱:2027224508@qq.com

 

如果大家有类似视频会议系统、在线培训系统、IM系统需要定制开发的,可以联系我们哦:)

 

虽然就如何将GG发展为一个有商业价值的产品,我还没有很清晰明确的思路,但是从GG发布以来,通过GG认识了一些朋友,也接了一些小单子,赚了一点小钱。有了一点甜头,目前和2、3个好朋友一起做做小项目也是不错的。

大家有什么问题和建议,都可以联系我,留言、加QQ、发邮件都可以。

欢迎大家与我探讨关于GG的一切!

Node.js + Web Socket 打造即时聊天程序嗨聊 - 刘哇勇 - 博客园

mikel阅读(1075)

来源: Node.js + Web Socket 打造即时聊天程序嗨聊 – 刘哇勇 – 博客园

前端一直是一块充满惊喜的土地,不仅是那些富有创造性的页面,还有那些惊赞的效果及不断推出的新技术。像node.js这样的后端开拓者直接将前端人员的能力扩大到了后端。瞬间就有了一统天下的感觉,来往穿梭于前后端之间代码敲得飞起,从此由前端晋升为’前后端’。

图片来自G+

本文将使用Node.js加web socket协议打造一个网页即时聊天程序,取名为HiChat,中文翻过来就是’嗨聊’,听中文名有点像是专为寂寞单身男女打造的~

其中将会使用到express和socket.io两个包模块,下面会有介绍。

源码&演示

在线演示 (heroku服务器网速略慢且免费套餐是小水管,建议下载代码本地运行)

源码可访问项目的GitHub页面下载

本地运行方法:

  • 命令行运行npm install
  • 模块下载成功后,运行node server启动服务器
  • 打开浏览器访问localhost

下图为效果预览:

 

准备工作

本文示例环境为Windows,Linux也就Node的安装与命令行稍有区别,程序实现部分基本与平台无关。

Node相关

  • 你需要在本机安装Node.js(废话)
  • 多少需要一点Node.js的基础知识,如果还未曾了解过Node.js,这里有一篇不错的入门教程

然后我们就可以开始创建一个简单的HTTP服务器啦。

类似下面非常简单的代码,它创建了一个HTTP服务器并监听系统的80端口。

//node server example

//引入http模块
var http = require('http'),
    //创建一个服务器
    server = http.createServer(function(req, res) {
        res.writeHead(200, {
            'Content-Type': 'text/plain'
        });
        res.write('hello world!');
        res.end();
    });
//监听80端口
server.listen(80);
console.log('server started');

将其保存为一个js文件比如server.js,然后从命令行运行node server或者node server.js,服务器便可启动了,此刻我们可以在浏览器地址栏输入localhost进行访问,也可以输入本机IP127.0.0.1,都不用加端口,因为我们服务器监听的是默认的80端口。当然,如果你机子上面80端口被其他程序占用了,可以选择其他端口比如8080,这样访问的时候需要显示地加上端口号localhost:8080。

Express

首先通过npm进行安装

  • 在我们的项目文件夹下打开命令行(tip: 按住Shift同时右击,可以在右键菜单中找到’从此处打开命令行’选项)
  • 在命令行中输入 npm install express 回车进行安装
  • 然后在server.js中通过require(‘express’)将其引入到项目中进行使用

express是node.js中管理路由响应请求的模块,根据请求的URL返回相应的HTML页面。这里我们使用一个事先写好的静态页面返回给客户端,只需使用express指定要返回的页面的路径即可。如果不用这个包,我们需要将HTML代码与后台JavaScript代码写在一起进行请求的响应,不太方便。

//返回一个简单的HTML内容

server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-Type': 'text/html' //将返回类型由text/plain改为text/html
    });
    res.write('<h1>hello world!</h1>'); //返回HTML标签
    res.end();
});

在存放上一步创建的server.js文件的地方,我们新建一个文件夹名字为www用来存放我们的网页文件,包括图片以及前端的js文件等。假设已经在www文件夹下写好了一个index.html文件(将在下一步介绍,这一步你可以放一个空的HTML文件),则可以通过以下方式使用express将该页面返回到浏览器。可以看到较最开始,我们的服务器代码简洁了不少。

//使用express模块返回静态页面

var express = require('express'), //引入express模块
    app = express(),
    server = require('http').createServer(app);
app.use('/', express.static(__dirname + '/www')); //指定静态HTML文件的位置
server.listen(80);

 

其中有四个按钮,分别是设置字体颜色,发送表情,发送图片和清除记录,将会在下面介绍其实现

 

socket.io

Node.js中使用socket的一个包。使用它可以很方便地建立服务器到客户端的sockets连接,发送事件与接收特定事件。

同样通过npm进行安装 npm install socket.io 。安装后在node_modules文件夹下新生成了一个socket.io文件夹,其中我们可以找到一个socket.io.js文件。将它引入到HTML页面,这样我们就可以在前端使用socket.io与服务器进行通信了。

<script src="/socket.io/socket.io.js"></script>

同时服务器端的server.js里跟使用express一样,也要通过require(‘socket.io’)将其引入到项目中,这样就可以在服务器端使用socket.io了。

使用socket.io,其前后端句法是一致的,即通过socket.emit()来激发一个事件,通过socket.on()来侦听和处理对应事件。这两个事件通过传递的参数进行通信。具体工作模式可以看下面这个示例。

比如我们在index.html里面有如下JavaScript代码(假设你已经在页面放了一个ID为sendBtn的按钮):

<script type="text/javascript">
	var socket=io.connect(),//与服务器进行连接
		button=document.getElementById('sendBtn');
	button.onclick=function(){
		socket.emit('foo', 'hello');//发送一个名为foo的事件,并且传递一个字符串数据‘hello’
	}
</script>

上述代码首先建立与服务器的连接,然后得到一个socket实例。之后如果页面上面一个ID为sendBtn的按钮被点击的话,我们就通过这个socket实例发起一个名为foo的事件,同时传递一个hello字符串信息到服务器。

与此同时,我们需要在服务器端写相应的代码来处理这个foo事件并接收传递来的数据。

为此,我们在server.js中可以这样写:

//服务器及页面响应部分
var express = require('express'),
    app = express(),
    server = require('http').createServer(app),
    io = require('socket.io').listen(server); //引入socket.io模块并绑定到服务器
app.use('/', express.static(__dirname + '/www'));
server.listen(80);

//socket部分
io.on('connection', function(socket) {
    //接收并处理客户端发送的foo事件
    socket.on('foo', function(data) {
        //将消息输出到控制台
        console.log(data);
    })
});

现在Ctrl+C关闭之前启动的服务器,再次输入node server启动服务器运行新代码查看效果,一切正常的话你会在点击了页面的按扭后,在命令行窗口里看到输出的’hello’字符串。

一如之前所说,socket.io在前后端的句法是一致的,所以相反地,从服务器发送事件到客户端,在客户端接收并处理消息也是显而易见的事件了。这里只是简单介绍,具体下面会通过发送聊天消息进一步介绍。

基本页面

有了上面一些基础的了解,下面可以进入聊天程序功能的开发了。

首先我们构建主页面。因为是比较大众化的应用了,界面不用多想,脑海中已经有大致的雏形,它有一个呈现消息的主窗体,还有一个输入消息的文本框,同时需要一个发送消息的按钮,这三个是必备的。

另外就是,这里还准备实现以下四个功能,所以界面上还有设置字体颜色,发送表情,发送图片和清除记录四个按钮。

最后的页面也就是先前截图展示的那们,而代码如下:

www/index.html

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <meta name="author" content="Wayou">
        <meta name="description" content="hichat | a simple chat application built with node.js and websocket">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>hichat</title>
        <link rel="stylesheet" href="styles/main.css">
        <link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
        <link rel="icon" href="favicon.ico" type="image/x-icon">
    </head>
    <body>
        <div class="wrapper">
            <div class="banner">
                <h1>HiChat :)</h1>
                <span id="status"></span>
            </div>
            <div id="historyMsg">
            </div>
            <div class="controls" >
                <div class="items">
                    <input id="colorStyle" type="color" placeHolder='#000' title="font color" />
                    <input id="emoji" type="button" value="emoji" title="emoji" />
                    <label for="sendImage" class="imageLable">
                        <input type="button" value="image"  />
                        <input id="sendImage" type="file" value="image"/>
                    </label>
                    <input id="clearBtn" type="button" value="clear" title="clear screen" />
                </div>
                <textarea id="messageInput" placeHolder="enter to send"></textarea>
                <input id="sendBtn" type="button" value="SEND">
                <div id="emojiWrapper">
                </div>
            </div>
        </div>
        <div id="loginWrapper">
            <p id="info">connecting to server...</p>
            <div id="nickWrapper">
                <input type="text" placeHolder="nickname" id="nicknameInput" />
                <input type="button" value="OK" id="loginBtn" />
            </div>
        </div>
        <script src="/socket.io/socket.io.js"></script>
        <script src="scripts/hichat.js"></script>
    </body>
</html>

 

样式文件 www/styles/main.css

html, body {
    margin: 0;
    background-color: #efefef;
    font-family: sans-serif;
}
.wrapper {
    width: 500px;
    height: 640px;
    padding: 5px;
    margin: 0 auto;
    background-color: #ddd;
}
#loginWrapper {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background-color: rgba(5, 5, 5, .6);
    text-align: center;
    color: #fff;
    display: block;
    padding-top: 200px;
}
#nickWrapper {
    display: none;
}
.banner {
    height: 80px;
    width: 100%;
}
.banner p {
    float: left;
    display: inline-block;
}
.controls {
    height: 100px;
    margin: 5px 0px;
    position: relative;
}
#historyMsg {
    height: 400px;
    background-color: #fff;
    overflow: auto;
    padding: 2px;
}
#historyMsg img {
    max-width: 99%;
}
.timespan {
    color: #ddd;
}
.items {
    height: 30px;
}
#colorStyle {
    width: 50px;
    border: none;
    padding: 0;
}
/*custom the file input*/

.imageLable {
    position: relative;
}
#sendImage {
    position: absolute;
    width: 52px;
    left: 0;
    opacity: 0;
    overflow: hidden;
}
/*end custom file input*/

#messageInput {
    width: 440px;
    max-width: 440px;
    height: 90px;
    max-height: 90px;
}
#sendBtn {
    width: 50px;
    height: 96px;
    float: right;
}
#emojiWrapper {
    display: none;
    width: 500px;
    bottom: 105px;
    position: absolute;
    background-color: #aaa;
    box-shadow: 0 0 10px #555;
}
#emojiWrapper img {
    margin: 2px;
    padding: 2px;
    width: 25px;
    height: 25px;
}
#emojiWrapper img:hover {
    background-color: blue;
}
.emoji{
    display: inline;
}
footer {
    text-align: center;
}

为了让项目有一个良好的目录结构便于管理,这里在www文件夹下又新建了一个styles文件夹存放样式文件main.css,然后新建一个scripts文件夹存放前端需要使用的js文件比如hichat.js(我们前端所有的js代码会放在这个文件中),而我们的服务器js文件server.js位置不变还是放在最外层。

同时再新建一个content文件夹用于存放其他资源比如图片等,其中content文件夹里再建一个emoji文件夹用于存入表情gif图,后面会用到。最后我们项目的目录结构应该是这样的了:

├─node_modules
└─www
    ├─content
    │  └─emoji
    ├─scripts
    └─styles

此刻打开页面你看到的是一个淡黑色的遮罩层,而接下来我们要实现的是用户昵称的输入与服务器登入。这个遮罩层用于显示连接到服务器的状态信息,而当连接完成之后,会出现一个输入框用于昵称输入。

上面HTML代码里已经看到,我们将www/scripts/hichat.js文件已经引入到页面了,下面开始写一些基本的前端js开始实现连接功能。

定义一个全局变量用于我们整个程序的开发HiChat,同时使用window.onload在页面准备好之后实例化HiChat,调用其init方法运行我们的程序。

www/scripts/Hichat.js

window.onload = function() {
    //实例并初始化我们的hichat程序
    var hichat = new HiChat();
    hichat.init();
};

//定义我们的hichat类
var HiChat = function() {
    this.socket = null;
};

//向原型添加业务方法
HiChat.prototype = {
    init: function() {//此方法初始化程序
        var that = this;
        //建立到服务器的socket连接
        this.socket = io.connect();
        //监听socket的connect事件,此事件表示连接已经建立
        this.socket.on('connect', function() {
            //连接到服务器后,显示昵称输入框
            document.getElementById('info').textContent = 'get yourself a nickname :)';
            document.getElementById('nickWrapper').style.display = 'block';
            document.getElementById('nicknameInput').focus();
        });
    }
};

上面的代码定义了整个程序需要使用的类HiChat,之后我们处理消息显示消息等所有业务逻辑均写在这个类里面。

首先定义了一个程序的初始化方法,这里面初始化socket,监听连接事件,一旦连接到服务器,便显示昵称输入框。当用户输入昵称后,便可以在服务器后台接收到然后进行下一步的处理了。

设置昵称

我们要求连接的用户需要首先设置一个昵称,且这个昵称还要唯一,也就是不能与别人同名。一是方便用户区分,二是为了统计在线人数,同时也方便维护一个保存所有用户昵称的数组。

为此在后台server.js中,我们创建一个名叫users的全局数组变量,当一个用户设置好昵称发送到服务器的时候,将昵称压入users数组。同时注意,如果用户断线离开了,也要相应地从users数组中移除以保证数据的正确性。

在前台,输入昵称点击OK提交后,我们需要发起一个设置昵称的事件以便服务器侦听到。将以下代码添加到之前的init方法中。

www/scripts/hichat.js

//昵称设置的确定按钮
document.getElementById('loginBtn').addEventListener('click', function() {
    var nickName = document.getElementById('nicknameInput').value;
    //检查昵称输入框是否为空
    if (nickName.trim().length != 0) {
        //不为空,则发起一个login事件并将输入的昵称发送到服务器
        that.socket.emit('login', nickName);
    } else {
        //否则输入框获得焦点
        document.getElementById('nicknameInput').focus();
    };
}, false);

server.js

//服务器及页面部分
var express = require('express'),
    app = express(),
    server = require('http').createServer(app),
    io = require('socket.io').listen(server),
    users=[];//保存所有在线用户的昵称
app.use('/', express.static(__dirname + '/www'));
server.listen(80);
//socket部分
io.on('connection', function(socket) {
    //昵称设置
    socket.on('login', function(nickname) {
        if (users.indexOf(nickname) > -1) {
            socket.emit('nickExisted');
        } else {
            socket.userIndex = users.length;
            socket.nickname = nickname;
            users.push(nickname);
            socket.emit('loginSuccess');
            io.sockets.emit('system', nickname); //向所有连接到服务器的客户端发送当前登陆用户的昵称 
        };
    });
});

 

需要解释一下的是,在connection事件的回调函数中,socket表示的是当前连接到服务器的那个客户端。所以代码socket.emit(‘foo’)则只有自己收得到这个事件,而socket.broadcast.emit(‘foo’)则表示向除自己外的所有人发送该事件,另外,上面代码中,io表示服务器整个socket连接,所以代码io.sockets.emit(‘foo’)表示所有人都可以收到该事件。

上面代码先判断接收到的昵称是否已经存在在users中,如果存在,则向自己发送一个nickExisted事件,在前端接收到这个事件后我们显示一条信息通知用户。

将下面代码添加到hichat.js的inti方法中。

www/scripts/hichat.js

this.socket.on('nickExisted', function() {
     document.getElementById('info').textContent = '!nickname is taken, choose another pls'; //显示昵称被占用的提示
 });

如果昵称没有被其他用户占用,则将这个昵称压入users数组,同时将其作为一个属性存到当前socket变量中,并且将这个用户在数组中的索引(因为是数组最后一个元素,所以索引就是数组的长度users.length)也作为属性保存到socket中,后面会用到。最后向自己发送一个loginSuccess事件,通知前端登陆成功,前端接收到这个成功消息后将灰色遮罩层移除显示聊天界面。

将下面代码添加到hichat.js的inti方法中。

www/scripts/hichat.js

this.socket.on('loginSuccess', function() {
     document.title = 'hichat | ' + document.getElementById('nicknameInput').value;
     document.getElementById('loginWrapper').style.display = 'none';//隐藏遮罩层显聊天界面
     document.getElementById('messageInput').focus();//让消息输入框获得焦点
 });

在线统计

这里实现显示在线用户数及在聊天主界面中以系统身份显示用户连接离开等信息。

上面server.js中除了loginSuccess事件,后面还有一句代码,通过io.sockets.emit 向所有用户发送了一个system事件,传递了刚登入用户的昵称,所有人接收到这个事件后,会在聊天窗口显示一条系统消息’某某加入了聊天室’。同时考虑到在前端我们无法得知用户是进入还是离开,所以在这个system事件里我们多传递一个数据来表明用户是进入还是离开。

将server.js中login事件更改如下:

server.js

socket.on('login', function(nickname) {
     if (users.indexOf(nickname) > -1) {
         socket.emit('nickExisted');
     } else {
         socket.userIndex = users.length;
         socket.nickname = nickname;
         users.push(nickname);
         socket.emit('loginSuccess');
         io.sockets.emit('system', nickname, users.length, 'login');
     };
 });

较之前,多传递了一个login字符串。

同时再添加一个用户离开的事件,这个可能通过socket.io自带的disconnect事件完成,当一个用户断开连接,disconnect事件就会触发。在这个事件中,做两件事情,一是将用户从users数组中删除,一是发送一个system事件通知所有人’某某离开了聊天室’。

将以下代码添加到server.js中connection的回调函数中。

server.js

//断开连接的事件
socket.on('disconnect', function() {
    //将断开连接的用户从users中删除
    users.splice(socket.userIndex, 1);
    //通知除自己以外的所有人
    socket.broadcast.emit('system', socket.nickname, users.length, 'logout');
});

上面代码通过JavaScript数组的splice方法将当前断开连接的用户从users数组中删除,这里我们看到之前保存的用户索引被使用了。同时发送和用户连接时一样的system事件通知所有人’某某离开了’,为了让前端知道是离开事件,所以发送了一个’logout’字符串。

下面开始前端的实现,也就是接收system事件。

在hichat.js中,将以下代码添加到init方法中。

www/scripts/hichat.js

this.socket.on('system', function(nickName, userCount, type) {
     //判断用户是连接还是离开以显示不同的信息
     var msg = nickName + (type == 'login' ? ' joined' : ' left');
     var p = document.createElement('p');
     p.textContent = msg;
     document.getElementById('historyMsg').appendChild(p);
     //将在线人数显示到页面顶部
     document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online';
 });

现在运行程序,打开多个浏览器标签,然后登陆离开,你就可以看到相应的系统提示消息了。

 

发送消息

用户连接以及断开我们需要显示系统消息,用户还要频繁的发送聊天消息,所以可以考虑将消息显示到页面这个功能单独写一个函数方便我们调用。为此我们向HiChat类中添加一个_displayNewMsg的方法,它接收要显示的消息,消息来自谁,以及一个颜色共三个参数。因为我们想系统消息区别于普通用户的消息,所以增加一个颜色参数。同时这个参数也方便我们之后实现让用户自定义文本颜色做准备。

将以下代码添加到的我的HiChat类当中。

www/scripts/hichat.js

//向原型添加业务方法
HiChat.prototype = {
    init: function() { //此方法初始化程序
        //...
    },
    _displayNewMsg: function(user, msg, color) {
        var container = document.getElementById('historyMsg'),
            msgToDisplay = document.createElement('p'),
            date = new Date().toTimeString().substr(0, 8);
        msgToDisplay.style.color = color || '#000';
        msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg;
        container.appendChild(msgToDisplay);
        container.scrollTop = container.scrollHeight;
    }
};

在_displayNewMsg方法中,我们还向消息添加了一个日期。我们也判断了该方法在调用时有没有传递颜色参数,没有传递颜色的话默认使用#000即黑色。

同时修改我们在system事件中显示系统消息的代码,让它调用这个_displayNewMsg方法。

www/scripts/hichat.js

this.socket.on('system', function(nickName, userCount, type) {
    var msg = nickName + (type == 'login' ? ' joined' : ' left');
    //指定系统消息显示为红色
    that._displayNewMsg('system ', msg, 'red');
    document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online';
});

现在的效果如下:

有了这个显示消息的方法后,下面就开始实现用户之间的聊天功能了。

做法也很简单,如果你掌握了上面所描述的emit发送事件,on接收事件,那么用户聊天消息的发送接收也就轻车熟路了。

首先为页面的发送按钮写一个click事件处理程序,我们通过addEventListner来监听这个click事件,当用户点击发送的时候,先检查输入框是否为空,如果不为空,则向服务器发送postMsg事件,将用户输入的聊天文本发送到服务器,由服务器接收并分发到除自己外的所有用户。

将以下代码添加到hichat.js的inti方法中。

www/scripts/hichat.js

document.getElementById('sendBtn').addEventListener('click', function() {
    var messageInput = document.getElementById('messageInput'),
        msg = messageInput.value;
    messageInput.value = '';
    messageInput.focus();
    if (msg.trim().length != 0) {
        that.socket.emit('postMsg', msg); //把消息发送到服务器
        that._displayNewMsg('me', msg); //把自己的消息显示到自己的窗口中
    };
}, false);

在server.js中添加代码以接收postMsg事件。

server.js

io.on('connection', function(socket) {
    //其他代码。。。

    //接收新消息
    socket.on('postMsg', function(msg) {
        //将消息发送到除自己外的所有用户
        socket.broadcast.emit('newMsg', socket.nickname, msg);
    });
});

然后在客户端接收服务器发送的newMsg事件,并将聊天消息显示到页面。

将以下代码显示添加到hichat.js的init方法中了。

this.socket.on('newMsg', function(user, msg) {
    that._displayNewMsg(user, msg);
});

运行程序,现在可以发送聊天消息了。

发送图片

上面已经实现了基本的聊天功能了,进一步,如果我们还想让用户可以发送图片,那程序便更加完美了。

图片不同于文字,但通过将图片转化为字符串形式后,便可以像发送普通文本消息一样发送图片了,只是在显示的时候将它还原为图片。

在这之前,我们已经将图片按钮在页面放好了,其实是一个文件类型的input,下面只需在它身上做功夫便可。

用户点击图片按钮后,弹出文件选择窗口供用户选择图片。之后我们可以在JavaScript代码中使用FileReader来将图片读取为base64格式的字符串形式进行发送。而base64格式的图片直接可以指定为图片的src,这样就可以将图片用img标签显示在页面了。

为此我们监听图片按钮的change事件,一但用户选择了图片,便显示到自己的屏幕上同时读取为文本发送到服务器。

将以下代码添加到hichat.js的init方法中。

www/scripts/hichat.js

document.getElementById('sendImage').addEventListener('change', function() {
    //检查是否有文件被选中
     if (this.files.length != 0) {
        //获取文件并用FileReader进行读取
         var file = this.files[0],
             reader = new FileReader();
         if (!reader) {
             that._displayNewMsg('system', '!your browser doesn\'t support fileReader', 'red');
             this.value = '';
             return;
         };
         reader.onload = function(e) {
            //读取成功,显示到页面并发送到服务器
             this.value = '';
             that.socket.emit('img', e.target.result);
             that._displayImage('me', e.target.result);
         };
         reader.readAsDataURL(file);
     };
 }, false);

上面图片读取成功后,调用_displayNImage方法将图片显示在自己的屏幕同时向服务器发送了一个img事件,在server.js中,我们通过这个事件来接收并分发图片到每个用户。同时也意味着我们还要在前端写相应的代码来接收。

这个_displayNImage还没有实现,将会在下面介绍。

将以下代码添加到server.js的socket回调函数中。

server.js

//接收用户发来的图片
 socket.on('img', function(imgData) {
    //通过一个newImg事件分发到除自己外的每个用户
     socket.broadcast.emit('newImg', socket.nickname, imgData);
 });

同时向hichat.js的init方法添加以下代码以接收显示图片。

 this.socket.on('newImg', function(user, img) {
     that._displayImage(user, img);
 });

有个问题就是如果图片过大,会破坏整个窗口的布局,或者会出现水平滚动条,所以我们对图片进行样式上的设置让它最多只能以聊天窗口的99%宽度来显示,这样过大的图片就会自己缩小了。

#historyMsg img {
    max-width: 99%;
}

但考虑到缩小后的图片有可能失真,用户看不清,我们需要提供一个方法让用户可以查看原尺寸大小的图片,所以将图片用一个链接进行包裹,当点击图片的时候我们打开一个新的窗口页面,并将图片按原始大小呈现到这个新页面中让用户查看。

所以最后我们实现的_displayNImage方法应该是这样的。

将以下代码添加到hichat.js的HiChat类中。

www/scripts/hichat.js

_displayImage: function(user, imgData, color) {
    var container = document.getElementById('historyMsg'),
        msgToDisplay = document.createElement('p'),
        date = new Date().toTimeString().substr(0, 8);
    msgToDisplay.style.color = color || '#000';
    msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span> <br/>' + '<a href="' + imgData + '" target="_blank"><img src="' + imgData + '"/></a>';
    container.appendChild(msgToDisplay);
    container.scrollTop = container.scrollHeight;
}

再次启动服务器打开程序,我们可以发送图片了。

发送表情

文字总是很难表达出说话时的面部表情的,于是表情就诞生了。

前面已经介绍过如何发送图片了,严格来说,表情也是图片,但它有特殊之处,因为表情可以穿插在文字中一并发送,所以就不能像处理图片那样来处理表情了。

根据以往的经验,其他聊天程序是把表情转为符号,比如我想发笑脸,并且规定’:)’这个符号代码笑脸表情,然后数据传输过程中其实转输的是一个冒号加右括号的组合,当每个客户端接收到消息后,从文字当中将这些表情符号提取出来,再用gif图片替换,这样呈现到页面我们就 看到了表情加文字的混排了。

你好,王尼玛[emoji:23]——>你好,王尼玛

上面形象地展示了我们程序中表情的使用,可以看出我规定了一种格式来代表表情,[emoji:xx],中括号括起来然后’emoji’加个冒号,后面跟一个数字,这个数字表示某个gif图片的编号。程序中,如果我们点击表情按扭,然后呈现所有可用的表情图片,当用户选择一个表情后,生成对应的代码插入到当前待发送的文字消息中。发出去后,每个人接收到的也是代码形式的消息,只是在将消息显示到页面前,我们将表情代码提取出来,获取图片编号,然后用相应的图片替换。

首先得将所有可用的表情图片显示到一个小窗口,这个窗口会在点击了表情按钮后显示如下图,在HTML代码中已经添加好了这个窗口了,下面只需实现代码部分。

我们使用兔斯基作为我们聊天程序的表情包。可以看到,有很多张gif图,如果手动编写的话,要花一些功夫,不断地写<img src=’xx.gif’/>,所以考虑将这个工作交给代码来自动完成,写一个方法来初始化所有表情。

为此将以下代码添加到HiChat类中,并在init方法中调用这个方法。

www/scripts/hichat.js

_initialEmoji: function() {
    var emojiContainer = document.getElementById('emojiWrapper'),
        docFragment = document.createDocumentFragment();
    for (var i = 69; i > 0; i--) {
        var emojiItem = document.createElement('img');
        emojiItem.src = '../content/emoji/' + i + '.gif';
        emojiItem.title = i;
        docFragment.appendChild(emojiItem);
    };
    emojiContainer.appendChild(docFragment);
}

同时将以下代码添加到hichat.js的init方法中。

www/scripts/hichat.js

this._initialEmoji();
 document.getElementById('emoji').addEventListener('click', function(e) {
     var emojiwrapper = document.getElementById('emojiWrapper');
     emojiwrapper.style.display = 'block';
     e.stopPropagation();
 }, false);
 document.body.addEventListener('click', function(e) {
     var emojiwrapper = document.getElementById('emojiWrapper');
     if (e.target != emojiwrapper) {
         emojiwrapper.style.display = 'none';
     };
 });

上面向页面添加了两个单击事件,一是表情按钮单击显示表情窗口,二是点击页面其他地方关闭表情窗口。

现在要做的就是,具体到某个表情被选中后,需要获取被选中的表情,然后转换为相应的表情代码插入到消息框中。

为此我们再写一个这些图片的click事件处理程序。将以下代码添加到hichat.js的inti方法中。

www/scripts/hichat.js

document.getElementById('emojiWrapper').addEventListener('click', function(e) {
    //获取被点击的表情
    var target = e.target;
    if (target.nodeName.toLowerCase() == 'img') {
        var messageInput = document.getElementById('messageInput');
        messageInput.focus();
        messageInput.value = messageInput.value + '[emoji:' + target.title + ']';
    };
}, false);

现在表情选中后,消息输入框中可以得到相应的代码了。

之后的发送也普通消息发送没区别,因为之前已经实现了文本消息的发送了,所以这里不用再实现什么,只是需要更改一下之前我们用来显示消息的代码,首先判断消息文本中是否含有表情符号,如果有,则转换为图片,最后再显示到页面。

为此我们写一个方法接收文本消息为参数,用正则搜索其中的表情符号,将其替换为img标签,最后返回处理好的文本消息。

将以下代码添加到HiChat类中。

www/scripts/hichat.js

_showEmoji: function(msg) {
    var match, result = msg,
        reg = /\[emoji:\d+\]/g,
        emojiIndex,
        totalEmojiNum = document.getElementById('emojiWrapper').children.length;
    while (match = reg.exec(msg)) {
        emojiIndex = match[0].slice(7, -1);
        if (emojiIndex > totalEmojiNum) {
            result = result.replace(match[0], '[X]');
        } else {
            result = result.replace(match[0], '<img class="emoji" src="../content/emoji/' + emojiIndex + '.gif" />');
        };
    };
    return result;
}

现在去修改之前我们显示消息的_displayNewMsg方法,让它在显示消息之前调用这个_showEmoji方法。

_displayNewMsg: function(user, msg, color) {
     var container = document.getElementById('historyMsg'),
         msgToDisplay = document.createElement('p'),
         date = new Date().toTimeString().substr(0, 8),
         //将消息中的表情转换为图片
         msg = this._showEmoji(msg);
     msgToDisplay.style.color = color || '#000';
     msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg;
     container.appendChild(msgToDisplay);
     container.scrollTop = container.scrollHeight;
 }

下面是实现后的效果:

主要功能已经完成得差不多了,为了让程序更加人性与美观,可以加入一个修改文字颜色的功能,以及键盘快捷键操作的支持,这也是一般聊天程序都有的功能,回车即可以发送消息。

文字颜色

万幸,HTML5新增了一个专门用于颜色选取的input标签,并且Chrome对它的支持非常之赞,直接弹出系统的颜色拾取窗口。

IE及FF中均是一个普通的文本框,不过不影响使用,只是用户只能通过输入具体的颜色值来进行颜色设置,没有Chrome里面那么方便也直观。

之前我们的_displayNewMsg方法可以接收一个color参数,现在要做的就是每次发送消息到服务器的时候,多加一个color参数就可以了,同时,在显示消息时调用_displayNewMsg的时候将这个color传递过去。

下面是修改hichat.js中消息发送按钮代码的示例:

document.getElementById('sendBtn').addEventListener('click', function() {
    var messageInput = document.getElementById('messageInput'),
        msg = messageInput.value,
        //获取颜色值
        color = document.getElementById('colorStyle').value;
    messageInput.value = '';
    messageInput.focus();
    if (msg.trim().length != 0) {
        //显示和发送时带上颜色值参数
        that.socket.emit('postMsg', msg, color);
        that._displayNewMsg('me', msg, color);
    };
}, false);

同时修改hichat.js中接收消息的代码,让它接收颜色值

this.socket.on('newMsg', function(user, msg, color) {
     that._displayNewMsg(user, msg, color);
 });

这只是展示了发送按钮的修改,改动非常小,只是每次消息发送时获取一下颜色值,同时emit事件到服务器的时候也带上这个颜色值,这样前端在显示时就可以根据这个颜色值为每个不两只用户显示他们自己设置的颜色了。剩下的就是按相同的做法把发送图片时也加上颜色,这里省略。

最后效果:

按键操作

将以下代码添加到hichat.js的inti方法中,这样在输入昵称后,按回车键就可以登陆,进入聊天界面后,回车键可以发送消息。

document.getElementById('nicknameInput').addEventListener('keyup', function(e) {
      if (e.keyCode == 13) {
          var nickName = document.getElementById('nicknameInput').value;
          if (nickName.trim().length != 0) {
              that.socket.emit('login', nickName);
          };
      };
  }, false);
  document.getElementById('messageInput').addEventListener('keyup', function(e) {
      var messageInput = document.getElementById('messageInput'),
          msg = messageInput.value,
          color = document.getElementById('colorStyle').value;
      if (e.keyCode == 13 && msg.trim().length != 0) {
          messageInput.value = '';
          that.socket.emit('postMsg', msg, color);
          that._displayNewMsg('me', msg, color);
      };
  }, false);

 

部署上线

最后一步,当然就是将我们的辛勤结晶部署到实际的站点。这应该是最激动人心也是如释重负的一刻。但在这之前,让我们先添加一个node.js程序通用的package.json文件,该文件里面可以指定我们的程序使用了哪些模块,这样别人在获取到代码后,只需通过npm install命令就可以自己下载安装程序中需要的模块了,而不用我们把模块随源码一起发布。

添加package.json文件

将以下代码保存为package.json保存到跟server.js相同的位置。

{
    "name": "hichat",
    "description": "a realtime chat web application",
    "version": "0.4.0",
    "main": "server.js",
    "dependencies": {
        "express": "3.4.x",
        "socket.io": "0.9.x"
    },
    "engines": {
        "node": "0.10.x",
        "npm": "1.2.x"
    }
}

 

云服务选择与部署

首先我们得选择一个支持Node.js同时又支持web socket协议的云服务器。因为只是用于测试,空间内存限制什么的都无所谓,只要免费就行。Node.js在GitHub的Wiki页面上列出了众多支持Node.js环境的云服务器,选来选去满足条件的只有heroku

如果你之前到heroku部署过相关Node程序的话,一定知道其麻烦之处,并且出错了非常不容易调试。不过当我在写这篇博客的时候,我发现了一个利器codeship,将它与你的github绑定之后,你每次提交了新的代码它会自动部署到heroku上面。什么都不用做!

代码更新,环境设置,编译部署,全部自动搞定,并且提供了详细的log信息及各步骤的状态信息。使用方法也是很简单,注册后按提示,两三步搞定,鉴于本文已经够长了,应该创纪录了,这里就不多说了。

已知问题

部署测试后,发现一些本地未出现的问题,主要有以下几点:

  • 首次连接过慢,有时会失败出现503错误,这个查了下heroku文档,官方表示程序首次接入时受资源限制确实会很慢的,这就是用免费套餐注定被鄙视的结果,不过用于线上测试这点还是能够忍受的
  • 发送表情时,Chrome会向服务器重新请求已经下载到客户端的gif图片,而IE和FF都无此问题,导致在Chrome里表情会有延迟,进而出现聊天主信息窗口滚动也不及时的现象
  • 用户未活动一定时间后会与服务器失连,socket自动断开,不知道是socket.io内部机制还是又是heroku捣鬼

总结展望

经过上面一番折腾,一个基本的聊天程序便打造完毕。可以完善的地方还有许多,比如利用CSS3的动画,完全可以制作出窗口抖动功能的。听起来很不错是吧。同时利用HTML5的Audio API,要实现类似微信的语音消息也不是不可能的,够震撼吧。甚至还有Geolocaiton API我们就可以联想到实现同城功能,利用Webcam可以打造出视频对聊,但这方面WebRTC已经做得很出色了。

PS:做程序员之前有两个想法,一是写个播放器,一是写个聊天程序,现在圆满了。

REFERENCE

  1. HOW TO SEND IMAGES THROUGH WEB SOCKETS WITH NODE.JS AND SOCKET.IO
  2. Simple Chat – Node.js + WebSockets
  3. Hosting compatible with Node
  4. What is a good tool to export a directory structure
  5. heroku
  6. codeship

Feel free to repost but keep the link to this page please!