如何在服务器端发送和接收WebSocket消息?

pim*_*vdb 82 encoding protocols decoding websocket

  • 根据协议,如何使用WebSocket在服务器端发送和接收消息?

  • 当我将数据从浏览器发送到服务器时,为什么我在服务器上看到看似随机的字节?它以某种方式编码数据?

  • 框架如何在服务器→客户端和客户端→服务器方向上工作?

pim*_*vdb 150

注意:这是关于如何实现一个非常简单的服务器的一些解释和伪代码,该服务器可以按照明确的帧格式处理传入和传出的WebSocket消息.它不包括握手过程.此外,这个答案是出于教育目的; 它不是一个功能齐全的实现.

规范(RFC 6455)


发送消息

(换句话说,服务器→浏览器)

您发送的帧需要根据WebSocket帧格式进行格式化.对于发送消息,此格式如下:

  • 一个字节,其中包含数据类型(以及一些超出简单服务器范围的其他信息)
  • 一个包含长度的字节
  • 如果长度不适合第二个字节,则为两个或八个字节(第二个字节是一个代码,表示长度使用了多少字节)
  • 实际(原始)数据

第一个字节将是1000 0001(或129)文本框架.

第二个字节的第一个位设置为,0因为我们没有对数据进行编码(从服务器到客户端的编码不是必需的).

有必要确定原始数据的长度,以便正确发送长度字节:

  • 如果0 <= length <= 125,你不需要额外的字节
  • 如果126 <= length <= 65535,你需要两个额外的字节,第二个字节是126
  • 如果length >= 65536,你需要8个额外的字节,第二个字节是127

长度必须分成单独的字节,这意味着你需要向右移位(数量为8位),然后只做保留最后8位AND 1111 1111(即255).

在长度字节之后出现原始数据.

这导致以下伪代码:

bytesFormatted[0] = 129

indexStartRawData = -1 // it doesn't matter what value is
                       // set here - it will be set now:

if bytesRaw.length <= 125
    bytesFormatted[1] = bytesRaw.length

    indexStartRawData = 2

else if bytesRaw.length >= 126 and bytesRaw.length <= 65535
    bytesFormatted[1] = 126
    bytesFormatted[2] = ( bytesRaw.length >> 8 ) AND 255
    bytesFormatted[3] = ( bytesRaw.length      ) AND 255

    indexStartRawData = 4

else
    bytesFormatted[1] = 127
    bytesFormatted[2] = ( bytesRaw.length >> 56 ) AND 255
    bytesFormatted[3] = ( bytesRaw.length >> 48 ) AND 255
    bytesFormatted[4] = ( bytesRaw.length >> 40 ) AND 255
    bytesFormatted[5] = ( bytesRaw.length >> 32 ) AND 255
    bytesFormatted[6] = ( bytesRaw.length >> 24 ) AND 255
    bytesFormatted[7] = ( bytesRaw.length >> 16 ) AND 255
    bytesFormatted[8] = ( bytesRaw.length >>  8 ) AND 255
    bytesFormatted[9] = ( bytesRaw.length       ) AND 255

    indexStartRawData = 10

// put raw data at the correct index
bytesFormatted.put(bytesRaw, indexStartRawData)


// now send bytesFormatted (e.g. write it to the socket stream)
Run Code Online (Sandbox Code Playgroud)

接收消息

(换句话说,浏览器→服务器)

您获得的帧格式如下:

  • 一个字节,包含数据类型
  • 一个包含长度的字节
  • 如果长度不适合第二个字节,则需要两个或八个附加字节
  • 四个字节是掩码(=解码密钥)
  • 实际数据

第一个字节通常无关紧要 - 如果您只是发送文本,则只使用文本类型.在那种情况下它将是1000 0001(或129).

第二个字节和额外的两个或八个字节需要一些解析,因为您需要知道长度使用了多少字节(您需要知道实际数据的起始位置).由于您已拥有数据,因此通常不需要长度本身.

