简述
如何使用
1.安装
npm install socket.io
2.Node服务器
服务器端的app.js
var app = require('http').createServer(handler);
var io = require('socket.io')(app);
var fs = require('fs');
app.listen(80);
function handler(req, res) {
fs.readFile(__dirname + '/index.html', function(err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading index.html');
}
res.writeHead(200);
res.end(data)
})
}
io.on('connection', function(socket) {
socket.emit('news', {hello: 'world'});
socket.on('my other event', function(data) {
console.log(data);
});
});
客户端(index.html)
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost');
socket.on('news', function(data) {
console.log(data);
socket.emit('my other event', { my: 'data'});
});
</script>
3.Express 3/4 服务器
服务器端的app.js
var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server);
server.listen(80);
app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});
io.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
客户端(index.html)
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
</script>
4.Express 2.X 服务器
服务器端的app.js
var app = require('express').createServer();
var io = require('socket.io')(app);
app.listen(80);
app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});
io.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
客户端(index.html)
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
</script>
5.发送和接收事件
Socket.IO允许你发送和接收常规事件。除了connect/message/disconnect
外,可以发送常规事件。
// note, io(<port>) will create a http server for you
var io = require('socket.io')(80);
io.on('connection', function (socket) {
io.emit('this', { will: 'be received by everyone'});
socket.on('private message', function (from, msg) {
console.log('I received a private message by ', from, ' saying ', msg);
});
socket.on('disconnect', function () {
io.emit('user disconnected');
});
});
6.命名空间限制
如果你可以控制所有发送至一个特殊应用得信息或事件,使用默认命名空间。如果你想插入第三方代码,或者想与他人共同开发,socket.io提供了一个命名socket的方法。
Server (app.js)
var io = require('socket.io')(80);
var chat = io
.of('/chat')
.on('connection', function (socket) {
socket.emit('a message', {
that: 'only'
, '/chat': 'will get'
});
chat.emit('a message', {
everyone: 'in'
, '/chat': 'will get'
});
});
var news = io
.of('/news')
.on('connection', function (socket) {
socket.emit('item', { news: 'item' });
});
Client (index.html)
<script>
var chat = io.connect('http://localhost/chat')
, news = io.connect('http://localhost/news');
chat.on('connect', function () {
chat.emit('hi!');
});
news.on('news', function () {
news.emit('woot');
});
</script>
简单实例(聊天应用)Get started
前言
用诸如PHP的技术写一个聊天应用一直以来都是非常困难的。它包括了轮询,跟踪时间戳,而且其速度总是比较慢。
Sockets是解决服务器和客户端之间即时通信的常规方法。
这意味着服务器可以主动将信息推送到客户端。无论何时你发送了一条信息,服务器都将得到它并且发送至其他已连接的客户端。
Web框架
1.初始化一个npm包
package.json
{
"name": "socket-chat-example",
"version": "0.0.1",
"description": "my first socket.io app",
"dependencies": {}
}
2.载入依赖express框架
npm install --save express@4.15.2
3.创建index.js文件,该文件将建立我们的应用
// express初始化app,使其成为可以提供给HTTP服务器的处理函数
var app = require('express')();
var http = require('http').Server(app);
// 定义路由路径'/'
app.get('/', function(req, res){
res.send('<h1>Hello world</h1>');
});
// 本地端口3000监听服务器
http.listen(3000, function(){
console.log('listening on *:3000');
});
此时运行node index,打开浏览器输入相应网址得到
4.网页不可能全靠字符串形式写在index.js中发送,稍作修改并添加index.html文件。
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
index.html
<!doctype html>
<html>
<head>
<title>Socket.IO chat</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font: 13px Helvetica, Arial; }
form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" /><button>Send</button>
</form>
</body>
</html>
5.现在只是创建了一个本地服务器,并发送相应的html文件,接下来需要整合Socket.IO
安装Socket.IO
npm install --save socket.io
稍作修改index.js文件
var app = require('express')();
var http = require('http').Server(app);
// 初始化socket.io实例并传入http服务器
var io = require('socket.io')(http);
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
// 实例监听connection事件,每次事件触发回调函数
io.on('connection', function(socket){
console.log('a user connected');
});
http.listen(3000, function(){
console.log('listening on *:3000');
});
客户端index.html也需要添加一些脚本
// 只需要这些代码就可以载入socket.io-client,并且可以提供全局io,连接
// io()没有传入路径,即使用默认路径'/'
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
</script>
每个socket可以发送特殊的断开事件
io.on('connection', function(socket){
console.log('a user connected');
socket.on('disconnect', function(){
console.log('user disconnected');
});
});
6.发送事件
Socket.IO背后的思想是你可以发送和接收任何事件,伴随任何数据,比如说可以被编译成JSON的数据或者二维码。
下列代码使用了自定义的chat message事件。
index.html文件
<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script>
$(function () {
var socket = io();
// 表单提交,socket发送chat message事件,并清空输入框,禁止了提交表单事件
$('form').submit(function(){
socket.emit('chat message', $('#m').val());
$('#m').val('');
return false;
});
});
</script>
index.js
io.on('connection', function(socket){
// 监听客户端发送过来的chat message事件,提供回调
socket.on('chat message', function(msg){
console.log('message: ' + msg);
});
});
7.广播
为了能把事件发送给所有人,而不是单纯的服务器,Socket.IO提供了一个API,io.emit
io.emit('some event', { for: 'everyone' });
如果想将信息发送给除了某个socket外的所有人,可以使用broadcast:
io.on('connection', function(socket){
socket.broadcast.emit('hi');
});
这里为了简洁,把信息发送给所有人,包括发送者。
io.on('connection', function(socket){
socket.on('chat message', function(msg){
io.emit('chat message', msg);
});
});
index.html有接受从服务器端传来的相应事件,并且有个回调处理相关信息
<script>
$(function () {
var socket = io();
$('form').submit(function(){
socket.emit('chat message', $('#m').val());
$('#m').val('');
return false;
});
socket.on('chat message', function(msg){
$('#messages').append($('<li>').text(msg));
});
});
</script>
src="https://i.cloudup.com/transcoded/J4xwRU9DRn.mp4" width="310" height="198">
简单实例(多人聊天应用)摘自官网demo
关于官网的多人聊天应用,其实目录结构和第一个实例差不多,在这里简单分析下期中的index.js文件(服务器)和main.js文件(客户端)。
index.js文件代码如下:
// 创建一个express服务器
// var app = require('express')(),提供给http创建服务器
var express = require('express');
var app = express();
var server = require('http').createServer(app);
// ../.. 路径是为了搜索sockiet.io模块
var io = require('../..')(server);
// 没有设置端口,则默认端口为3000
var port = process.env.PORT || 3000;
//监听端口,回调函数确认成功启动
server.listen(port, function () {
console.log('Server listening at port %d', port);
});
// 设置路由
app.use(express.static(__dirname + '/public'));
// 聊天室
// 定义使用人数初始变量为0
var numUsers = 0;
// socket实例io监听connection事件,回调函数参数为socket
io.on('connection', function (socket) {
// 是否已有用户
var addedUser = false;
// 如果客户端socket发送 'new message', 下列代码会监听并执行,回调函数为data,也就时客户端socket.emit传递的第二个参数
socket.on('new message', function (data) {
// 发送给客户端 'new messag'事件,传递了一个对象,包括用户名和消息等信息
socket.broadcast.emit('new message', {
username: socket.username,
message: data
});
});
// 监听客户端发送过来的'add user'事件,回调传递了username参数
socket.on('add user', function (username) {
if (addedUser) return;
// 如果不存在该用户名,则把客户端传递过来的用户名储存在socket会话里,添加用户人数,修改存在用户状态,发送'login'事件给客户端,传递一个对象,里面包含了聊天室的人数
socket.username = username;
++numUsers;
addedUser = true;
socket.emit('login', {
numUsers: numUsers
});
// 通知所有的客户端,新加入一个人
// 即广播发送'user joined'事件,发送一个对象,包含用户名信息和聊天室人数
socket.broadcast.emit('user joined', {
username: socket.username,
numUsers: numUsers
});
});
// 如果客户端发送typing事件,我们把它传递给其他人
socket.on('typing', function () {
socket.broadcast.emit('typing', {
username: socket.username
});
});
// 如果客户端发送stop typing事件,我们同样把他传递给其他人
socket.on('stop typing', function () {
socket.broadcast.emit('stop typing', {
username: socket.username
});
});
// 断开连接,则执行下列代码,如果存在用户名,人数减1
socket.on('disconnect', function () {
if (addedUser) {
--numUsers;
// 通知所有人,这个用户离开了当前聊天框
socket.broadcast.emit('user left', {
username: socket.username,
numUsers: numUsers
});
}
});
});
main.js 客户端JS代码
// 引用了jQuery
$(function() {
// 定义常量
var FADE_TIME = 150; // ms
var TYPING_TIMER_LENGTH = 400; // ms
var COLORS = [
'#e21400', '#91580f', '#f8a700', '#f78b00',
'#58dc00', '#287b00', '#a8f07a', '#4ae8c4',
'#3b88eb', '#3824aa', '#a700ff', '#d300e7'
];
// 初始化部分变量
var $window = $(window);
var $usernameInput = $('.usernameInput'); // Input for username
var $messages = $('.messages'); // Messages area
var $inputMessage = $('.inputMessage'); // Input message input box
var $loginPage = $('.login.page'); // The login page
var $chatPage = $('.chat.page'); // The chatroom page
// 设置用户名的提示框
var username;
var connected = false;
var typing = false;
var lastTypingTime;
var $currentInput = $usernameInput.focus();
// 创建一个IO全局实例
var socket = io();
// 得到一个信息对象,根据其numUsers属性值得不同,将不同的文本信息传递给log函数制成完整野生li标签
function addParticipantsMessage (data) {
var message = '';
if (data.numUsers === 1) {
message += "there's 1 participant";
} else {
message += "there are " + data.numUsers + " participants";
}
log(message);
}
// 设置客户端的用户名
function setUsername () {
// 得到干净的用户名
username = cleanInput($usernameInput.val().trim());
// 如果用户名存在
if (username) {
// 登录界面消退
// 聊天界面出现
// 移除登录界面的点击事件,避免浪费内存
// 自动获取聊天输入框的焦点
$loginPage.fadeOut();
$chatPage.show();
$loginPage.off('click');
$currentInput = $inputMessage.focus();
// 发送'add user'事件,并带有username信息
socket.emit('add user', username);
}
}
// 发送一条聊天消息
function sendMessage () {
var message = $inputMessage.val();
// Prevent markup from being injected into the message
message = cleanInput(message);
// if there is a non-empty message and a socket connection
if (message && connected) {
// 清空输入框
$inputMessage.val('');
addChatMessage({
username: username,
message: message
});
// 客户端发送'new message'事件,并传递了message信息
socket.emit('new message', message);
}
}
// 记录一条信息,根据信息创建一个野生的li标签,调用aME函数添加到DOM文档
function log (message, options) {
var $el = $('<li>').addClass('log').text(message);
addMessageElement($el, options);
}
// 把聊天信息添加到message list
function addChatMessage (data, options) {
// Don't fade the message in if there is an 'X was typing'
var $typingMessages = getTypingMessages(data);
// 设置options或者使用默认option
options = options || {};
if ($typingMessages.length !== 0) {
options.fade = false;
$typingMessages.remove();
}
// 创建一个class为username的span野生标签
// 文本元素是data.username
// 设置css样式
var $usernameDiv = $('<span class="username"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
// 设置一个class为messageBody的span野生标签
// 内容为data.message
var $messageBodyDiv = $('<span class="messageBody">')
.text(data.message);
var typingClass = data.typing ? 'typing' : '';
// 创建一个class为message的野生li标签
// 设置该li标签有个username数据,其值为data.username
// 添加类
// 野生li标签内添加名字span,消息主题span,形成一个完整的野生li标签
var $messageDiv = $('<li class="message"/>')
.data('username', data.username)
.addClass(typingClass)
.append($usernameDiv, $messageBodyDiv);
// 把野生li标签添加DOM文档
addMessageElement($messageDiv, options);
}
// data传递给aCM函数前,添加两个属性
function addChatTyping (data) {
data.typing = true;
data.message = 'is typing';
addChatMessage(data);
}
// 移除‘X正在输入’消息
function removeChatTyping (data) {
getTypingMessages(data).fadeOut(function () {
$(this).remove();
});
}
// 添加一个message元素(li)给类为messages的(ul),并且拉倒底部
// el为准备加入的message元素
// options选项,fade属性表示元素是否应该有fade-in动画效果
// options的prepend属性表示元素时候应该添加到其他所有的messages的内容前边
function addMessageElement (el, options) {
var $el = $(el);
// 设置options的默认参数
if (!options) {
options = {};
}
if (typeof options.fade === 'undefined') {
options.fade = true;
}
if (typeof options.prepend === 'undefined') {
options.prepend = false;
}
// Apply options
if (options.fade) {
$el.hide().fadeIn(FADE_TIME);
}
if (options.prepend) {
$messages.prepend($el);
} else {
$messages.append($el);
}
$messages[0].scrollTop = $messages[0].scrollHeight;
}
// 返回的还是input,消除了输入内插标记的隐患
function cleanInput (input) {
return $('<div/>').text(input).text();
}
// 更新输入事件
function updateTyping () {
if (connected) {
if (!typing) {
typing = true;
socket.emit('typing');
}
lastTypingTime = (new Date()).getTime();
// 每个一段时间,如果正在输入而且时间差大于TTL,则发送'stop typing'事件,同时typing设置为false
setTimeout(function () {
var typingTimer = (new Date()).getTime();
var timeDiff = typingTimer - lastTypingTime;
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
socket.emit('stop typing');
typing = false;
}
}, TYPING_TIMER_LENGTH);
}
}
// 得到用户'X正在输入..'的信息,只选取那些保存数据username与data参数username属性一致的元素,返回的是过滤过的message list
function getTypingMessages (data) {
return $('.typing.message').filter(function (i) {
return $(this).data('username') === data.username;
});
}
// 通过哈希函数得到用户名的颜色
function getUsernameColor (username) {
// 计算哈希值
var hash = 7;
for (var i = 0; i < username.length; i++) {
hash = username.charCodeAt(i) + (hash << 5) - hash;
}
// 计算颜色
var index = Math.abs(hash % COLORS.length);
return COLORS[index];
}
// 键盘事件
$window.keydown(function (event) {
// 输入时自动聚焦当前输入框
if (!(event.ctrlKey || event.metaKey || event.altKey)) {
$currentInput.focus();
}
// 键盘提交
if (event.which === 13) {
if (username) {
sendMessage();
socket.emit('stop typing');
typing = false;
} else {
setUsername();
}
}
});
输入框的input事件。更新typing状态
$inputMessage.on('input', function() {
updateTyping();
});
// 点击事件
// 点击登录界面的任何位置,聚焦名称输入框
$loginPage.click(function () {
$currentInput.focus();
});
// Focus input when clicking on the message input's border
$inputMessage.click(function () {
$inputMessage.focus();
});
// Socket事件
// Whenever the server emits 'login', log the login message
// 无论何时服务器发送了login事件,都
socket.on('login', function (data) {
connected = true;
// 欢迎信息
var message = "Welcome to Socket.IO Chat – ";
log(message, {
prepend: true
});
addParticipantsMessage(data);
});
// 服务器发送'new message'事件,添加到聊天框
socket.on('new message', function (data) {
addChatMessage(data);
});
// 服务器发送'user joined'事件,添加新消息到聊天框
socket.on('user joined', function (data) {
log(data.username + ' joined');
addParticipantsMessage(data);
});
// Whenever the server emits 'user left', log it in the chat body
socket.on('user left', function (data) {
log(data.username + ' left');
addParticipantsMessage(data);
removeChatTyping(data);
});
// Whenever the server emits 'typing', show the typing message
socket.on('typing', function (data) {
addChatTyping(data);
});
// Whenever the server emits 'stop typing', kill the typing message
socket.on('stop typing', function (data) {
removeChatTyping(data);
});
socket.on('disconnect', function () {
log('you have been disconnected');
});
socket.on('reconnect', function () {
log('you have been reconnected');
if (username) {
socket.emit('add user', username);
}
});
socket.on('reconnect_error', function () {
log('attempt to reconnect has failed');
});
});
流程解析
一、打开chat页面
1.这时候登录界面是显示的,而聊天界面display为none;
二、输入nickname,回车
1.监听$window对象的keydown事件,全局变量username此时值为undefined,调用全局函数setUsername();
2.全局函数setUsername(),取得登录界面输入框的值,经过处理后(清洗、去空),如果该值非空,登录界面loginPage消失,chatPage展示,注销loginPage的点击事件,获取当前聊天界面输入框,并设置为当前输入框。向服务器发送’add user’事件,附带username值。
3.打开页面后,服务器已经接受到了socket的’connection’事件。现在服务器又接收到了客户端传来的’add user’事件。判断addedUser变量是否为true(初始设置为false),如果为false,继续执行。把接收的username值储存在socket.username属性里(socket会话),变量numUsers加1,变量addUser设置为true。服务器端向对应客户端发送’login’事件,附带一个对象(包含人数)。广播所有的已连接客户端’user joined’事件,附带一个对象(包含用户名,人数)。
4.对于单个客户端而言:设置全局变量connected为true,调用log函数(传递一个字符串和一个对象),调用addParticipantsMessage函数(传递服务器传来的对象参数)。
5.log函数接收两个参数,创建一个野生的标签<li class="log">Welcome to Socket.IO Chat –</li>
,调用addMessageElement函数(传递两个参数,一个是DOM元素,一个是options设置选项)。
6.addMessageElement函数接收两个参数,默认设置选项options。如果存在fade效果,逐渐显示,将野生的标签<li class="log">Welcome to Socket.IO Chat –</li>
添加到<ul class="messages">
。聊天界面滚动始终在最下方。
7.addParticipantsMessage函数接收一个参数,创建一个野生的标签<li class="log">there are XX participants</li>
,添加到<ul class="messages">
。
8.所有客户端接收一个’user joined’事件,挂载’Helium joined’,并继续调用addParticipantsMessage显示在线XXX个用户。
三、输入聊天信息
1.监听聊天输入框的’input’事件,调用updateTyping事件。
2.updateTyping事件先判断是否连接,如果是,再判断是否正在输入中,发送给服务器’typing’事件。过段时间执行另一个函数,如果设置的延迟时间内,没有触发第二次’input’事件,则发送给服务器’stop typing’事件,同时设置typing值为false。
3、typing事件。服务器端接收后,向所有的连接客户端广播’typing’事件,附带用户名信息。
4.客户端接收’typing’事件后,调用addChatTyping函数。addChatTyping函数给传入参数添加了typing和message属性,调用addChatMessage函数。
5.addChatMessage函数接受一个数据对象(包含用户名,typing以及message属性)和一个设置选项,首先调用getTypingMessage函数,如果还有用户正在输入,则不把消息显示。创建一个组合标签
<li class="message typingClass" username="data.username">
<span class="username" style="color">data.username</span>
<span class="messageBody">is typing</span>
</li>
挂载到聊天框内。
6.stop typing事件。服务器端接收后,向所有的连接客户端广播’stop typing’事件,附带用户名信息。
7.所有连接客户端调用removeChatTyping函数。得到正在输入的消息,移除。
四、回车输入聊天消息。
1.调用sendMessage函数,同时向服务器发送’stop typing’事件,移除’X正在输入中的信息’
2.sendMessage得到聊天输入框的值,处理后调用addChatMessage函数,传入一个数据对象(username和message),清空聊天输入框,同时发送’new message’事件及message字符串。addChatMessage挂载了
<li class="message typingClass" username="data.username">
<span class="username" style="color">data.username</span>
<span class="messageBody">data.message</span>
</li>
3.服务器端接收到’new message’事件,广播’new message’事件,伴随数据对象(username,message),客户端调用addChatMessage添加聊天信息到聊天界面。
五、退出界面。
1.服务器端检测到’disconnect’,numUsers用户人数减1,广播发送’user left’事件,伴随一个数据对象(username和numUsers)
2.客户端接收事件,log(’someone left’),显示聊天室在线人数。