OLD | NEW |
(Empty) | |
| 1 """ |
| 2 websocket - WebSocket client library for Python |
| 3 |
| 4 Copyright (C) 2010 Hiroki Ohtani(liris) |
| 5 |
| 6 This library is free software; you can redistribute it and/or |
| 7 modify it under the terms of the GNU Lesser General Public |
| 8 License as published by the Free Software Foundation; either |
| 9 version 2.1 of the License, or (at your option) any later version. |
| 10 |
| 11 This library is distributed in the hope that it will be useful, |
| 12 but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 14 Lesser General Public License for more details. |
| 15 |
| 16 You should have received a copy of the GNU Lesser General Public |
| 17 License along with this library; if not, write to the Free Software |
| 18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 19 |
| 20 """ |
| 21 |
| 22 |
| 23 import socket |
| 24 from urlparse import urlparse |
| 25 import os |
| 26 import struct |
| 27 import uuid |
| 28 import hashlib |
| 29 import base64 |
| 30 import logging |
| 31 |
| 32 """ |
| 33 websocket python client. |
| 34 ========================= |
| 35 |
| 36 This version support only hybi-13. |
| 37 Please see http://tools.ietf.org/html/rfc6455 for protocol. |
| 38 """ |
| 39 |
| 40 |
| 41 # websocket supported version. |
| 42 VERSION = 13 |
| 43 |
| 44 # closing frame status codes. |
| 45 STATUS_NORMAL = 1000 |
| 46 STATUS_GOING_AWAY = 1001 |
| 47 STATUS_PROTOCOL_ERROR = 1002 |
| 48 STATUS_UNSUPPORTED_DATA_TYPE = 1003 |
| 49 STATUS_STATUS_NOT_AVAILABLE = 1005 |
| 50 STATUS_ABNORMAL_CLOSED = 1006 |
| 51 STATUS_INVALID_PAYLOAD = 1007 |
| 52 STATUS_POLICY_VIOLATION = 1008 |
| 53 STATUS_MESSAGE_TOO_BIG = 1009 |
| 54 STATUS_INVALID_EXTENSION = 1010 |
| 55 STATUS_UNEXPECTED_CONDITION = 1011 |
| 56 STATUS_TLS_HANDSHAKE_ERROR = 1015 |
| 57 |
| 58 logger = logging.getLogger() |
| 59 |
| 60 class WebSocketException(Exception): |
| 61 """ |
| 62 websocket exeception class. |
| 63 """ |
| 64 pass |
| 65 |
| 66 class WebSocketConnectionClosedException(WebSocketException): |
| 67 """ |
| 68 If remote host closed the connection or some network error happened, |
| 69 this exception will be raised. |
| 70 """ |
| 71 pass |
| 72 |
| 73 default_timeout = None |
| 74 traceEnabled = False |
| 75 |
| 76 def enableTrace(tracable): |
| 77 """ |
| 78 turn on/off the tracability. |
| 79 |
| 80 tracable: boolean value. if set True, tracability is enabled. |
| 81 """ |
| 82 global traceEnabled |
| 83 traceEnabled = tracable |
| 84 if tracable: |
| 85 if not logger.handlers: |
| 86 logger.addHandler(logging.StreamHandler()) |
| 87 logger.setLevel(logging.DEBUG) |
| 88 |
| 89 def setdefaulttimeout(timeout): |
| 90 """ |
| 91 Set the global timeout setting to connect. |
| 92 |
| 93 timeout: default socket timeout time. This value is second. |
| 94 """ |
| 95 global default_timeout |
| 96 default_timeout = timeout |
| 97 |
| 98 def getdefaulttimeout(): |
| 99 """ |
| 100 Return the global timeout setting(second) to connect. |
| 101 """ |
| 102 return default_timeout |
| 103 |
| 104 def _parse_url(url): |
| 105 """ |
| 106 parse url and the result is tuple of |
| 107 (hostname, port, resource path and the flag of secure mode) |
| 108 |
| 109 url: url string. |
| 110 """ |
| 111 if ":" not in url: |
| 112 raise ValueError("url is invalid") |
| 113 |
| 114 scheme, url = url.split(":", 1) |
| 115 |
| 116 parsed = urlparse(url, scheme="http") |
| 117 if parsed.hostname: |
| 118 hostname = parsed.hostname |
| 119 else: |
| 120 raise ValueError("hostname is invalid") |
| 121 port = 0 |
| 122 if parsed.port: |
| 123 port = parsed.port |
| 124 |
| 125 is_secure = False |
| 126 if scheme == "ws": |
| 127 if not port: |
| 128 port = 80 |
| 129 elif scheme == "wss": |
| 130 is_secure = True |
| 131 if not port: |
| 132 port = 443 |
| 133 else: |
| 134 raise ValueError("scheme %s is invalid" % scheme) |
| 135 |
| 136 if parsed.path: |
| 137 resource = parsed.path |
| 138 else: |
| 139 resource = "/" |
| 140 |
| 141 if parsed.query: |
| 142 resource += "?" + parsed.query |
| 143 |
| 144 return (hostname, port, resource, is_secure) |
| 145 |
| 146 def create_connection(url, timeout=None, **options): |
| 147 """ |
| 148 connect to url and return websocket object. |
| 149 |
| 150 Connect to url and return the WebSocket object. |
| 151 Passing optional timeout parameter will set the timeout on the socket. |
| 152 If no timeout is supplied, the global default timeout setting returned by ge
tdefauttimeout() is used. |
| 153 You can customize using 'options'. |
| 154 If you set "header" dict object, you can set your own custom header. |
| 155 |
| 156 >>> conn = create_connection("ws://echo.websocket.org/", |
| 157 ... header={"User-Agent: MyProgram", |
| 158 ... "x-custom: header"}) |
| 159 |
| 160 |
| 161 timeout: socket timeout time. This value is integer. |
| 162 if you set None for this value, it means "use default_timeout value
" |
| 163 |
| 164 options: current support option is only "header". |
| 165 if you set header as dict value, the custom HTTP headers are added. |
| 166 """ |
| 167 websock = WebSocket() |
| 168 websock.settimeout(timeout != None and timeout or default_timeout) |
| 169 websock.connect(url, **options) |
| 170 return websock |
| 171 |
| 172 _MAX_INTEGER = (1 << 32) -1 |
| 173 _AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1) |
| 174 _MAX_CHAR_BYTE = (1<<8) -1 |
| 175 |
| 176 # ref. Websocket gets an update, and it breaks stuff. |
| 177 # http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html |
| 178 |
| 179 def _create_sec_websocket_key(): |
| 180 uid = uuid.uuid4() |
| 181 return base64.encodestring(uid.bytes).strip() |
| 182 |
| 183 _HEADERS_TO_CHECK = { |
| 184 "upgrade": "websocket", |
| 185 "connection": "upgrade", |
| 186 } |
| 187 |
| 188 class _SSLSocketWrapper(object): |
| 189 def __init__(self, sock): |
| 190 self.ssl = socket.ssl(sock) |
| 191 |
| 192 def recv(self, bufsize): |
| 193 return self.ssl.read(bufsize) |
| 194 |
| 195 def send(self, payload): |
| 196 return self.ssl.write(payload) |
| 197 |
| 198 _BOOL_VALUES = (0, 1) |
| 199 def _is_bool(*values): |
| 200 for v in values: |
| 201 if v not in _BOOL_VALUES: |
| 202 return False |
| 203 |
| 204 return True |
| 205 |
| 206 class ABNF(object): |
| 207 """ |
| 208 ABNF frame class. |
| 209 see http://tools.ietf.org/html/rfc5234 |
| 210 and http://tools.ietf.org/html/rfc6455#section-5.2 |
| 211 """ |
| 212 |
| 213 # operation code values. |
| 214 OPCODE_TEXT = 0x1 |
| 215 OPCODE_BINARY = 0x2 |
| 216 OPCODE_CLOSE = 0x8 |
| 217 OPCODE_PING = 0x9 |
| 218 OPCODE_PONG = 0xa |
| 219 |
| 220 # available operation code value tuple |
| 221 OPCODES = (OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, |
| 222 OPCODE_PING, OPCODE_PONG) |
| 223 |
| 224 # opcode human readable string |
| 225 OPCODE_MAP = { |
| 226 OPCODE_TEXT: "text", |
| 227 OPCODE_BINARY: "binary", |
| 228 OPCODE_CLOSE: "close", |
| 229 OPCODE_PING: "ping", |
| 230 OPCODE_PONG: "pong" |
| 231 } |
| 232 |
| 233 # data length threashold. |
| 234 LENGTH_7 = 0x7d |
| 235 LENGTH_16 = 1 << 16 |
| 236 LENGTH_63 = 1 << 63 |
| 237 |
| 238 def __init__(self, fin = 0, rsv1 = 0, rsv2 = 0, rsv3 = 0, |
| 239 opcode = OPCODE_TEXT, mask = 1, data = ""): |
| 240 """ |
| 241 Constructor for ABNF. |
| 242 please check RFC for arguments. |
| 243 """ |
| 244 self.fin = fin |
| 245 self.rsv1 = rsv1 |
| 246 self.rsv2 = rsv2 |
| 247 self.rsv3 = rsv3 |
| 248 self.opcode = opcode |
| 249 self.mask = mask |
| 250 self.data = data |
| 251 self.get_mask_key = os.urandom |
| 252 |
| 253 @staticmethod |
| 254 def create_frame(data, opcode): |
| 255 """ |
| 256 create frame to send text, binary and other data. |
| 257 |
| 258 data: data to send. This is string value(byte array). |
| 259 if opcode is OPCODE_TEXT and this value is uniocde, |
| 260 data value is conveted into unicode string, automatically. |
| 261 |
| 262 opcode: operation code. please see OPCODE_XXX. |
| 263 """ |
| 264 if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode): |
| 265 data = data.encode("utf-8") |
| 266 # mask must be set if send data from client |
| 267 return ABNF(1, 0, 0, 0, opcode, 1, data) |
| 268 |
| 269 def format(self): |
| 270 """ |
| 271 format this object to string(byte array) to send data to server. |
| 272 """ |
| 273 if not _is_bool(self.fin, self.rsv1, self.rsv2, self.rsv3): |
| 274 raise ValueError("not 0 or 1") |
| 275 if self.opcode not in ABNF.OPCODES: |
| 276 raise ValueError("Invalid OPCODE") |
| 277 length = len(self.data) |
| 278 if length >= ABNF.LENGTH_63: |
| 279 raise ValueError("data is too long") |
| 280 |
| 281 frame_header = chr(self.fin << 7 |
| 282 | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 |
| 283 | self.opcode) |
| 284 if length < ABNF.LENGTH_7: |
| 285 frame_header += chr(self.mask << 7 | length) |
| 286 elif length < ABNF.LENGTH_16: |
| 287 frame_header += chr(self.mask << 7 | 0x7e) |
| 288 frame_header += struct.pack("!H", length) |
| 289 else: |
| 290 frame_header += chr(self.mask << 7 | 0x7f) |
| 291 frame_header += struct.pack("!Q", length) |
| 292 |
| 293 if not self.mask: |
| 294 return frame_header + self.data |
| 295 else: |
| 296 mask_key = self.get_mask_key(4) |
| 297 return frame_header + self._get_masked(mask_key) |
| 298 |
| 299 def _get_masked(self, mask_key): |
| 300 s = ABNF.mask(mask_key, self.data) |
| 301 return mask_key + "".join(s) |
| 302 |
| 303 @staticmethod |
| 304 def mask(mask_key, data): |
| 305 """ |
| 306 mask or unmask data. Just do xor for each byte |
| 307 |
| 308 mask_key: 4 byte string(byte). |
| 309 |
| 310 data: data to mask/unmask. |
| 311 """ |
| 312 _m = map(ord, mask_key) |
| 313 _d = map(ord, data) |
| 314 for i in range(len(_d)): |
| 315 _d[i] ^= _m[i % 4] |
| 316 s = map(chr, _d) |
| 317 return "".join(s) |
| 318 |
| 319 class WebSocket(object): |
| 320 """ |
| 321 Low level WebSocket interface. |
| 322 This class is based on |
| 323 The WebSocket protocol draft-hixie-thewebsocketprotocol-76 |
| 324 http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 |
| 325 |
| 326 We can connect to the websocket server and send/recieve data. |
| 327 The following example is a echo client. |
| 328 |
| 329 >>> import websocket |
| 330 >>> ws = websocket.WebSocket() |
| 331 >>> ws.connect("ws://echo.websocket.org") |
| 332 >>> ws.send("Hello, Server") |
| 333 >>> ws.recv() |
| 334 'Hello, Server' |
| 335 >>> ws.close() |
| 336 |
| 337 get_mask_key: a callable to produce new mask keys, see the set_mask_key |
| 338 function's docstring for more details |
| 339 """ |
| 340 def __init__(self, get_mask_key = None): |
| 341 """ |
| 342 Initalize WebSocket object. |
| 343 """ |
| 344 self.connected = False |
| 345 self.io_sock = self.sock = socket.socket() |
| 346 self.get_mask_key = get_mask_key |
| 347 |
| 348 def set_mask_key(self, func): |
| 349 """ |
| 350 set function to create musk key. You can custumize mask key generator. |
| 351 Mainly, this is for testing purpose. |
| 352 |
| 353 func: callable object. the fuct must 1 argument as integer. |
| 354 The argument means length of mask key. |
| 355 This func must be return string(byte array), |
| 356 which length is argument specified. |
| 357 """ |
| 358 self.get_mask_key = func |
| 359 |
| 360 def settimeout(self, timeout): |
| 361 """ |
| 362 Set the timeout to the websocket. |
| 363 |
| 364 timeout: timeout time(second). |
| 365 """ |
| 366 self.sock.settimeout(timeout) |
| 367 |
| 368 def gettimeout(self): |
| 369 """ |
| 370 Get the websocket timeout(second). |
| 371 """ |
| 372 return self.sock.gettimeout() |
| 373 |
| 374 def connect(self, url, **options): |
| 375 """ |
| 376 Connect to url. url is websocket url scheme. ie. ws://host:port/resource |
| 377 You can customize using 'options'. |
| 378 If you set "header" dict object, you can set your own custom header. |
| 379 |
| 380 >>> ws = WebSocket() |
| 381 >>> ws.connect("ws://echo.websocket.org/", |
| 382 ... header={"User-Agent: MyProgram", |
| 383 ... "x-custom: header"}) |
| 384 |
| 385 timeout: socket timeout time. This value is integer. |
| 386 if you set None for this value, |
| 387 it means "use default_timeout value" |
| 388 |
| 389 options: current support option is only "header". |
| 390 if you set header as dict value, |
| 391 the custom HTTP headers are added. |
| 392 |
| 393 """ |
| 394 hostname, port, resource, is_secure = _parse_url(url) |
| 395 # TODO: we need to support proxy |
| 396 self.sock.connect((hostname, port)) |
| 397 if is_secure: |
| 398 self.io_sock = _SSLSocketWrapper(self.sock) |
| 399 self._handshake(hostname, port, resource, **options) |
| 400 |
| 401 def _handshake(self, host, port, resource, **options): |
| 402 sock = self.io_sock |
| 403 headers = [] |
| 404 headers.append("GET %s HTTP/1.1" % resource) |
| 405 headers.append("Upgrade: websocket") |
| 406 headers.append("Connection: Upgrade") |
| 407 if port == 80: |
| 408 hostport = host |
| 409 else: |
| 410 hostport = "%s:%d" % (host, port) |
| 411 headers.append("Host: %s" % hostport) |
| 412 headers.append("Origin: %s" % hostport) |
| 413 |
| 414 key = _create_sec_websocket_key() |
| 415 headers.append("Sec-WebSocket-Key: %s" % key) |
| 416 headers.append("Sec-WebSocket-Version: %s" % VERSION) |
| 417 if "header" in options: |
| 418 headers.extend(options["header"]) |
| 419 |
| 420 headers.append("") |
| 421 headers.append("") |
| 422 |
| 423 header_str = "\r\n".join(headers) |
| 424 sock.send(header_str) |
| 425 if traceEnabled: |
| 426 logger.debug( "--- request header ---") |
| 427 logger.debug( header_str) |
| 428 logger.debug("-----------------------") |
| 429 |
| 430 status, resp_headers = self._read_headers() |
| 431 if status != 101: |
| 432 self.close() |
| 433 raise WebSocketException("Handshake Status %d" % status) |
| 434 |
| 435 success = self._validate_header(resp_headers, key) |
| 436 if not success: |
| 437 self.close() |
| 438 raise WebSocketException("Invalid WebSocket Header") |
| 439 |
| 440 self.connected = True |
| 441 |
| 442 def _validate_header(self, headers, key): |
| 443 for k, v in _HEADERS_TO_CHECK.iteritems(): |
| 444 r = headers.get(k, None) |
| 445 if not r: |
| 446 return False |
| 447 r = r.lower() |
| 448 if v != r: |
| 449 return False |
| 450 |
| 451 result = headers.get("sec-websocket-accept", None) |
| 452 if not result: |
| 453 return False |
| 454 result = result.lower() |
| 455 |
| 456 value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" |
| 457 hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower
() |
| 458 return hashed == result |
| 459 |
| 460 def _read_headers(self): |
| 461 status = None |
| 462 headers = {} |
| 463 if traceEnabled: |
| 464 logger.debug("--- response header ---") |
| 465 |
| 466 while True: |
| 467 line = self._recv_line() |
| 468 if line == "\r\n": |
| 469 break |
| 470 line = line.strip() |
| 471 if traceEnabled: |
| 472 logger.debug(line) |
| 473 if not status: |
| 474 status_info = line.split(" ", 2) |
| 475 status = int(status_info[1]) |
| 476 else: |
| 477 kv = line.split(":", 1) |
| 478 if len(kv) == 2: |
| 479 key, value = kv |
| 480 headers[key.lower()] = value.strip().lower() |
| 481 else: |
| 482 raise WebSocketException("Invalid header") |
| 483 |
| 484 if traceEnabled: |
| 485 logger.debug("-----------------------") |
| 486 |
| 487 return status, headers |
| 488 |
| 489 def send(self, payload, opcode = ABNF.OPCODE_TEXT): |
| 490 """ |
| 491 Send the data as string. |
| 492 |
| 493 payload: Payload must be utf-8 string or unicoce, |
| 494 if the opcode is OPCODE_TEXT. |
| 495 Otherwise, it must be string(byte array) |
| 496 |
| 497 opcode: operation code to send. Please see OPCODE_XXX. |
| 498 """ |
| 499 frame = ABNF.create_frame(payload, opcode) |
| 500 if self.get_mask_key: |
| 501 frame.get_mask_key = self.get_mask_key |
| 502 data = frame.format() |
| 503 self.io_sock.send(data) |
| 504 if traceEnabled: |
| 505 logger.debug("send: " + repr(data)) |
| 506 |
| 507 def ping(self, payload = ""): |
| 508 """ |
| 509 send ping data. |
| 510 |
| 511 payload: data payload to send server. |
| 512 """ |
| 513 self.send(payload, ABNF.OPCODE_PING) |
| 514 |
| 515 def pong(self, payload): |
| 516 """ |
| 517 send pong data. |
| 518 |
| 519 payload: data payload to send server. |
| 520 """ |
| 521 self.send(payload, ABNF.OPCODE_PONG) |
| 522 |
| 523 def recv(self): |
| 524 """ |
| 525 Receive string data(byte array) from the server. |
| 526 |
| 527 return value: string(byte array) value. |
| 528 """ |
| 529 opcode, data = self.recv_data() |
| 530 return data |
| 531 |
| 532 def recv_data(self): |
| 533 """ |
| 534 Recieve data with operation code. |
| 535 |
| 536 return value: tuple of operation code and string(byte array) value. |
| 537 """ |
| 538 while True: |
| 539 frame = self.recv_frame() |
| 540 if not frame: |
| 541 # handle error: |
| 542 # 'NoneType' object has no attribute 'opcode' |
| 543 raise WebSocketException("Not a valid frame %s" % frame) |
| 544 elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): |
| 545 return (frame.opcode, frame.data) |
| 546 elif frame.opcode == ABNF.OPCODE_CLOSE: |
| 547 self.send_close() |
| 548 return (frame.opcode, None) |
| 549 elif frame.opcode == ABNF.OPCODE_PING: |
| 550 self.pong("Hi!") |
| 551 |
| 552 |
| 553 def recv_frame(self): |
| 554 """ |
| 555 recieve data as frame from server. |
| 556 |
| 557 return value: ABNF frame object. |
| 558 """ |
| 559 header_bytes = self._recv(2) |
| 560 if not header_bytes: |
| 561 return None |
| 562 b1 = ord(header_bytes[0]) |
| 563 fin = b1 >> 7 & 1 |
| 564 rsv1 = b1 >> 6 & 1 |
| 565 rsv2 = b1 >> 5 & 1 |
| 566 rsv3 = b1 >> 4 & 1 |
| 567 opcode = b1 & 0xf |
| 568 b2 = ord(header_bytes[1]) |
| 569 mask = b2 >> 7 & 1 |
| 570 length = b2 & 0x7f |
| 571 |
| 572 length_data = "" |
| 573 if length == 0x7e: |
| 574 length_data = self._recv(2) |
| 575 length = struct.unpack("!H", length_data)[0] |
| 576 elif length == 0x7f: |
| 577 length_data = self._recv(8) |
| 578 length = struct.unpack("!Q", length_data)[0] |
| 579 |
| 580 mask_key = "" |
| 581 if mask: |
| 582 mask_key = self._recv(4) |
| 583 data = self._recv_strict(length) |
| 584 if traceEnabled: |
| 585 recieved = header_bytes + length_data + mask_key + data |
| 586 logger.debug("recv: " + repr(recieved)) |
| 587 |
| 588 if mask: |
| 589 data = ABNF.mask(mask_key, data) |
| 590 |
| 591 frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, mask, data) |
| 592 return frame |
| 593 |
| 594 def send_close(self, status = STATUS_NORMAL, reason = ""): |
| 595 """ |
| 596 send close data to the server. |
| 597 |
| 598 status: status code to send. see STATUS_XXX. |
| 599 |
| 600 reason: the reason to close. This must be string. |
| 601 """ |
| 602 if status < 0 or status >= ABNF.LENGTH_16: |
| 603 raise ValueError("code is invalid range") |
| 604 self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) |
| 605 |
| 606 |
| 607 |
| 608 def close(self, status = STATUS_NORMAL, reason = ""): |
| 609 """ |
| 610 Close Websocket object |
| 611 |
| 612 status: status code to send. see STATUS_XXX. |
| 613 |
| 614 reason: the reason to close. This must be string. |
| 615 """ |
| 616 if self.connected: |
| 617 if status < 0 or status >= ABNF.LENGTH_16: |
| 618 raise ValueError("code is invalid range") |
| 619 |
| 620 try: |
| 621 self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) |
| 622 timeout = self.sock.gettimeout() |
| 623 self.sock.settimeout(3) |
| 624 try: |
| 625 frame = self.recv_frame() |
| 626 if logger.isEnabledFor(logging.DEBUG): |
| 627 logger.error("close status: " + repr(frame.data)) |
| 628 except: |
| 629 pass |
| 630 self.sock.settimeout(timeout) |
| 631 self.sock.shutdown(socket.SHUT_RDWR) |
| 632 except: |
| 633 pass |
| 634 self._closeInternal() |
| 635 |
| 636 def _closeInternal(self): |
| 637 self.connected = False |
| 638 self.sock.close() |
| 639 self.io_sock = self.sock |
| 640 |
| 641 def _recv(self, bufsize): |
| 642 bytes = self.io_sock.recv(bufsize) |
| 643 if bytes == 0: |
| 644 raise WebSocketConnectionClosedException() |
| 645 return bytes |
| 646 |
| 647 def _recv_strict(self, bufsize): |
| 648 remaining = bufsize |
| 649 bytes = "" |
| 650 while remaining: |
| 651 bytes += self._recv(remaining) |
| 652 remaining = bufsize - len(bytes) |
| 653 |
| 654 return bytes |
| 655 |
| 656 def _recv_line(self): |
| 657 line = [] |
| 658 while True: |
| 659 c = self._recv(1) |
| 660 line.append(c) |
| 661 if c == "\n": |
| 662 break |
| 663 return "".join(line) |
| 664 |
| 665 class WebSocketApp(object): |
| 666 """ |
| 667 Higher level of APIs are provided. |
| 668 The interface is like JavaScript WebSocket object. |
| 669 """ |
| 670 def __init__(self, url, |
| 671 on_open = None, on_message = None, on_error = None, |
| 672 on_close = None, keep_running = True, get_mask_key = None): |
| 673 """ |
| 674 url: websocket url. |
| 675 on_open: callable object which is called at opening websocket. |
| 676 this function has one argument. The arugment is this class object. |
| 677 on_message: callbale object which is called when recieved data. |
| 678 on_message has 2 arguments. |
| 679 The 1st arugment is this class object. |
| 680 The passing 2nd arugment is utf-8 string which we get from the server. |
| 681 on_error: callable object which is called when we get error. |
| 682 on_error has 2 arguments. |
| 683 The 1st arugment is this class object. |
| 684 The passing 2nd arugment is exception object. |
| 685 on_close: callable object which is called when closed the connection. |
| 686 this function has one argument. The arugment is this class object. |
| 687 keep_running: a boolean flag indicating whether the app's main loop shoul
d |
| 688 keep running, defaults to True |
| 689 get_mask_key: a callable to produce new mask keys, see the WebSocket.set_
mask_key's |
| 690 docstring for more information |
| 691 """ |
| 692 self.url = url |
| 693 self.on_open = on_open |
| 694 self.on_message = on_message |
| 695 self.on_error = on_error |
| 696 self.on_close = on_close |
| 697 self.keep_running = keep_running |
| 698 self.get_mask_key = get_mask_key |
| 699 self.sock = None |
| 700 |
| 701 def send(self, data): |
| 702 """ |
| 703 send message. data must be utf-8 string or unicode. |
| 704 """ |
| 705 if self.sock.send(data) == 0: |
| 706 raise WebSocketConnectionClosedException() |
| 707 |
| 708 def close(self): |
| 709 """ |
| 710 close websocket connection. |
| 711 """ |
| 712 self.keep_running = False |
| 713 self.sock.close() |
| 714 |
| 715 def run_forever(self): |
| 716 """ |
| 717 run event loop for WebSocket framework. |
| 718 This loop is infinite loop and is alive during websocket is available. |
| 719 """ |
| 720 if self.sock: |
| 721 raise WebSocketException("socket is already opened") |
| 722 try: |
| 723 self.sock = WebSocket(self.get_mask_key) |
| 724 self.sock.connect(self.url) |
| 725 self._run_with_no_err(self.on_open) |
| 726 while self.keep_running: |
| 727 data = self.sock.recv() |
| 728 if data is None: |
| 729 break |
| 730 self._run_with_no_err(self.on_message, data) |
| 731 except Exception, e: |
| 732 self._run_with_no_err(self.on_error, e) |
| 733 finally: |
| 734 self.sock.close() |
| 735 self._run_with_no_err(self.on_close) |
| 736 self.sock = None |
| 737 |
| 738 def _run_with_no_err(self, callback, *args): |
| 739 if callback: |
| 740 try: |
| 741 callback(self, *args) |
| 742 except Exception, e: |
| 743 if logger.isEnabledFor(logging.DEBUG): |
| 744 logger.error(e) |
| 745 |
| 746 |
| 747 if __name__ == "__main__": |
| 748 enableTrace(True) |
| 749 ws = create_connection("ws://echo.websocket.org/") |
| 750 print "Sending 'Hello, World'..." |
| 751 ws.send("Hello, World") |
| 752 print "Sent" |
| 753 print "Receiving..." |
| 754 result = ws.recv() |
| 755 print "Received '%s'" % result |
| 756 ws.close() |
OLD | NEW |