下载中心 - 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阅读(1076)

来源: 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!

nodejs 搭建IM即时通讯平台 - 无线时代技术 - 博客频道 - CSDN.NET

mikel阅读(1120)

node服务器安装说明 1、访问http://nodejs.org/download/,下载对应平台的程序。 (Windows系统可以用“node”目录下的文件直接安装) 安装express,mySQL,socket.io 2、MYSQL数据表CREATE TABLE IF NOT EXISTS shopnc_chat_msg ( m_id int(10) unsign

来源: nodejs 搭建IM即时通讯平台 – 无线时代技术 – 博客频道 – CSDN.NET

node服务器安装说明
1、访问http://nodejs.org/download/,下载对应平台的程序。
(Windows系统可以用“node”目录下的文件直接安装)
安装express,MySQL,socket.io
2、mysql数据表

CREATE TABLE IF NOT EXISTS shopnc_chat_msg (
m_id int(10) unsigned NOT NULL auto_increment COMMENT ‘记录ID’,
f_id int(10) unsigned NOT NULL COMMENT ‘会员ID’,
f_name varchar(50) NOT NULL COMMENT ‘会员名’,
f_ip varchar(15) NOT NULL COMMENT ‘发自IP’,
t_id int(10) unsigned NOT NULL COMMENT ‘接收会员ID’,
t_name varchar(50) NOT NULL COMMENT ‘接收会员名’,
t_msg varchar(300) default NULL COMMENT ‘消息内容’,
r_state tinyint(1) unsigned default ‘2’ COMMENT ‘状态:1为已读,2为未读,默认为2’,
add_time int(10) unsigned default ‘0’ COMMENT ‘添加时间’,
PRIMARY KEY (m_id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT=’消息表’;

CREATE TABLE IF NOT EXISTS shopnc_chat_log (
m_id int(10) unsigned NOT NULL auto_increment COMMENT ‘记录ID’,
f_id int(10) unsigned NOT NULL COMMENT ‘会员ID’,
f_name varchar(50) NOT NULL COMMENT ‘会员名’,
f_ip varchar(15) NOT NULL COMMENT ‘发自IP’,
t_id int(10) unsigned NOT NULL COMMENT ‘接收会员ID’,
t_name varchar(50) NOT NULL COMMENT ‘接收会员名’,
t_msg varchar(300) default NULL COMMENT ‘消息内容’,
add_time int(10) unsigned default ‘0’ COMMENT ‘添加时间’,
PRIMARY KEY (m_id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT=’消息记录表’;

3、配置环境

把这三个文件放到安装nodejs目录下,运行

MagicStarter.exe下载MagicStarter.exe

添加程序,路经为

D:\Program Files\nodejs\im.bat

根据自己安装nodejs路经修改

确定-开始服务即可。

MagicStarter.exe可以关掉,不影响。

4、根据实际运行环境修改config.js中相关参数。
5、在命令行模式下用node chat启动程序,看到如下信息表示成功。
info – socket.io started
mySQL connected

 

 

connecting to server…

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

(function(i,s,o,g,r,a,m){i[‘GoogleAnalyticsObject’]=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,’script’,’//www.google-analytics.com/analytics.js’,’ga’);

ga(‘create’, ‘UA-46794744-7’, ”);
ga(‘send’, ‘pageview’);

 

运行如图
这里写图片描述

Petya勒索病毒疫苗出现,分分钟让电脑对病毒免疫

mikel阅读(843)

继wannacry之后,Petya勒索软件攻击再次席卷全球,对欧洲、俄罗斯等多国政府、银行、电力系统、通讯系统、企业以及机场造成了不同程度的影响。

 

研究发现,Petya 会锁定磁盘的 MFT 和 MBR 区,导致计算机无法启动。除非受害者支付赎金解锁,否则无法恢复他们的系统。但在此前的wannacry勒索软件事件发生的时候,阿里聚安全就建议大家不要支付赎金,一方面支付赎金后不一定能找回数据,其次这些赎金会进一步刺激攻击者挖掘漏洞,并升级攻击手段。

 

好消息是Cybereason安全研究员Amit Serper已经找到了一种方法来防止Petya (notpetya / sortapetya / petna)勒索软件来感染电脑,这种方法简直一劳永逸!

 

研究员蜂拥寻找killswitch机制

在Petya 爆发的第一时间,国内外安全研究人员们蜂拥而上对其进行分析,一开始他们认为Petya无非是新瓶装旧酒,和其他勒索软件相似。但在进一步研究过程中,他们发现这是一种全新的勒索蠕虫病毒。因此它的名字从Petya逐渐变为Notpetya,Petna,以及SortaPetya。

 

研究员分析勒索软件的运作机制后,发现NotPetya会搜索本地文件,如果文件已经在磁盘上存在,那么勒索软件就会退出加密。这意味着,受害者只需要在自己电脑上创建这个文件,并将其设置为只读,就可以成功封锁Notpetya勒索软件的执行。

 

这种方法不能阻止勒索病毒的运行,因为它就像注射疫苗让设备免疫病毒攻击,而不是杀死病毒。它可以让受害者一劳永逸,不再受到勒索软件的感染。

 

这一发现就得到了 PT Security、TrustedSec、以及 Emsisoft 等安全研究机构的证实。

 

如何注射Notpetya//Petna/Petya疫苗

批量创建方法,适用于运维管理

在C盘的Windows文件夹下创建一个perfc文件,并设置为只读。对于那些想要快速创建文件的人,Lawrence Abrams演示了批量创建文件的步骤。请注意,批量创建文件的同时还需要创建perfc.dat和perfc.dll两个文件。

 

批量文件处理工具下载地址:https://download.bleepingcomputer.com/bats/nopetyavac.bat

 

手动创建方法,适用于个人

1、首先,在 Windows 资源管理器中去除“隐藏已知文件类型的扩展名”的勾选(文件夹选项 -> 查看 ->隐藏已知文件类型的扩展名)

 

 

2、打开 C:Windows文件夹,选中记事本(notepad.exe)程序,复制并粘贴它的副本。粘贴文件时,可能需要赋予管理员权限。

 

 

3、将 notepad(副本).exe重命名为perfc,记得扩展名也去掉。

 

 

4、右键选中该文件,点击属性,在弹出的文件属性对话框中,将其设置为“只读”

 

 

创建好文件后,建议同时创建在C盘的Windows文件夹下创建perfc.dat和perfc.dll。

 

此方法来源于bleepingcomputer,原文地址:https://www.bleepingcomputer.com/news/security/vaccine-not-killswitch-found-for-petya-notpetya-ransomware-outbreak/

mongo数据库表怎么增加字段_百度知道

mikel阅读(1121)

来源: mongo数据库表怎么增加字段_百度知道

mongodb常用操作语句

  1、现有表以及数据添加字段
  db.tbGoodsConsultant.update({}, {$set:{nFlagState:0}}, false, true);
  2、给表字段添加索引
  db.tbGoodsConsultant.ensureIndex({nFlagState:1});
  3、增加数据
  > db.food.save({"name":"jack","address":{"city":"Shanghai","post":021},"phone":[138,139]});
  > db.food.save({"uid":"","AL":['','']});
  4、删除表、数据库
  > db.users.drop();
  > db.dropDatabase();
  5、创建索引、数字1表示升序 -1 表示降序
  > db.user.ensureIndex({"lId":1,"name":-1});
  > db.system.indexes.find();
  6、删除索引
  > db.mycoll.dropIndex(name)
  7、去掉重复数据
  > db.user.distinct('name');
  8、排序sort 1:ASC -1:DESC
  >db.user.find().sort({"age":1});
  9、查询name中包含mongo的数据 %y%
  > db.user.find({name:/y/});
  10、查询name中以d开头的 like 'd%'
  > db.user.find({name:/^d/});
  11、查询指定列name、age数据(name也可以用true||false,true和name:1等同)
  > db.user.find({},{name:1,age:1});
  12、查询2条以后的数据
  > db.user.find().skip(2);
  13、查询在2-10之间的数据
  > db.user.find().limit(10).skip(2);

chrome谷歌浏览器-DevTool开发者工具-详细总结 - Nirvana_zsy - 博客园

mikel阅读(1221)

来源: chrome谷歌浏览器-DevTool开发者工具-详细总结 – Nirvana_zsy – 博客园

chrome的开发者工具可以说是十分强大了,是web开发者的一大利器,作为我个人而言平时用到的几率很大,相信大家也很常见,但是不要仅仅停留在点选元素看看样式的层面上哦,跟着我的总结一起学习实践一下吧(能帮到你的话随便copy好了,开源时代赛高,感谢我就给我留个评论吧。但是码字就码了一两个小时,中午的午休都没了还啪啪啪的敲键盘打扰同事休息,一定要好好学啊)。

 

目录:


 

一、概述

1.官方文档

2.打开方法:

3.前言:

二、九个模块:

1.设备模式Device Mode

2.元素面板Elements

3.控制台面板Console

4.源代码面板Sources

5.网络面板Network

6.性能面板Performance  以前的版本叫Timeline 

7.内存面板Memory  以前的版本叫分析面板 

8.应用面板Application 以前版本叫资源面板 

9.安全面板Security 

三、 注 

 


 

一、概述

1.官方文档

https://developers.google.com/web/tools/chrome-devtools/ (需科学上网)

 

2.打开方法:

*在Chrome菜单中选择 更多工具 > 开发者工具

*在页面元素上右键点击,选择 “检查”

*使用 快捷键 Ctrl+Shift+I (Windows) 或 Cmd+Opt+I (Mac)

 

3.前言:

chrome的开发者工具可以说是十分强大了,是web开发者的一大利器,作为我个人而言平时用到的几率很大,相信大家也很常见,但是不要仅仅停留在点选元素看看样式的层面上哦,跟着我的总结一起学习实践一下吧(能帮到你的话随便copy好了,开源时代赛高,感谢我就给我留个评论吧。但是码字就码了一两个小时,中午的午休都没了还啪啪啪的敲键盘打扰同事休息,一定要好好学啊)。

 

二、九个模块:

 

1.设备模式Device Mode

支持响应式:可以自己调节页面大小;

 

可以按照设备选择用户代理/设备,也可以在devtools setting中增加自定义设备,模仿设备触摸、滚动事件,支持横竖屏,支持选择是否显示设备外框;

 

菜单中可以开启媒体查询(查看css中@media设置的各个查询);

 

可调节DPR(设备像素比,逻辑像素/物理像素,带Retina屏的设备像素密度高,DPR大),在响应式中可以设置该值,在选择了特定设备时不可选,因为特定的设备的DPR值固定;

 

“throttling”可以模拟不同的网络环境,可以限制网速;

 

能模拟替换地理位置,模拟加速度计(设备方向);

2.元素面板Elements

 

能在元素面板查看/选择当前页的DOM树,也可以在页面上点选;

选中节点后会在元素面板左边显示元素源代码所在位置,右边会显示节点的各级样式,盒模型(Computed),事件,断点,属性;

DOM代码和样式可以实时修改调试(修改后再刷新页面,修改就失效了,可以选择保存修改,下面sources面板会提到);

可以在每行代码前面右键添加断点,这点很实用。断点有三种类型:属性改变,子节点改变,节点删除;

可以右键节点选择节点的状态:active :hover :visited :focus,当你无法模拟这些状态时,这个功能就很实用;

3.控制台面板Console

在控制台能显示浏览器的消息,其中消息分为info,verbose(长消息),error,warning四个等级;

相同的消息是默认堆叠的,可以设置成按时间戳(timestamps)显示;

可以选择是否保留历史记录Preverse Log,是否显示网络消息和XHR请求;

可以用filter过滤器筛选信息;

可以选择执行环境:top(默认)、 iframe等;

可以在控制台执行代码段,chrome浏览器内置了一些函数可以使用,有意思的是你可以直接在控制台里定义修改它的函数再使用;

4.源代码面板Sources

 

在这里可以看到一些源文件;

类似元素面板中设置断点,这里也可以在每一行代码前设置断点,利用这些断点使代码运行到适当的时候或位置停下来,以便查看特定时刻的变量值、调用堆栈、样式等;

 

不过这里的断点设置比前面的更强,可以自定义断点条件,运行到这行代码表达式为true就会停下来;

中间部分左下角有一个{}大括号图标,可以将压缩的代码格式化(这个很实用啊666);

右边部分也是关于断点的,可以在这里查看所有的断点,选择一系列事件断点,比如动画,键盘,Load事件,XHR事件等等;

5.网络面板Network

 

网络面板分为上下五个窗格。

第一行是control窗格,窗格中的选项用于控制network面板的外观和具体功能;

功能包括录制屏幕,录制后将会以类似“ppt”的形式将录制的每一帧显示出来(我平时调试动画会用到,很实用),可以拉动时间轴;

可以选择打开filter过滤器,控制在Request Table窗格(第四个) 中显示哪些下载资源,支持正则表达式过滤,支持选择不同类型资源;

值得一提的是“Disable cache”这个选项,用于控制是否支持缓存(当你在服务器多次更改css,js文件然后需要在浏览器查看更改效果的时候,你可以打开这个选项,以防浏览器使用的旧的缓存的文件,那样就看不到更改效果了);

类似设备模式中的throttling,这里也可以模拟网络环境;

第三个“Overview”窗格,这里面的图标显示了资源检索时间的时间线(资源多的话像瀑布一样),你可以点选时间线的一段时间查看;

第四个“Request Table”窗格就列出了检索的每一个资源,以及他们的加载时间,资源类型,服务器返回状态码(比如200),大小,鼠标移到右边的彩色条上就能看的详细的瀑布图,更厉害的是右键资源,有很多操作,比如复制资源请求/响应的报头、报文以及URL(报头报文不知道的了解一下计算机网络里面的http协议);

6.性能面板Performance以前的版本叫Timeline

在这个面板你可以录制一段操作,然后查看录制期间的一些页面性能信息,录制前有几个选项可选,包括网络环境模拟,CPU速度模拟,是否开启录制期间截屏;

性能面板包含四个窗格:

 

第一个是控制窗格,包含一些录制选项:CPU速度模拟,网络环境模拟,录制期间是否截图,是否开启js记录等等,录制要注意几点:

  *尽可能保持记录简短。简短的记录通常会让分析更容易。

  *避免不必要的操作。避免与您想要记录和分析的活动无关联的操作(鼠标点击、网络加载,等等)。例如,如果您想要记录点击 Login 按钮后发生的事件,请不要滚动页面、加载图像,等等。

  *停用浏览器缓存。记录网络操作时,最好从 DevTools 的 Settings 面板或 Network conditions 抽屉式导航栏停用浏览器的缓存。

  *停用扩展程序。Chrome 扩展程序会给应用的 Timeline 记录增加不相关的噪声。 以隐身模式打开 Chrome 窗口或者创建新的 Chrome 用户个人资料,确保您的环境中没有扩展程序。

第二窗格是Overview,是录制页面性能的汇总,包含以下三个图表:

FPS。每秒帧数。绿色竖线越高,FPS 越高。 FPS 图表上的红色块表示长时间帧,很可能会出现卡顿。如果录制开启了截屏,当鼠标悬停在overview窗格上时,会显示出每一帧的图片。

CPU。 CPU 资源。此面积图指示消耗 CPU 资源的事件类型。

NET。每条彩色横杠表示一种资源。横杠越长,检索资源所需的时间越长。 每个横杠的浅色部分表示等待时间(从请求资源到第一个字节下载完成的时间)。

深色部分表示传输时间(下载第一个和最后一个字节之间的时间)。

横杠按照以下方式进行彩色编码:

HTML 文件为蓝色。

脚本为黄色。

样式表为紫色。

媒体文件为绿色。

其他资源为灰色。

 

 

另外,按 Cmd+F (Mac) 或 Ctrl+F (Windows / Linux) 可以打开一个查找工具栏。键入想要检查的事件类型的名称,如 Event。

搜索工具栏仅适用于当前选定的时间范围。选定时间范围以外的任何事件都不会包含在结果中。利用上下箭头,可以按照时间顺序在结果中移动,第一个结果表示选定时间范围内最早的事件,最后一个结果表示最后的事件。每次按向上或向下箭头会选择一个新事件,可以在 Details 窗格中查看其详细信息。按向上和向下箭头等同于在火焰图中点击事件。

第三个窗格是火焰图。 CPU 堆叠追踪的可视化。

可以在火焰图上看到一到三条垂直的虚线。蓝线代表 DOMContentLoaded 事件。 绿线代表首次绘制的时间。 红线代表 load 事件。

开始记录前,不要开启Disable JS samples复选框,以便在时间线记录中捕捉 JavaScript 堆栈。 启用 JS 分析器后,火焰图会显示调用的每个 JavaScript 函数。

火焰图可以用鼠标滚轮调整大小;

 

 

第四个是Details窗格。选择事件后,此窗格会显示与该事件有关的更多信息。 未选择事件时,此窗格会显示选定时间范围的相关信息。

7.内存面板Memory以前的版本叫分析面板

在此面板录制,可以使用CPU 分析器识别开销大的js函数

CPU 分析器准确地记录调用了哪些函数和每个函数花费的时间。也可以将配置文件可视化为火焰图。(执行js比较卡的时候可以用这个工具来查找原因);

火焰图分为以下两部分:

概览:整个记录的鸟瞰图。 条的高度与调用堆栈的深度相对应。 所以,栏越高,调用堆栈越深。

调用堆栈:这里可以详细深入地查看记录过程中调用的函数。 横轴是时间,纵轴是调用堆栈。 堆栈由上而下组织。所以,上面的函数调用它下面的函数,以此类推。

 

 

高调用堆栈不一定很重要,只是表示调用了大量的函数。但宽条表示调用需要很长时间完成。 这些需要优化。

鼠标悬停在上面可以看到函数详情。

!此外,除了用内存面板查看函数的内存堆栈情况外,还可以使用 Chrome 任务管理器作为内存问题调查的起点。

任务管理器是一个实时监视器,可以告诉您页面当前正在使用的内存量。

按 Shift+Esc 或者转到 Chrome 主菜单并选择 More tools > Task manager,打开任务管理器。

右键点击任务管理器的表格标题并启用 JavaScript memory

下面两列是与页面的内存使用有关的不同信息:

Memory 列表示原生内存。DOM 节点存储在原生内存中。 如果此值正在增大,则说明正在创建 DOM 节点。

JavaScript Memory 列表示 JS 堆。此列包含两个值。 您感兴趣的值是实时数字(括号中的数字)。 实时数字表示您的页面上的可到达对象正在使用的内存量。 如果此数字在增大,要么是正在创建新对象,要么是现有对象正在增长。

8.应用面板Application以前版本叫资源面板

查看和修改Local Storage与Session Storage:

 

可以从 Local Storage 窗格和Session Storage窗格中检查、修改和删除键值对(KVP);

使用 IndexedDB 窗格可以检查、修改和删除 IndexedDB 数据。

展开 IndexedDB 窗格时,IndexedDB 下的第一个级别是数据库。 如果存在多个活动的数据库,会看到多个条目。 在下面的屏幕截图中,页面只有一个活动的数据库。

点击数据库的名称可以查看该数据库的安全源、名称和版本。

展开数据库可以查看其键值对 (KVP)。

 

使用 Start from key 文本字段旁的箭头按钮可以在 KVP 的页面之间移动。

展开值并双击可以编辑该值。在您添加、修改或删除值时,这些更改不会实时更新。

 

在 Start from key 文本字段中输入键可以过滤出值小于该值的所有键。

 

在添加、修改或删除值时,这些更改不会实时更新。 点击 refresh 按钮 () 可以更新数据库。

点击 Clear Object Store 按钮 () 可以删除数据库中的所有数据。 从 Clear storage 窗格中,点击一次按钮注销服务工作线程并移除其他存储与缓存也可以实现此目标。

使用 Web SQL 窗格可以查询和修改 Web SQL 数据库:

点击数据库名称可以打开该数据库的控制台。从这里,可以对数据库执行语句。

 

点击数据库表可以查看该表的数据。

无法从这里更新值,但是可以通过数据库控制台(参见上文)更新。

点击列标题可以按该列排序表格。

在 Visibile columns 文本字段中输入一个由空格分隔或逗号分隔的列名称列表可以仅显示列表中包含的列。

使用 Application Cache 窗格可以检查通过 Application Cache API (HTML5中新加的API)创建的资源和规则:

 

每一行表示一个资源。

Type 列的值为以下值之一:

Master。资源上指示此缓存为其主文件的 manifest 属性。

Explicit。此资源在清单中明确列出。

Network。指定此资源的清单必须来自网络。

Fallback。Resource 列中的网址作为另一个网址(未在 DevTools 中显示)的回退网址形式列出。

表格底部拥有指示网络连接和应用缓存状态的状态图标。 应用缓存可能拥有以下状态:

IDLE。缓存没有新更改。

CHECKING。正在提取清单并检查有无更新。

DOWNLOADING。正在将资源添加到缓存中。

UPDATEREADY。存在新版本的缓存。

OBSOLETE。正在删除缓存

清除服务工作线程、存储、数据库和缓存:

有时,只需要擦除给定源的所有数据。利用 Application 面板上的 Clear Storage 窗格,可以选择性地注销服务工作线程、存储和缓存。要清除数据,只需启用想要擦除的组件旁的复选框,然后点击 Clear site data。操作将擦除 Clear storage 标签下所列源的所有数据。

 

9.安全面板Security

使用 Security Overview 可以一目了然的查看当前页面是否安全。

点击 View certificate 检查各个源以查看连接和证书详情(安全源)或找出具体哪些请求未受保护(非安全源)。

Security 面板可以区分两种非安全页面。

如果请求的页面通过 HTTP 提供,则主源会被标记为不安全。

如果请求的页面通过 HTTPS 检索,但页面会继续使用 HTTP 检索其他源的内容,然后页面仍然会被标记为不安全。这称为混合内容页面。 混合内容页面仅受部分保护,因为 HTTP 内容可以被嗅探器获取到且易受到中间人攻击

使用左侧面板可以检查各个安全或非安全源。点击安全源查看该源的连接和证书详情。

三、

Author:nirvana-zsy

Time:2016.6.26

写在最前面 - 每天5分钟玩转容器技术(1) - CloudMan - 博客园

mikel阅读(1160)

来源: 写在最前面 – 每天5分钟玩转容器技术(1) – CloudMan – 博客园

《每天5分钟玩转容器技术》是一个有关容器技术的教程,有下面两个特点:

  1. 系统讲解当前最流行的容器技术。
    从容器的整个生态环境到各个具体的技术,从整体到细节逐一讨论。
  2. 重实践并兼顾理论。
    从实际操作的角度带领大家学习容器技术。

为什么要写这个?

简单回答是:容器技术非常热门,但门槛高

容器技术是继大数据和云计算之后又一炙手可热的技术,而且未来相当一段时间内都会非常流行。

对 IT 行业来说,这是一项非常有价值的技术。而对 IT 从业者来说,掌握容器技术是市场的需要,也是提升自我价值的重要途径。

拿我自己的工作经历来说,毕业后的头几年是做 J2EE 应用开发。后来到一家大型IT公司,公司的产品从中间件到操作系统,从服务器到存储,从虚拟化到云计算都有涉及。

我所在的部门是专门做 IT 基础设施实施服务的,最开始是做传统的 IT 项目,包括服务器配置,双机 HA 等。随着虚拟化技术成熟,工作上也开始涉及各种虚拟化技术的规划和实施,包括 VMWare,KVM,PowerVM等。后来云计算兴起,在公司业务和个人兴趣的驱动下,开始学习和实践 OpenStack,在这个过程中写了《每天5分钟玩转OpenStack》教程并得到大家的认可。

现在以 Docker 为代表的容器技术来了,而且关注度越来越高,这一点可以从 google trend 中 Docker 的搜索上升趋势(蓝色曲线)中清楚看到。

每一轮新技术的兴起,无论对公司还是个人既是机会也是挑战。

我个人的看法是:如果某项新技术未来将成为主流,就应该及早尽快掌握。 因为:

  1. 新技术意味着新的市场和新的需求。
    初期掌握这种技术的人不会很多,而市场需求会越来越大,因而会形成供不应求的卖方市场,物以稀为贵,这对技术人员将是一个难得的价值提升机会。
  2. 学习新技术需要时间和精力,早起步早成材。

机会讲过了,咱们再来看看挑战。

新技术往往意味着技术上的突破和创新,会有不少新的概念和方法。
而且从大数据,云计算和容器技术来看,这些新技术都是平台级别,覆盖的技术范围非常广,包括了计算、网络、存储、高可用、监控、安全等多个方面,要掌握这些新技术对 IT 老兵尚有不小难道,更别说新人了。

由于对技术一直保持着很高的热诚和执着,在掌握了 OpenStack 相关 IaaS 技术后,我便开始调研 PaaS 技术栈。正好这时 Docker 也越来越流行,自然而然便开始了容器相关技术的学习研究和实践。

学习容器技术的过程可以说是惊喜不断,经常惊叹于容器理念的先进和容器生态环境的完整和强大。很多传统软件开发和运维中的难题在容器世界里都能轻松解决,也渐渐理解了容器为何如此受到青睐。

不夸张的说,容器为我打开了一扇通往另一个软件世界的大门,让我沉浸其中,激动不已。高兴之余,我也迫不及待地想把我所看到所学到和所想到的有关容器的知识介绍给更多的人,让更多的 IT 工程师能够从容器技术中受益。

我希望这个教程也能为大家打开这扇门,降低学习的曲线,系统地学习和掌握容器技术。写给谁看?

写给谁看?

这套教程的目标读者包括:

软件开发人员

相信微服务架构(Microservice Architectur)会逐渐成为开发应用系统的主流。而容器则是这种架构的基石。市场将需要更多能够开发出基于容器的应用程序的软件开发人员。

IT 实施和运维工程师

容器为应用提供了更好的打包和部署方式。越来越多的应用将以容器的方式在开发、测试和生产环境中运行。掌握容器相关技术将成为实施和运维工程师的核心竞争力。

我自己

我坚信最好的学习方法是分享。编写这个教程同时也是对自己学习和实践容器技术的总结。对于知识,只有把它写出来并能够让其他人理解,才能说明真正掌握了这项知识。

包含哪些内容?

如下图,三大块:

下面分别介绍各部分包含的内容。

启程

“启程”会介绍容器的生态系统,让大家先从整体上了解容器都包含那些技术,各种技术之间的相互关系是什么,然后再来看我们的教程都会涉及生态中的哪些部分。

为了让大家尽快对容器有个感性认识,我们会搭建实验环境并运行第一个容器,为之后的学习热身。

容器技术

这是教程的主要内容,包含“容器核心知识”和“容器进阶知识”两部分。

核心知识主要回答有关容器 what, why 和 how 三方面的问题。其中以 how 为重,将展开讨论架构、镜像、容器、网络和存储。

进阶知识包括将容器真正用于生成所必须的技术,包括多主机管理、跨主机网络、监控、数据管理、日志管理和安全管理。

容器平台技术

容器平台技术在生态环境中占据着举足轻重的位置,对于容器是否能够落地,是否能应用于生产至关重要。我们将详细讨论容器编排引擎、容器管理平台和基于容器的 PaaS,学习和实践业界最具代表性的开源产品。

怎样的编写方式?

我会继续采用《每天5分钟玩转OpenStack》的方式,通过大量的实验由浅入深地探讨和实践容器技术,力求达到如下目标:

  1. 快速上手:以最直接、最有效的方式让大家把容器用起来。
  2. 循序渐进:由易到难,从浅入深,详细分析容器的各种功能和配置使用方法。
  3. 理解架构:从设计原理和架构分析入手,深入探讨容器的架构和运行机理。
  4. 注重实践:以大量实际操作案例为基础,让大家能够掌握真正的实施技能。

在内容的发布上还是通过微信公众号(cloudman6)每周 1、3、5 定期分享。 欢迎大家通过公众号提出问题和建议,进行技术交流。

为什么叫《每天5分钟玩转容器技术》?

为了降低学习的难度并且考虑到移动端碎片化阅读的特点,每次推送的内容大家只需要花5分钟就能看完(注意这里说的是看完,有时候要完全理解可能需要更多时间),每篇内容包含1-3个知识点,这就是我把教程命名为《每天5分钟玩转容器技术》的原因。虽然是碎片化推送,但整个教程是系统、连贯和完整的,只是化整为零了。

好了,今天这5分钟算是开了个头,下次我们正式开始玩转容器技术。

二维码+指纹.png

超好用的Redis管理及监控工具,使用后可大大提高你的工作效率! - marko39 - 博客园

mikel阅读(1016)

来源: 超好用的Redis管理及监控工具,使用后可大大提高你的工作效率! – marko39 – 博客园

Redis做为现在web应用开发的黄金搭担组合,大量的被应用,广泛用于存储session信息,权限信息,交易作业等热数据。做为一名有10年以上JAVA开发经验的程序员,工作中项目也是广泛使用了Redis,工作中也遇到了Redis的数据可视化不便、Redis的数据查看维护困难、Redis状态监控运维不易等问题。

相信大家在工作中也会遇到我说的这些问题,在工作中大家可能都装过一些可视化的工具,客户端工具,状态监控工具。例如Redis Desktop Manager,Redis Live,Redis browser等,这些工具开发语言也五花八门,php, ruby,python, qt等,安装前置条件也一堆,安装过程的痛苦,装过就知道了。

由于我也是一线的开发,也深切的感受到了没有顺手Redis管理工具带来的不便,经过一段时间的加班加点,挑灯开发,一款用JAVA语言开发的Redis管理及监控工具treeNMS横空出世了。

下面我就详细给大家介绍一下treeNMS的安装及各项功能。

 

1、  安装

相信大家windows下安装工具都得心应手,但Linux中安装就头大了吧,treeNMS管理工具,直接到http://www.treesoft.cn/dms.html下载,是用JAVA开发的,基于WEB方式对Redis管理,windows环境下载解压即可使用,Linux环境中也只需将软件复制过去,配置JAVA环境就可以使用了。MAC系统中也可以直接复制过去用,前提是有JAVA运行环境。

因为是基于WEB方式的,所以可以直接在服务器上布署一份,那么大家都可以用浏览器直接访问操作,避免了每个开发人员都要安装工具软件的麻烦,工作效率大大提高。

 

2、  运行及参数初始化

按说明运行startup.bat文件,软件就运行起来了show一下后台主页。

主面右上角有一个参数配置按钮,点击后直接进行连接信息的填写及连接测试。直接在线编辑连接信息的好处是,无需访问远程服务器,直接就完成修改连接信息。

 

3、状态监控

Redis做为缓存数据库,对内存占用率等指标很有必要定期监控,目前市面上多是国外的Redis监控软件,并且大部分功能较单一,例如Redis Live, 展示的指标有限,而treeNMS提供了详细的Redis状态值 ,达68项之多,并且有状态实时监控。

 

4、 Redis数据的查看,编辑,维护

说到数据查看,之前我也用过Redis Desktop Manager,这是单机版本的,也是国人用JAVA开发的,挺好的,就是数据大时,不稳定。单机版的问题就是团队中相关人员都要安装,服务器远程管理Redis不便,treeNMS就解决了这些问题。

详细数据的展示,查看,编辑,新增,删除等,支持string,list ,set,zset,hash等数据类型。

有些情况会看到展示的数据是乱码,这是由于存入的数据已压缩或序列化处理过,这种类型的数据是展示时就会变成乱码是正常现象。

 

5、json数据格式化

JSON格式的数据具有占用空间小,更易解析等优点,大量数据会以JSON格式存入Redis中。

treeNMS提供了JSON数据格式化的功能,支持格式化,缩进,层级控制,查看,方便数据分析。 做开发,测试都会用到这个功能的。 

 

6、在线数据备份及还原

Redis是支持数据存储及持久化的,treeNMS 支持在线redis数据备份及下载,有个细节:程序要与redis安装在同一台电脑中,才能备份哦。

 

总结:有了这款treeNMS软件,就可以轻松驾驭redis了,也希望国产软件能引领技术革新,走向世界!

 

如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的研发动力!欢迎各位转载.

基于SignalR的小型IM系统 - 程序诗人 - 博客园

mikel阅读(1198)

来源: 基于SignalR的小型IM系统 – 程序诗人 – 博客园

这个IM系统真是太轻量级了,提供的功能如下:

1.聊天内容美化

2.用户上下线提示

3.心跳包检测机制

4.加入用户可群聊

下面来一步一步的讲解具体的制作方法。

开篇准备工作

首先,巧妇难为无米之炊,这是总所周知的。这里我们需要两个东西,一个是ASP.NET MVC4项目;另一个是Signalr组件。

新建一个ASP.NET MVC4项目,然后通过以下命令安装Signalr组件:

1
Install-Package Microsoft.AspNet.SignalR -Version 1.1.3

这样我们就将组件安装完毕了。

后台交互部分

接着在项目中,新建一个文件夹名称为Hubs,在这个文件夹下面新建一个名称为IChatHub的接口,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface IChatHub
   {
       //服务器下发消息到各个客户端
       void SendChat(string id, string name, string message);
       //用户上线通知
       void SendLogin(string id, string name);
       //用户下线通知
       void SendLogoff(string id, string name);
       //接收客户端发送的心跳包并处理
       void TriggerHeartbeat(string id, string name);
   }

其中,SendChat方法主要用户Signalr后端向前台发送数据;SendLogin方法主要用于通知用户上线;SendLogoff方法主要用于通知用户下线;而TriggerHeartbeat方法主要用于接收前端发送的心跳包并做处理,以便于判断用户是否断开连接(有时候用户直接关闭浏览器或者在任务管理器中关闭浏览器,是无法检测用户离线与否的,所以这里引入了心跳包机制,一旦用户在20秒之后未发送任何心跳包到后端,则视为掉线)。

接下来添加一个ChatHub的类,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class ChatHub:Hub, IChatHub
    {
        private IList<UserChat> userList = ChatUserCache.userList;
        public void SendChat(string id, string name, string message)
        {
            Clients.All.addNewMessageToPage(id, name + " " + DateTime.Now.ToString("yyyy/MM/dd hh:mm:ss"), message);
        }
        public void TriggerHeartbeat(string id, string name)
        {
            var userInfo = userList.Where(x => x.ID.Equals(id) && x.Name.Equals(name)).FirstOrDefault();
            userInfo.count = 0;  //收到心跳,重置计数器
        }
        public void SendLogin(string id,string name)
        {
            var userInfo = new UserChat() { ID = id, Name = name };
            userInfo.action += () =>
            {
                //用户20s无心跳包应答,则视为掉线,会抛出事件,这里会接住,然后处理用户掉线动作。
                SendLogoff(id, name);
            };
            var comparison = new ChatUserCompare();
            if (!userList.Contains<UserChat>(userInfo, comparison))
                userList.Add(userInfo);
            Clients.All.loginUser(userList);
            SendChat(id, name, "<====用户 " + name + " 加入了讨论组====>");
        }
        public void SendLogoff(string id,string name)
        {
            var userInfo = userList.Where(x => x.ID.Equals(id) && x.Name.Equals(name)).FirstOrDefault();
            if (userInfo != null)
            {
                if (userList.Remove(userInfo))
                {
                    Clients.All.logoffUser(userList);
                    SendChat(id, name, "<====用户 " + name + " 退出了讨论组====>");
                }
            }
        }
    }

这个类的设计思想有如下几个部分:

首先,所有用户的登陆信息,我持久化到了缓存集合中:IList<UserChat>,这个缓存集合的定义如下:

1
2
3
4
public static class ChatUserCache
    {
        public static IList<UserChat> userList = new List<UserChat>();
    }

这样,用户登陆信息就会保存到内存中,一旦有新用户进来或者是旧用户退出,我就可以通过新增条目或者删除条目来维护这个列表,维护完毕,将这个列表推到前端。这样前台用户就能实时看到,哪些用户上线,哪些用户下线了。

其次,心跳包检测机制部分,前端用户每隔5秒钟会发送一次心跳包到处理中心,处理中心收到心跳包,会将实体类的计数器置为0;也就是说,如果用户登陆正常,那么用户实体中的计数器每隔5秒钟自动置为0;但是如果用户不按正常渠道退出(直接关闭浏览器或者在任务管理器中关闭浏览器),那么用户实体中的计数器就会一直递增,直到加到第20秒,然后会抛出事件,提示当前用户已经断开连接。

用户实体设计如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class UserChat
    {
        public UserChat()
        {
            count = 0;
            if (Timer == null) Timer = new Timer();
            Timer.Interval = 1000;  //1s触发一次
            Timer.Start();
            Timer.Elapsed += (sender, args) =>
            {
                count++;
                if (count >= 20)
                    action();  //该用户掉线了,抛出事件通知
            };
        }
        private readonly Timer Timer;
        public event Action action;
        
        public string ID { get; set; }
        public string Name { get; set; }
        //内部计数器(每次递增1),如果服务端每5s能收到客户端的心跳包,那么count被重置为0;
        //如果服务端20s后仍未收到客户端心跳包,那么视为掉线
        public int count{get;set;}
    }

当用户意外退出,会有一个action事件抛出,我们在SendLogin方法中进行了接收,当这个事件抛出,就会立马触发用户的Logoff事件,通知掉线:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void SendLogin(string id,string name)
       {
           var userInfo = new UserChat() { ID = id, Name = name };
           userInfo.action += () =>
           {
               //用户20s无心跳包应答,则视为掉线,会抛出事件,这里会接住,然后处理用户掉线动作。
               SendLogoff(id, name);
           };
           var comparison = new ChatUserCompare();
           if (!userList.Contains<UserChat>(userInfo, comparison))
               userList.Add(userInfo);
           Clients.All.loginUser(userList);
           SendChat(id, name, "<====用户 " + name + " 加入了讨论组====>");
       }

这就是处理中心的所有内容了。

需要注意的是,在ChatHub类中,SendChat方法,TriggerHeartbeat方法,SendLogin方法,SendLogoff方法都是Singnalr处理对象所拥有的方法,而addNewMessageToPage方法,loginUser方法,logoffUser方法则是其回调方法。也就是说,当你在前台通过SendChat方法向处理中心发送数据的时候,你可以注册addNewMessageToPage方法来接收处理中心返回给你的数据。

image

前端逻辑及布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
<div id="tb" class="easyui-panel panel" title="专家在线咨询系统" >
    <div id="messageboard">
        <ul id="discussion"></ul>
    </div>
    <div id="userContainer">
        <ul id="userList"></ul>
    </div>
    <div id="messagecontainer" >
        <textarea id="message" class="rte-zone" rows="3"></textarea>
        <div>
            <input type="button" id="send" class="btn" value="发送" />
            <input type="button" id="close" class="btn" value="关闭" /><input type="hidden" id="displayname" />
        </div>
    </div>
</div>
@section scripts{
    <style>
    .panel{padding:5px;height:auto;min-height:650px;}
    .current{color:Green;}
    .rte-zone{width:815px;margin:0;padding:0;height:160px;border:1px #999 solid;clear:both}
    .rte-toolbar{width:800px;margin-top:10px;}
    .rte-toolbar div{float:left;width:100%;}
    .rte-toolbar a,.rte-toolbar a img{border:0}
    .rte-toolbar p{float:left;margin:0;padding-right:5px}
    #messageboard{border:1px solid #B6DF7D;float:left;width:800px;padding:10px;height:450px;overflow:auto;border-radius:10px; -moz-box-shadow:2px 2px 5px #333333; -webkit-box-shadow:2px 2px 5px #333333; box-shadow:2px 2px 5px #333333;}
    #userContainer{border:1px solid #B6DF7D;float:right;width:200px;height:565px;padding:5px;border-radius:10px; -moz-box-shadow:2px 2px 5px #333333; -webkit-box-shadow:2px 2px 5px #333333; box-shadow:2px 2px 5px #333333;}
    #messagecontainer{float:left;width:800px;}
    #messagecontainer div{float:right;}
    #message{border:1px solid #B6DF7D;width:815px; height:70px;margin-top:5px;border-radius:10px; -moz-box-shadow:2px 2px 5px #333333; -webkit-box-shadow:2px 2px 5px #333333; box-shadow:2px 2px 5px #333333;}
    #userList li{border-bottom:1px solid #B6DF7D;cursor:pointer;}
    #userList li:hover{background-color:#ccc;}
    .btn{width:75px;height:25px;}
    </style>
    <script src="../../Content/JQueryplugin/JQuery.rte.js" type="text/JavaScript"></script>
    <!--Reference the SignalR library. -->
    <script src="../../Scripts/jQuery.signalR-1.1.4.min.js" type="text/JavaScript"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="../../signalr/hubs"></script>
    <script>
        $(function () {
            $('.rte-zone').rte("css url", "http://batiste.dosimple.ch/blog/posts/2007-09-11-1/");
            //添加对自动生成的Hub的引用
            var chat = $.connection.chatHub;
            //调用Hub的callback回调方法
            //后端SendChat调用后,产生的addNewMessageToPage回调
            chat.client.addNewMessageToPage = function (id, name, message) {
                $('#discussion').append('<li style="color:blue;">' + htmlEncode(name) + '</li><li> ' + htmlEncode(message) + '</li>')
            };
            //后端SendLogin调用后,产生的loginUser回调
            chat.client.loginUser = function (userlist) {
                reloadUser(userlist);
            };
            //后端SendLogoff调用后,产生的logoffUser回调
            chat.client.logoffUser = function (userlist) {
                reloadUser(userlist);
            };
            $('#displayname').val(prompt('请输入昵称:', ''));
            //启动链接
            $.connection.hub.start().done(function () {
                var userid = guid();
                var username = $('#displayname').val();
                //发送上线信息
                chat.server.sendLogin(userid, username);
                //点击按钮,发送聊天内容
                $('#send').click(function () {
                    var chatContent = $('#message').contents().find('.frameBody').html();
                    chat.server.sendChat(userid, username, chatContent);
                });
                //点击按钮,发送用户下线信息
                $('#close').click(function () {
                    chat.server.sendLogoff(userid, username);
                    $("#send").css("display", "none");
                });
                //每隔5秒,发送心跳包信息
                setInterval(function () {
                    chat.server.triggerHeartbeat(userid, username);
                }, 5000);
            });
        });
        //重新加载用户列表
        var reloadUser = function (userlist) {
            $("#userList").children("li").remove();
            for (i = 0; i < userlist.length; i++) {
                $("#userList").append("<li><img src='../../Content/images/charge_100.png' />" + userlist[i].Name + "</li>");
            }
        }
        //div内容html化
        var htmlEncode = function (value) {
            var encodedValue = $('<div />').html(value).html();
            return encodedValue;
        }
        //guid序号生成
        var guid = (function () {
            function s4() {
                return Math.floor((1 + Math.random()) * 0x10000)
                           .toString(16)
                           .substring(1);
            }
            return function () {
                return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
                       s4() + '-' + s4() + s4() + s4();
            };
        })();
    </script>
}

在如上代码中:

第49行,加载一个富文本编辑器

第51行,添加对自动生成的proxy的引用

第56行~第68行,注册回调方法,以便于更新前台UI

第73行,打开处理中心hub

第79行,发送用户上线信息

第94行,每隔5秒钟发送一次心跳包

如此而已,非常简便。

运行截图

打开界面,首先提示输入用户昵称:

image

输入完毕之后,用户上线:

image

后续两个用户加入进来:

image

用户聊天内容记录:

image

用户“浅浅的”正常退出:

image

用户“书韵妍香”非正常退出:

image

 

点击下载

参考文章:Tutorial: Getting Started with SignalR 1.x

 

切不要忘记在Global.asax中添加映射:

RouteTable.Routes.MapHubs();

Apache MINA实战之 牛刀小试 - kongxx的专栏 - 博客频道 - CSDN.NET

mikel阅读(972)

 

来源: Apache MINA实战之 牛刀小试 – kongxx的专栏 – 博客频道 – CSDN.NET

本文链接:http://blog.csdn.NET/kongxx/article/details/7520259

Apache的MINA是一个被用来构建高性能和高可伸缩性应用程序的网络应用框架,它提供了一套统一的建立在Java NIO之上的事件驱动的异步API。

对于MINA框架的了解,MINA官方的几篇文章是必须要看的,如下:

* Application Architecture http://mina.apache.org/mina-based-application-architecture.html
* Server Architecture http://mina.apache.org/server-architecture.html
* Client Architecture http://mina.apache.org/client-architecture.html

其中几个主要的组件如下:

I/O Service – 用来处理I/O流,对于Server端就是IOAcceptor的实现类接受Client端的连接请求,对于Client端就是IoConnector的实现类来建立到Server端的连接。

I/O Filter Chain – 用来过滤或转化数据。对于Server端和Client端来说都是IoFilter接口的实现类,MINA自己内建了很多IoFilter接口的实现类。具体可以参考官方文档。

I/O Handler – 用来处理真正业务逻辑的类。对于Server端和Client端来说都是IoHandler接口的实现类,通常来说需要自己编写。

由于Server端和Client端都是基于以上三个组件的,因此对于Server端和Client端编程来说就都有类似的代码结构。

对于Server端来说:

1. 创建I/O service – 这里就是创建IOAcceptor类监听端口。

2. 创建I/O Filter Chain – 这里就是告诉使用那些IoFilter。
3. 创建I/O Handler – 自己的业务逻辑。

对于Client端来说:

1. 创建I/O service – 这里就是创建IOConnector类来建立到Server端的连接。

2. 创建I/O Filter Chain – 这里就是告诉使用那些IoFilter。

3. 创建I/O Handler – 自己的业务逻辑。

下面来通过一个例子看看MINA是怎样工作的。由于大多数应用都是基于TCP/IP的应用,所以这里也就不再说UDP/IP了。

这里我使用了Maven来创建了一个简单Java应用程序,具体步骤请参Maven的官方手册。这里只是将我用到的maven配置文件pom.xml列出,方便下面及后续文章使用。具体pom.xml文件内容如下:

  1. <project xmlns=“http://maven.apache.org/POM/4.0.0” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”
  2.     xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd”>
  3.     <modelVersion>4.0.0</modelVersion>
  4.     <groupId>com.google.code.garbagecan.minastudy</groupId>
  5.     <artifactId>minastudy</artifactId>
  6.     <packaging>jar</packaging>
  7.     <version>1.0-SNAPSHOT</version>
  8.     <name>minastudy</name>
  9.     <url>http://maven.apache.org</url>
  10.     <dependencies>
  11.         <dependency>
  12.             <groupId>org.apache.mina</groupId>
  13.             <artifactId>mina-core</artifactId>
  14.             <version>2.0.4</version>
  15.         </dependency>
  16.         <dependency>
  17.             <groupId>org.apache.mina</groupId>
  18.             <artifactId>mina-filter-compression</artifactId>
  19.             <version>2.0.4</version>
  20.         </dependency>
  21.         <dependency>
  22.             <groupId>org.slf4j</groupId>
  23.             <artifactId>slf4j-api</artifactId>
  24.             <version>1.3.0</version>
  25.         </dependency>
  26.         <dependency>
  27.             <groupId>org.slf4j</groupId>
  28.             <artifactId>slf4j-log4j12</artifactId>
  29.             <version>1.3.0</version>
  30.         </dependency>
  31.     </dependencies>
  32. </project>

首先来看Server端的代码

  1. package com.google.code.garbagecan.minastudy.sample1;
  2. import java.io.IOException;
  3. import java.net.InetSocketAddress;
  4. import java.nio.charset.Charset;
  5. import org.apache.mina.core.service.IoAcceptor;
  6. import org.apache.mina.core.service.IoHandlerAdapter;
  7. import org.apache.mina.core.session.IdleStatus;
  8. import org.apache.mina.core.session.IoSession;
  9. import org.apache.mina.filter.codec.ProtocolCodecFilter;
  10. import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
  11. import org.apache.mina.filter.logging.LoggingFilter;
  12. import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
  13. import org.slf4j.Logger;
  14. import org.slf4j.LoggerFactory;
  15. public class MyServer {
  16.     private static final Logger logger = LoggerFactory.getLogger(MyServer.class);
  17.     public static void main(String[] args) {
  18.         IoAcceptor acceptor = new NioSocketAcceptor();
  19.         acceptor.getFilterChain().addLast(“logger”new LoggingFilter());
  20.         acceptor.getFilterChain().addLast(“codec”new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName(“UTF-8”))));
  21.         acceptor.setHandler(new IoHandlerAdapter() {
  22.             @Override
  23.             public void sessionCreated(IoSession session) throws Exception {
  24.             }
  25.             @Override
  26.             public void sessionOpened(IoSession session) throws Exception {
  27.             }
  28.             @Override
  29.             public void sessionClosed(IoSession session) throws Exception {
  30.             }
  31.             @Override
  32.             public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
  33.             }
  34.             @Override
  35.             public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
  36.                 logger.error(cause.getMessage(), cause);
  37.                 session.close(true);
  38.             }
  39.             @Override
  40.             public void messageReceived(IoSession session, Object message) throws Exception {
  41.                 logger.info(“Received message “ + message);
  42.                 session.write(message);
  43.             }
  44.             @Override
  45.             public void messageSent(IoSession session, Object message) throws Exception {
  46.                 logger.info(“Sent message “ + message);
  47.             }
  48.         });
  49.         try {
  50.             acceptor.bind(new InetSocketAddress(10000));
  51.         } catch (IOException ex) {
  52.             logger.error(ex.getMessage(), ex);
  53.         }
  54.     }
  55. }

1. 首先创建I/O Service,这里使用的是NioSocketAcceptor类来创建了一个IoAcceptor实例。

2. 创建I/O Filter Chain,这里使用了两个IoFilter,一个是LoggingFilter用来记录日志和打印事件消息,另一个是ProtocolCodecFilter实例用来编码数据,这里其实就是将传递的数据编码成文本。

3. 创建I/O Handler,不要害怕,看起来代码多,其实就是一个实现了IoHandler接口的子类,自己需要实现其中的一些方法,这里方法比较多,但是我在这里只实现了messageSent,messageReceived和exceptionCaught方法。

4. 最后就是让IoAcceptor类实例绑定端口实现监听。

下面看看Client端的代码

  1. package com.google.code.garbagecan.minastudy.sample1;
  2. import java.net.InetSocketAddress;
  3. import java.nio.charset.Charset;
  4. import org.apache.mina.core.RuntimeIoException;
  5. import org.apache.mina.core.future.ConnectFuture;
  6. import org.apache.mina.core.service.IoConnector;
  7. import org.apache.mina.core.service.IoHandlerAdapter;
  8. import org.apache.mina.core.session.IdleStatus;
  9. import org.apache.mina.core.session.IoSession;
  10. import org.apache.mina.filter.codec.ProtocolCodecFilter;
  11. import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
  12. import org.apache.mina.filter.logging.LoggingFilter;
  13. import org.apache.mina.transport.socket.nio.NioSocketConnector;
  14. import org.slf4j.Logger;
  15. import org.slf4j.LoggerFactory;
  16. public class MyClient {
  17.     private static final Logger logger = LoggerFactory.getLogger(MyClient.class);
  18.     public static void main(String[] args) {
  19.         IoConnector connector = new NioSocketConnector();
  20.         connector.setConnectTimeoutMillis(10 * 1000);
  21.         connector.getFilterChain().addLast(“logger”new LoggingFilter());
  22.         connector.getFilterChain().addLast(“codec”new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName(“UTF-8”))));
  23.         connector.setHandler(new IoHandlerAdapter() {
  24.             @Override
  25.             public void sessionCreated(IoSession session) throws Exception {
  26.             }
  27.             @Override
  28.             public void sessionOpened(IoSession session) throws Exception {
  29.                 for (int i = 0; i < 10; i++) {
  30.                     session.write(“Hello user_” + i);
  31.                 }
  32.                 session.write(“Bye”);
  33.             }
  34.             @Override
  35.             public void sessionClosed(IoSession session) throws Exception {
  36.             }
  37.             @Override
  38.             public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
  39.             }
  40.             @Override
  41.             public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
  42.                 logger.error(cause.getMessage(), cause);
  43.                 session.close(true);
  44.             }
  45.             @Override
  46.             public void messageReceived(IoSession session, Object message) throws Exception {
  47.                 logger.info(“Received message “ + message);
  48.                 if (message.toString().equalsIgnoreCase(“Bye”)) {
  49.                     session.close(true);
  50.                 }
  51.             }
  52.             @Override
  53.             public void messageSent(IoSession session, Object message) throws Exception {
  54.                 logger.info(“Sent message “ + message);
  55.             }
  56.         });
  57.         IoSession session = null;
  58.         try {
  59.             ConnectFuture future = connector.connect(new InetSocketAddress(“localhost”10000));
  60.             future.awaitUninterruptibly();
  61.             session = future.getSession();
  62.         } catch (RuntimeIoException e) {
  63.             logger.error(e.getMessage(), e);
  64.         }
  65.         session.getCloseFuture().awaitUninterruptibly();
  66.         connector.dispose();
  67.     }
  68. }

 

1. 首先创建I/O Service,这里使用的是NioSocketConnector类来创建了一个IoConnector实例,并设置连接超时为10秒。

2. 创建I/O Filter Chain,和服务器端同样设置了两个IoFilter,一个是LoggingFilter用来记录日志和打印事件消息,另一个是ProtocolCodecFilter实例用来编码数据,这里其实就是将传递的数据编码成文本。

3. 创建I/O Handler,也不要害怕,看起来代码多,其实也是一个实现了IoHandler接口的子类,并且自己实现了sessionOpened,messageSent,messageReceived和exceptionCaught方法。实现sessionOpened方法是为了在建立连接后向Server端发送消息。另外看一下messageReceived方法实现,在接收到服务器端的消息后关闭会话。从而可以使Client程序最终能够退出。

4. 最后就是IoConnector实例类连接远端的Server。

 

下面测试一下上面的程序,首先运行MyServer类,然后运行MyClient类,就可以分别在各自的终端上看到事件日志以及发送/接收的消息了