Отправка/получение сообщения WebSocket через сокет Python/клиент WebSocket

Я написал простой клиент WebSocket. Я использовал код, который я нашел на SO, здесь: Как отправлять и получать сообщения WebSocket на стороне сервера?.

Я использую Python 2.7, а мой сервер - echo.websocket.org на 80 TCP-порт. В принципе, я думаю, что у меня проблема с получением сообщений. (Или, может быть, отправка неверна?)

По крайней мере, я уверен, что рукопожатие все в порядке, так как я получаю хороший ответ на рукопожатие:

HTTP/1.1 101 Web Socket Protocol Handshake
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Headers: authorization
Access-Control-Allow-Headers: x-websocket-extensions
Access-Control-Allow-Headers: x-websocket-version
Access-Control-Allow-Headers: x-websocket-protocol
Access-Control-Allow-Origin: http://example.com
Connection: Upgrade
Date: Tue, 02 May 2017 21:54:31 GMT
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Server: Kaazing Gateway
Upgrade: websocket

И мой код:

#!/usr/bin/env python
import socket

def encode_text_msg_websocket(data):
    bytesFormatted = []
    bytesFormatted.append(129)

    bytesRaw = data.encode()
    bytesLength = len(bytesRaw)

    if bytesLength <= 125:
        bytesFormatted.append(bytesLength)
    elif 126 <= bytesLength <= 65535:
        bytesFormatted.append(126)
        bytesFormatted.append((bytesLength >> 8) & 255)
        bytesFormatted.append(bytesLength & 255)
    else:
        bytesFormatted.append(127)
        bytesFormatted.append((bytesLength >> 56) & 255)
        bytesFormatted.append((bytesLength >> 48) & 255)
        bytesFormatted.append((bytesLength >> 40) & 255)
        bytesFormatted.append((bytesLength >> 32) & 255)
        bytesFormatted.append((bytesLength >> 24) & 255)
        bytesFormatted.append((bytesLength >> 16) & 255)
        bytesFormatted.append((bytesLength >> 8) & 255)
        bytesFormatted.append(bytesLength & 255)

    bytesFormatted = bytes(bytesFormatted)
    bytesFormatted = bytesFormatted + bytesRaw
    return bytesFormatted


def dencode_text_msg_websocket(stringStreamIn):
    byteArray = [ord(character) for character in stringStreamIn]
    datalength = byteArray[1] & 127
    indexFirstMask = 2
    if datalength == 126:
        indexFirstMask = 4
    elif datalength == 127:
        indexFirstMask = 10
    masks = [m for m in byteArray[indexFirstMask: indexFirstMask + 4]]
    indexFirstDataByte = indexFirstMask + 4
    decodedChars = []
    i = indexFirstDataByte
    j = 0
    while i < len(byteArray):
        decodedChars.append(chr(byteArray[i] ^ masks[j % 4]))
        i += 1
        j += 1
    return ''.join(decodedChars)

# connect 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((socket.gethostbyname('echo.websocket.org'), 80))

# handshake
handshake = 'GET / HTTP/1.1\r\nHost: echo.websocket.org\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: gfhjgfhjfj\r\nOrigin: http://example.com\r\nSec-WebSocket-Protocol: echo\r\n' \
        'Sec-WebSocket-Version: 13\r\n\r\n'
sock.send(handshake)
print sock.recv(1024)

# send test msg
msg = encode_text_msg_websocket('hello world!')
sock.sendall(msg)

# receive it back
response = dencode_text_msg_websocket(sock.recv(1024))
print '--%s--' % response

sock.close()

Что здесь не так? После рукопожатия становится сложнее.

Метод dencode_text_msg_websocket возвращает пустую строку, но он должен возвращать ту же строку, которую я отправляю на сервер, которая hello world!.

Я НЕ ХОЧУ использовать библиотеки (я знаю, как их использовать). Речь идет о том, чтобы сделать то же самое без библиотек, используя только сокеты.

