├── .bowerrc ├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── modules ├── config │ └── configUtils.js ├── logger │ └── logUtils.js └── other │ └── pathUtils.js ├── package.json ├── public ├── javascripts │ └── client.js └── stylesheets │ └── style.css └── web ├── middlewares └── socketHandler.js ├── www.js └── www ├── routers └── chat.js └── views ├── chat.ejs └── login.ejs /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "strict-ssl": false, 3 | "registry": "http://bower.herokuapp.com" 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | #idea 6 | .idea 7 | .DS_Store 8 | 9 | # config files 10 | *.cfg 11 | *.ini 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.rdb 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | # Commenting this out is preferred by some people, see 33 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 34 | node_modules 35 | 36 | # Users Environment Variables 37 | .lock-wscript 38 | 39 | #test*.js 40 | /test/test*.js 41 | 42 | #bower 43 | bower_components 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mo Ye 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SocketChat 2 | ========== 3 | 基于socket.io的聊天室 4 | 5 | 环境变量设置 6 | ====== 7 | linux下: 8 | ```shell 9 | export chat_home=/xx/xxx/repo/SocketChat 10 | ``` 11 | 12 | windows下: 13 | 设置系统变量 `chat_home` 14 | d:\repo\SocketChat 15 | 16 | 新建config.cfg,置于项目根目录 17 | ====== 18 | ```JSON 19 | { 20 | "SECRET": "chat__socket", 21 | "www_port": 3000 22 | } 23 | ``` 24 | 源码下载后,请在项目目录下依次执行命令: 25 | ===== 26 | ```shell 27 | $ sudo npm install 28 | $ sudo bower install 29 | ``` 30 | Node版本 31 | ===== 32 | v0.11.14 33 | 34 | 35 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SocketChat", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/rockdragon/SocketChat", 5 | "authors": [ 6 | "Locke " 7 | ], 8 | "description": "Chat room scaffolding", 9 | "main": "./web/www.js", 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "jquery": "2.1.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/config/configUtils.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var cfgFileName = 'config.cfg'; 5 | var cache = {}; 6 | var filePath = path.join(process.env.chat_home, cfgFileName); 7 | 8 | function getConfigs() { 9 | if (!cache[cfgFileName]) { 10 | if (!process.env.cloudDriveConfig) { 11 | process.env.cloudDriveConfig = filePath; 12 | } 13 | if (fs.existsSync(process.env.cloudDriveConfig)) { 14 | var contents = fs.readFileSync(process.env.cloudDriveConfig, {encoding: 'utf-8'}); 15 | cache[cfgFileName] = JSON.parse(contents); 16 | } 17 | } 18 | return cache[cfgFileName]; 19 | } 20 | module.exports.getConfigs = getConfigs; 21 | 22 | function fileChanged(curr, prev){ 23 | if(curr.mtime !== prev.mtime) { 24 | console.log('config has been changed. curr mtime is: ', 25 | curr.mtime, 'prev mtime was: ' + prev.mtime); 26 | if (fs.existsSync(filePath)) { 27 | var contents = fs.readFileSync(filePath, {encoding: 'utf-8'}); 28 | cache[cfgFileName] = JSON.parse(contents); 29 | console.log(cache[cfgFileName]); 30 | } 31 | } 32 | } 33 | fs.watchFile(filePath, fileChanged); 34 | 35 | module.exports.isDevelopment = function(){ 36 | return getConfigs().node_env === 'development'; 37 | }; 38 | -------------------------------------------------------------------------------- /modules/logger/logUtils.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | var pathUtils = require('../other/pathUtils'); 5 | var getAbsolutePath = require('../other/pathUtils').getAbsolutePath; 6 | var logPath = getAbsolutePath('logs/'); 7 | 8 | if (!fs.existsSync(logPath)) { 9 | pathUtils.mkdirAbsoluteSync(logPath); 10 | } 11 | 12 | var logger = new (winston.Logger)({ 13 | transports: [ 14 | new (winston.transports.Console)({colorize: true}), 15 | new (winston.transports.DailyRotateFile)({ 16 | name: 'file', 17 | datePattern: '.yyyy-MM-dd.log', 18 | json: false, 19 | filename: path.join(logPath, 'log') 20 | }) 21 | ] 22 | }); 23 | module.exports = logger; -------------------------------------------------------------------------------- /modules/other/pathUtils.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var rimraf = require('rimraf'); 4 | var _ = require('underscore'); 5 | /* 6 | recursive create directory 7 | @dirPath: absolute path 8 | */ 9 | module.exports.mkdirAbsoluteSync = function (dirPath, mode) { 10 | var delimiter = path.sep; 11 | dirPath = dirPath.trim(delimiter); 12 | var currentPath = ''; 13 | var pathParts = dirPath.split(delimiter); 14 | for (var i = 0; i < pathParts.length; i++) { 15 | currentPath += (process.platform === 'win32' && pathParts[i].contains(':') ? '' : delimiter) + pathParts[i]; 16 | if (!fs.existsSync(currentPath)) { 17 | fs.mkdirSync(currentPath, mode || 0755); 18 | } 19 | } 20 | }; 21 | 22 | /* 23 | recursive delete path entirely 24 | @dirPath: absolute path 25 | */ 26 | module.exports.deleteTreeSync = function (dirPath) { 27 | if (fs.existsSync(dirPath)) { 28 | rimraf.sync(dirPath); 29 | } 30 | }; 31 | 32 | module.exports.renameSync = function (dirPath, newName) { 33 | console.log(dirPath, newName); 34 | if (fs.existsSync(dirPath)) { 35 | var dir = path.dirname(dirPath); 36 | var newPath = path.join(dir, newName); 37 | fs.renameSync(dirPath, newPath); 38 | return newPath; 39 | } 40 | }; 41 | 42 | /* 43 | concatenate part with slash / 44 | * */ 45 | function join() { 46 | var args = Array.prototype.slice.call(arguments); 47 | args = args || []; 48 | if (args.length > 1) { 49 | return args.join('/').replace(/[\/]{2,}/g, '/'); 50 | } 51 | return ''; 52 | } 53 | module.exports.join = join; 54 | 55 | /* 56 | get all sub-directories in absolute form 57 | */ 58 | function getSubDirectories(dir) { 59 | var res = []; 60 | var files = fs.readdirSync(dir); 61 | _.each(files, function (f) { 62 | var filePath = path.join(dir, f); 63 | if (fs.statSync(filePath).isDirectory()) 64 | res.push(filePath); 65 | }); 66 | return res; 67 | } 68 | module.exports.getSubDirectories = getSubDirectories; 69 | /* 70 | get all sub-directories name in 71 | */ 72 | function getSubDirNames(dir) { 73 | var res = []; 74 | var files = fs.readdirSync(dir); 75 | _.each(files, function (f) { 76 | var filePath = path.join(dir, f); 77 | if (fs.statSync(filePath).isDirectory()) 78 | res.push(f); 79 | }); 80 | return res; 81 | } 82 | module.exports.getSubDirNames = getSubDirNames; 83 | 84 | function getRootURL(url) { 85 | var reg = new RegExp('http(s)?:\/\/[^\/]+/'); 86 | var m = reg.exec(url); 87 | return m ? m[0] : null; 88 | } 89 | module.exports.getRootURL = getRootURL; 90 | 91 | function getAbsolutePath(suffix) { 92 | var root = process.env.chat_home || process.cwd(); 93 | return path.join(root, suffix); 94 | } 95 | module.exports.getAbsolutePath = getAbsolutePath; 96 | 97 | function readFile(fileName) { 98 | return function (fn) { 99 | fs.readFile(fileName, {encoding: 'utf8', flag: 'r'}, fn); 100 | } 101 | } 102 | module.exports.readFile = readFile; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SocketChat", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "start": "node --harmony ./web/www.js" 6 | }, 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/rockdragon/SocketChat.git" 10 | }, 11 | "dependencies": { 12 | "rimraf": "*", 13 | "underscore": "*", 14 | "underscore.string": "*", 15 | "winston": "*", 16 | "socket.io": "^1.2.1", 17 | "co": "^4.0", 18 | "koa": "^0.14.0", 19 | "koa-mount": "*", 20 | "koa-ejs": "*", 21 | "koa-static": "*", 22 | "koa-router": "*", 23 | "koa-session": "*", 24 | "co-body": "*" 25 | }, 26 | "engines": { 27 | "node": ">=0.11.14" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/javascripts/client.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | // generate socket connection 3 | var socketClient = function () { 4 | return io.connect('/', { 5 | reconnection: false 6 | }); 7 | }; 8 | 9 | var socket = socketClient(); 10 | socket.on('connect', function () { 11 | console.log('connection established.'); 12 | }); 13 | socket.on('disconnect', function () { 14 | console.log('disconnected.'); 15 | }); 16 | 17 | // message handler 18 | socket.on('private', function (data) {//接收到私聊 19 | $('#messageBoard').append('[私聊] 来自 [' + data.name + ']: ' + data.msg + '\n'); 20 | }); 21 | function sendPrivate(session_id, msg) {//发送私聊 22 | socket.emit('private', {to_session_id: session_id, msg: msg}); 23 | } 24 | 25 | socket.on('broadcast', function (data) {//接收到广播 26 | $('#messageBoard').append('[广播] 来自 [' + data.name + ']: ' + data.msg + '\n'); 27 | }); 28 | function sendBroadcast(msg) {//发送广播 29 | socket.emit('broadcast', {msg: msg}); 30 | } 31 | 32 | //UI event 33 | function getSelectedItem() { 34 | var selected = $('#target').find('option:selected'); 35 | return {text: selected.text(), value: selected.val()}; 36 | } 37 | 38 | $('#clear').click(function () { 39 | $('#messageBoard').text(''); 40 | }); 41 | $('#send').click(function () { 42 | var message = $('#message'); 43 | var msg = message.val(); 44 | if (msg) { 45 | message.val(''); 46 | var selected = getSelectedItem(); 47 | if (selected.value === 'broadcast') { 48 | $('#messageBoard').append('[我] 对所有人说: ' + msg + '\n'); 49 | sendBroadcast(msg); 50 | } else { 51 | $('#messageBoard').append('[我] 对 [' + selected.text + '] 说: ' + msg + '\n'); 52 | sendPrivate(selected.value, msg); 53 | } 54 | } 55 | }); 56 | 57 | //定时获取其他人列表 58 | function updateOthers() { 59 | $.post('/chat/others', function (others) { 60 | for (var i = 0, len = others.length; i < len; i++) { 61 | var one = others[i]; 62 | var optGroup = $('#optGroup'); 63 | var opt = optGroup.find('option[value="' + one.session_id + '"]'); 64 | console.log(opt); 65 | if (opt.length === 0) { 66 | optGroup.append(''); 68 | } 69 | } 70 | setTimeout(updateOthers, 1000); 71 | }); 72 | } 73 | 74 | setTimeout(updateOthers, 1000); 75 | }); 76 | 77 | 78 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | body { 5 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 6 | margin: 0px; 7 | padding: 0px; 8 | height: -moz-calc(100% - 20px); 9 | height: -webkit-calc(100% - 20px); 10 | height: calc(100% - 20px); 11 | } 12 | section#chatroom { 13 | height: -moz-calc(100% - 80px); 14 | height: -webkit-calc(100% - 80px); 15 | height: calc(100% - 80px); 16 | background-color: #EFFFEC; 17 | } 18 | div#login, div#messages{ 19 | height: -moz-calc(100% - 35px); 20 | height: -webkit-calc(100% - 35px); 21 | height: calc(100% - 35px); 22 | padding: 10px; 23 | -moz-box-sizing:border-box; 24 | -webkit-box-sizing:border-box; 25 | box-sizing:border-box; 26 | } 27 | select#target { 28 | width: 120px; 29 | } 30 | input#message { 31 | width: -moz-calc(100% - 290px); 32 | width: -webkit-calc(100% - 290px); 33 | width: calc(100% - 290px); 34 | } 35 | input#send, input#clear { 36 | width: 74px; 37 | } 38 | textarea#messageBoard{ 39 | -moz-box-shadow:1px 1px 0 #E7E7E7; 40 | -moz-box-sizing:border-box; 41 | border-color:#CCCCCC #999999 #999999 #CCCCCC; 42 | border-style:solid; 43 | border-width:1px; 44 | font-family:arial,sans-serif; 45 | font-size:13px; 46 | height: -moz-calc(100% - 15px); 47 | height: -webkit-calc(100% - 15px); 48 | height: calc(100% - 15px); 49 | margin:10px auto; 50 | outline-color:-moz-use-text-color; 51 | outline-style:none; 52 | outline-width:medium; 53 | padding:10px; 54 | width:100%; 55 | } 56 | header{ 57 | background-color:#4192C1; 58 | text-align: right; 59 | margin-top: 15px; 60 | } 61 | header h1{ 62 | padding: 5px; 63 | padding-right: 15px; 64 | color: #FFFFFF; 65 | margin: 0px; 66 | } 67 | footer{ 68 | padding: 6px; 69 | background-color:#4192C1; 70 | color: #FFFFFF; 71 | bottom: 0; 72 | position: absolute; 73 | width: 100%; 74 | margin: 0px; 75 | margin-bottom: 10px; 76 | -moz-box-sizing:border-box; 77 | -webkit-box-sizing:border-box; 78 | box-sizing:border-box; 79 | } 80 | a { 81 | color: #00B7FF; 82 | } -------------------------------------------------------------------------------- /web/middlewares/socketHandler.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io'); 2 | var http = require('http'); 3 | var config = require('../../modules/config/configUtils').getConfigs(); 4 | 5 | module.exports.createServer = createServer; 6 | module.exports.addUser = addUser; 7 | module.exports.otherUsers = otherUsers; 8 | 9 | /* 10 | * 内部数据结构:用户列表 11 | * [{name, session_id, socket} ...] 12 | * */ 13 | var users = []; 14 | 15 | function findInUsers(session_id) {//通过session_id查找 16 | var index = -1; 17 | for (var j = 0, len = users.length; j < len; j++) { 18 | if (users[j].session_id === session_id) 19 | index = j; 20 | } 21 | return index; 22 | } 23 | function addUser(name, session_id) {//添加用户 24 | var index = findInUsers(session_id); 25 | if (index === -1) //not exist 26 | users.push({name: name, session_id: session_id, socket: null}); 27 | else { 28 | if (users[index].name !== name) //update name 29 | users[index].name = name; 30 | } 31 | } 32 | function setUserSocket(session_id, socket){//更新用户socket 33 | var index = findInUsers(session_id); 34 | if (index !== -1){ 35 | users[index].socket = socket; 36 | } 37 | } 38 | function findUser(session_id) {//查找 39 | var index = findInUsers(session_id); 40 | return index > -1 ? users[index] : null; 41 | } 42 | function otherUsers(session_id){//其他人 43 | var results = []; 44 | for (var j = 0, len = users.length; j < len; j++) { 45 | if (users[j].session_id !== session_id) 46 | results.push({session_id: users[j].session_id, name: users[j].name}); 47 | } 48 | return results; 49 | } 50 | 51 | /* 52 | * @app: Koa application 53 | * */ 54 | function createServer(app) { 55 | var server = http.Server(app.callback()); 56 | io = io(server); 57 | messageHandler(io); 58 | return server; 59 | } 60 | 61 | /* 62 | * socket event handler 63 | */ 64 | function messageHandler(io) { 65 | io.on('connection', function (socket) { 66 | console.log(socket.id, ' just connected.'); 67 | var sessionId = getSessionId(socket.request.headers.cookie, 'koa:sess'); 68 | if(sessionId){ 69 | setUserSocket(sessionId, socket); 70 | } 71 | 72 | socket.on('broadcast', function (data) { 73 | //广播 74 | var fromUser = findUser(sessionId); 75 | if(fromUser) { 76 | socket.broadcast.emit('broadcast', { 77 | name: fromUser.name, 78 | msg: data.msg 79 | }); 80 | } 81 | }); 82 | 83 | socket.on('private', function (data) { 84 | //私聊 {to_session_id, msg} 85 | var fromUser = findUser(sessionId); 86 | if(fromUser) { 87 | var toUser = findUser(data.to_session_id); 88 | if (toUser) 89 | toUser.socket.emit('private', { 90 | name: fromUser.name, 91 | msg: data.msg 92 | }); 93 | } 94 | }); 95 | 96 | socket.on('disconnect', function () { 97 | console.log(this.id, ' has been disconnect.'); 98 | }); 99 | }); 100 | } 101 | 102 | function getSessionId(cookieString, cookieName) { 103 | var matches = new RegExp(cookieName + '=([^;]+);', 'gmi').exec(cookieString); 104 | return matches[1] ? matches[1] : null; 105 | } -------------------------------------------------------------------------------- /web/www.js: -------------------------------------------------------------------------------- 1 | var koa = require('koa'); 2 | var mount = require('koa-mount'); 3 | var router = require('koa-router'); 4 | var render = require('koa-ejs'); 5 | var serve = require('koa-static'); 6 | var session = require('koa-session'); 7 | var getAbsolutePath = require('../modules/other/pathUtils').getAbsolutePath; 8 | var config = require("../modules/config/configUtils").getConfigs(); 9 | var logger = require("../modules/logger/logUtils"); 10 | var socketHandler = require('./middlewares/socketHandler'); 11 | 12 | //settings 13 | var app = koa(); 14 | app.keys = [config.SECRET]; 15 | app.use(session(app)); 16 | 17 | app.on('error', function(err){ 18 | logger.error(err, err.ctx); 19 | }); 20 | app.use(router(app)); 21 | app.use(serve(getAbsolutePath('public'))); 22 | app.use(serve(getAbsolutePath('bower_components'), { 23 | maxAge: 1000 * 86400 * 30 24 | })); 25 | render(app, { 26 | root: getAbsolutePath('web/views'), 27 | layout: false, 28 | viewExt: 'ejs', 29 | cache: false, 30 | debug: true 31 | }); 32 | 33 | //routes 34 | var chat = require(getAbsolutePath('web/www/routers/chat')); 35 | app.use(mount('/chat', chat.middleware())); 36 | 37 | //listen 38 | var server = socketHandler.createServer(app); 39 | server.listen(config.www_port); 40 | console.log('listening on port', config.www_port); -------------------------------------------------------------------------------- /web/www/routers/chat.js: -------------------------------------------------------------------------------- 1 | var Router = require('koa-router'), 2 | router = new Router(); 3 | var parse = require('co-body'); 4 | var socketHandler = require('../../middlewares/socketHandler'); 5 | 6 | router.get('/', function *() { 7 | var session_id = this.cookies.get('koa:sess'); 8 | var name = this.session.name; 9 | console.log('session_id', session_id, 'name', name); 10 | if(session_id && name) {//添加到用户列表 11 | socketHandler.addUser(name, session_id); 12 | yield this.render('../www/views/chat'); 13 | } else { 14 | this.redirect('/chat/login'); 15 | } 16 | }); 17 | 18 | //登录 19 | router.get('/login', function*(){ 20 | yield this.render('../www/views/login') 21 | }); 22 | router.post('/login', function*(){ 23 | var body = yield parse(this); 24 | this.session.name = body.name || 'guest'; 25 | this.redirect('/chat') 26 | }); 27 | 28 | //获取其他人列表 29 | router.post('/others', function*(){ 30 | var session_id = this.cookies.get('koa:sess'); 31 | var name = this.session.name; 32 | if(session_id && name) { 33 | this.type = 'application/json'; 34 | this.body = socketHandler.otherUsers(session_id); 35 | } else { 36 | this.status = 404; 37 | } 38 | }); 39 | 40 | module.exports = router; -------------------------------------------------------------------------------- /web/www/views/chat.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket chat 6 | 7 | 8 | 9 | 12 |
13 |
14 | 20 | 21 | 22 | 23 |
24 | 25 |
26 |
27 |
28 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /web/www/views/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket chat 6 | 7 | 8 | 9 | 12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 |
20 | 23 | 24 | 25 | --------------------------------------------------------------------------------