WebSocketについて
socketについて
C言語で記述されたBerkeleyソケット(BSDソケット)は、プロセス間通信に使用されるインターネットソケットおよびその他のunixドメインソケット用のAPIを備えたライブラリです。 APIはPOSIXに相当するものであまり変更されていないため、POSIXソケットは基本的にBerkeleyソケットです。
最新のオペレーティングシステムはすべて、インターネットに接続するための標準インターフェイスになっているため、Berkeleyソケットインターフェイスが実装されています。
さまざまなプログラミング言語が同様のインターフェースを提供し、本質的にBSDソケットのC APIのラッパーです。
Pythonソケット
Gordon McMillanは、ソケットの概要と、Pythonの標準ソケットモジュールを使用してIPC(プロセス間通信)用のソケットを簡単に作成する方法について説明しました。
python2:http://docs.python.org/2/howto/sockets.html
python3:http://docs.python.org/3/howto/sockets.html
ソケットが作成されると、通信用のエンドポイントが使用可能になり、対応するファイル記述子が返されます。ファイル記述子は、ファイルにアクセスするための単なる抽象的なインジケータであり、標準入力(stdin)、標準出力(stdout)、および標準エラー(stderr)に対応する0、1、2の整数値を持ちます。
簡単な例は、サーバーソケットとクライアントソケットを作成して相互にデータを送信する方法を示しています。
code: server.py
import socket
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind(('localhost', 8089))
serversocket.listen(5)
while True:
connection, address = serversocket.accept()
buf = connection.recv(64)
if len(buf)>0:
print buf
code: client.py
import socket
clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientsocket.connect(('localhost', 8089))
clientsocket.send('hello')
サーバーを起動します。
code: bash
$ python server.py
続けて、クライアントを起動します。
code: bash
$ python client.py
"hello"がserver.pyプロセスの標準出力(stdout)に出力されます。
TCPのHTTPプロトコルを介してデータが相互に渡される基準はソケットになります。ソケットは、HTTP、HTTPS、FTP、SMTPプロトコル(これらはすべてTCPプロトコル)を定義するための基本的な構成要素です。
DNS、DHCP、TFTP、SNMP、RIP、VOIPプロトコルはUDPプロトコルですが、これらもソケットの上に構築されています。
WebSocketについて
WebSocketは、単一のTCPプロトコルで接続した全二重通信チャネルです。これは独立したTCPプロトコルで、HTTPとの唯一の関連は、そのハンドシェイクがHTTPサーバーによってアップグレード要求として解釈されることです。 HTTP 1.1ではUPGRADEヘッダーフィールドが導入され、この接続UPGRADEリクエストはクライアントから送信される必要があります(つまり、このUPGRADEヘッダーは、これがWebSocket接続であることをサーバーに通知するためにSocketIO Javascriptクライアントによって送信されます)。サーバーは426upgraderequired応答をクライアントに返送することでアップグレードを強制することもでき、クライアントはそれに応じてこの応答を処理する必要があります。アップグレードするか、WebSocket接続の試行に失敗します。
これが、WebSocketがHTTPサーバーとシームレスに連携する方法です。
WebSocketはブラウザー/クライアント機能であり、それをサポートするブラウザー(または、カスタムネイティブアプリを作成している場合はカスタムクライアント)でのみ機能します。 Socket.IOクライアントライブラリは、ロードされているブラウザがWebSocketをサポートしているかどうかをインテリジェントに判断します。含まれている場合は、WebSocketを使用してサーバー側のSocketIOサーバーと通信します。そうでない場合は、フォールバックトランスポートメカニズムの1つを使用しようとします。
WebSocketは、バイトストリームではなくメッセージストリームを有効にするという点で、HTTPのようなTCPプロトコルとは異なります。 WebSocketが登場する前は、Cometを使用してポート80の全二重通信を実現できました。ただし、WebSocketと比較すると、cometの実装は重要であり、TCPハンドシェイクとHTTPヘッダーのオーバーヘッドのために小さなメッセージには非効率的です。
WebSocketプロトコルハンドシェイクは次のようになります。
クライアントはWebSocketハンドシェイクリクエストを送信します。
code: WebSocket Handshake
GET /mychat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13
サーバーは次の応答を返します。
code: WebSocker Handshake Response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
HTTPのようなプロトコルは、1回の転送にのみ(BSDソケット)ソケットを使用します。 クライアントは要求を送信してから応答を読み取り、ソケットは破棄されます。 これは、HTTPクライアントが0バイトを受信することで応答の終わりを検出できることを意味します。
WebSocketの場合、接続が確立されると、クライアントとサーバーはWebSocketデータまたはテキストフレームを全二重モードで送受信できます。 データ自体は最小限のフレームで構成され、小さなヘッダーとデータ本体が含まれています。 WebSocketの送信は「メッセージ」として説明され、オプションで1つのメッセージを複数のデータフレームに分割できます。 これにより、初期データは利用可能であるがメッセージの完全な長さが不明な場合にメッセージを送信できます(最後に到達してFINビットでマークされるまでデータフレームを次々に送信します)。 プロトコルの拡張により、これは複数のストリームを同時に多重化するためにも使用できます。これは、たとえば、単一の大きなデータに対するソケットの使用を独占することを回避するためです。
Socket.IOについて
Socket.IOは、リアルタイムWebアプリケーション用のJavaScriptライブラリで、2つコンポーネントで構成されています。
ブラウザで実行されるクライアント側ライブラリ
node.jsのサーバー側ライブラリ。
どちらのコンポーネントにも同一のAPIがあり、イベント駆動型です。
他の言語でのサーバー側ライブラリの実装があります。
ServerIOServerの例
SocketIOServerを起動する最も簡単な方法は次のようになります。
code: python
from gevent import monkey
from socketio.server import SocketIOServer
monkey.patch_all()
PORT = 5000
if __name__ == '__main__':
SocketIOServer(('', PORT), app,
resource="socket.io").serve_forever()
SocketIOServerは、元のSocket.IOサーバー側nodejsライブラリのPython実装です。
そして、(host, port)引数、appインスタンス引数、resource引数、オプションでトランスポートのリスト、ブール値としてのフラッシュpolicy_server、およびキーワード引数の任意のリストを受け入れることができます。
SocketIOServerのホストとポート
ホストとポートのタプル引数は単純です。SocketIOServerを実行するIPアドレスとポートを指定します。
SocketIOServerアプリケーションインスタンス
app引数は、実行するPythonアプリケーションの単なるインスタンスです。
これがdjangoの例です:
code: python
# runserver_socketio.py
from gevent import monkey
from socketio.server import SocketIOServer
import django.core.handlers.wsgi
import os
import sys
monkey.patch_all()
try:
import settings
except ImportError:
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
sys.exit(1)
PORT = 9000
app = django.core.handlers.wsgi.WSGIHandler()
sys.path.insert(0, os.path.join(settings.PROJECT_ROOT, "apps"))
if __name__ == '__main__':
SocketIOServer(('', PORT), app, resource="socket.io").serve_forever()
SocketIOServerリソース
resource引数は、実際のPythonアプリケーションのSocket.IO URLを定義する必要がある場所です。
djangoアプリケーションの場合、urls.pyで次のように定義します
code: python
# urls.py
from django.conf.urls.defaults import patterns, include, url
import socketio.sdjango
socketio.sdjango.autodiscover()
urlpatterns = patterns("chat.views",
url("^socket\.io", include(socketio.sdjango.urls)),
djangoは、gevent-socketioライブラリで利用できる事前に作成された統合モジュールであり、django.urlsの次の定義が含まれています。
code: python
SOCKETIO_NS = {}
class namespace(object):
def __init__(self, name=''):
self.name = name
def __call__(self, handler):
return handler
@csrf_exempt
def socketio(request):
try:
socketio_manage(request.environ, SOCKETIO_NS, request)
except:
logging.getLogger("socketio").error("Exception while handling socketio connection", exc_info=True)
return HttpResponse("")
urls = patterns("", (r'', socketio))
SocketIO名前空間の例
クライアント(javascript)側に名前空間を実装する簡単な例は次のとおりです。
code: javascript
var socket = io.connect("/chat");
名前空間は、SocketIOを初めて使用する人にとっては「URL」と混同される可能性があります。 これは実際にはURL(MVCデザインパターンのルーターが話す)ではなく、実際にはコントローラーです。
サーバー側では、名前空間はBaseNamespaceクラスを介して実装されます。
code: python
from socketio.namespace import BaseNamespace
from socketio import socketio_manage
class ChatNamespace(BaseNamespace):
def on_user_msg(self, msg):
self.emit('user_msg', msg)
def socketio_service(request):
socketio_manage(request.environ,
{'/chat': ChatNamespace}, request)
return 'out'
この例では、user_msgイベントは/ chat名前空間にあります。したがって、/ chat名前空間にはon_user_msgメソッドが含まれていると言えます。
socketio_manage()は、SocketIOServerが起動したときに実行されるメソッドであり、クライアントとサーバー間のリアルタイム通信はそのメソッドを介して行われます。
長いポーリングメカニズムを使用している場合でも、socketio_manage()関数はソケットの開口部ごとに1回だけ呼び出されます。後続の呼び出し(長いポーリングの場合)は、サーバーレベルで直接フックされ、アクティブなSocketインスタンスと対話します。これは、将来のリクエストまたは環境オブジェクトにアクセスできないことを意味します。これは、セッションに関して特に重要です。セッションは、ソケットの開始時に1回開かれ、ソケットが閉じられるまで閉じられません。データを残りのGET / POST呼び出しと同期させたい場合は、Cookieベースのセッションを自分で開いたり閉じたりする責任があります。
ここでもう少し複雑なdjangoの例 `:
code: python
# sockets.py
import logging
from socketio.namespace import BaseNamespace
from socketio.mixins import RoomsMixin, BroadcastMixin
from socketio.sdjango import namespace
@namespace('/chat')
class ChatNamespace(BaseNamespace, RoomsMixin, BroadcastMixin):
nicknames = []
def initialize(self):
self.logger = logging.getLogger("socketio.chat")
self.log("Socketio session started")
def log(self, message):
self.logger.info("{0} {1}".format(self.socket.sessid, message)) def on_join(self, room):
self.room = room
self.join(room)
return True
def on_nickname(self, nickname):
self.log('Nickname: {0}'.format(nickname))
self.nicknames.append(nickname)
self.broadcast_event('announcement', '%s has connected' % nickname)
self.broadcast_event('nicknames', self.nicknames)
return True, nickname
def recv_disconnect(self):
# Remove nickname from the list.
self.log('Disconnected')
self.nicknames.remove(nickname)
self.broadcast_event('announcement',
f'{nickname} has disconnected')
self.broadcast_event('nicknames', self.nicknames)
self.disconnect(silent=True)
return True
def on_user_message(self, msg):
self.log('User message: {0}'.format(msg))
self.emit_to_room(self.room, 'msg_to_room',
return True
sdjangoモジュールは、完全に定義されたChatNamespaceサブクラス(BaseNamespaceのサブクラス)に対応するデコレーターとして使用できる名前空間の名前(この場合は/ chat)を受け入れる素敵な名前空間クラスを定義しました。 すべてのイベント処理メソッドはこのクラスに実装されており、を介して接続するjavascriptクライアントで動作します
code: javascript
var socket = io.connect("/chat");`
これは、io.connect(“/chat”)を呼び出します。
gevent-socketio APIの概要
ここで取り上げた主な概念と使用法は次のとおりです。
socketio.socketio_manage(sdjango.pyモジュールで見られる使用法)
socketio.namespace(BaseNamespace親クラスとdjangoの@namespaceデコレータの実装で見られる使用法)
socketio.server(SocketIOServerインスタンスのインスタンス化で見られる使用法)
上記のdjangoの例では、socketio.mixinsを使用して、一般的なチャットプロジェクトに役立つメソッドを含む事前に作成されたクラス(具体的にはRoomsMixinとBroadcastMixin)を渡します。
その他のAPIは次のとおりです。
socketio.virtsocket
socketio.packet
socketio.handler
socketio.transports
SocketIOクライアントからSocketIOServerロジックへ
djangoチャットアプリのレイアウトの例を次に示します(djangoプロジェクト内)。
code: bash
chat
├── __init__.py
├── admin.py
├── management
│ ├── __init__.py
│ └── commands
│ ├── __init__.py
│ ├── runserver_socketio.py
├── models.py
├── sockets.py
├── static
│ ├── css
│ │ └── chat.css
│ ├── flashsocket
│ │ └── WebSocketMain.swf
│ └── js
│ ├── chat.js
│ └── socket.io.js
├── templates
│ ├── base.html
│ ├── room.html
│ └── rooms.html
├── tests.py
├── urls.py
└── views.py
クライアント側のロジックはchat.jsにあります。 これは、クライアント側の「/ chatという名前のコントローラー上のコントローラーロジック」と考えることができます。 必要に応じて、異なるコントローラー名で2番目以上のソケットをいつでもインスタンス化できます。
code: javascript
// chat.js
var socket = io.connect("/chat");
socket.on('connect', function () {
$('#chat').addClass('connected');
socket.emit('join', window.room);
});
socket.on('announcement', function (msg) {
$('#lines').append($('<p>').append($('<em>').text(msg)));
});
socket.on('nicknames', function (nicknames) {
console.log("nicknames: " + nicknames);
$('#nicknames').empty().append($('<span>Online: </span>'));
for (var i in nicknames) {
$('#nicknames').append($('<b>').text(nicknamesi)); }
});
socket.on('msg_to_room', message);
socket.on('reconnect', function () {
$('#lines').remove();
message('System', 'Reconnected to the server');
});
socket.on('reconnecting', function () {
message('System', 'Attempting to re-connect to the server');
});
socket.on('error', function (e) {
message('System', e ? e : 'A unknown error occurred');
});
function message (from, msg) {
$('#lines').append($('<p>').append($('<b>').text(from), msg));
}
// DOM manipulation
$(function () {
$('#set-nickname').submit(function (ev) {
socket.emit('nickname', $('#nick').val(), function (set) {
if (set) {
clear();
return $('#chat').addClass('nickname-set');
}
$('#nickname-err').css('visibility', 'visible');
});
return false;
});
$('#send-message').submit(function () {
//message('me', "Fake it first: " + $('#message').val());
socket.emit('user message', $('#message').val());
clear();
$('#lines').get(0).scrollTop = 10000000;
return false;
});
function clear () {
$('#message').val('').focus();
}
});
クライアント側のSocketIOライブラリは簡単に使用できます。
socket.onは、最初の引数としてevent_name(サーバー側コードが発行する)と2番目の引数としてイベントコールバック関数の2つの引数を受け取ります。 イベントが発生すると、(コールバック)関数がトリガーされます。
socket.emitも2つの引数を受け取ります。最初の引数はevent_nameで、2番目の引数はメッセージです。 Emmit( ‘<event_name>’,<message>)はサーバーにメッセージを送信します。pythonメソッド on_<event_name>(<message>)はクライアント側のemit()呼び出しを待機しています。
これは、ChatNamespaceクラスの対応するサーバー側コードです。 /chatという名前のこのサーバー側コントローラーは、/chatという名前のクライアント側コントローラーに対応します。つまり、onまたはemitは対応するものを参照します。
code: python
# sockets.py
@namespace('/chat')
class ChatNamespace(BaseNamespace, LonelyRoomMixin, BroadcastMixin):
nicknames = []
def initialize(self):
self.logger = logging.getLogger("socketio.chat")
self.log("Socketio session started")
def log(self, message):
self.logger.info("{0} {1}".format(self.socket.sessid, message)) def on_join(self, room):
self.room = room
self.join(room)
return True
def on_nickname(self, nickname):
print("Creating the nickname: " + nickname)
self.log('Nickname: {0}'.format(nickname))
self.nicknames.append(nickname)
self.broadcast_event('announcement', '%s has connected' % nickname)
self.broadcast_event('nicknames', self.nicknames)
return True, nickname
def recv_disconnect(self):
self.log('Disconnected')
self.nicknames.remove(nickname)
self.broadcast_event('announcement', '%s has disconnected' % nickname)
self.broadcast_event('nicknames', self.nicknames)
self.disconnect(silent=True)
return True
def on_user_message(self, msg):
self.log('User message: {0}'.format(msg))
# TODO: dig into the logic of emit_to_room
self.emit_to_room(self.room, 'msg_to_room',
return True
参考