Я хочу отправить сообщение echo.websocket.org server и получить ответ, что все. Я не хочу изменять заголовки, просто создавайте заголовки, как они используются этим сервером. Я проверил, как они должны выглядеть, используя Wireshark, и попытался собрать те же пакеты с Python.

Для тестов ниже я использовал свой браузер:

Не замаскированные данные, от сервера к клиенту:

введите описание изображения здесь

Маскированные данные, от клиента к серверу:

введите описание изображения здесь

Ответ 1

Согласование https://tools.ietf.org/html/rfc6455#section-5.1:

Необходимо маскировать клиентские фреймы. (И серверные рамки вообще не маскируются.)

  • клиент должен маскировать все фреймы, которые он отправляется на сервер (подробнее см. раздел 5.3). (Заметка что маскирование выполняется независимо от того, выполняется ли протокол WebSocket над TLS.) Сервер ДОЛЖЕН закрыть соединение после получения который не замаскирован. В этом случае сервер МОЖЕТ отправить Закрыть с кодом состояния 1002 (ошибка протокола), как определено в Раздел 7.4.1. Сервер НЕ ДОЛЖЕН маскировать любые кадры, которые он отправляет клиент. Клиент ДОЛЖЕН закрыть соединение, если он обнаруживает маскировку кадр.

Это рабочая версия:

import os
import array
import six
import socket
import struct

OPCODE_TEXT = 0x1

try:
    # If wsaccel is available we use compiled routines to mask data.
    from wsaccel.xormask import XorMaskerSimple

    def _mask(_m, _d):
        return XorMaskerSimple(_m).process(_d)

except ImportError:
    # wsaccel is not available, we rely on python implementations.
    def _mask(_m, _d):
        for i in range(len(_d)):
            _d[i] ^= _m[i % 4]

        if six.PY3:
            return _d.tobytes()
        else:
            return _d.tostring()


def get_masked(data):
    mask_key = os.urandom(4)
    if data is None:
        data = ""

    bin_mask_key = mask_key
    if isinstance(mask_key, six.text_type):
        bin_mask_key = six.b(mask_key)

    if isinstance(data, six.text_type):
        data = six.b(data)

    _m = array.array("B", bin_mask_key)
    _d = array.array("B", data)
    s = _mask(_m, _d)

    if isinstance(mask_key, six.text_type):
        mask_key = mask_key.encode('utf-8')
    return mask_key + s


def ws_encode(data="", opcode=OPCODE_TEXT, mask=1):
    if opcode == OPCODE_TEXT and isinstance(data, six.text_type):
        data = data.encode('utf-8')

    length = len(data)
    fin, rsv1, rsv2, rsv3, opcode = 1, 0, 0, 0, opcode

    frame_header = chr(fin << 7 | rsv1 << 6 | rsv2 << 5 | rsv3 << 4 | opcode)

    if length < 0x7e:
        frame_header += chr(mask << 7 | length)
        frame_header = six.b(frame_header)
    elif length < 1 << 16:
        frame_header += chr(mask << 7 | 0x7e)
        frame_header = six.b(frame_header)
        frame_header += struct.pack("!H", length)
    else:
        frame_header += chr(mask << 7 | 0x7f)
        frame_header = six.b(frame_header)
        frame_header += struct.pack("!Q", length)

    if not mask:
        return frame_header + data
    return frame_header + get_masked(data)


def ws_decode(data):
    """
    ws frame decode.
    :param data:
    :return:
    """
    _data = [ord(character) for character in data]
    length = _data[1] & 127
    index = 2
    if length < 126:
        index = 2
    if length == 126:
        index = 4
    elif length == 127:
        index = 10
    return array.array('B', _data[index:]).tostring()


# connect
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((socket.gethostbyname('echo.websocket.org'), 80))

