通过阅读一个小的nodeJS project来学习nodeJS是一个不错的主意,而ry/node_chat代码相当精巧,而且贴近我们日常场景,所以选择它来开始我的nodeJS project源码阅读:
ry/node_chat的代码结构如下:
我会主要分析服务器端的两个nodeJS文件,fu.js和server.js,而client.js涉及的部分只是为了帮助理解server.js的逻辑。
fu.js的源代码分析(汉字注释部分是我添加的内容)
// 通过require引用http, fs, sys, url对象或者其中的方法 var createServer = require("http").createServer; var readFile = require("fs").readFile; var sys = require("sys"); var url = require("url"); // 默认关闭DEBUG DEBUG = false; // 下面出现的fu全部代表exports,所以以'fu.'开头的都可以通过require(./fu)被外部使用 var fu = exports; // 对于一些没有handler的url请求,我们可以返回一个"Not Found"页面 var NOT_FOUND = "Not Found/n"; function notFound(req, res) { res.writeHead(404, { "Content-Type": "text/plain" , "Content-Length": NOT_FOUND.length }); res.end(NOT_FOUND); } // getMap就是一个列表,path到handler的映射,所以确定path以后我们就可以使用getMap[path]来处理 var getMap = {}; fu.get = function (path, handler) { getMap[path] = handler; }; /** * 这里重要的东西就是传给http的createServer的callback,它主要处理GET/HEAD请求 * * 其中为res定义了两个新的方法simpleText和simpleJSON,它们将在handler中被使用 * * 那么通过url.parse方法获得pathname,然后在getMap中找到对应的handler,下面就 * 是把req, res交给handler来具体处理了。 * * 关于req.url可以参考:http://nodejs.org/docs/v0.4.6/api/url.html来学些url中 * 没一部分代表什么意思。 * * JSON.stringify是用来将一个JSON Object转化为字符串 */ var server = createServer(function (req, res) { if (req.method === "GET" || req.method === "HEAD") { var handler = getMap[url.parse(req.url).pathname] || notFound; res.simpleText = function (code, body) { res.writeHead(code, { "Content-Type": "text/plain" , "Content-Length": body.length }); res.end(body); }; res.simpleJSON = function (code, obj) { var body = new Buffer(JSON.stringify(obj)); res.writeHead(code, { "Content-Type": "text/json" , "Content-Length": body.length }); res.end(body); }; handler(req, res); } }); // 设置node server使用的host和port,调用了listen以后,就可以接受client的请求了 fu.listen = function (port, host) { server.listen(port, host); sys.puts("Server at http://" + (host || "127.0.0.1") + ":" + port.toString() + "/"); }; // 关闭node server,就是调用一下close方法,最多可以再加一个console.log来输出些提示信息 fu.close = function () { server.close(); }; // 如果path是tb/shawn.json,那么extname返回.json function extname (path) { var index = path.lastIndexOf("."); return index < 0 ? "" : path.substring(index); } /** * 这个是通过filename返回一个handler,而handler返回以后 * 我们可以通过调用handler(req, res)来执行loadResponseData * * loadResponseData里面一个重要的东西就是异步的方法readFile * 它主要是用来读取文件,然后执行它的callback方法 */ fu.staticHandler = function (filename) { var body, headers; var content_type = fu.mime.lookupExtension(extname(filename)); function loadResponseData(callback) { if (body && headers && !DEBUG) { callback(); return; } sys.puts("loading " + filename + "..."); readFile(filename, function (err, data) { if (err) { sys.puts("Error loading " + filename); } else { body = data; headers = { "Content-Type": content_type , "Content-Length": body.length }; if (!DEBUG) headers["Cache-Control"] = "public"; sys.puts("static file " + filename + " loaded"); callback(); } }); } return function (req, res) { loadResponseData(function () { res.writeHead(200, headers); res.end(req.method === "HEAD" ? "" : body); }); } }; // stolen from jack- thanks fu.mime = { // returns MIME type for extension, or fallback, or octet-steam lookupExtension : function(ext, fallback) { return fu.mime.TYPES[ext.toLowerCase()] || fallback || 'application/octet-stream'; }, // List of most common mime-types, stolen from Rack. TYPES : { ".3gp" : "video/3gpp" , ".a" : "application/octet-stream" , ".ai" : "application/postscript" , ".aif" : "audio/x-aiff" , ".aiff" : "audio/x-aiff" , ".asc" : "application/pgp-signature" , ".asf" : "video/x-ms-asf" , ".asm" : "text/x-asm" , ".asx" : "video/x-ms-asf" , ".atom" : "application/atom+xml" , ".au" : "audio/basic" , ".avi" : "video/x-msvideo" , ".bat" : "application/x-msdownload" , ".bin" : "application/octet-stream" , ".bmp" : "image/bmp" , ".bz2" : "application/x-bzip2" , ".c" : "text/x-c" , ".cab" : "application/vnd.ms-cab-compressed" , ".cc" : "text/x-c" , ".chm" : "application/vnd.ms-htmlhelp" , ".class" : "application/octet-stream" , ".com" : "application/x-msdownload" , ".conf" : "text/plain" , ".cpp" : "text/x-c" , ".crt" : "application/x-x509-ca-cert" , ".css" : "text/css" , ".csv" : "text/csv" , ".cxx" : "text/x-c" , ".deb" : "application/x-debian-package" , ".der" : "application/x-x509-ca-cert" , ".diff" : "text/x-diff" , ".djv" : "image/vnd.djvu" , ".djvu" : "image/vnd.djvu" , ".dll" : "application/x-msdownload" , ".dmg" : "application/octet-stream" , ".doc" : "application/msword" , ".dot" : "application/msword" , ".dtd" : "application/xml-dtd" , ".dvi" : "application/x-dvi" , ".ear" : "application/java-archive" , ".eml" : "message/rfc822" , ".eps" : "application/postscript" , ".exe" : "application/x-msdownload" , ".f" : "text/x-fortran" , ".f77" : "text/x-fortran" , ".f90" : "text/x-fortran" , ".flv" : "video/x-flv" , ".for" : "text/x-fortran" , ".gem" : "application/octet-stream" , ".gemspec" : "text/x-script.ruby" , ".gif" : "image/gif" , ".gz" : "application/x-gzip" , ".h" : "text/x-c" , ".hh" : "text/x-c" , ".htm" : "text/html" , ".html" : "text/html" , ".ico" : "image/vnd.microsoft.icon" , ".ics" : "text/calendar" , ".ifb" : "text/calendar" , ".iso" : "application/octet-stream" , ".jar" : "application/java-archive" , ".java" : "text/x-java-source" , ".jnlp" : "application/x-java-jnlp-file" , ".jpeg" : "image/jpeg" , ".jpg" : "image/jpeg" , ".js" : "application/javascript" , ".json" : "application/json" , ".log" : "text/plain" , ".m3u" : "audio/x-mpegurl" , ".m4v" : "video/mp4" , ".man" : "text/troff" , ".mathml" : "application/mathml+xml" , ".mbox" : "application/mbox" , ".mdoc" : "text/troff" , ".me" : "text/troff" , ".mid" : "audio/midi" , ".midi" : "audio/midi" , ".mime" : "message/rfc822" , ".mml" : "application/mathml+xml" , ".mng" : "video/x-mng" , ".mov" : "video/quicktime" , ".mp3" : "audio/mpeg" , ".mp4" : "video/mp4" , ".mp4v" : "video/mp4" , ".mpeg" : "video/mpeg" , ".mpg" : "video/mpeg" , ".ms" : "text/troff" , ".msi" : "application/x-msdownload" , ".odp" : "application/vnd.oasis.opendocument.presentation" , ".ods" : "application/vnd.oasis.opendocument.spreadsheet" , ".odt" : "application/vnd.oasis.opendocument.text" , ".ogg" : "application/ogg" , ".p" : "text/x-pascal" , ".pas" : "text/x-pascal" , ".pbm" : "image/x-portable-bitmap" , ".pdf" : "application/pdf" , ".pem" : "application/x-x509-ca-cert" , ".pgm" : "image/x-portable-graymap" , ".pgp" : "application/pgp-encrypted" , ".pkg" : "application/octet-stream" , ".pl" : "text/x-script.perl" , ".pm" : "text/x-script.perl-module" , ".png" : "image/png" , ".pnm" : "image/x-portable-anymap" , ".ppm" : "image/x-portable-pixmap" , ".pps" : "application/vnd.ms-powerpoint" , ".ppt" : "application/vnd.ms-powerpoint" , ".ps" : "application/postscript" , ".psd" : "image/vnd.adobe.photoshop" , ".py" : "text/x-script.python" , ".qt" : "video/quicktime" , ".ra" : "audio/x-pn-realaudio" , ".rake" : "text/x-script.ruby" , ".ram" : "audio/x-pn-realaudio" , ".rar" : "application/x-rar-compressed" , ".rb" : "text/x-script.ruby" , ".rdf" : "application/rdf+xml" , ".roff" : "text/troff" , ".rpm" : "application/x-redhat-package-manager" , ".rss" : "application/rss+xml" , ".rtf" : "application/rtf" , ".ru" : "text/x-script.ruby" , ".s" : "text/x-asm" , ".sgm" : "text/sgml" , ".sgml" : "text/sgml" , ".sh" : "application/x-sh" , ".sig" : "application/pgp-signature" , ".snd" : "audio/basic" , ".so" : "application/octet-stream" , ".svg" : "image/svg+xml" , ".svgz" : "image/svg+xml" , ".swf" : "application/x-shockwave-flash" , ".t" : "text/troff" , ".tar" : "application/x-tar" , ".tbz" : "application/x-bzip-compressed-tar" , ".tcl" : "application/x-tcl" , ".tex" : "application/x-tex" , ".texi" : "application/x-texinfo" , ".texinfo" : "application/x-texinfo" , ".text" : "text/plain" , ".tif" : "image/tiff" , ".tiff" : "image/tiff" , ".torrent" : "application/x-bittorrent" , ".tr" : "text/troff" , ".txt" : "text/plain" , ".vcf" : "text/x-vcard" , ".vcs" : "text/x-vcalendar" , ".vrml" : "model/vrml" , ".war" : "application/java-archive" , ".wav" : "audio/x-wav" , ".wma" : "audio/x-ms-wma" , ".wmv" : "video/x-ms-wmv" , ".wmx" : "video/x-ms-wmx" , ".wrl" : "model/vrml" , ".wsdl" : "application/wsdl+xml" , ".xbm" : "image/x-xbitmap" , ".xhtml" : "application/xhtml+xml" , ".xls" : "application/vnd.ms-excel" , ".xml" : "application/xml" , ".xpm" : "image/x-xpixmap" , ".xsl" : "application/xml" , ".xslt" : "application/xslt+xml" , ".yaml" : "text/yaml" , ".yml" : "text/yaml" , ".zip" : "application/zip" } };
server.js的源代码分析
// node server使用的host和port HOST = null; // localhost PORT = 8001; // when the daemon started var starttime = (new Date()).getTime(); // 通过memoryUsage每10秒中获得一次内存使用情况 var mem = process.memoryUsage(); // every 10 seconds poll for the memory. setInterval(function () { mem = process.memoryUsage(); }, 10*1000); /** * server.js将使用的nodejs module,其中fu是我们自己定义的 * 所以require的使用前面有一个"./" */ var fu = require("./fu"), sys = require("sys"), url = require("url"), qs = require("querystring"); /** * 为messages和sessions设定的阈值,在使用它们的地方我会再提及它们 */ var MESSAGE_BACKLOG = 200, SESSION_TIMEOUT = 60 * 1000; /** * channel的定义主要包含四个部分: * * messages和callbacks数组,messages就是用来存放消息的,每个element是 * json对象,如下面定义的m;而callbacks数组是用来存放暂时不能满足请求 * 的回调函数,也就是说它们会推迟执行 * * appendMessage就是用来构造message,然后将它push到messages数组中,而起 * 它会通过一个while循环去执行callbacks数组里面的回调函数,通过它们将这个消 * 息发送出去,另外它会检查messages数组中存放的消息数量是否超过了阈值(MESSAGE_BACKLOG) * 如果超过就从头开始扔掉一些老的message,只到恢复正常的阈值范围之内。 * * query主要从messages数组中查询某个时间段内的所有message,如果查找到了就会 * 立即执行callback函数,否则就不得不把callback包装成JSON对象丢入callbacks中, * 等待机会来的时候去执行 * * 对于那些在callbacks数组中存放了太久的callback我们会定期清除它们,因为 * 这些callback很可能已经是垃圾callback了,没有必要去执行它们了。 */ var channel = new function () { var messages = [], callbacks = []; this.appendMessage = function (nick, type, text) { var m = { nick: nick , type: type // "msg", "join", "part" , text: text , timestamp: (new Date()).getTime() }; switch (type) { case "msg": sys.puts("<" + nick + "> " + text); break; case "join": sys.puts(nick + " join"); break; case "part": sys.puts(nick + " part"); break; } messages.push( m ); while (callbacks.length > 0) { callbacks.shift().callback([m]); } while (messages.length > MESSAGE_BACKLOG) messages.shift(); }; this.query = function (since, callback) { var matching = []; for (var i = 0; i < messages.length; i++) { var message = messages[i]; if (message.timestamp > since) matching.push(message) } if (matching.length != 0) { callback(matching); } else { callbacks.push({ timestamp: new Date(), callback: callback }); } }; // clear old callbacks // they can hang around for at most 30 seconds. setInterval(function () { var now = new Date(); while (callbacks.length > 0 && now - callbacks[0].timestamp > 30*1000) { callbacks.shift().callback([]); } }, 3000); }; /** * 下面就是session方面的管理,这里是用过一个sessions列表管理的 * * 在createSession里面会根据一些条件判断是否需要创建新的session * 而每个session对象由下面几个部分组成的: * nick, id, timestamp属性, poke方法就是用来更新timestamp的,而 * destory是用来销毁一个session,会通过appendMessage发送一个part * 消息出去 * */ var sessions = {}; function createSession (nick) { if (nick.length > 50) return null; if (/[^/w_/-^!]/.exec(nick)) return null; for (var i in sessions) { var session = sessions[i]; if (session && session.nick === nick) return null; } var session = { nick: nick, id: Math.floor(Math.random()*99999999999).toString(), timestamp: new Date(), poke: function () { session.timestamp = new Date(); }, destroy: function () { channel.appendMessage(session.nick, "part"); delete sessions[session.id]; } }; sessions[session.id] = session; return session; } /** * 就像清除callback类似,我们也会定期清除一些过期的session * 主要这里会遍历sessions中的所有session对象,而不能像处理 * callback那样,当遇到不满足条件的时候就结束了,因为session * 会有可能会更新timestamp,而callbacks中的所以callback可以 * 是按照timestamp排序了的,所以它可以提前终止后面的判断 */ // interval to kill off old sessions setInterval(function () { var now = new Date(); for (var id in sessions) { if (!sessions.hasOwnProperty(id)) continue; var session = sessions[id]; if (now - session.timestamp > SESSION_TIMEOUT) { session.destroy(); } } }, 1000); // server开始监听了,HOST + PORT fu.listen(Number(process.env.PORT || PORT), HOST); /** * 下面这些代码都是route信息,就是对于给定路径调用相应的handler * 其中对于"/", "/style.css", "/client.js", "/jquery-1.2.6.min.js" * 都是要在客户端使用或者执行的,是一些固定的东西,统一由staticHandler * 来处理 */ fu.get("/", fu.staticHandler("index.html")); fu.get("/style.css", fu.staticHandler("style.css")); fu.get("/client.js", fu.staticHandler("client.js")); fu.get("/jquery-1.2.6.min.js", fu.staticHandler("jquery-1.2.6.min.js")); /** * 下面这些get需要结合客户端进行讲解,所以我在后面结合client.js来分析 * 逻辑处理过程,也就是谁触发了这个url * * 从代码层次我先介绍每个get对相应路径做了些什么事情 */ /** * 查看当前的nicks,只要到sessions取出所有的nicks,通过 * simpleJSON发送给client */ fu.get("/who", function (req, res) { var nicks = []; for (var id in sessions) { if (!sessions.hasOwnProperty(id)) continue; var session = sessions[id]; nicks.push(session.nick); } res.simpleJSON(200, { nicks: nicks , rss: mem.rss }); }); /** * 对于join请求,首先要通过createSession为其创建一个session * 然后通过chanel的appendMessage发送一个join消息,最后再使用 * simpleJSON把一些结果(id, nick, rss, starttime)发送给客户端 * * qs.parse(url.parse(req.url).query)其实做了两件事情,一个是 * 从url中得到query部分的字符串,然后交给querystring去解析返回 * 一个JSON object * * 发送给client的消息是通过res.simpleJSON发送的 * * 后面对于的代码分析,parse和simleJSON就不再提及了,你懂的 */ fu.get("/join", function (req, res) { var nick = qs.parse(url.parse(req.url).query).nick; if (nick == null || nick.length == 0) { res.simpleJSON(400, {error: "Bad nick."}); return; } var session = createSession(nick); if (session == null) { res.simpleJSON(400, {error: "Nick in use"}); return; } //sys.puts("connection: " + nick + "@" + res.connection.remoteAddress); channel.appendMessage(session.nick, "join"); res.simpleJSON(200, { id: session.id , nick: session.nick , rss: mem.rss , starttime: starttime }); }); // 对于part请求,需要根据id得到其对应的session,将其从sessions清除 fu.get("/part", function (req, res) { var id = qs.parse(url.parse(req.url).query).id; var session; if (id && sessions[id]) { session = sessions[id]; session.destroy(); } res.simpleJSON(200, { rss: mem.rss }); }); /** * 对于recv请求,url query中的since不能为空,否则下面的事情不干 * * 根据id更新其对应session的timestamp * 根据since到channel中查找满足条件的messages,然后发送给client * */ fu.get("/recv", function (req, res) { if (!qs.parse(url.parse(req.url).query).since) { res.simpleJSON(400, { error: "Must supply since parameter" }); return; } var id = qs.parse(url.parse(req.url).query).id; var session; if (id && sessions[id]) { session = sessions[id]; session.poke(); } var since = parseInt(qs.parse(url.parse(req.url).query).since, 10); channel.query(since, function (messages) { if (session) session.poke(); res.simpleJSON(200, { messages: messages, rss: mem.rss }); }); }); /** * 对于send请求,client发来了消息,所以更细其session的timestamp,然后 * 将这个消息放到channel的messages数组中(appendMessage) */ fu.get("/send", function (req, res) { var id = qs.parse(url.parse(req.url).query).id; var text = qs.parse(url.parse(req.url).query).text; var session = sessions[id]; if (!session || !text) { res.simpleJSON(400, { error: "No such session id" }); return; } session.poke(); channel.appendMessage(session.nick, "msg", text); res.simpleJSON(200, { rss: mem.rss }); });