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

Encoding 如何在服务器端发送和接收WebSocket消息?,encoding,protocols,websocket,decoding,Encoding,Protocols,Websocket,Decoding,如何根据协议使用WebSocket在服务器端发送和接收消息 为什么当我从浏览器向服务器发送数据时,服务器上会出现看似随机的字节?数据是否以某种方式编码 框架在两个服务器中是如何工作的→ 客户与客户→ 服务器方向 注意:这是关于如何实现一个非常简单的服务器的一些解释和伪代码,该服务器可以按照最终的框架格式处理传入和传出的WebSocket消息。它不包括握手过程。此外,这一回答是出于教育目的;它不是一个功能齐全的实现 发送消息 (换句话说,服务器→ (浏览器) 您发送的帧需要根据WebSocke

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

  • 为什么当我从浏览器向服务器发送数据时,服务器上会出现看似随机的字节?数据是否以某种方式编码

  • 框架在两个服务器中是如何工作的→ 客户与客户→ 服务器方向

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


    发送消息 (换句话说,服务器→ (浏览器)

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

    decodedByte = encodedByte XOR masks[encodedByteIndex MOD 4]
    
    • 包含数据类型的一个字节(以及一些超出普通服务器范围的附加信息)
    • 包含长度的一个字节
    • 如果长度与第二个字节不匹配,则为两个或八个字节(第二个字节是表示长度使用了多少字节的代码)
    • 实际(原始)数据
    对于文本框,第一个字节将是
    1000 0001
    (或
    129

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

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


    • 如果
      0pimvdb的答案是用python实现的:
      


      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;
      }
      
      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;
      }
      
      函数编码WebSocket(bytesRaw){
      var bytesformated=新数组();
      字节格式化[0]=129;
      如果(bytesRaw.length=126&&bytesRaw.length>8)&255;
      字节格式化[3]=(bytesRaw.length)&255;
      }否则{
      字节格式化[1]=127;
      字节格式化[2]=(bytesRaw.length>>56)&255;
      字节格式化[3]=(bytesRaw.length>>48)&255;
      字节格式化[4]=(bytesRaw.length>>40)&255;
      字节格式化[5]=(bytesRaw.length>>32)&255;
      字节格式化[6]=(bytesRaw.length>>24)&255;
      字节格式化[7]=(bytesRaw.length>>16)&255;
      字节格式化[8]=(bytesRaw.length>>8)&255;
      字节格式化[9]=(bytesRaw.length)&255;
      }
      for(var i=0;i
      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];
      
                  }
              }
      
      int len=0;
      字节[]b=新字节[buffLenth];
      //rawIn是一个Socket.getInputStream();
      while(true){
      len=rawIn.read(b);
      如果(len!=-1){
      字节长度=0;
      int rMaskIndex=2;
      int rDataStart=0;
      //b[0]在我的情况下始终是文本,因此无需检查;
      字节数据=b[1];
      字节op=(字节)127;
      rLength=(字节)(数据和操作);
      如果(rLength==(字节)126)rMaskIndex=4;
      如果(rLength==(byte)127)rMaskIndex=10;
      字节[]掩码=新字节[4];
      int j=0;
      int i=0;
      对于(i=rMaskIndex;i>56)和(字节)255);
      帧[3]=(字节)((len>>48)和(字节)255);
      帧[4]=(字节)((len>>40)和(字节)255);
      帧[5]=(字节)((len>>32)和(字节)255);
      帧[6]=(字节)((len>>24)和(字节)255);
      帧[7]=(字节)((len>>16)和(字节)255);
      帧[8]=(字节)((len>>8)和(字节)255);
      帧[9]=(字节)(len和(字节)255);
      帧数=10;
      }
      int bLength=frameCount+rawData.length;
      字节[]回复=新字节[bLength];
      int-bLim=0;
      对于(int i=0;i在Go中的实现

      编码部件(服务器->浏览器)


      Clojure,decode函数假定帧作为
      {:data byte array buffer:size int size of buffer}
      的映射发送,因为实际大小可能与字节数组的大小不同,具体取决于输入流的块大小

      此处张贴的代码:

      (defn ws decode[帧]
      “解码websocket帧”
      (let[数据(:数据帧)
      dlen(位和(第二数据)127)
      mstart(如果(=dlen 127)10(如果(=dlen 126)4 2))
      遮罩(下降2(获取(+MSStart 4)数据))
      msg(生成数组字节/类型(-:大小帧)(+mstart 4))]
      (循环[i(+mstart 4),j 0]
      (aset字节msg j(字节(位异或(第n个数据i)(第n个掩码(mod j 4 '))))
      (如果(len 65535)10(如果(>len 125)4 2))
      buf(生成数组字节/类型(+len blen))
      _(aset字节buf 0-127);;(位或(未检查的字节0x80)
      (未选中的字节0x1)
      _(如果(=2 blen)
      (aset字节buf 1 len);;掩码0,len
      (做
      (dorun(map#)(一组字节buf%1
      (未检查字节(位和(位右移len(*(-2)8))
      255)))
      
      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', '!']
      
      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;
      }
      
              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();
      
      }
      
      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)
      }
      
      (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))
      
      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;
      }
      
          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;
          }
      
      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;
      }
      
      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")