Socket.io 原始碼分析之建立連線

首先我們先來看看最一開始時,要建立連線會那些事情,假設我們的 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裡面做的事情,因為我們是要建立新的連線。

lookup 原始碼

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部份。

Manger 原始碼

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的程式碼。

Manger.prototype.open 原始碼傳送門

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 原始碼傳送門

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。

attachServe 傳送門

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 原始碼

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 checksid就是每個 client 的 session id,在這邊會檢查該條 sid 是否存在以及如果存在是否合法。

檢查完這個 request 請求後,接下來就看看這個請求的 client 有沒有建立請起,如果沒有則執行handshake,有的話則執行onRequest

handleRequest 原始碼

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,並且處理一些連線的事務。

bind 與 onconnection 原始碼

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 原始碼

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 原始碼

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 端。

onconnect 原始碼

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 });
  }
};

後端的流程圖

最後,這邊將提供後端的流程圖,讓我們更容易的理解它的流程。

comments powered by Disqus