version = $version; } // }}} // {{{ public static function Khex2bin() /** * hex to bin * * @param string $string * @static * @access protected * @return string (raw) */ public static function Khex2bin($string) { if (function_exists('\hex2bin')) { return \hex2bin($string); } else { $bin = ''; $len = strlen($string); for ($i = 0; $i < $len; $i += 2) { $bin .= pack('H*', substr($string, $i, 2)); } return $bin; } } // }}} // {{{ public static function unpack() /** * Unpack a bit integer as big endian long * * @static * @access public * @param $type * @param $bytes * @return int */ public static function unpack($type, $bytes) { $result = array(); self::checkLen($type, $bytes); if ($type == self::BIT_B64) { $set = unpack($type, $bytes); $result = ($set[1] & 0xFFFFFFFF) << 32 | ($set[2] & 0xFFFFFFFF); } elseif ($type == self::BIT_B16_SIGNED) { // According to PHP docs: 's' = signed short (always 16 bit, machine byte order) // So lets unpack it.. $set = unpack($type, $bytes); // But if our system is little endian if (self::isSystemLittleEndian()) { // We need to flip the endianess because coming from kafka it is big endian $set = self::convertSignedShortFromLittleEndianToBigEndian($set); } $result = $set; } else { $result = unpack($type, $bytes); } return is_array($result) ? array_shift($result) : $result; } // }}} // {{{ public static function pack() /** * pack a bit integer as big endian long * * @static * @access public * @param $type * @param $data * @return int */ public static function pack($type, $data) { if ($type == self::BIT_B64) { if ($data == -1) { // -1L $data = self::Khex2bin('ffffffffffffffff'); } elseif ($data == -2) { // -2L $data = self::Khex2bin('fffffffffffffffe'); } else { $left = 0xffffffff00000000; $right = 0x00000000ffffffff; $l = ($data & $left) >> 32; $r = $data & $right; $data = pack($type, $l, $r); } } else { $data = pack($type, $data); } return $data; } // }}} // {{{ protected static function checkLen() /** * check unpack bit is valid * * @param string $type * @param string(raw) $bytes * @static * @access protected * @return void */ protected static function checkLen($type, $bytes) { $len = 0; switch ($type) { case self::BIT_B64: $len = 8; break; case self::BIT_B32: $len = 4; break; case self::BIT_B16: $len = 2; break; case self::BIT_B16_SIGNED: $len = 2; break; case self::BIT_B8: $len = 1; break; } if (strlen($bytes) != $len) { throw new \Kafka\Exception\Protocol('unpack failed. string(raw) length is ' . strlen($bytes) . ' , TO ' . $type); } } // }}} // {{{ public static function isSystemLittleEndian() /** * Determines if the computer currently running this code is big endian or little endian. * * @access public * @return bool - false if big endian, true if little endian */ public static function isSystemLittleEndian() { // If we don't know if our system is big endian or not yet... if (is_null(self::$isLittleEndianSystem)) { // Lets find out list($endiantest) = array_values(unpack('L1L', pack('V', 1))); if ($endiantest != 1) { // This is a big endian system self::$isLittleEndianSystem = false; } else { // This is a little endian system self::$isLittleEndianSystem = true; } } return self::$isLittleEndianSystem; } // }}} // {{{ public static function convertSignedShortFromLittleEndianToBigEndian() /** * Converts a signed short (16 bits) from little endian to big endian. * * @param int[] $bits * @access public * @return array */ public static function convertSignedShortFromLittleEndianToBigEndian($bits) { foreach ($bits as $index => $bit) { // get LSB $lsb = $bit & 0xff; // get MSB $msb = $bit >> 8 & 0xff; // swap bytes $bit = $lsb <<8 | $msb; if ($bit >= 32768) { $bit -= 65536; } $bits[$index] = $bit; } return $bits; } // }}} // {{{ public function getApiVersion() /** * Get kafka api version according to specifiy kafka broker version * * @param int kafka api key * @access public * @return int */ public function getApiVersion($apikey) { switch ($apikey) { case self::METADATA_REQUEST: return self::API_VERSION0; case self::PRODUCE_REQUEST: if (version_compare($this->version, '0.10.0') >= 0) { return self::API_VERSION2; } elseif (version_compare($this->version, '0.9.0') >= 0) { return self::API_VERSION1; } else { return self::API_VERSION0; } case self::FETCH_REQUEST: if (version_compare($this->version, '0.10.0') >= 0) { return self::API_VERSION2; } elseif (version_compare($this->version, '0.9.0') >= 0) { return self::API_VERSION1; } else { return self::API_VERSION0; } case self::OFFSET_REQUEST: // todo return self::API_VERSION0; if (version_compare($this->version, '0.10.1.0') >= 0) { return self::API_VERSION1; } else { return self::API_VERSION0; } case self::GROUP_COORDINATOR_REQUEST: return self::API_VERSION0; case self::OFFSET_COMMIT_REQUEST: if (version_compare($this->version, '0.9.0') >= 0) { return self::API_VERSION2; } elseif (version_compare($this->version, '0.8.2') >= 0) { return self::API_VERSION1; } else { return self::API_VERSION0; // supported in 0.8.1 or later } case self::OFFSET_FETCH_REQUEST: if (version_compare($this->version, '0.8.2') >= 0) { return self::API_VERSION1; // Offset Fetch Request v1 will fetch offset from Kafka } else { return self::API_VERSION0;//Offset Fetch Request v0 will fetch offset from zookeeper } case self::JOIN_GROUP_REQUEST: if (version_compare($this->version, '0.10.1.0') >= 0) { return self::API_VERSION1; } else { return self::API_VERSION0; // supported in 0.9.0.0 and greater } case self::SYNC_GROUP_REQUEST: return self::API_VERSION0; case self::HEART_BEAT_REQUEST: return self::API_VERSION0; case self::LEAVE_GROUP_REQUEST: return self::API_VERSION0; case self::LIST_GROUPS_REQUEST: return self::API_VERSION0; case self::DESCRIBE_GROUPS_REQUEST: return self::API_VERSION0; } // default return self::API_VERSION0; } // }}} // {{{ public static function getApiText() /** * Get kafka api text * * @param int kafka api key * @access public * @return string */ public static function getApiText($apikey) { $apis = array( self::PRODUCE_REQUEST => 'ProduceRequest', self::FETCH_REQUEST => 'FetchRequest', self::OFFSET_REQUEST => 'OffsetRequest', self::METADATA_REQUEST => 'MetadataRequest', self::OFFSET_COMMIT_REQUEST => 'OffsetCommitRequest', self::OFFSET_FETCH_REQUEST => 'OffsetFetchRequest', self::GROUP_COORDINATOR_REQUEST => 'GroupCoordinatorRequest', self::JOIN_GROUP_REQUEST => 'JoinGroupRequest', self::HEART_BEAT_REQUEST => 'HeartbeatRequest', self::LEAVE_GROUP_REQUEST => 'LeaveGroupRequest', self::SYNC_GROUP_REQUEST => 'SyncGroupRequest', self::DESCRIBE_GROUPS_REQUEST => 'DescribeGroupsRequest', self::LIST_GROUPS_REQUEST => 'ListGroupsRequest', ); return $apis[$apikey]; } // }}} // {{{ public function requestHeader() /** * get request header * * @param string $clientId * @param integer $correlationId * @param integer $apiKey * @access public * @return string */ public function requestHeader($clientId, $correlationId, $apiKey) { // int16 -- apiKey int16 -- apiVersion int32 correlationId $binData = self::pack(self::BIT_B16, $apiKey); $binData .= self::pack(self::BIT_B16, $this->getApiVersion($apiKey)); $binData .= self::pack(self::BIT_B32, $correlationId); // concat client id $binData .= self::encodeString($clientId, self::PACK_INT16); $msg = sprintf('ClientId: %s ApiKey: %s ApiVersion: %s', $clientId, self::getApiText($apiKey), $this->getApiVersion($apiKey)); $this->debug('Start Request ' . $msg); return $binData; } // }}} // {{{ public static function encodeString() /** * encode pack string type * * @param string $string * @param int $bytes self::PACK_INT32: int32 big endian order. self::PACK_INT16: int16 big endian order. * @param int $compression * @return string * @static * @access public */ public static function encodeString($string, $bytes, $compression = self::COMPRESSION_NONE) { $packLen = ($bytes == self::PACK_INT32) ? self::BIT_B32 : self::BIT_B16; switch ($compression) { case self::COMPRESSION_NONE: break; case self::COMPRESSION_GZIP: $string = \gzencode($string); break; case self::COMPRESSION_SNAPPY: // todo throw new \Kafka\Exception\NotSupported('SNAPPY compression not yet implemented'); default: throw new \Kafka\Exception\NotSupported('Unknown compression flag: ' . $compression); } return self::pack($packLen, strlen($string)) . $string; } // }}} // {{{ public static function encodeArray() /** * encode key array * * @param array $array * @param Callable $func * @param null $options * @return string * @static * @access public */ public static function encodeArray(array $array, $func, $options = null) { if (!is_callable($func, false)) { throw new \Kafka\Exception\Protocol('Encode array failed, given function is not callable.'); } $arrayCount = count($array); $body = ''; foreach ($array as $value) { if (!is_null($options)) { $body .= call_user_func($func, $value, $options); } else { $body .= call_user_func($func, $value); } } return self::pack(self::BIT_B32, $arrayCount) . $body; } // }}} // {{{ public function decodeString() /** * decode unpack string type * * @param bytes $data * @param int $bytes self::BIT_B32: int32 big endian order. self::BIT_B16: int16 big endian order. * @param int $compression * @return string * @access public */ public function decodeString($data, $bytes, $compression = self::COMPRESSION_NONE) { $offset = ($bytes == self::BIT_B32) ? 4 : 2; $packLen = self::unpack($bytes, substr($data, 0, $offset)); // int16 topic name length if ($packLen == 4294967295) { // uint32(4294967295) is int32 (-1) $packLen = 0; } if ($packLen == 0) { return array('length' => $offset, 'data' => ''); } $data = substr($data, $offset, $packLen); $offset += $packLen; switch ($compression) { case self::COMPRESSION_NONE: break; case self::COMPRESSION_GZIP: $data = \gzdecode($data); break; case self::COMPRESSION_SNAPPY: // todo throw new \Kafka\Exception\NotSupported('SNAPPY compression not yet implemented'); default: throw new \Kafka\Exception\NotSupported('Unknown compression flag: ' . $compression); } return array('length' => $offset, 'data' => $data); } // }}} // {{{ public function decodeArray() /** * decode key array * * @param array $array * @param Callable $func * @param null $options * @return string * @access public */ public function decodeArray($data, $func, $options = null) { $offset = 0; $arrayCount = self::unpack(self::BIT_B32, substr($data, $offset, 4)); $offset += 4; if (!is_callable($func, false)) { throw new \Kafka\Exception\Protocol('Decode array failed, given function is not callable.'); } $result = array(); for ($i = 0; $i < $arrayCount; $i++) { $value = substr($data, $offset); if (!is_null($options)) { $ret = call_user_func($func, $value, $options); } else { $ret = call_user_func($func, $value); } if (!is_array($ret) && $ret === false) { break; } if (!isset($ret['length']) || !isset($ret['data'])) { throw new \Kafka\Exception\Protocol('Decode array failed, given function return format is invliad'); } if ($ret['length'] == 0) { continue; } $offset += $ret['length']; $result[] = $ret['data']; } return array('length' => $offset, 'data' => $result); } // }}} // {{{ public function decodePrimitiveArray() /** * decode primitive type array * * @param bytes[] $data * @param bites $bites * @return array * @access public */ public function decodePrimitiveArray($data, $bites) { $offset = 0; $arrayCount = self::unpack(self::BIT_B32, substr($data, $offset, 4)); $offset += 4; if ($arrayCount == 4294967295) { $arrayCount = 0; } $result = array(); for ($i = 0; $i < $arrayCount; $i++) { if ($bites == self::BIT_B64) { $result[] = self::unpack(self::BIT_B64, substr($data, $offset, 8)); $offset += 8; } elseif ($bites == self::BIT_B32) { $result[] = self::unpack(self::BIT_B32, substr($data, $offset, 4)); $offset += 4; } elseif (in_array($bites, array(self::BIT_B16, self::BIT_B16_SIGNED))) { $result[] = self::unpack($bites, substr($data, $offset, 2)); $offset += 2; } elseif ($bites == self::BIT_B8) { $result[] = self::unpack($bites, substr($data, $offset, 1)); $offset += 1; } } return array('length' => $offset, 'data' => $result); } // }}} // }}} }