如何按照协议使用WebSocket在服务器端发送和接收消息?
当我从浏览器向服务器发送数据时,为什么在服务器上看似随机字节?数据以某种方式编码了吗?
成帧如何在服务器→客户端和客户端→服务器方向上工作?
#1 楼
注意:这是一些解释和伪代码,说明如何实现非常琐碎的服务器,该服务器可以按照确定的帧格式处理传入和传出的WebSocket消息。它不包括握手过程。此外,这个答案是出于教育目的。它不是功能齐全的实现。规范(RFC 6455)
发送消息
(换句话说,服务器→浏览器)
您要发送的帧需要根据WebSocket框架格式进行格式化。对于发送消息,此格式如下:
一个字节,其中包含数据类型(以及一些其他信息,这些信息对于普通服务器来说是超出范围的)
包含长度的字节
如果长度不适合第二个字节,则为两个或八个字节(然后第二个字节是一个代码,说明用于该长度的字节数)
实际(原始)data
文本帧的第一个字节为
1000 0001
(或129
)。第二个字节的第一位设置为
0
,因为我们不编码数据(从服务器到客户端的编码不是强制性的)。有必要确定原始数据的长度,以便正确发送长度字节:
如果
0 <= length <= 125
不需要附加字节如果
126 <= length <= 65535
则需要附加两个字节,第二个字节是126
如果
length >= 65536
则需要附加八个字节,第二个字节是127
必须将长度切成单独的字节,这意味着您需要向右移(总共8位),然后通过执行
AND 1111 1111
(即255
)仅保留最后8位)。长度字节到达原始数据之后。
这将导致以下伪代码:
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)
接收消息
(换句话说,浏览器→服务器)
您获得的帧采用以下格式:
一个字节包含数据类型
一个字节包含长度
两个或八个额外的字节(如果长度不适合第二个字节)
四个字节,它们是掩码(=解码键)
实际数据
第一个字节通常不很重要-如果您只是发送文本,则仅使用文本类型。在这种情况下,它将是
1000 0001
(或129
)。第二个字节和其他两个或八个字节需要进行解析,因为您需要知道该长度使用了多少个字节(您需要知道真实数据从哪里开始)。通常不需要长度本身,因为您已经有数据。
第二个字节的第一位始终是
1
,这意味着数据被屏蔽(=编码)。从客户端到服务器的消息始终被屏蔽。您需要执行secondByte AND 0111 1111
删除该第一位。在两种情况下,结果字节不代表长度,因为它不适合第二个字节:0111 1110
或126
的第二个字节表示以下两个字节用于长度0111 1111
的第二个字节,即127
,意味着以下八个字节用于长度这四个掩码字节用于解码已存储的实际数据。已发送。解码算法如下:其中
encodedByte
是数据中的原始字节,encodedByteIndex
是从第一个字节开始计数的字节的索引(偏移)索引为0
的实际数据。 masks
是一个包含四个掩码字节的数组。 这将导致以下伪代码进行解码:
decodedByte = encodedByte XOR masks[encodedByteIndex MOD 4]
#2 楼
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];
}
}
编写:从服务器到客户端
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();
}
评论
读操作的合适缓冲区长度是多少?
– jackgerrits
15年2月28日在0:45
不幸的是,它不起作用。我只是将无效广播(从服务器到客户端)复制到了程序中。套接字已成功连接,消息已成功发送到浏览器,但浏览器未收到任何消息。
–尼克
17-2-26在15:27
#3 楼
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;
}
评论
可能值得注意的是,JavaScript实际上并不支持大于2 ^ 31-1的数字进行移位。
– pimvdb
2012年5月7日13:49
#4 楼
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);
}
服务器->浏览器
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;
}
评论
解码函数总是返回我的特定消息,并带有一个未定义的附录,例如test.c =ܝX [,其中“ test”是我的消息。另一部分是什么呢?
– Snickbrack
15年8月5日在15:06
这么晚才回复很抱歉。我创建了一个小的C#应用程序(控制台和Web)来试用Web套接字。您可以从此处下载它们,以了解其编码方式。链接:dropbox.com/s/gw8hjsov1u6f7c0/Web%20Sockets.rar?dl = 0
– Nitij
2015年8月11日在13:56
这对我来说在大消息上失败了。我将长度> 65535的代码替换为:var l = Convert.ToUInt64(length); var b = BitConverter.GetBytes(l); Array.Reverse(b,0,b.Length); b.CopyTo(frame,2); ...似乎有固定的东西。
– Sean
16年4月19日在6:10
做得好。仅有一件事:在DecodeMessage上,我基于数据帧中包含的有效载荷长度数据来计算“已解码”数组长度,因为“字节”数组长度可能不准确。 “字节”数组的长度取决于读取流的方式。
–user1011138
16年6月29日在10:16
@Sean您可以告诉我您解决大型邮件问题的完整示例吗?我无法将代码更改为您的示例。
–阿里·尤塞菲(Ali Yousefi)
16年8月28日在7:22
#5 楼
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
用法示例:
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', '!']
评论
我试图在脚本中使用您的代码,但没有成功。您也许能提供帮助? stackoverflow.com/questions/43748377/…
–y牛
17年5月3日在17:08
#6 楼
除了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;
}
我已经在易于使用的WebSocket PHP中实现了此功能以及其他功能在这里上课。
#7 楼
PHP实现:function encode($message)
{
$length = strlen($message);
$bytesHeader = [];
$bytesHeader[0] = 129; // 0x1 text frame (FIN + opcode)
if ($length <= 125) {
$bytesHeader[1] = $length;
} else if ($length >= 126 && $length <= 65535) {
$bytesHeader[1] = 126;
$bytesHeader[2] = ( $length >> 8 ) & 255;
$bytesHeader[3] = ( $length ) & 255;
} else {
$bytesHeader[1] = 127;
$bytesHeader[2] = ( $length >> 56 ) & 255;
$bytesHeader[3] = ( $length >> 48 ) & 255;
$bytesHeader[4] = ( $length >> 40 ) & 255;
$bytesHeader[5] = ( $length >> 32 ) & 255;
$bytesHeader[6] = ( $length >> 24 ) & 255;
$bytesHeader[7] = ( $length >> 16 ) & 255;
$bytesHeader[8] = ( $length >> 8 ) & 255;
$bytesHeader[9] = ( $length ) & 255;
}
$str = implode(array_map("chr", $bytesHeader)) . $message;
return $str;
}
#8 楼
感谢您的回答,如果有兴趣的话,我想添加到hfern的(以上)Python版本中以包含Sending函数。def DecodedWebsockRecieve(stringStreamIn):
byteArray = 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)
def EncodeWebSockSend(socket,data):
bytesFormatted = []
bytesFormatted.append(129)
bytesRaw = data.encode()
bytesLength = len(bytesRaw)
if bytesLength <= 125 :
bytesFormatted.append(bytesLength)
elif bytesLength >= 126 and 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
socket.send(bytesFormatted)
阅读用途:
bufSize = 1024
read = DecodedWebsockRecieve(socket.recv(bufSize))
写作用途:
EncodeWebSockSend(sock,"hellooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo")
#9 楼
Go中的实现编码部分(服务器->浏览器)
func encode (message string) (result []byte) {
rawBytes := []byte(message)
var idxData int
length := byte(len(rawBytes))
if len(rawBytes) <= 125 { //one byte to store data length
result = make([]byte, len(rawBytes) + 2)
result[1] = length
idxData = 2
} else if len(rawBytes) >= 126 && len(rawBytes) <= 65535 { //two bytes to store data length
result = make([]byte, len(rawBytes) + 4)
result[1] = 126 //extra storage needed
result[2] = ( length >> 8 ) & 255
result[3] = ( length ) & 255
idxData = 4
} else {
result = make([]byte, len(rawBytes) + 10)
result[1] = 127
result[2] = ( length >> 56 ) & 255
result[3] = ( length >> 48 ) & 255
result[4] = ( length >> 40 ) & 255
result[5] = ( length >> 32 ) & 255
result[6] = ( length >> 24 ) & 255
result[7] = ( length >> 16 ) & 255
result[8] = ( length >> 8 ) & 255
result[9] = ( length ) & 255
idxData = 10
}
result[0] = 129 //only text is supported
// put raw data at the correct index
for i, b := range rawBytes {
result[idxData + i] = b
}
return
}
解码部分(浏览器->服务器)
func decode (rawBytes []byte) string {
var idxMask int
if rawBytes[1] == 126 {
idxMask = 4
} else if rawBytes[1] == 127 {
idxMask = 10
} else {
idxMask = 2
}
masks := rawBytes[idxMask:idxMask + 4]
data := rawBytes[idxMask + 4:len(rawBytes)]
decoded := make([]byte, len(rawBytes) - idxMask + 4)
for i, b := range data {
decoded[i] = b ^ masks[i % 4]
}
return string(decoded)
}
#10 楼
Clojure,解码功能假定帧作为{:data byte-array-buffer :size int-size-of-buffer}
的映射发送,因为实际大小可能与字节数组的大小不同,具体取决于输入流的块大小。此处发布的代码:https ://gist.github.com/viperscape/8918565
(defn ws-decode [frame]
"decodes websocket frame"
(let [data (:data frame)
dlen (bit-and (second data) 127)
mstart (if (== dlen 127) 10 (if (== dlen 126) 4 2))
mask (drop 2 (take (+ mstart 4) data))
msg (make-array Byte/TYPE (- (:size frame) (+ mstart 4)))]
(loop [i (+ mstart 4), j 0]
(aset-byte msg j (byte (bit-xor (nth data i) (nth mask (mod j 4)))))
(if (< i (dec(:size frame))) (recur (inc i) (inc j))))
msg))
(defn ws-encode [data]
"takes in bytes, return websocket frame"
(let [len (count data)
blen (if (> len 65535) 10 (if (> len 125) 4 2))
buf (make-array Byte/TYPE (+ len blen))
_ (aset-byte buf 0 -127) ;;(bit-or (unchecked-byte 0x80)
(unchecked-byte 0x1)
_ (if (= 2 blen)
(aset-byte buf 1 len) ;;mask 0, len
(do
(dorun(map #(aset-byte buf %1
(unchecked-byte (bit-and (bit-shift-right len (*(- %2 2) 8))
255)))
(range 2 blen) (into ()(range 2 blen))))
(aset-byte buf 1 (if (> blen 4) 127 126))))
_ (System/arraycopy data 0 buf blen len)]
buf))
评论
为什么将文本框设置为1000 0001(129)?规范说:%x1表示文本框架。所以应该是0000 0001(0x01),还是?
–丹尼斯
2012年1月16日13:32
@Dennis:帧操作码为0001,因为它在规范那部分的标头中指出:“操作码:4位”。第一个字节由FIN,RSV1-3和操作码组成。 FIN为1,RSV1-3均为三个0,操作码为0001,第一个字节的总和为1000 0001。另请参见规范中的插图,该插图显示了字节如何在不同部分中拆分。
– pimvdb
2012年1月16日在21:33
在服务器->客户端模型中,您有几行读为'bytesFormatted [2] =(bytesRaw.length >> 56)和255'-您可以为我分解一下吗?对我来说,AND似乎是一个逻辑运算符,所以我不能期望在C#中简单地将数字放在后面会对我有任何帮助。同样,我不确定您的标记中的“ >>”应该表示什么,但是它确实会转移到C#中……对我而言意味着什么……:P
– DigitalJedi805
2012年4月24日在21:07
如果有人真的可以帮我解决这个问题,我很乐意将C#实现发布为答案。
– DigitalJedi805
2012年4月24日在21:14
@Neevek:它们的含义是掩码字节本身必须不可预测。如果它们恒定不变,那么它们就没什么意义了。基本上,当恶意用户拥有一片数据时,如果没有这些掩码,他应该无法对其进行解码。如果遮罩的位置不可预测,那么对于真正的服务器来说,解码就有点困难了:)
– pimvdb
2012年7月19日在8:31