第二个字节的第一位始终1表示数据被屏蔽(=编码).始终屏蔽从客户端到服务器的消息.您需要通过执行删除第一位secondByte AND 0111 1111.有两种情况,结果字节不代表长度,因为它不适合第二个字节:

  • 的第二个字节0111 1110,或者126,是指以下两个字节用于长度
  • 的第二个字节0111 1111,或者127,是指以下八个字节用于长度

四个掩码字节用于解码已发送的实际数据.解码算法如下:

decodedByte = encodedByte XOR masks[encodedByteIndex MOD 4]
Run Code Online (Sandbox Code Playgroud)

数据中encodedByte的原始字节在哪里,encodedByteIndex是从具有索引的实数据的第一个字节开始计数的字节的索引(偏移量)0.masks是一个包含四个掩码字节的数组.

这导致以下用于解码的伪代码:

secondByte = bytes[1]

length = secondByte AND 127 // may not be the actual length in the two special cases

indexFirstMask = 2          // if not a special case

if length == 126            // if a special case, change indexFirstMask
    indexFirstMask = 4

else if length == 127       // ditto
    indexFirstMask = 10

masks = bytes.slice(indexFirstMask, 4) // four bytes starting from indexFirstMask

indexFirstDataByte = indexFirstMask + 4 // four bytes further

decoded = new array

decoded.length = bytes.length - indexFirstDataByte // length of real data

for i = indexFirstDataByte, j = 0; i < bytes.length; i++, j++
    decoded[j] = bytes[i] XOR masks[j MOD 4]


// now use "decoded" to interpret the received data
Run Code Online (Sandbox Code Playgroud)

  • @Dennis:帧操作码是'0001`,因为它在规范的那一部分的标题中说明:"操作码:4位".第一个字节由FIN,RSV1-3和操作码组成.FIN为"1",RSV1-3均为"0",操作码为"0001",第一个字节加起来为"1000 0001".另请参阅规范中的图稿,其中显示了字节在不同部分中的分割方式. (3认同)
  • 比规格更容易阅读.谢谢〜 (2认同)

小智 26

Java实现(如果需要)

阅读:客户端到服务器

        int len = 0;            
        byte[] b = new byte[buffLenth];
        //rawIn is a Socket.getInputStream();
        while(true){
            len = rawIn.read(b);
            if(len!=-1){

                byte rLength = 0;
                int rMaskIndex = 2;
                int rDataStart = 0;
                //b[0] is always text in my case so no need to check;
                byte data = b[1];
                byte op = (byte) 127;
                rLength = (byte) (data & op);

                if(rLength==(byte)126) rMaskIndex=4;
                if(rLength==(byte)127) rMaskIndex=10;

                byte[] masks = new byte[4];

                int j=0;
                int i=0;
                for(i=rMaskIndex;i<(rMaskIndex+4);i++){
                    masks[j] = b[i];
                    j++;
                }

                rDataStart = rMaskIndex + 4;

                int messLen = len - rDataStart;

                byte[] message = new byte[messLen];

                for(i=rDataStart, j=0; i<len; i++, j++){
                    message[j] = (byte) (b[i] ^ masks[j % 4]);
                }

                parseMessage(new String(message)); 
                //parseMessage(new String(b));

                b = new byte[buffLenth];

            }
        }
Run Code Online (Sandbox Code Playgroud)

写作:服务器到客户端