# handshake
handshake = 'GET / HTTP/1.1\r\nHost: echo.websocket.org\r\nUpgrade: websocket\r\nConnection: ' \
            'Upgrade\r\nSec-WebSocket-Key: gfhjgfhjfj\r\nOrigin: http://example.com\r\nSec-WebSocket-Protocol: ' \
            'echo\r\n' \
            'Sec-WebSocket-Version: 13\r\n\r\n'

sock.send(handshake)
print(sock.recv(1024))

sock.sendall(ws_encode(data='Hello, China!', opcode=OPCODE_TEXT))

# receive it back
response = ws_decode(sock.recv(1024))
print('--%s--' % response)

sock.close()

Ответ 2

Я взломал ваш код во что-то, что по крайней мере отправляет ответ и получает ответ, изменяя кодировку, чтобы использовать chr() для вставки строк байта, а не десятичных знаков в заголовок. Расшифровка я оставил один, но другой ответ здесь имеет решение для этого.
Настоящие кишки этого подробно описаны здесь https://www.rfc-editor.org/rfc/rfc6455.txt
который точно определяет, что вам нужно делать

#!/usr/bin/env python
import socket
def encode_text_msg_websocket(data):
    bytesFormatted = []
    bytesFormatted.append(chr(129))
    bytesRaw = data.encode()
    bytesLength = len(bytesRaw)
    if bytesLength <= 125:
        bytesFormatted.append(chr(bytesLength))
    elif 126 <= bytesLength <= 65535:
        bytesFormatted.append(chr(126))
        bytesFormatted.append((chr(bytesLength >> 8)) & 255)
        bytesFormatted.append(chr(bytesLength) & 255)
    else:
        bytesFormatted.append(chr(127))
        bytesFormatted.append(chr((bytesLength >> 56)) & 255)
        bytesFormatted.append(chr((bytesLength >> 48)) & 255)
        bytesFormatted.append(chr((bytesLength >> 40)) & 255)
        bytesFormatted.append(chr((bytesLength >> 32)) & 255)
        bytesFormatted.append(chr((bytesLength >> 24)) & 255)
        bytesFormatted.append(chr((bytesLength >> 16)) & 255)
        bytesFormatted.append(chr((bytesLength >> 8)) & 255)
        bytesFormatted.append(chr(bytesLength) & 255)
    send_str = ""
    for i in bytesFormatted:
        send_str+=i
    send_str += bytesRaw
    return send_str

# connect 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0)
try:
    sock.connect((socket.gethostbyname('ws.websocket.org'), 80))
except:
    print "Connection failed"
handshake = '\
GET /echo HTTP/1.1\r\n\
Host: echo.websocket.org\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n\
Origin: http://example.com\r\n\
WebSocket-Protocol: echo\r\n\
Sec-WebSocket-Version: 13\r\n\r\n\
'
sock.send(bytes(handshake))
data = sock.recv(1024).decode('UTF-8')
print data

# send test msg
msg = encode_text_msg_websocket('Now is the winter of our discontent, made glorious Summer by this son of York')
print "Sent: ",repr(msg)
sock.sendall(bytes(msg))
# receive it back
response = sock.recv(1024)
#decode not sorted so ignore the first 2 bytes
print "\nReceived: ", response[2:].decode()
sock.close()

Результат:

HTTP/1.1 101 Web Socket Protocol Handshake
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Headers: authorization
Access-Control-Allow-Headers: x-websocket-extensions
Access-Control-Allow-Headers: x-websocket-version
Access-Control-Allow-Headers: x-websocket-protocol
Access-Control-Allow-Origin: http://example.com
Connection: Upgrade
Date: Mon, 08 May 2017 15:08:33 GMT
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Server: Kaazing Gateway
Upgrade: websocket


Sent:  '\x81MNow is the winter of our discontent, made glorious Summer by this son of York'

Received:  Now is the winter of our discontent, made glorious Summer by this son of York

Я должен отметить здесь, что это будет свинья для кодирования, не втягивая некоторые дополнительные библиотеки, как это сделал @gushitong.