使用socket.io打造公共聊天室
最近的计算机网络课上老师开始讲socket,tcp相关的知识,当时脑袋里就蹦出一个想法,那就是打造一个聊天室。实现方式也挺多的,常见的可以用C++或者Java进行socket编程来构建这么一个聊天室。当然,我毫不犹豫选择了node来写,node有一个名叫socket.io的框架已经很完善的封装了socket相关API,所以无论是学习还是使用都是非常容易上手的,在这里强烈推荐!demo已经做好并放到我的个人网站了,大家可以试试,挺好玩的。
进去试试 -> http://www.yinxiangyu.com:9000 (改编了socket.io官方提供的例子)
源码 -> https://github.com/yxy19950717/js-practice-demo/tree/master/2016-4/chat
在梳理整个demo之前,先来看看聊天室构建所要用到的原理性的东西。
何为socket
首先要很明确web聊天室客户端是如何与服务器进行通信的。没错,正是socket(套接字)对这样的通信负责。打个比方,如果你正使用你的计算机浏览页面,并且打开了1个telnet和1个ssh会话,那样你就有3个应用进程。当你的计算机中的运输层(tcp,udp)从底层的网络层接收数据时,它需要将接收到的数据定向到三个进程中的一个。而每个进程都有一个或多个套接字,它相当于从网络向进程传递数据和从进程向网络传递数据的门户。
如上图,在接收端,运输层检查报文段中的字段,标识出接收套接字,进而将报文定向该套接字。这样将运输层报文段中的数据交付到正确的套接字的工作称为多路分解。同样在源主机从不同套接字中收集数据块,并为每个数据封装上首部信息(用于分解)从而生成报文段,然后将报文段传递到网络层,这样的工作叫做多路复用。
WebSocket与HTTP
了解完socket套接字的基本原理,可以知道socket始终不是应用层的东西,它是连接应用层与传输层的一个桥梁,那从实现角度上考虑,我们应该如何来编写聊天室这样一个应用呢?
HTTP是无状态的协议,何为无状态?就是指HTTP服务器并不保存关于客户的任何信息。因为TCP为HTTP提供了可靠数据传输服务,意味着一个客户进程发出的每个HTTP请求报文都能完整地到达服务器。HTTP的无状态的特点源于分层体系结构,它的优点也很明显,不用担心数据丢失。但也会出现这样的现象:服务器向客户发送被请求的文件,而不存储任何关于该客户的状态信息。也就是说当一个客户端接连两次请求同一个文件,服务器并不会因为刚刚为该客户提供了该文件而不再做出反应,而是重新发送,HTTP不记得之前做过什么事了!
当然在传统的HTTP应用中,客户端和服务器端时而需要在一个相当长的时间内进行通信,通常会带上cookie进行认证通信,而长时间保持一个连接,会耗费时间和带宽,这样一来,性能会不是很好,而聊天室需要的是实时通信,所以我们更需要WebSocket这样的协议。(部分浏览器还不支持WebSocket,在不是很追求实时的情况下,仍然可以采用HTTP中ajax的方式进行通信)。
WebSocket是html5的一个新协议,它的出现主要是为了解决ajax轮询和long poll时给服务器带来的压力。在HTTP中,通过ajax轮询和Long poll是不断监听服务器是否有新消息,而在WebSocket中,每当服务器有新消息时才会推送,而且它能与代理服务器(一般来说是nginx或者apache)保持长久连接,但与HTTP不同的是,它只需要一次请求即可保持连接。
而对于socket.io这个框架,它兼容了WebSocket以及HTTP两种协议的使用,在部分不能使用WebSocket协议的浏览器中,采用ajax轮询方式进行消息交换。
若想对WebSocket做更多了解,可以阅读此文: WebSocket 是什么原理?为什么可以实现持久连接?
使用socket.io
socket.io是一个完全由JavaScript实现、基于Node.js、支持WebSocket的协议用于实时通信、跨平台的开源框架,它包括了客户端的JavaScript和服务器端的Node.js。Socket.IO除了支持WebSocket通讯协议外,还支持许多种轮询(Polling)机制以及其它实时通信方式,并封装成了通用的接口,并且在服务端实现了这些实时机制的相应代码。Socket.IO实现的Polling通信机制包括Adobe Flash Socket、AJAX长轮询、AJAX multipart streaming、持久Iframe、JSONP轮询等。Socket.IO能够根据浏览器对通讯机制的支持情况自动地选择最佳的方式来实现网络实时应用。
有了这样一个框架,对于了解socket编程的你相信运用起来会非常容易上手了。socket.io的API可以在以下两个网站上进行学习
github: https://github.com/socketio/socket.io
要打造一个聊天室应用,首先确定聊天中服务器需要接收的几个事件响应,分为如下几点:
1.新用户进来时 ('add user')
2.用户正在输入时 ('typing')
3.用户停止输入时 ('stop typing')
4.用户发送消息时 ('new message')
5.用户离开时 ('disconnect')
其次是客户端的用户(们)需要接收到的事件响应:
1.我进来了 ('login')
2.有人进来了 ('user joined')
3.有人正在输入 ('typing')
4.有人停止了输入 ('stop typing')
5.有人发送了新消息 ('new message')
6.有人离开了 ('user left')
接下来我们需要用socket的on和emit接口进行编写,服务器端代码如下:
index.js:
// Setup basic express server
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io')(server);
var port = process.env.PORT || 9000; server.listen(port, function () {
console.log('Server listening at port %d', port);
}); //路由,链接到public,访问时直接访问到index.html
app.use(express.static(__dirname + '/public')); // Chatroom // 在线人数
var numUsers = 0; // 连接打开
io.on('connection', function (socket) {
var addedUser = false; // when the client emits 'new message', this listens and executes
// 接收到客户端发送的new message
socket.on('new message', function (data) {
socket.pic = data.pic;
// we tell the client to execute 'new message'
// 广播发送new message 到客户端
socket.broadcast.emit('new message', {
username: socket.username,
message: data.message,
pic: socket.pic
});
}); // when the client emits 'add user', this listens and executes
// 有新用户进入时
socket.on('add user', function (username) {
if (addedUser) return; // we store the username in the socket session for this client
// 将名字保存在socket的session中
socket.username = username;
++numUsers;
addedUser = true;
socket.emit('login', {
numUsers: numUsers
});
// echo globally (all clients) that a person has connected
// 广播发送user joined到客户端
socket.broadcast.emit('user joined', {
username: socket.username,
numUsers: numUsers
});
}); // when the client emits 'typing', we broadcast it to others
// 接收到xxx输入的消息
socket.on('typing', function (data) {
// 广播发送typing到客户端
socket.broadcast.emit('typing', {
username: socket.username,
pic: data.pic
});
}); // when the client emits 'stop typing', we broadcast it to others
socket.on('stop typing', function () {
socket.broadcast.emit('stop typing', {
username: socket.username
});
}); // when the user disconnects.. perform this
socket.on('disconnect', function () {
if (addedUser) {
--numUsers; // echo globally that this client has left
socket.broadcast.emit('user left', {
username: socket.username,
numUsers: numUsers
});
}
});
});
在客户端,也必须有接收发送消息的脚本
main.js:
$(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'
];
// Initialize variables
var $document = $(document);
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 $headPic = $('.headPic li'); // Prompt for setting a username
var username;
var connected = false;
var typing = false;
var lastTypingTime;
var yourHeadPic;
// 直接聚焦到输入框
var $currentInput = $usernameInput.focus(); var socket = io(); function addParticipantsMessage (data) {
var message = '';
if (data.numUsers === 1) {
message += "there's 1 participant";
} else {
message += "there are " + data.numUsers + " participants";
}
log(message);
} // Sets the client's username
function setUsername () {
username = cleanInput($usernameInput.val().trim()); // If the username is valid
if (username) {
$loginPage.fadeOut();
$chatPage.show();
$loginPage.off('click');
$currentInput = $inputMessage.focus(); // Tell the server your username
socket.emit('add user', username);
}
} // Sends a chat message
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({
pic: yourHeadPic,
username: username,
message: message,
owner: true
});
// tell server to execute 'new message' and send along one parameter
socket.emit('new message', {
message: message,
pic: yourHeadPic
});
}
} // Log a message
function log (message, options) {
var $el = $('<li>').addClass('log').text(message);
addMessageElement($el, options);
} // Adds the visual chat message to the message list
function addChatMessage (data, options) {
// Don't fade the message in if there is an 'X was typing'
var $typingMessages = getTypingMessages(data);
options = options || {};
if ($typingMessages.length !== 0) {
options.fade = false;
$typingMessages.remove();
}
// 选中的头像
if(data.owner) {
//自己的话在右边
var $img = $('<span class="myHeadPicRight"><img src='+data.pic+'.png></span>'); var $usernameDiv = $('<span class="yourUsername"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
var $messageBodyDiv = $('<span class="messageBody">')
.css('float', 'right')
.css('padding-right', '15px')
.text(data.message); var $rightDiv = $('<p style="float:right; width:90%">')
.append($usernameDiv, $messageBodyDiv);
var typingClass = data.typing ? 'typing' : '';
var $messageDiv = $('<li class="message clearfix"/>')
.data('username', data.username)
.addClass(typingClass)
.append($img, $rightDiv); addMessageElement($messageDiv, options);
}else{
var $img = $('<span class="myHeadPic"><img src='+data.pic+'.png></span>'); var $usernameDiv = $('<span class="username"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
var $messageBodyDiv = $('<span class="messageBody">')
.text(data.message); var $rightDiv = $('<p style="float:left; width:90%">')
.append($usernameDiv, $messageBodyDiv);
var typingClass = data.typing ? 'typing' : '';
var $messageDiv = $('<li class="message clearfix"/>')
.data('username', data.username)
.addClass(typingClass)
.append($img, $rightDiv); addMessageElement($messageDiv, options);
}
} // Adds the visual chat typing message
function addChatTyping (data) {
data.typing = true;
data.message = '正在输入...';
addChatMessage(data);
} // Removes the visual chat typing message
function removeChatTyping (data) {
getTypingMessages(data).fadeOut(function () {
$(this).remove();
});
} // Adds a message element to the messages and scrolls to the bottom
// el - The element to add as a message
// options.fade - If the element should fade-in (default = true)
// options.prepend - If the element should prepend
// all other messages (default = false)
function addMessageElement (el, options) {
var $el = el; // Setup default 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;
} // Prevents input from having injected markup
function cleanInput (input) {
return $('<div/>').text(input).text();
} // Updates the typing event
function updateTyping () {
if (connected) {
if (!typing) {
typing = true;
socket.emit('typing',{
pic: yourHeadPic
});
}
lastTypingTime = (new Date()).getTime(); 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);
}
} // Gets the 'X is typing' messages of a user
function getTypingMessages (data) {
return $('.typing.message').filter(function (i) {
return $(this).data('username') === data.username;
});
} // Gets the color of a username through our hash function
// hash确定名字颜色
function getUsernameColor (username) {
// Compute hash code
var hash = 7;
for (var i = 0; i < username.length; i++) {
hash = username.charCodeAt(i) + (hash << 5) - hash;
}
// Calculate color
var index = Math.abs(hash % COLORS.length);
return COLORS[index];
} // Keyboard events
$document.on('keydown',function (event) {
// Auto-focus the current input when a key is typed
// 按ctrl,alt,meta以外的键可以键入文字字母数字等...
if (!(event.ctrlKey || event.metaKey || event.altKey)) {
$currentInput.focus();
}
// When the client hits ENTER on their keyboard
if (event.which === 13 ) {
// username已存在,已经登录
if (username) {
sendMessage();
socket.emit('stop typing');
typing = false;
} else if(!yourHeadPic) {
// 没有选择头像
alert('请选择头像!');
return false;
} else {
// 首次登录
setUsername();
}
}
}); // 输入框一旦change就发送消息
$inputMessage.on('input', function() {
updateTyping();
}); // Click events // Focus input when clicking anywhere on login page
$loginPage.click(function () {
$currentInput.focus();
}); // Focus input when clicking on the message input's border
$inputMessage.click(function () {
$inputMessage.focus();
}); // 选择头像
$headPic.on('click', function() {
var which = parseInt($(this).attr('class').slice(3))-1;
$('.chosePic li').each(function(i, item) {
$(item).children().remove();
yourHeadPic = undefined;
});
$('.chosePic li:eq(' + which + ')').append($('<span></span>'));
yourHeadPic = which + 1;
}); // Socket events // 客户端socket接收到Login指令
// Whenever the server emits 'login', log the login message
socket.on('login', function (data) {
connected = true;
// Display the welcome message
var message = "welcome to sharlly's chatroom";
//传给Log函数
log(message, {
prepend: true
});
addParticipantsMessage(data);
}); // Whenever the server emits 'new message', update the chat body
socket.on('new message', function (data) {
addChatMessage(data);
}); // Whenever the server emits 'user joined', log it in the chat body
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运行只需关注socket.on,socket.broadcast.emit这几个函数。socket.on提供了接收消息的方法,接收到后,其第二个参数就是回调函数,而socket.broadcast.emit是广播发送,向每个用户发送一个对象或一个字符串。到这里你可能会觉得socket.io非常简单,当然这只是它的一些功能,更多用法大家可以自行学习。
刚刚提供的这个例子改编于socket.io的官方实例,博主在写的时候对前端界面增加了头像选择,以及第一人称第三人称文字的排版布局改动,所以在main.js中可以代码有些繁杂(所以只用关注有socket.的地方),完整代码请到我的github上下载: socket.io打造的公共聊天室
最后,欢迎大家无聊的时候来我的聊天室聊天哦!
使用socket.io打造公共聊天室的更多相关文章
- Express+Socket.IO 实现简易聊天室
代码地址如下:http://www.demodashi.com/demo/12477.html 闲暇之余研究了一下 Socket.io,搭建了一个简易版的聊天室,如有不对之处还望指正,先上效果图: 首 ...
- AngularJS+Node.js+socket.io 开发在线聊天室
所有文章搬运自我的个人主页:sheilasun.me 不得不说,上手AngularJS比我想象得难多了,把官网提供的PhoneCat例子看完,又跑到慕课网把大漠穷秋的AngularJS实战系列看了一遍 ...
- node+express+socket.io制作一个聊天室功能
首先是下载包: npm install express npm install socket.io 建立文件: 服务器端代码:server.js var http=require("http ...
- 利用socket.io构建一个聊天室
利用socket.io来构建一个聊天室,输入自己的id和消息,所有的访问用户都可以看到,类似于群聊. socket.io 这里只用来做一个简单的聊天室,官网也有例子,很容易就做出来了.其实主要用的东西 ...
- Socket.io文字直播聊天室的简单代码
直接上代码吧,被注释掉的主要是调试代码,和技术选型的测试代码 var app = require('express')(); var server = require('http').Server(a ...
- 利用socket.io+nodejs打造简单聊天室
代码地址如下:http://www.demodashi.com/demo/11579.html 界面展示: 首先展示demo的结果界面,只是简单消息的发送和接收,包括发送文字和发送图片. ws说明: ...
- java基于socket公共聊天室的实现
项目:一个公共聊天室功能的实现,实现了登录聊天,保存聊天记录等功能. 一.实现代码 1.客户端 ChatClient.java import java.io.BufferedReader; impor ...
- Node+Express+MongoDB + Socket.io搭建实时聊天应用
Node+Express+MongoDB + Socket.io搭建实时聊天应用 前言 本来开始写博客的时候只是想写一下关于MongoDB的使用总结的,后来觉得还不如干脆写一个node项目实战教程实战 ...
- Node+Express+MongoDB + Socket.io搭建实时聊天应用实战教程(二)--node解析与环境搭建
前言 本来开始写博客的时候只是想写一下关于MongoDB的使用总结的,后来觉得还不如干脆写一个node项目实战教程实战.写教程一方面在自己写的过程中需要考虑更多的东西,另一方面希望能对node入门者有 ...
随机推荐
- 九度OJ 1373 整数中1出现的次数(从1到n整数中1出现的次数)
题目地址:http://ac.jobdu.com/problem.php?pid=1373 题目描述: 亲们!!我们的外国友人YZ这几天总是睡不好,初中奥数里有一个题目一直困扰着他,特此他向JOBDU ...
- ubuntu vim 插件安装
参考:http://blog.sina.com.cn/s/blog_00f0230d0100y7ih.html 不过由于时间久远,有些已经失效,以上是我的修改过程 参考:https://github. ...
- VMware中Ubuntu忘记密码的解决办法
在VMware中安装了Ubuntu 11.04,经过了一个长假,再次登录的时候居然进不去了,一开始不知道怎样在虚拟机中进入到Grub启动界面,网上搜索了一番,按照以下步骤重新为用户设定了新密码. 重启 ...
- ubuntu14.04+opencv 3.0+python2.7安装及测试
本文记录了ubuntu下使用源码手动安装opencv的过程.步骤来自opencv官网 此外记录了在python中安装及载入opencv的方法. 1.安装opencv所需的库(编译器.必须库.可选库) ...
- WORDPRESS插件开发(二)HELLO WORLD改进版
在上一篇文章中WORDPRESS插件开发(一)HELLO WORLD,演示了Hello World的最简单实现,只是在每篇文章的后面加入Hello World字符,而且字符也是写死的. 如果用户需要自 ...
- php购物车原理
<?php/*购物车原理在产品展示页面时(如 shop.php?id=888),点击购买或添加到购物车时,根据相应的产品标识符(如 id),查询相应的数据库,如果查询表示有此产品,用 $_SES ...
- jquery 中的 this 和 $(this)
this,表示当前的上下文对象是一个html对象,可以调用html对象所拥有的属性,方法 $(this),代表的上下文对象是一个jquery的上下文对象,可以调用jquery的方法和属性值. 亦即: ...
- TDirectory.IsEmpty判断指定目录是否为空
使用函数: System.IOUtils.TDirectory.IsEmpty class function IsEmpty(const Path: string): Boolean; static; ...
- POJ 1905 Expanding Rods 二分答案几何
题目:http://poj.org/problem?id=1905 恶心死了,POJ的输出一会要lf,一会要f,而且精度1e-13才过,1e-12都不行,错了一万遍终于对了. #include < ...
- POJ 3126 Prime Path 素数筛,bfs
题目: http://poj.org/problem?id=3126 困得不行了,没想到敲完一遍直接就A了,16ms,debug环节都没进行.人品啊. #include <stdio.h> ...