public void brodcast(String mess) throws IOException{
    byte[] rawData = mess.getBytes();

    int frameCount  = 0;
    byte[] frame = new byte[10];

    frame[0] = (byte) 129;

    if(rawData.length <= 125){
        frame[1] = (byte) rawData.length;
        frameCount = 2;
    }else if(rawData.length >= 126 && rawData.length <= 65535){
        frame[1] = (byte) 126;
        int len = rawData.length;
        frame[2] = (byte)((len >> 8 ) & (byte)255);
        frame[3] = (byte)(len & (byte)255); 
        frameCount = 4;
    }else{
        frame[1] = (byte) 127;
        int len = rawData.length;
        frame[2] = (byte)((len >> 56 ) & (byte)255);
        frame[3] = (byte)((len >> 48 ) & (byte)255);
        frame[4] = (byte)((len >> 40 ) & (byte)255);
        frame[5] = (byte)((len >> 32 ) & (byte)255);
        frame[6] = (byte)((len >> 24 ) & (byte)255);
        frame[7] = (byte)((len >> 16 ) & (byte)255);
        frame[8] = (byte)((len >> 8 ) & (byte)255);
        frame[9] = (byte)(len & (byte)255);
        frameCount = 10;
    }

    int bLength = frameCount + rawData.length;

    byte[] reply = new byte[bLength];

    int bLim = 0;
    for(int i=0; i<frameCount;i++){
        reply[bLim] = frame[i];
        bLim++;
    }
    for(int i=0; i<rawData.length;i++){
        reply[bLim] = rawData[i];
        bLim++;
    }

    out.write(reply);
    out.flush();

}
Run Code Online (Sandbox Code Playgroud)

  • 读取操作的适当缓冲区长度是多少? (3认同)

Ric*_*ury 16

JavaScript实现:

function encodeWebSocket(bytesRaw){
    var bytesFormatted = new Array();
    bytesFormatted[0] = 129;
    if (bytesRaw.length <= 125) {
        bytesFormatted[1] = bytesRaw.length;
    } else if (bytesRaw.length >= 126 && bytesRaw.length <= 65535) {
        bytesFormatted[1] = 126;
        bytesFormatted[2] = ( bytesRaw.length >> 8 ) & 255;
        bytesFormatted[3] = ( bytesRaw.length      ) & 255;
    } else {
        bytesFormatted[1] = 127;
        bytesFormatted[2] = ( bytesRaw.length >> 56 ) & 255;
        bytesFormatted[3] = ( bytesRaw.length >> 48 ) & 255;
        bytesFormatted[4] = ( bytesRaw.length >> 40 ) & 255;
        bytesFormatted[5] = ( bytesRaw.length >> 32 ) & 255;
        bytesFormatted[6] = ( bytesRaw.length >> 24 ) & 255;
        bytesFormatted[7] = ( bytesRaw.length >> 16 ) & 255;
        bytesFormatted[8] = ( bytesRaw.length >>  8 ) & 255;
        bytesFormatted[9] = ( bytesRaw.length       ) & 255;
    }
    for (var i = 0; i < bytesRaw.length; i++){
        bytesFormatted.push(bytesRaw.charCodeAt(i));
    }
    return bytesFormatted;
}

function decodeWebSocket (data){
    var datalength = data[1] & 127;
    var indexFirstMask = 2;
    if (datalength == 126) {
        indexFirstMask = 4;
    } else if (datalength == 127) {
        indexFirstMask = 10;
    }
    var masks = data.slice(indexFirstMask,indexFirstMask + 4);
    var i = indexFirstMask + 4;
    var index = 0;
    var output = "";
    while (i < data.length) {
        output += String.fromCharCode(data[i++] ^ masks[index++ % 4]);
    }
    return output;
}
Run Code Online (Sandbox Code Playgroud)

  • 可能值得注意的是,JavaScript实际上并不支持使用大于"2 ^ 31 - 1"的数字进行移位. (5认同)

Nit*_*tij 11

C#实现

