首先我們先來看看最一開始時,要建立連線會那些事情,假設我們的 server 已經開啟 :
var io = require('socket.io').listen(8080);
io.sockets.on('connection', function (socket) {
console.log("Hello xxxx client");
});
接下來我們要從前端開始追蹤它做了那些事情。
Client 端它做了什麼呢 ??
Socket.io-client 建立連線的地方
在最開始時,一定是前端會去進行連線,那我們來看看他在socket.io-client
中什麼地方行處理。
前端與 server 端連結的程式碼如下,從下面程式碼可知,我們執行io('xxxx')
時,他就會去後端建立連線。
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io('http://localhost');
socket.on('connect', function(){});
</script>
然後我們來看看 socket.io-client 的這段程式碼長啥樣子,如下,但下面程式碼我們只要先注意newConnection
裡面做的事情,因為我們是要建立新的連線。
function lookup (uri, opts) {
....
if (newConnection) {
debug('ignoring socket cache for %s', source);
io = Manager(source, opts);
} else {
if (!cache[id]) {
debug('new io instance for %s', source);
cache[id] = Manager(source, opts);
}
io = cache[id];
}
if (parsed.query && !opts.query) {
opts.query = parsed.query;
}
return io.socket(parsed.path, opts);
}
下面這段程式碼為manager
裡面的程式碼,大部份都是在進行屬性初使化,並且還有一些重連機制的設定,這邊我們直接來看最下面的this.open
部份。
function Manager (uri, opts) {
if (!(this instanceof Manager)) return new Manager(uri, opts);
if (uri && ('object' === typeof uri)) {
opts = uri;
uri = undefined;
}
opts = opts || {};
...
if (this.autoConnect) this.open();
}
this.open 裡面的程式碼如下,這段就是socket.io-client
建立連線的地方,但這邊要注意,我們實際建立連線的地方為eio(this.uri, this.opts)
這段程式碼,所以我們接下來要去看engion.io-client
的程式碼。
Manager.prototype.open =
Manager.prototype.connect = function (fn, opts) {
debug('readyState %s', this.readyState);
if (~this.readyState.indexOf('open')) return this;
debug('opening %s', this.uri);
this.engine = eio(this.uri, this.opts);
var socket = this.engine;
var self = this;
this.readyState = 'opening';
this.skipReconnect = false;
....
return this;
};
engine.io-client 實際建立連線的地方
這段程式碼是我們實際建立連線的地方,首先他會判斷我們的transport
是什麼,是要用websocket
還是polling
,然後確定好後,就使用this.createTransport
來建立實際上要用的transport
,最後在將要使用的 transport 開啟,然後他裡面將會建立連線了。
這裡要注意一下,如果在建立連線時什麼都沒有指定,那他會先使用 polling 來進行連線,並且在升級為 websocket。
Socket.prototype.open = function () {
var transport;
if (this.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf('websocket') !== -1) {
transport = 'websocket';
} else if (0 === this.transports.length) {
// Emit error on next tick so it can be listened to
var self = this;
setTimeout(function () {
self.emit('error', 'No transports available');
}, 0);
return;
} else {
transport = this.transports[0];
}
this.readyState = 'opening';
// Retry with the next transport if the transport is disabled (jsonp: false)
try {
transport = this.createTransport(transport);
} catch (e) {
this.transports.shift();
this.open();
return;
}
transport.open(); // 使用指定的 transport 來建立連線
this.setTransport(transport);
};
前端流程圖
前端這邊,我們最後補上一張流程圖,好讓各位官爺更好的追 code 。
後端接受到請求後,它做了啥 ??
每當我們 socket io server 啟動時,會將 engine io server attach 到 http server (srv) 上監聽request
事件,下面程式碼的 srv 就是我們 attach 的 server。
Server.prototype.initEngine = function(srv, opts){
// initialize engine
debug('creating engine.io instance with opts %j', opts);
this.eio = engine.attach(srv, opts);
// attach static file serving
if (this._serveClient) this.attachServe(srv);
// Export http server
this.httpServer = srv;
// bind to engine events
this.bind(this.eio);
};
當收到一個 http 請求時,會轉到 engine.io 下面這段程式碼中,然後會在handleRequest進行主要處理
,它這裡只會簡單的檢查一下 req 這個參數,而這參數就是我們 http request 請求內容。
server.on('request', function (req, res) {
if (check(req)) {
debug('intercepting request for path "%s"', path);
if ('OPTIONS' === req.method && 'function' === typeof options.handlePreflightRequest) {
options.handlePreflightRequest.call(server, req, res);
} else {
self.handleRequest(req, res);
}
} else {
for (var i = 0, l = listeners.length; i < l; i++) {
listeners[i].call(server, req, res);
}
}
});
接下來,下面這段程式碼為handleRequest
程式碼,主要用來處理這個請求。首先他會先使用verify
進行檢查,看看這一次的請求是不是合法的,而它主要檢查兩個點,首先是transport
的檢查,我們要先確定req._query.transport
這個參數是否合法,因為我們是要用這參數來決定我們要用那種傳輸方式,而第二個檢查為sid check
,sid
就是每個 client 的 session id,在這邊會檢查該條 sid 是否存在以及如果存在是否合法。
檢查完這個 request 請求後,接下來就看看這個請求的 client 有沒有建立請起,如果沒有則執行handshake
,有的話則執行onRequest
。
Server.prototype.handleRequest = function (req, res) {
debug('handling "%s" http request "%s"', req.method, req.url);
this.prepare(req);
req.res = res;
var self = this;
this.verify(req, false, function (err, success) {
if (!success) {
sendErrorMessage(req, res, err);
return;
}
if (req._query.sid) {
debug('setting new request for existing client');
self.clients[req._query.sid].transport.onRequest(req);
} else {
self.handshake(req._query.transport, req);
}
});
};
接下來我們來看看,如果這個 client 還沒建立的流程,也就是要進行handshake
,
這個方法最主要的功能就是用來建立一條新的連線,然後最後會發送一個connection
事件到socket.io
。
Server.prototype.handshake 原始碼
Server.prototype.handshake = function (transportName, req) {
var self = this;
this.generateId(req, function (err, id) {
if (err) {
sendErrorMessage(req, req.res, Server.errors.BAD_REQUEST);
return;
}
debug('handshaking client "%s"', id);
try {
var transport = new transports[transportName](req);
……
var socket = new Socket(id, self, transport, req);
……
transport.onRequest(req);
self.clients[id] = socket;
self.clientsCount++;
socket.once('close', function () {
delete self.clients[id];
self.clientsCount--;
});
self.emit('connection', socket);
});
};
當我們engion.io
已經建立好連線後,它會發送connection
訊息到socket.io
,然後它在下面這段程式碼,會收到這個事件,然後他這邊會建立一個 client,並且處理一些連線的事務。
Server.prototype.bind = function(engine){
this.engine = engine;
this.engine.on('connection', this.onconnection.bind(this));
return this;
};
Server.prototype.onconnection = function(conn){
debug('incoming connection with id %s', conn.id);
var client = new Client(this, conn);
client.connect('/');
return this;
};
然後我們來看看client.connection
這段程式碼做的事情,這裡它主要會將這位client
加入到nsp
中也就是 io 的 namespace 裡,但這邊 socket 還沒有產生喔。
Client.prototype.connect = function(name, query){
debug('connecting to namespace %s', name);
var nsp = this.server.nsps[name];
if (!nsp) {
this.packet({ type: parser.ERROR, nsp: name, data : 'Invalid namespace'});
return;
}
if ('/' != name && !this.nsps['/']) {
this.connectBuffer.push(name);
return;
}
var self = this;
var socket = nsp.add(this, query, function(){
self.sockets[socket.id] = socket;
self.nsps[nsp.name] = socket;
if ('/' == nsp.name && self.connectBuffer.length > 0) {
self.connectBuffer.forEach(self.connect, self);
self.connectBuffer = [];
}
});
};
然後在add
中,會將這個 client 產生一個 socket,然後將這條 socket 進行onconnect
,並且在最後,會執行self.emit('connection', socket)
這裡也就是我們在最上面,實際上觸發connection
事件的地方。
Namespace.prototype.add = function(client, query, fn){
debug('adding socket to nsp %s', this.name);
var socket = new Socket(this, client, query);
var self = this;
this.run(socket, function(err){
process.nextTick(function(){
if ('open' == client.conn.readyState) {
if (err) return socket.error(err.data || err.message);
// track socket
self.sockets[socket.id] = socket;
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket.onconnect();
if (fn) fn();
// fire user-set events
self.emit('connect', socket);
self.emit('connection', socket);
} else {
debug('next called after client was closed - ignoring socket');
}
});
});
return socket;
};
我們最後來看一下onconnect
實際上做了那些事情,這個方法是當我們 connect 確定建立起來後,會進行的動作,它會將這條 socket 連線加入到 nsp 裡的 connected 這個地方,這個屬性也可以讓我們知道,一個 nsp 中有那些 socket 在進行連線。
然後並且會將這條 socket ,加入到一個已自已 id 為名的房間,所以假設我們要追蹤某個 client ,也可以選擇加入到該名使用者為名的房間,這樣該名使用者收到的事件,我們也都可以收到,不過這只是變化用法,到不是這邊的重點。
最後他會執行this.packet
就是會將這個訊息,傳送到 client 端。
Socket.prototype.onconnect = function(){
debug('socket connected - writing packet');
this.nsp.connected[this.id] = this;
this.join(this.id);
var skip = this.nsp.name === '/' && this.nsp.fns.length === 0;
if (skip) {
debug('packet already sent in initial handshake');
} else {
this.packet({ type: parser.CONNECT });
}
};
後端的流程圖
最後,這邊將提供後端的流程圖,讓我們更容易的理解它的流程。