使用socket.io打造公共聊天室

时间:2021-09-13 08:42:03

  最近的计算机网络课上老师开始讲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)从底层的网络层接收数据时,它需要将接收到的数据定向到三个进程中的一个。而每个进程都有一个或多个套接字,它相当于从网络向进程传递数据和从进程向网络传递数据的门户。

  使用socket.io打造公共聊天室

  如上图,在接收端,运输层检查报文段中的字段,标识出接收套接字,进而将报文定向该套接字。这样将运输层报文段中的数据交付到正确的套接字的工作称为多路分解。同样在源主机从不同套接字中收集数据块,并为每个数据封装上首部信息(用于分解)从而生成报文段,然后将报文段传递到网络层,这样的工作叫做多路复用

  

  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

    官网: http://socket.io/docs/

  要打造一个聊天室应用,首先确定聊天中服务器需要接收的几个事件响应,分为如下几点:

    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打造的公共聊天室

  最后,欢迎大家无聊的时候来我的聊天室聊天哦!