浏览器 - >服务器

    private String DecodeMessage(Byte[] bytes)
    {
        String incomingData = String.Empty;
        Byte secondByte = bytes[1];
        Int32 dataLength = secondByte & 127;
        Int32 indexFirstMask = 2;
        if (dataLength == 126)
            indexFirstMask = 4;
        else if (dataLength == 127)
            indexFirstMask = 10;

        IEnumerable<Byte> keys = bytes.Skip(indexFirstMask).Take(4);
        Int32 indexFirstDataByte = indexFirstMask + 4;

        Byte[] decoded = new Byte[bytes.Length - indexFirstDataByte];
        for (Int32 i = indexFirstDataByte, j = 0; i < bytes.Length; i++, j++)
        {
            decoded[j] = (Byte)(bytes[i] ^ keys.ElementAt(j % 4));
        }

        return incomingData = Encoding.UTF8.GetString(decoded, 0, decoded.Length);
    }
Run Code Online (Sandbox Code Playgroud)

服务器 - >浏览器

    private static Byte[] EncodeMessageToSend(String message)
    {
        Byte[] response;
        Byte[] bytesRaw = Encoding.UTF8.GetBytes(message);
        Byte[] frame = new Byte[10];

        Int32 indexStartRawData = -1;
        Int32 length = bytesRaw.Length;

        frame[0] = (Byte)129;
        if (length <= 125)
        {
            frame[1] = (Byte)length;
            indexStartRawData = 2;
        }
        else if (length >= 126 && length <= 65535)
        {
            frame[1] = (Byte)126;
            frame[2] = (Byte)((length >> 8) & 255);
            frame[3] = (Byte)(length & 255);
            indexStartRawData = 4;
        }
        else
        {
            frame[1] = (Byte)127;
            frame[2] = (Byte)((length >> 56) & 255);
            frame[3] = (Byte)((length >> 48) & 255);
            frame[4] = (Byte)((length >> 40) & 255);
            frame[5] = (Byte)((length >> 32) & 255);
            frame[6] = (Byte)((length >> 24) & 255);
            frame[7] = (Byte)((length >> 16) & 255);
            frame[8] = (Byte)((length >> 8) & 255);
            frame[9] = (Byte)(length & 255);

            indexStartRawData = 10;
        }

        response = new Byte[indexStartRawData + length];

        Int32 i, reponseIdx = 0;

        //Add the frame bytes to the reponse
        for (i = 0; i < indexStartRawData; i++)
        {
            response[reponseIdx] = frame[i];
            reponseIdx++;
        }

        //Add the data bytes to the response
        for (i = 0; i < length; i++)
        {
            response[reponseIdx] = bytesRaw[i];
            reponseIdx++;
        }

        return response;
    }
Run Code Online (Sandbox Code Playgroud)


Hun*_*des 6

pimvdb的答案在python中实现:

def DecodedCharArrayFromByteStreamIn(stringStreamIn):
    #turn string values into opererable numeric byte values
    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 decodedChars
Run Code Online (Sandbox Code Playgroud)

用法示例:

fromclient = '\x81\x8c\xff\xb8\xbd\xbd\xb7\xdd\xd1\xd1\x90\x98\xea\xd2\x8d\xd4\xd9\x9c'
# this looks like "?ŒOÇ¿¢gÓ ç\Ð=«ož" in unicode, received by server
print DecodedCharArrayFromByteStreamIn(fromclient)
# ['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!']
Run Code Online (Sandbox Code Playgroud)


Tac*_*cus 5

除了PHP帧编码功能之外,还有一个解码函数:

function Decode($M){
    $M = array_map("ord", str_split($M));
    $L = $M[1] AND 127;

    if ($L == 126)
        $iFM = 4;
    else if ($L == 127)
        $iFM = 10;
    else
        $iFM = 2;

    $Masks = array_slice($M, $iFM, 4);

    $Out = "";
    for ($i = $iFM + 4, $j = 0; $i < count($M); $i++, $j++ ) {
        $Out .= chr($M[$i] ^ $Masks[$j % 4]);
    }
    return $Out;
}
Run Code Online (Sandbox Code Playgroud)

我在这里使用易于使用的WebSocket PHP类实现了这个以及其他功能.