├── Connect.php ├── Deal.php ├── README.md ├── bin ├── BinLogColumns.php ├── BinLogEvent.php └── BinLogPack.php ├── config └── Config.php ├── const ├── ConstAuth.php ├── ConstCapability.php ├── ConstCommand.php ├── ConstEventType.php ├── ConstFieldType.php └── ConstMy.php ├── db ├── DBHelper.php ├── DBMysql.php ├── Log.php └── TimeDate.php ├── file-pos ├── mysql-replication.png ├── pack ├── Pack.php ├── PackAuth.php ├── RowEvent.php └── ServerInfo.php └── run.php /Connect.php: -------------------------------------------------------------------------------- 1 | 2, 'usec' => 5000]); 90 | // socket_set_option(self::$_SOCKET,SOL_SOCKET,SO_RCVTIMEO,['sec' => 2, 'usec' => 5000]); 91 | 92 | self::$_FLAG = ConstCapability::$CAPABILITIES ;//| S::$MULTI_STATEMENTS; 93 | if(self::$_DB) { 94 | self::$_FLAG |= ConstCapability::$CONNECT_WITH_DB; 95 | } 96 | 97 | self::$_FLAG |= ConstCapability::$MULTI_RESULTS; 98 | 99 | // 连接到mysql 100 | self::_connect(); 101 | } 102 | 103 | 104 | private static function _connect() { 105 | 106 | // create socket 107 | if(!socket_connect(self::$_SOCKET, self::$_HOST, self::$_PORT)) { 108 | throw new BinLogException( 109 | sprintf( 110 | 'error:%s, msg:%s', 111 | socket_last_error(), 112 | socket_strerror(socket_last_error()) 113 | ) 114 | ); 115 | } 116 | 117 | // 获取server信息 118 | self::_serverInfo(); 119 | 120 | // 认证 121 | self::auth(); 122 | 123 | // 124 | self::getBinlogStream(); 125 | } 126 | 127 | 128 | 129 | private static function _write($data) { 130 | if(socket_write(self::$_SOCKET, $data, strlen($data))=== false ) 131 | { 132 | throw new BinLogException( sprintf( "Unable to write to socket: %s", socket_strerror( socket_last_error()))); 133 | } 134 | return true; 135 | } 136 | 137 | private static function _readBytes($data_len) { 138 | 139 | // server gone away 140 | if($data_len == 5) { 141 | throw new BinLogException('read 5 bytes from mysql server has gone away'); 142 | } 143 | 144 | try{ 145 | $bytes_read = 0; 146 | $body = ''; 147 | while ($bytes_read < $data_len) { 148 | $resp = socket_read(self::$_SOCKET, $data_len - $bytes_read); 149 | 150 | // 151 | if($resp === false) { 152 | self::_goneAway('remote host has closed the connection'); 153 | throw new BinLogException( 154 | sprintf( 155 | 'remote host has closed. error:%s, msg:%s', 156 | socket_last_error(), 157 | socket_strerror(socket_last_error()) 158 | )); 159 | } 160 | 161 | // server kill connection or server gone away 162 | if(strlen($resp) === 0){ 163 | self::_goneAway('read less data'); 164 | throw new BinLogException("read less " . ($data_len - strlen($body))); 165 | } 166 | $body .= $resp; 167 | $bytes_read += strlen($resp); 168 | } 169 | if(strlen($body) < $data_len){ 170 | self::_goneAway('read undone data'); 171 | throw new BinLogException("read less " . ($data_len - strlen($body))); 172 | } 173 | return $body; 174 | }catch (Exception $e) { 175 | self::_goneAway('socekt read fail!'); 176 | throw new BinLogException(var_export($e, true)); 177 | } 178 | 179 | } 180 | 181 | /** 182 | * mysql gone away 183 | * @param $msg 184 | */ 185 | private static function _goneAway($msg) { 186 | Log::error($msg . 'mysql server has gone away', 'mysqlBinlog', Config::$LOG['binlog']['error']); 187 | } 188 | 189 | private static function _readPacket() { 190 | 191 | //消息头 192 | $header = self::_readBytes(4); 193 | if($header === false) return false; 194 | //消息体长度3bytes 小端序 195 | $a = unpack("L",$header[0].$header[1].$header[2].chr(0))[1]; 196 | 197 | //$a = (int)(ord($header[0]) & 0xFF); 198 | //$a += (int)((ord($header[1])& 0xFF) << 8); 199 | //$a += (int)((ord($header[2])& 0xFF) << 16); 200 | //序号 1byte 确认消息的顺序 201 | //$pack_num = unpack("C",$header[3])[1]; 202 | 203 | $result = self::_readBytes($a); 204 | //echo '消息长度'.$a.' read -> '.strlen($result)."\n"; 205 | return $result; 206 | } 207 | 208 | /** 209 | * @brief 获取ServerInfo 210 | * @return void 211 | */ 212 | private static function _serverInfo() { 213 | $pack = self::_readPacket(); 214 | ServerInfo::run($pack); 215 | // 加密salt 216 | self::$_SALT = ServerInfo::getSalt(); 217 | } 218 | 219 | private static function auth() { 220 | // pack拼接 221 | $data = PackAuth::initPack(self::$_FLAG, self::$_USER, self::$_PASS, self::$_SALT, self::$_DB); 222 | 223 | self::_write($data); 224 | // 225 | $result = self::_readPacket(); 226 | 227 | // 认证是否成功 228 | PackAuth::success($result); 229 | } 230 | 231 | 232 | public static function excute($sql) { 233 | $chunk_size = strlen($sql) + 1; 234 | $prelude = pack('LC',$chunk_size, 0x03); 235 | self::_write($prelude . $sql); 236 | } 237 | 238 | public static function getBinlogStream() { 239 | 240 | // checksum 241 | self::$_CHECKSUM = DBHelper::isCheckSum(); 242 | if(self::$_CHECKSUM){ 243 | self::excute("set @master_binlog_checksum= @@global.binlog_checksum"); 244 | self::_readPacket(); 245 | } 246 | 247 | //heart_period 248 | $heart = (int)Config::$DB_CONFIG['heartbeat']; 249 | if($heart) { 250 | self::excute("set @master_heartbeat_period=".($heart*1000000000)); 251 | self::_readPacket(); 252 | } 253 | 254 | self::_writeRegisterSlaveCommand(); 255 | 256 | // 开始读取的二进制日志位置 257 | if(!self::$_FILE) { 258 | $logInfo = DBHelper::getPos(); 259 | self::$_FILE = $logInfo['File']; 260 | if(!self::$_POS) { 261 | self::$_POS = $logInfo['Position']; 262 | } 263 | } 264 | 265 | // 初始化 266 | BinLogPack::setFilePos(self::$_FILE, self::$_POS); 267 | 268 | $header = pack('l', 11 + strlen(self::$_FILE)); 269 | 270 | // COM_BINLOG_DUMP 271 | $data = $header . chr(ConstCommand::COM_BINLOG_DUMP); 272 | $data .= pack('L', self::$_POS); 273 | $data .= pack('s', 0); 274 | $data .= pack('L', self::$_SLAVE_SERVER_ID); 275 | $data .= self::$_FILE; 276 | 277 | self::_write($data); 278 | 279 | //认证 280 | $result = self::_readPacket(); 281 | PackAuth::success($result); 282 | self::_writeRegisterSlaveCommand(); 283 | } 284 | 285 | /** 286 | * @breif 解析binlog 287 | */ 288 | public static function analysisBinLog($flag = false) { 289 | 290 | $pack = self::_readPacket(); 291 | 292 | // 校验数据包格式 293 | PackAuth::success($pack); 294 | 295 | //todo eof pack 0xfe 296 | 297 | $binlog = BinLogPack::getInstance(); 298 | $result = $binlog->init($pack, self::$_CHECKSUM); 299 | 300 | // debug 301 | if(DEBUG) { 302 | Log::out(round(memory_get_usage()/1024/1024, 2).'MB'); 303 | } 304 | 305 | //持久化当前读到的file pos 306 | if($flag) { 307 | return self::_sync($result, $flag); 308 | }else{ 309 | if($result) return $result; 310 | } 311 | } 312 | 313 | private static function _sync($result, $flag) { 314 | 315 | if($flag) { 316 | if(!self::putFilePos()) { 317 | Log::out('write file pos fail');exit; 318 | } 319 | } 320 | return $result; 321 | } 322 | 323 | /** 324 | * todo 325 | * @breif 注册成slave 326 | * @return void 327 | */ 328 | private static function _writeRegisterSlaveCommand() { 329 | $len = 18+strlen(Config::$DB_CONFIG['username'])+ 330 | strlen(Config::$DB_CONFIG['hostname'])+ 331 | strlen(Config::$DB_CONFIG['password']); 332 | $header = pack('l', $len); 333 | 334 | // COM_BINLOG_DUMP 335 | $data = $header . chr(ConstCommand::COM_REGISTER_SLAVE); 336 | 337 | $data .= pack('l', self::$_SLAVE_SERVER_ID); 338 | $data .= pack('C',strlen(Config::$DB_CONFIG['hostname'])); 339 | $data .= Config::$DB_CONFIG['hostname']; 340 | $data .= pack('C',strlen(Config::$DB_CONFIG['username'])); 341 | $data .= Config::$DB_CONFIG['username']; 342 | $data .= pack('C',strlen(Config::$DB_CONFIG['password'])); 343 | $data .= Config::$DB_CONFIG['password']; 344 | 345 | $data .= pack('s', Config::$DB_CONFIG['port']); 346 | $data .= pack('l', 0); 347 | $data .= pack('l', 1); 348 | self::_write($data); 349 | self::_readPacket(); 350 | } 351 | 352 | /** 353 | * @breif 注册成slave 354 | * @return void 355 | */ 356 | private static function _writeRegisterSlaveCommand2() { 357 | $header = pack('l', 18); 358 | 359 | // COM_BINLOG_DUMP 360 | $data = $header . chr(ConstCommand::COM_REGISTER_SLAVE); 361 | $data .= pack('L', self::$_SLAVE_SERVER_ID); 362 | $data .= chr(0); 363 | $data .= chr(0); 364 | $data .= chr(0); 365 | 366 | $data .= pack('s', ''); 367 | 368 | $data .= pack('L', 0); 369 | $data .= pack('L', 1); 370 | 371 | self::_write($data); 372 | 373 | $result = self::_readPacket(); 374 | PackAuth::success($result); 375 | } 376 | 377 | /** 378 | * @brief 读取当前binlog的位置 379 | * @return array|bool 380 | */ 381 | public static function getFilePos() { 382 | 383 | $filename = $pos = ''; 384 | if(file_exists(self::$FILE_POS)) { 385 | $data = file_get_contents(self::$FILE_POS); 386 | list($filename, $pos, $date) = explode("|", $data); 387 | } 388 | if($filename && $pos) { 389 | return array($filename, $pos); 390 | } else{ 391 | return false; 392 | } 393 | } 394 | 395 | /** 396 | * @brief 写入当前binlog的位置 397 | * @return array|bool 398 | */ 399 | public static function putFilePos() { 400 | list($filename, $pos) = BinLogPack::getFilePos(); 401 | $data = sprintf("%s|%s|%s", $filename, $pos, date('Y-m-d H:i:s')); 402 | return file_put_contents(self::$FILE_POS, $data); 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /Deal.php: -------------------------------------------------------------------------------- 1 | 'localhost:2181' 15 | ]; 16 | 17 | private static $_TOPIC = 'uid_order_center'; 18 | 19 | private static function _init() { 20 | return self::_getKafkaClient(); 21 | } 22 | 23 | private static function _getKafkaClient() { 24 | 25 | if(self::$_KAFKA) { 26 | return self::$_KAFKA; 27 | } 28 | //connect kafka 29 | 30 | try{ 31 | self::$_KAFKA = \Kafka\Produce::getInstance(self::$_CONFIG[1], 3000); 32 | }catch(\Kafka\Exception $e) { 33 | Log::error('connect to kafka error', 'connectError', Config::$LOG['kafka']['error']); 34 | return false; 35 | } 36 | return true; 37 | } 38 | 39 | 40 | public static function push($data) { 41 | if(self::_init() === false) return false; 42 | 43 | $lines = []; 44 | foreach($data as $value) { 45 | $lines[] =json_encode($value); 46 | } 47 | 48 | // push to kafka 49 | if($lines) { 50 | try { 51 | self::$_KAFKA->setRequireAck(-1); 52 | 53 | self::$_KAFKA->setMessages(self::$_TOPIC, 0, $lines); 54 | $result = self::$_KAFKA->send(); 55 | // 写入成功 56 | if($result[self::$_TOPIC][0]['errCode']!=0) { 57 | Log::error('write to kafka error', 'writeError', Config::$LOG['kafka']['error']); 58 | self::$_KAFKA = null; 59 | return false; 60 | } 61 | if(DEBUG) { 62 | Log::notice(json_encode($result), 'kafkaPush', Config::$LOG['kafka']['notice']); 63 | } 64 | return true; 65 | }catch (\Kafka\Exception $e) { 66 | /* 67 | $trace = $e->getTrace(); 68 | foreach($trace as &$value){ 69 | if($value['class'] == 'Deal' && $value['function'] == 'push') { 70 | $value['args'] = null; 71 | } 72 | }*/ 73 | \Kafka\Produce::destoryInstance(); 74 | self::$_KAFKA = null; 75 | $message = [ 76 | 'message' => $e->getMessage(), 77 | 'code' => $e->getCode(), 78 | 'file' => $e->getFile(), 79 | 'line' => $e->getLine(), 80 | // 'trace' => $trace, 81 | ]; 82 | Log::error('push to kafka error' . var_export($message, true), 'pushError', Config::$LOG['kafka']['error']); 83 | return false; 84 | } 85 | 86 | } 87 | return false; 88 | 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-mysql-replication 2 | 3 | 参考python版本:https://github.com/noplay/python-mysql-replication 4 | 5 | ## 运行环境 6 | 目前只支持数据库utf8编码 7 | php版本>=5.4 8 | mysql版本>=5.1 9 | 需要安装php sockets扩展 10 | 运行用户需要有创建文件的权限 11 | 12 | ## Config.php 配置文件 13 | 14 | 15 | 运行run.php 目前只支持row模式 16 | 项目中 可以用supervisor监控 run.php 进程 17 | Connect::analysisBinLog bool true存储当前的file pos 18 | 本例中 通过读取binlog存储到kafka中 kafka版本 0.8.2.0 19 | kafka-client 用到了github开源的一个项目 https://github.com/nmred/kafka-php 20 | BinLogPack.php打印了事件类型 21 | 22 | 23 | ## 配置mysql,打开mysql的binlog,配置binlog格式为row 24 | log-bin=mysql-bin 25 | server-id=1 26 | binlog_format=row 27 | 28 | ## 持久化 29 | file-pos 保存了当前读取到binlog的filename和pos,保证程序异常退出后能继续读取binlog 30 | 新项目运行时 要删除file-pos,从当前show master status,读取到的filename pos开始读取 31 | 可以设置file-pos,程序则从当前设置的位置读取binlog 32 | 33 | ## 流程 34 | ![image](https://github.com/fengxiangyun/mysql-replication/blob/master/mysql-replication.png) 35 | 36 | ## 联系我 37 | 任何问题可以mail 38 | zhaozhiqiang1734@163.com 39 | QQ:838⑤91688 40 | 41 | 42 | -------------------------------------------------------------------------------- /bin/BinLogColumns.php: -------------------------------------------------------------------------------- 1 | read(2))[1]; 27 | }elseif (self::$field['type'] == ConstFieldType::DOUBLE){ 28 | self::$field['size'] = $packet->readUint8(); 29 | }elseif (self::$field['type'] == ConstFieldType::FLOAT){ 30 | self::$field['size'] = $packet->readUint8(); 31 | }elseif (self::$field['type'] == ConstFieldType::TIMESTAMP2){ 32 | self::$field['fsp'] = $packet->readUint8(); 33 | }elseif (self::$field['type'] == ConstFieldType::DATETIME2){ 34 | self::$field['fsp']= $packet->readUint8(); 35 | }elseif (self::$field['type'] == ConstFieldType::TIME2) { 36 | self::$field['fsp'] = $packet->readUint8(); 37 | }elseif (self::$field['type'] == ConstFieldType::TINY && $column_schema["COLUMN_TYPE"] == "tinyint(1)") { 38 | self::$field['type_is_bool'] = True; 39 | }elseif (self::$field['type'] == ConstFieldType::VAR_STRING || self::$field['type'] == ConstFieldType::STRING){ 40 | self::_read_string_metadata($packet, $column_schema); 41 | }elseif( self::$field['type'] == ConstFieldType::BLOB){ 42 | self::$field['length_size'] = $packet->readUint8(); 43 | }elseif (self::$field['type'] == ConstFieldType::GEOMETRY){ 44 | self::$field['length_size'] = $packet->readUint8(); 45 | }elseif( self::$field['type'] == ConstFieldType::NEWDECIMAL){ 46 | self::$field['precision'] = $packet->readUint8(); 47 | self::$field['decimals'] = $packet->readUint8(); 48 | }elseif (self::$field['type'] == ConstFieldType::BIT) { 49 | $bits = $packet->readUint8(); 50 | $bytes = $packet->readUint8(); 51 | self::$field['bits'] = ($bytes * 8) + $bits; 52 | self::$field['bytes'] = int((self::$field['bits'] + 7) / 8); 53 | } 54 | return self::$field; 55 | } 56 | 57 | private static function _read_string_metadata($packet, $column_schema){ 58 | 59 | $metadata = ($packet->readUint8() << 8) + $packet->readUint8(); 60 | $real_type = $metadata >> 8; 61 | if($real_type == ConstFieldType::SET || $real_type == ConstFieldType::ENUM) { 62 | self::$field['type'] = $real_type; 63 | self::$field['size'] = $metadata & 0x00ff; 64 | self::_read_enum_metadata($column_schema); 65 | } else { 66 | self::$field['max_length'] = ((($metadata >> 4) & 0x300) ^ 0x300) + ($metadata & 0x00ff); 67 | } 68 | } 69 | private static function _read_enum_metadata($column_schema) { 70 | $enums = $column_schema["COLUMN_TYPE"]; 71 | if (self::$field['type'] == ConstFieldType::ENUM) { 72 | $enums = str_replace('enum(', '', $enums); 73 | $enums = str_replace(')', '', $enums); 74 | $enums = str_replace('\'', '', $enums); 75 | self::$field['enum_values'] = explode(',', $enums); 76 | } else { 77 | $enums = str_replace('set(', '', $enums); 78 | $enums = str_replace(')', '', $enums); 79 | $enums = str_replace('\'', '', $enums); 80 | self::$field['set_values'] = explode(',', $enums); 81 | } 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /bin/BinLogEvent.php: -------------------------------------------------------------------------------- 1 | read(1)) & 0xFF); 56 | $a += (int)((ord(self::$PACK->read(1)) & 0xFF) << 8); 57 | $a += (int)((ord(self::$PACK->read(1)) & 0xFF) << 16); 58 | $a += (int)((ord(self::$PACK->read(1)) & 0xFF) << 24); 59 | $a += (int)((ord(self::$PACK->read(1)) & 0xFF) << 32); 60 | $a += (int)((ord(self::$PACK->read(1)) & 0xFF) << 40); 61 | return $a; 62 | } 63 | 64 | public static function bitCount($bitmap) { 65 | $n = 0; 66 | for($i=0;$iadvance(1); 47 | self::$EVENT_INFO['time'] = $timestamp = unpack('L', $this->read(4))[1]; 48 | self::$EVENT_INFO['type'] = self::$EVENT_TYPE = unpack('C', $this->read(1))[1]; 49 | self::$EVENT_INFO['id'] = $server_id = unpack('L', $this->read(4))[1]; 50 | self::$EVENT_INFO['size'] = $event_size = unpack('L', $this->read(4))[1]; 51 | 52 | //position of the next event 53 | self::$EVENT_INFO['pos'] = $log_pos = unpack('L', $this->read(4))[1];// 54 | self::$EVENT_INFO['flag'] = $flags = unpack('S', $this->read(2))[1]; 55 | $event_size_without_header = $checkSum === true ? ($event_size -23) : $event_size - 19; 56 | $data = []; 57 | 58 | // 映射fileds相关信息 59 | if (self::$EVENT_TYPE == ConstEventType::TABLE_MAP_EVENT) { 60 | RowEvent::tableMap(self::getInstance(), self::$EVENT_TYPE); 61 | } elseif(in_array(self::$EVENT_TYPE,[ConstEventType::UPDATE_ROWS_EVENT_V2,ConstEventType::UPDATE_ROWS_EVENT_V1])) { 62 | $data = RowEvent::updateRow(self::getInstance(), self::$EVENT_TYPE, $event_size_without_header); 63 | self::$_POS = self::$EVENT_INFO['pos']; 64 | }elseif(in_array(self::$EVENT_TYPE,[ConstEventType::WRITE_ROWS_EVENT_V1, ConstEventType::WRITE_ROWS_EVENT_V2])) { 65 | $data = RowEvent::addRow(self::getInstance(), self::$EVENT_TYPE, $event_size_without_header); 66 | self::$_POS = self::$EVENT_INFO['pos']; 67 | }elseif(in_array(self::$EVENT_TYPE,[ConstEventType::DELETE_ROWS_EVENT_V1, ConstEventType::DELETE_ROWS_EVENT_V2])) { 68 | $data = RowEvent::delRow(self::getInstance(), self::$EVENT_TYPE, $event_size_without_header); 69 | self::$_POS = self::$EVENT_INFO['pos']; 70 | }elseif(self::$EVENT_TYPE == 16) { 71 | //var_dump(bin2hex($pack),$this->readUint64()); 72 | //return RowEvent::delRow(self::getInstance(), self::$EVENT_TYPE); 73 | }elseif(self::$EVENT_TYPE == ConstEventType::ROTATE_EVENT) { 74 | $log_pos = $this->readUint64(); 75 | self::$_FILE_NAME = $this->read($event_size_without_header-8); 76 | }elseif(self::$EVENT_TYPE == ConstEventType::GTID_LOG_EVENT) { 77 | //gtid event 78 | 79 | }elseif(self::$EVENT_TYPE == 15) { 80 | //$pack = self::getInstance(); 81 | //$pack->read(4); 82 | } elseif(self::$EVENT_TYPE == ConstEventType::QUERY_EVENT) { 83 | 84 | } elseif(self::$EVENT_TYPE == ConstEventType::HEARTBEAT_LOG_EVENT) { 85 | //心跳检测机制 86 | $binlog_name = $this->read($event_size_without_header); 87 | echo 'heart beat '.$binlog_name."\n"; 88 | } 89 | 90 | if(DEBUG) { 91 | $msg = self::$_FILE_NAME; 92 | $msg .= ' --- ' . $timestamp; 93 | $msg .= '-- next pos -> '.$log_pos; 94 | $msg .= ' -- typeEvent -> '.self::$EVENT_TYPE; 95 | Log::out($msg); 96 | } 97 | return $data; 98 | } 99 | 100 | public function read($length) { 101 | $length = (int)$length; 102 | $n=''; 103 | 104 | if(self::$_BUFFER) { 105 | $n = substr(self::$_BUFFER, 0 , $length); 106 | if(strlen($n) == $length) { 107 | self::$_BUFFER = substr(self::$_BUFFER, $length);; 108 | return $n; 109 | } else { 110 | self::$_BUFFER = ''; 111 | $length = $length - strlen($n); 112 | } 113 | 114 | } 115 | 116 | for($i = self::$_PACK_KEY; $i < self::$_PACK_KEY + $length; $i++) { 117 | $n .= self::$_PACK[$i]; 118 | } 119 | 120 | self::$_PACK_KEY += $length; 121 | 122 | return $n; 123 | 124 | } 125 | 126 | public function get_key() { 127 | return self::$_PACK_KEY; 128 | } 129 | 130 | public function get_pack() { 131 | return self::$_PACK; 132 | } 133 | 134 | public function unread($data) { 135 | self::$_BUFFER.=$data; 136 | } 137 | /** 138 | * @brief 前进步长 139 | * @param $length 140 | */ 141 | public function advance($length) { 142 | $this->read($length); 143 | } 144 | 145 | /** 146 | * @brief read a 'Length Coded Binary' number from the data buffer. 147 | ** Length coded numbers can be anywhere from 1 to 9 bytes depending 148 | ** on the value of the first byte. 149 | ** From PyMYSQL source code 150 | * @return int|string 151 | */ 152 | 153 | public function readCodedBinary(){ 154 | $c = ord($this->read(1)); 155 | if($c == ConstMy::NULL_COLUMN) { 156 | return ''; 157 | } 158 | if($c < ConstMy::UNSIGNED_CHAR_COLUMN) { 159 | return $c; 160 | } elseif($c == ConstMy::UNSIGNED_SHORT_COLUMN) { 161 | return $this->unpackUint16($this->read(ConstMy::UNSIGNED_SHORT_LENGTH)); 162 | 163 | }elseif($c == ConstMy::UNSIGNED_INT24_COLUMN) { 164 | return $this->unpackInt24($this->read(ConstMy::UNSIGNED_INT24_LENGTH)); 165 | } 166 | elseif($c == ConstMy::UNSIGNED_INT64_COLUMN) { 167 | return $this->unpackInt64($this->read(ConstMy::UNSIGNED_INT64_LENGTH)); 168 | } 169 | } 170 | 171 | public function unpackUint16($data) { 172 | return unpack("S",$data[0] . $data[1])[1]; 173 | } 174 | 175 | public function unpackInt24($data) { 176 | $a = (int)(ord($data[0]) & 0xFF); 177 | $a += (int)((ord($data[1]) & 0xFF) << 8); 178 | $a += (int)((ord($data[2]) & 0xFF) << 16); 179 | return $a; 180 | } 181 | 182 | //ok 183 | public function unpackInt64($data) { 184 | $a = (int)(ord($data[0]) & 0xFF); 185 | $a += (int)((ord($data[1]) & 0xFF) << 8); 186 | $a += (int)((ord($data[2]) & 0xFF) << 16); 187 | $a += (int)((ord($data[3]) & 0xFF) << 24); 188 | $a += (int)((ord($data[4]) & 0xFF) << 32); 189 | $a += (int)((ord($data[5]) & 0xFF) << 40); 190 | $a += (int)((ord($data[6]) & 0xFF) << 48); 191 | $a += (int)((ord($data[7]) & 0xFF) << 56); 192 | return $a; 193 | } 194 | 195 | public function read_int24() 196 | { 197 | $data = unpack("CCC", $this->read(3)); 198 | 199 | $res = $data[1] | ($data[2] << 8) | ($data[3] << 16); 200 | if ($res >= 0x800000) 201 | $res -= 0x1000000; 202 | return $res; 203 | } 204 | 205 | public function read_int24_be() 206 | { 207 | $data = unpack('C3', $this->read(3)); 208 | $res = ($data[1] << 16) | ($data[2] << 8) | $data[3]; 209 | if ($res >= 0x800000) 210 | $res -= 0x1000000; 211 | return $res; 212 | } 213 | 214 | // 215 | public function readUint8() 216 | { 217 | $read = $this->read(1); 218 | return unpack('C', $read)[1]; 219 | } 220 | 221 | // 222 | public function readUint16() 223 | { 224 | return unpack('S', $this->read(2))[1]; 225 | } 226 | 227 | public function readUint24() 228 | { 229 | $data = unpack("C3", $this->read(3)); 230 | return $data[1] + ($data[2] << 8) + ($data[3] << 16); 231 | } 232 | 233 | // 234 | public function readUint32() 235 | { 236 | return unpack('I', $this->read(4))[1]; 237 | } 238 | 239 | public function readUint40() 240 | { 241 | $data = unpack("CI", $this->read(5)); 242 | return $data[1] + ($data[2] << 8); 243 | } 244 | 245 | public function read_int40_be() 246 | { 247 | $data1= unpack("N", $this->read(4))[1]; 248 | $data2 = unpack("C", $this->read(1))[1]; 249 | return $data2 + ($data1 << 8); 250 | } 251 | 252 | // 253 | public function readUint48() 254 | { 255 | $data = unpack("vvv", $this->read(6)); 256 | return $data[1] + ($data[2] << 16) + ($data[3] << 32); 257 | } 258 | 259 | // 260 | public function readUint56() 261 | { 262 | $data = unpack("CSI", $this->read(7)); 263 | return $data[1] + ($data[2] << 8) + ($data[3] << 24); 264 | } 265 | 266 | /* 267 | * 不支持unsigned long long,溢出 268 | */ 269 | public function readUint64() { 270 | $d = $this->read(8); 271 | $data = unpack('V*', $d); 272 | $bigInt = bcadd($data[1], bcmul($data[2], bcpow(2, 32))); 273 | return $bigInt; 274 | 275 | // $unpackArr = unpack('I2', $d); 276 | //$data = unpack("C*", $d); 277 | //$r = $data[1] + ($data[2] << 8) + ($data[3] << 16) + ($data[4] << 24);//+ 278 | //$r2= ($data[5]) + ($data[6] << 8) + ($data[7] << 16) + ($data[8] << 24); 279 | 280 | // return $unpackArr[1] + ($unpackArr[2] << 32); 281 | } 282 | 283 | public function readInt64() 284 | { 285 | return $this->readUint64(); 286 | } 287 | 288 | public function read_uint_by_size($size) 289 | { 290 | 291 | if($size == 1) 292 | return $this->readUint8(); 293 | elseif($size == 2) 294 | return $this->readUint16(); 295 | elseif($size == 3) 296 | return $this->readUint24(); 297 | elseif($size == 4) 298 | return $this->readUint32(); 299 | elseif($size == 5) 300 | return $this->readUint40(); 301 | elseif($size == 6) 302 | return $this->readUint48(); 303 | elseif($size == 7) 304 | return $this->readUint56(); 305 | elseif($size == 8) 306 | return $this->readUint64(); 307 | } 308 | public function read_length_coded_pascal_string($size) 309 | { 310 | $length = $this->read_uint_by_size($size); 311 | return $this->read($length); 312 | } 313 | 314 | /** 315 | * @param $size 316 | * @return float|int 317 | */ 318 | public function read_int_be_by_size($size) { 319 | //Read a big endian integer values based on byte number 320 | if ($size == 1) { 321 | $re = unpack('c', $this->read($size))[1]; 322 | if($re>127) { 323 | return $re-pow(2,8); 324 | } 325 | return $re; 326 | } elseif( $size == 2) { 327 | $re = unpack('n', $this->read($size))[1]; 328 | if($re>32767) { 329 | return $re-pow(2,16); 330 | } 331 | return $re; 332 | } 333 | elseif( $size == 3) 334 | return $this->read_int24_be(); 335 | elseif( $size == 4){ 336 | $re = unpack('N', $this->read($size))[1]; 337 | if($re > 2147483647) { 338 | return ($re-pow(2,32)); 339 | } 340 | return $re; 341 | 342 | } 343 | elseif( $size == 5) 344 | return $this->read_int40_be(); 345 | //TODO 346 | elseif($size == 8) { 347 | $re = unpack('N', $this->read($size))[1]; 348 | return $re; 349 | if($re > 2147483647) { 350 | return ($re-pow(2,64)); 351 | } 352 | return $re; 353 | } 354 | } 355 | 356 | /** 357 | * @return bool 358 | */ 359 | public function isComplete($size) { 360 | // 20解析server_id ... 361 | if(self::$_PACK_KEY + 1 - 20 < $size) { 362 | return false; 363 | } 364 | return true; 365 | } 366 | 367 | /** 368 | * @biref 初始化设置 file,pos,解决持久化file为空问题 369 | * @param $file 370 | * @param $pos 371 | */ 372 | public static function setFilePos($file, $pos) { 373 | self::$_FILE_NAME = $file; 374 | self::$_POS = $pos; 375 | } 376 | 377 | /** 378 | * @brief 获取binlog file,pos持久化 379 | * @return array 380 | */ 381 | public static function getFilePos() { 382 | return array(self::$_FILE_NAME, self::$_POS); 383 | } 384 | 385 | } 386 | -------------------------------------------------------------------------------- /config/Config.php: -------------------------------------------------------------------------------- 1 | 'root', 12 | 'hostname' => '127.0.0.1', 13 | 'host' => '127.0.0.1', 14 | 'port' => 3306, 15 | 'password' => '123456', 16 | 'db_name' => 'zzq', 17 | 'heartbeat' => 5// 心跳检测0不开启,>0 开启,(second) 18 | ); 19 | 20 | // 默认100次mysql dml操作记录一次 pos,filename到文件 21 | public static $BINLOG_COUNT = 100; 22 | 23 | // 记录当前执行到的pos,filename 24 | public static $BINLOG_NAME_PATH = 'file-pos'; 25 | // 26 | public static $OUT = 'out.log'; 27 | 28 | // log记录 29 | public static $LOG_ERROR_PATH = 'log/error.log'; 30 | public static $LOG_WARN_PATH = 'log/warn.log'; 31 | public static $LOG_NOTICE_PATH = 'log/notice.log'; 32 | 33 | public static $LOG = [ 34 | 'binlog' => [ 35 | 'error' => 'log/binlog-error.log' 36 | ], 37 | 'kafka' => [ 38 | 'error' => 'log/kafka-error.log', 39 | 'notice' => 'log/kafka-notice.log' 40 | ], 41 | 'mysql' => [ 42 | 'error' => 'log/mysql-error.log', 43 | 'notice' => 'log/mysql-notice.log', 44 | 'warn' => 'log/mysql-error.log' 45 | ], 46 | ]; 47 | 48 | public static function init() { 49 | self::$BINLOG_NAME_PATH = ROOT . 'file-pos'; 50 | self::$OUT = ROOT . 'out.log'; 51 | foreach(self::$LOG as $key => $value) { 52 | foreach($value as $k => $v) { 53 | self::$LOG[$key][$k] = ROOT . $v; 54 | } 55 | } 56 | } 57 | } 58 | 59 | Config::init(); 60 | 61 | -------------------------------------------------------------------------------- /const/ConstAuth.php: -------------------------------------------------------------------------------- 1 | 65535)) 65 | break; 66 | } 67 | $host = $db_config_array['host']; 68 | if (strlen($host) == 0) 69 | break; 70 | $username = $db_config_array['username']; 71 | if (strlen($username) == 0) 72 | break; 73 | $password = $db_config_array['password']; 74 | if (strlen($password) == 0) 75 | break; 76 | 77 | $handle = @mysqli_connect($host, $username, $password, $db_name, $port); 78 | // 如果连接失败,再重试2次 79 | for ($i = 1; ($i < 3) && (FALSE === $handle); $i++) { 80 | // 重试前需要sleep 50毫秒 81 | usleep(50000); 82 | $handle = @mysqli_connect($host, $username, $password, $db_name, $port); 83 | } 84 | if (FALSE === $handle) 85 | break; 86 | 87 | if (FALSE === mysqli_set_charset($handle, "utf8")) { 88 | self::logError( sprintf("Connect Set Charset Failed2:%s", mysqli_error($handle)), 'mysqlns.connect'); 89 | mysqli_close($handle); 90 | break; 91 | } 92 | 93 | 94 | self::$_HANDLE_ARRAY[$handle_key] = $handle; 95 | 96 | return $handle; 97 | } while (FALSE); 98 | 99 | // to_do, 连接失败 100 | self::logError( sprintf("Connect failed:time=%s", date('Y-m-d H:i:s',time())), 'mysqlns.connect'); 101 | return FALSE; 102 | } 103 | 104 | /// 释放通过getDBHandle或者getDBHandleByName 返回的句柄资源 105 | /// @param[in] handle $handle, 你懂的 106 | /// @return void 107 | public static function releaseDBHandle($handle) { 108 | if (!self::_checkHandle($handle)) 109 | return; 110 | foreach (self::$_HANDLE_ARRAY as $handle_key => $handleObj) { 111 | if ($handleObj->thread_id == $handle->thread_id) { 112 | unset(self::$_HANDLE_ARRAY[$handle_key]); 113 | } 114 | } 115 | mysqli_close($handle); 116 | } 117 | 118 | /// 将所有结果存入数组返回 119 | /// @param[in] handle $handle, 操作数据库的句柄 120 | /// @param[in] string $sql, 具体执行的sql语句 121 | /// @return FALSE表示执行失败, 否则返回执行的结果, 结果格式为一个数组,数组中每个元素都是mysqli_fetch_assoc的一条结果 122 | public static function query($handle, $sql) { 123 | do { 124 | if (($result = self::mysqliQueryApi($handle, $sql)) === FALSE){ 125 | break; 126 | } 127 | if ($result === true) { 128 | self::logWarn("err.func.query,SQL=$sql", 'mysqlns.query' ); 129 | return array(); 130 | } 131 | $res = array(); 132 | while($row = mysqli_fetch_assoc($result)) { 133 | $res[] = $row; 134 | } 135 | mysqli_free_result($result); 136 | return $res; 137 | } while (FALSE); 138 | // to_do, execute sql语句失败, 需要记log 139 | self::logError( "SQL Error: $sql, errno=" . self::getLastError($handle), 'mysqlns.sql'); 140 | 141 | return FALSE; 142 | } 143 | 144 | /// 将查询的第一条结果返回 145 | /// @param[in] handle $handle, 操作数据库的句柄 146 | /// @param[in] string $sql, 具体执行的sql语句 147 | /// @return FALSE表示执行失败, 否则返回执行的结果, 执行结果就是mysqli_fetch_assoc的结果 148 | public static function queryFirst($handle, $sql) { 149 | if (!self::_checkHandle($handle)) 150 | return FALSE; 151 | do { 152 | if (($result = self::mysqliQueryApi($handle, $sql)) === FALSE) 153 | break; 154 | $row = mysqli_fetch_assoc($result); 155 | mysqli_free_result($result); 156 | return $row; 157 | } while (FALSE); 158 | // to_do, execute sql语句失败, 需要记log 159 | self::logError( "SQL Error: $sql," . self::getLastError($handle), 'mysqlns.sql'); 160 | return FALSE; 161 | } 162 | 163 | /** 164 | * 将所有结果存入数组返回 165 | * @param Mysqli $handle 句柄 166 | * @param string $sql 查询语句 167 | * @return FALSE表示执行失败, 否则返回执行的结果, 结果格式为一个数组,数组中每个元素都是mysqli_fetch_assoc的一条结果 168 | */ 169 | public static function getAll($handle , $sql) { 170 | return self::query($handle, $sql); 171 | } 172 | 173 | /** 174 | * 将查询的第一条结果返回 175 | * @param[in] Mysqli $handle, 操作数据库的句柄 176 | * @param[in] string $sql, 具体执行的sql语句 177 | * @return FALSE表示执行失败, 否则返回执行的结果, 执行结果就是mysqli_fetch_assoc的结果 178 | */ 179 | public static function getRow($handle , $sql) { 180 | return self::queryFirst($handle, $sql); 181 | } 182 | 183 | /** 184 | * 查询第一条结果的第一列 185 | * @param Mysqli $handle, 操作数据库的句柄 186 | * @param string $sql, 具体执行的sql语句 187 | */ 188 | public static function getOne($handle , $sql) { 189 | $row = self::getRow($handle, $sql); 190 | if (is_array($row)) 191 | return current($row); 192 | return $row; 193 | } 194 | 195 | /// 得到最近一次操作影响的行数 196 | /// @param[in] handle $handle, 操作数据库的句柄 197 | /// @return FALSE表示执行失败, 否则返回影响的行数 198 | public static function lastAffected($handle) { 199 | if (!is_object($handle)) 200 | return FALSE; 201 | $affected_rows = mysqli_affected_rows($handle); 202 | if ($affected_rows < 0) 203 | return FALSE; 204 | return $affected_rows; 205 | } 206 | 207 | /* 208 | * 返回最后一次查询自动生成并使用的id 209 | * @param[in] handle $handle, 操作数据库的句柄 210 | * @return FALSE表示执行失败, 否则id 211 | */ 212 | public static function getLastInsertId($handle) { 213 | if (!is_object($handle)) { 214 | return false ; 215 | } 216 | if (($lastInsertId = mysqli_insert_id($handle)) <= 0) { 217 | return false ; 218 | } 219 | return $lastInsertId; 220 | } 221 | 222 | /// 得到最近一次操作错误的信息 223 | /// @param[in] handle $handle, 操作数据库的句柄 224 | /// @return FALSE表示执行失败, 否则返回 'errorno: errormessage' 225 | public static function getLastError($handle) { 226 | if(($handle)) { 227 | return mysqli_errno($handle).': '.mysqli_error($handle); 228 | } 229 | return FALSE; 230 | } 231 | 232 | /** 233 | * @brief 检查handle 234 | * @param[in] handle $handle, 操作数据库的句柄 235 | * @return boolean true|成功, false|失败 236 | */ 237 | private static function _checkHandle($handle, $log_category = 'mysqlns.handle') { 238 | if (!is_object($handle) || $handle->thread_id < 1) { 239 | if ($log_category) { 240 | self::logError(sprintf("handle Error: handle='%s'",var_export($handle, true)), $log_category); 241 | } 242 | return false; 243 | } 244 | return true; 245 | } 246 | 247 | 248 | public static function mysqliQueryApi($handle, $sql) { 249 | do { 250 | $result = mysqli_query($handle, $sql); 251 | 252 | return $result; 253 | } while (0); 254 | return false; 255 | } 256 | 257 | /** 258 | * @breif 记录统一错误日志 259 | */ 260 | protected static function logError($message, $category) { 261 | Log::error( $message, $category , Config::$LOG['mysql']['error']); 262 | } 263 | 264 | /** 265 | * @breif 记录统一警告日志 266 | */ 267 | protected static function logWarn($message, $category) { 268 | 269 | Log::warn( $message, $category , Config::$LOG['mysql']['warn']); 270 | 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /db/Log.php: -------------------------------------------------------------------------------- 1 | read($length); 56 | } 57 | 58 | 59 | public function unpackUint16($data) { 60 | return unpack("S",$data[0] . $data[1])[1]; 61 | } 62 | 63 | public function unpackInt24($data) { 64 | $a = (int)(ord($data[0]) & 0xFF); 65 | $a += (int)((ord($data[1]) & 0xFF) << 8); 66 | $a += (int)((ord($data[2]) & 0xFF) << 16); 67 | return $a; 68 | } 69 | 70 | public function unpackInt64($data) { 71 | $a = (int)(ord($data[0]) & 0xFF); 72 | $a += (int)((ord($data[1]) & 0xFF) << 8); 73 | $a += (int)((ord($data[2]) & 0xFF) << 16); 74 | $a += (int)((ord($data[3]) & 0xFF) << 24); 75 | $a += (int)((ord($data[4]) & 0xFF) << 32); 76 | $a += (int)((ord($data[5]) & 0xFF) << 40); 77 | $a += (int)((ord($data[6]) & 0xFF) << 48); 78 | $a += (int)((ord($data[7]) & 0xFF) << 56); 79 | return $a; 80 | } 81 | 82 | public function readInt24() 83 | { 84 | $data = unpack("CCC", $this->read(3)); 85 | 86 | $res = $data[1] | ($data[2] << 8) | ($data[3] << 16); 87 | if ($res >= 0x800000) 88 | $res -= 0x1000000; 89 | return $res; 90 | } 91 | 92 | public function readInt24Be() 93 | { 94 | $data = unpack('CCC', $this->read(3)); 95 | $res = ($data[1] << 16) | ($data[2] << 8) | $data[3]; 96 | if ($res >= 0x800000) 97 | $res -= 0x1000000; 98 | return $res; 99 | } 100 | 101 | // 102 | public function readUint8() 103 | { 104 | return unpack('C', $this->read(1))[1]; 105 | } 106 | 107 | // 108 | public function readUint16() 109 | { 110 | return unpack('S', $this->read(2))[1]; 111 | } 112 | 113 | public function readUint24() 114 | { 115 | $data = unpack("CCC", $this->read(3)); 116 | return $data[1] + ($data[2] << 8) + ($data[3] << 16); 117 | } 118 | 119 | // 120 | public function readUint32() 121 | { 122 | return unpack('I', $this->read(4))[1]; 123 | } 124 | 125 | public function readUint40() 126 | { 127 | $data = unpack("CI", $this->read(5)); 128 | return $data[1] + ($data[2] << 8); 129 | } 130 | 131 | public function readInt40Be() 132 | { 133 | $data = unpack("IC", $this->read(5)); 134 | return $data[2] + ($data[1] << 8); 135 | } 136 | 137 | // 138 | public function readUint48() 139 | { 140 | $data = unpack("vvv", $this->read(6)); 141 | return $data[1] + ($data[2] << 16) + ($data[3] << 32); 142 | } 143 | 144 | // 145 | public function readUint56() 146 | { 147 | $data = unpack("CSI", $this->read(7)); 148 | return $data[1] + ($data[2] << 8) + ($data[3] << 24); 149 | } 150 | 151 | /* 152 | * 不支持unsigned long long,溢出 153 | */ 154 | public function readUint64() 155 | { 156 | $d = $this->read(8); 157 | $unpackArr = unpack('I2', $d); 158 | //$data = unpack("C*", $d); 159 | //$r = $data[1] + ($data[2] << 8) + ($data[3] << 16) + ($data[4] << 24);//+ 160 | //$r2= ($data[5]) + ($data[6] << 8) + ($data[7] << 16) + ($data[8] << 24); 161 | 162 | return $unpackArr[1] + ($unpackArr[2] << 32); 163 | } 164 | 165 | public function readInt64() 166 | { 167 | return $this->readUint64(); 168 | } 169 | 170 | 171 | } -------------------------------------------------------------------------------- /pack/PackAuth.php: -------------------------------------------------------------------------------- 1 | 33 28 | $data .= chr(33); 29 | 30 | 31 | // 空 bytes23 32 | for($i=0;$i<23;$i++){ 33 | $data .=chr(0); 34 | } 35 | 36 | // http://dev.mysql.com/doc/internals/en/secure-password-authentication.html#packet-Authentication::Native41 37 | $result = sha1($pass, true) ^ sha1($salt . sha1(sha1($pass, true), true),true); 38 | 39 | //转码 8是 latin1 40 | //$user = iconv('utf8', 'latin1', $user); 41 | 42 | // 43 | $data = $data . $user . chr(0) . chr(strlen($result)) . $result; 44 | if($db) { 45 | $data .= $db . chr(0); 46 | } 47 | 48 | // V L 小端,little endian 49 | $str = pack("L", strlen($data)); 50 | $s =$str[0].$str[1].$str[2]; 51 | 52 | $data = $s . chr(1) . $data; 53 | 54 | return $data; 55 | } 56 | 57 | /** 58 | * @breif 校验数据包格式是否正确,验证是否成功 59 | * @param $pack 60 | * @return array 61 | */ 62 | public static function success($pack) { 63 | $head = ord($pack[0]); 64 | if(in_array($head, ConstAuth::$OK_PACK_HEAD)) { 65 | return ['status' => true, 'code' => 0, 'msg' => '']; 66 | } else{ 67 | $error_code = unpack("v", $pack[1] . $pack[2])[1]; 68 | $error_msg = ''; 69 | for($i = 9; $i < strlen($pack); $i ++) { 70 | $error_msg .= $pack[$i]; 71 | } 72 | var_dump(['code' => $error_code, 'msg' => $error_msg]); 73 | exit; 74 | } 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pack/RowEvent.php: -------------------------------------------------------------------------------- 1 | read(2))[1]; 19 | 20 | self::$EXTRA_DATA_LENGTH = unpack('S', self::$PACK->read(2))[1]; 21 | 22 | self::$EXTRA_DATA = self::$PACK->read(self::$EXTRA_DATA_LENGTH / 8); 23 | 24 | } else { 25 | self::$FLAGS = unpack('S', self::$PACK->read(2))[1]; 26 | } 27 | 28 | // Body 29 | self::$COLUMNS_NUM = self::$PACK->readCodedBinary(); 30 | } 31 | 32 | 33 | public static function tableMap(BinLogPack $pack, $event_type) 34 | { 35 | parent::_init($pack, $event_type); 36 | 37 | self::$TABLE_ID = self::readTableId(); 38 | 39 | self::$FLAGS = unpack('S', self::$PACK->read(2))[1]; 40 | 41 | $data = []; 42 | $data['schema_length'] = unpack("C", $pack->read(1))[1]; 43 | 44 | $data['schema_name'] = self::$SCHEMA_NAME = $pack->read($data['schema_length']); 45 | 46 | // 00 47 | self::$PACK->advance(1); 48 | 49 | $data['table_length'] = unpack("C", self::$PACK->read(1))[1]; 50 | $data['table_name'] = self::$TABLE_NAME = $pack->read($data['table_length']); 51 | 52 | // 00 53 | self::$PACK->advance(1); 54 | 55 | self::$COLUMNS_NUM = self::$PACK->readCodedBinary(); 56 | 57 | // 58 | $column_type_def = self::$PACK->read(self::$COLUMNS_NUM); 59 | 60 | 61 | // 避免重复读取 表信息 62 | if (isset(self::$TABLE_MAP[self::$SCHEMA_NAME][self::$TABLE_NAME]['table_id']) 63 | && self::$TABLE_MAP[self::$SCHEMA_NAME][self::$TABLE_NAME]['table_id']== self::$TABLE_ID) { 64 | return $data; 65 | } 66 | 67 | 68 | self::$TABLE_MAP[self::$SCHEMA_NAME][self::$TABLE_NAME] = array( 69 | 'schema_name'=> $data['schema_name'], 70 | 'table_name' => $data['table_name'], 71 | 'table_id' => self::$TABLE_ID 72 | ); 73 | 74 | 75 | self::$PACK->readCodedBinary(); 76 | 77 | 78 | // fields 相应属性 79 | $colums = DBHelper::getFields($data['schema_name'], $data['table_name']); 80 | 81 | self::$TABLE_MAP[self::$SCHEMA_NAME][self::$TABLE_NAME]['fields'] = []; 82 | 83 | for ($i = 0; $i < strlen($column_type_def); $i++) { 84 | $type = ord($column_type_def[$i]); 85 | if(!isset($colums[$i])){ 86 | Log::warn(var_export($colums, true).var_export($data, true), 'tableMap', Config::$LOG['binlog']['error']); 87 | } 88 | self::$TABLE_MAP[self::$SCHEMA_NAME][self::$TABLE_NAME]['fields'][$i] = BinLogColumns::parse($type, $colums[$i], self::$PACK); 89 | 90 | } 91 | 92 | return $data; 93 | 94 | 95 | } 96 | 97 | public static function addRow(BinLogPack $pack, $event_type, $size) 98 | { 99 | self::rowInit($pack, $event_type, $size); 100 | 101 | $result = []; 102 | // ???? 103 | //$result['extra_data'] = getData($data, ); 104 | // $result['columns_length'] = unpack("C", self::$PACK->read(1))[1]; 105 | //$result['schema_name'] = getData($data, 29, 28+$result['schema_length'][1]); 106 | $len = (int)((self::$COLUMNS_NUM + 7) / 8); 107 | 108 | 109 | $result['bitmap'] = self::$PACK->read($len); 110 | 111 | //nul-bitmap, length (bits set in 'columns-present-bitmap1'+7)/8 112 | $value['add'] = self::_getAddRows($result, $len); 113 | return $value; 114 | } 115 | 116 | public static function delRow(BinLogPack $pack, $event_type, $size) 117 | { 118 | self::rowInit($pack, $event_type, $size); 119 | 120 | $result = []; 121 | // ???? 122 | //$result['extra_data'] = getData($data, ); 123 | // $result['columns_length'] = unpack("C", self::$PACK->read(1))[1]; 124 | //$result['schema_name'] = getData($data, 29, 28+$result['schema_length'][1]); 125 | $len = (int)((self::$COLUMNS_NUM + 7) / 8); 126 | 127 | 128 | $result['bitmap'] = self::$PACK->read($len); 129 | 130 | 131 | //nul-bitmap, length (bits set in 'columns-present-bitmap1'+7)/8 132 | $value['del'] = self::_getDelRows($result, $len); 133 | 134 | return $value; 135 | } 136 | 137 | public static function updateRow(BinLogPack $pack, $event_type, $size) 138 | { 139 | 140 | self::rowInit($pack, $event_type, $size); 141 | 142 | $result = []; 143 | // ???? 144 | //$result['extra_data'] = getData($data, ); 145 | // $result['columns_length'] = unpack("C", self::$PACK->read(1))[1]; 146 | //$result['schema_name'] = getData($data, 29, 28+$result['schema_length'][1]); 147 | $len = (int)((self::$COLUMNS_NUM + 7) / 8); 148 | 149 | 150 | $result['bitmap1'] = self::$PACK->read($len); 151 | 152 | $result['bitmap2'] = self::$PACK->read($len); 153 | 154 | 155 | //nul-bitmap, length (bits set in 'columns-present-bitmap1'+7)/8 156 | $value['update'] = self::_getUpdateRows($result, $len); 157 | 158 | return $value; 159 | } 160 | 161 | public static function BitGet($bitmap, $position) 162 | { 163 | $bit = $bitmap[intval($position / 8)]; 164 | 165 | if (is_string($bit)) { 166 | 167 | $bit = ord($bit); 168 | } 169 | 170 | return $bit & (1 << ($position & 7)); 171 | } 172 | 173 | public static function _is_null($null_bitmap, $position) 174 | { 175 | $bit = $null_bitmap[intval($position / 8)]; 176 | if (is_string($bit)) { 177 | $bit = ord($bit); 178 | } 179 | 180 | 181 | return $bit & (1 << ($position % 8)); 182 | } 183 | 184 | private static function _read_string($size, $column) 185 | { 186 | $string = self::$PACK->read_length_coded_pascal_string($size); 187 | if ($column['character_set_name']) { 188 | //string = string . decode(column . character_set_name) 189 | } 190 | return $string; 191 | } 192 | 193 | private static function _read_column_data($cols_bitmap, $len) 194 | { 195 | $values = []; 196 | 197 | //$l = (int)(($len * 8 + 7) / 8); 198 | $l = (int)((self::bitCount($cols_bitmap) + 7) / 8); 199 | 200 | # null bitmap length = (bits set in 'columns-present-bitmap'+7)/8 201 | # See http://dev.mysql.com/doc/internals/en/rows-event.html 202 | 203 | 204 | $null_bitmap = self::$PACK->read($l); 205 | 206 | $nullBitmapIndex = 0; 207 | foreach (self::$TABLE_MAP[self::$SCHEMA_NAME][self::$TABLE_NAME]['fields'] as $i => $value) { 208 | $column = $value; 209 | $name = $value['name']; 210 | $unsigned = $value['unsigned']; 211 | 212 | 213 | if (self::BitGet($cols_bitmap, $i) == 0) { 214 | $values[$name] = null; 215 | continue; 216 | } 217 | 218 | if (self::_is_null($null_bitmap, $nullBitmapIndex)) { 219 | $values[$name] = null; 220 | } elseif ($column['type'] == ConstFieldType::TINY) { 221 | if ($unsigned) { 222 | $values[$name] = unpack("C", self::$PACK->read(1))[1]; 223 | } else { 224 | $values[$name] = unpack("c", self::$PACK->read(1))[1]; 225 | } 226 | } elseif ($column['type'] == ConstFieldType::SHORT) { 227 | if ($unsigned) { 228 | $values[$name] = unpack("S", self::$PACK->read(2))[1]; 229 | } else { 230 | $values[$name] = unpack("s", self::$PACK->read(2))[1]; 231 | } 232 | } elseif ($column['type'] == ConstFieldType::LONG) { 233 | if ($unsigned) { 234 | $values[$name] = unpack("I", self::$PACK->read(4))[1]; 235 | } else { 236 | $values[$name] = unpack("i", self::$PACK->read(4))[1]; 237 | } 238 | } elseif ($column['type'] == ConstFieldType::INT24) { 239 | if ($unsigned) { 240 | $values[$name] = self::$PACK->read_uint24(); 241 | } else { 242 | $values[$name] = self::$PACK->read_int24(); 243 | } 244 | } elseif ($column['type'] == ConstFieldType::FLOAT) { 245 | $values[$name] = unpack("f", self::$PACK->read(4))[1]; 246 | } 247 | elseif ($column['type'] == ConstFieldType::DOUBLE) { 248 | $values[$name] = unpack("d", self::$PACK->read(8))[1]; 249 | } elseif ($column['type'] == ConstFieldType::VARCHAR || 250 | $column['type'] == ConstFieldType::STRING 251 | ) { 252 | if ($column['max_length'] > 255) { 253 | $values[$name] = self::_read_string(2, $column); 254 | } else { 255 | $values[$name] = self::_read_string(1, $column); 256 | } 257 | } elseif ($column['type'] == ConstFieldType::NEWDECIMAL) { 258 | $values[$name] = self::_read_new_decimal($column); 259 | } elseif ($column['type'] == ConstFieldType::BLOB) { 260 | //ok 261 | $values[$name] = self::_read_string($column['length_size'], $column); 262 | } elseif ($column['type'] == ConstFieldType::DATETIME) { 263 | $values[$name] = self::_read_datetime(); 264 | } elseif ($column['type'] == ConstFieldType::DATETIME2) { 265 | //ok 266 | $values[$name] = self::_read_datetime2($column); 267 | }elseif ($column['type'] == ConstFieldType::TIME2) { 268 | $values[$name] = self::_read_time2($column); 269 | } elseif ($column['type'] == ConstFieldType::TIMESTAMP2){ 270 | //ok 271 | $time = date('Y-m-d H:i:m',self::$PACK->read_int_be_by_size(4)); 272 | // 微妙 273 | $micro = self::_add_fsp_to_time($column); 274 | if($micro) { 275 | $time .= '.' . self::_add_fsp_to_time($column); 276 | } 277 | $values[$name] = $time; 278 | } 279 | elseif ($column['type'] == ConstFieldType::DATE) { 280 | $values[$name] = self::_read_date(); 281 | } 282 | elseif ($column['type'] == ConstFieldType::TIMESTAMP) { 283 | $values[$name] = date('Y-m-d H:i:s', self::$PACK->readUint32()); 284 | } elseif ($column['type'] == ConstFieldType::LONGLONG) { 285 | if ($unsigned) { 286 | $values[$name] = self::$PACK->readUint64(); 287 | } else { 288 | $values[$name] = self::$PACK->readInt64(); 289 | } 290 | 291 | } elseif($column['type'] == ConstFieldType::ENUM) { 292 | $values[$name] = $column['enum_values'][self::$PACK->read_uint_by_size($column['size']) - 1]; 293 | } else { 294 | echo $column['type'] . " type(ConstFieldType.php) have not been support ,if need feedback";exit; 295 | } 296 | /* 297 | elseif ($column['type'] == ConstFieldType::TIME: 298 | $values[$name] = self.__read_time() 299 | elseif ($column['type'] == ConstFieldType::YEAR: 300 | $values[$name] = self::$PACK->read_uint8() + 1900 301 | elseif ($column['type'] == ConstFieldType::SET: 302 | # We read set columns as a bitmap telling us which options 303 | # are enabled 304 | bit_mask = self::$PACK->read_uint_by_size(column.size) 305 | $values[$name] = set( 306 | val for idx, val in enumerate(column.set_values) 307 | if bit_mask & 2 ** idx 308 | ) or None 309 | 310 | elseif ($column['type'] == ConstFieldType::BIT: 311 | $values[$name] = self.__read_bit(column) 312 | elseif ($column['type'] == ConstFieldType::GEOMETRY: 313 | $values[$name] = self::$PACK->read_length_coded_pascal_string( 314 | column.length_size) 315 | else: 316 | raise NotImplementedError("Unknown MySQL column type: %d" % 317 | (column.type)) 318 | */ 319 | $nullBitmapIndex += 1; 320 | } 321 | $values['table_name'] = self::$TABLE_NAME; 322 | return $values; 323 | } 324 | 325 | 326 | private static function _read_datetime() 327 | { 328 | $value = self::$PACK->readUint64(); 329 | if ($value == 0) # nasty mysql 0000-00-00 dates 330 | return null; 331 | 332 | $date = $value / 1000000; 333 | $time = (int)($value % 1000000); 334 | 335 | $year = (int)($date / 10000); 336 | $month = (int)(($date % 10000) / 100); 337 | $day = (int)($date % 100); 338 | if ($year == 0 or $month == 0 or $day == 0) 339 | return null; 340 | 341 | return $year.'-'.$month.'-'.$day .' '.intval($time / 10000).':'.intval(($time % 10000) / 100).':'.intval($time % 100); 342 | 343 | } 344 | 345 | private static function _read_date() { 346 | $time = self::$PACK->readUint24(); 347 | 348 | if ($time == 0) # nasty mysql 0000-00-00 dates 349 | return null; 350 | 351 | $year = ($time & ((1 << 15) - 1) << 9) >> 9; 352 | $month = ($time & ((1 << 4) - 1) << 5) >> 5; 353 | $day = ($time & ((1 << 5) - 1)); 354 | if ($year == 0 || $month == 0 || $day == 0) 355 | return null; 356 | 357 | return $year.'-'.$month.'-'.$day; 358 | } 359 | 360 | private static function _read_time2($column) { 361 | /* 362 | https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html 363 | TIME encoding for nonfractional part: 364 | 365 | 1 bit sign (1= non-negative, 0= negative) 366 | 1 bit unused (reserved for future extensions) 367 | 10 bits hour (0-838) 368 | 6 bits minute (0-59) 369 | 6 bits second (0-59) 370 | --------------------- 371 | 24 bits = 3 bytes 372 | */ 373 | $data = self::$PACK->read_int_be_by_size(3); 374 | 375 | $sign = 1; 376 | if (self::_read_binary_slice($data, 0, 1, 24) ) { 377 | } else { 378 | $sign = -1; 379 | } 380 | if ($sign == -1) { 381 | # negative integers are stored as 2's compliment 382 | # hence take 2's compliment again to get the right value. 383 | $data = ~$data + 1; 384 | } 385 | 386 | $hours=$sign*self::_read_binary_slice($data, 2, 10, 24); 387 | $minutes=self::_read_binary_slice($data, 12, 6, 24); 388 | $seconds=self::_read_binary_slice($data, 18, 6, 24); 389 | $microseconds=self::_add_fsp_to_time($column); 390 | $t = $hours.':'.$minutes.':'.$seconds; 391 | if($microseconds) { 392 | $t .= '.'.$microseconds; 393 | } 394 | return $t; 395 | } 396 | 397 | private static function _read_datetime2($column) { 398 | /*DATETIME 399 | 400 | 1 bit sign (1= non-negative, 0= negative) 401 | 17 bits year*13+month (year 0-9999, month 0-12) 402 | 5 bits day (0-31) 403 | 5 bits hour (0-23) 404 | 6 bits minute (0-59) 405 | 6 bits second (0-59) 406 | --------------------------- 407 | 40 bits = 5 bytes 408 | */ 409 | $data = self::$PACK->read_int_be_by_size(5); 410 | 411 | $year_month = self::_read_binary_slice($data, 1, 17, 40); 412 | 413 | 414 | $year=(int)($year_month / 13); 415 | $month=$year_month % 13; 416 | $day=self::_read_binary_slice($data, 18, 5, 40); 417 | $hour=self::_read_binary_slice($data, 23, 5, 40); 418 | $minute=self::_read_binary_slice($data, 28, 6, 40); 419 | $second=self::_read_binary_slice($data, 34, 6, 40); 420 | if($hour < 10) { 421 | $hour ='0'.$hour; 422 | } 423 | if($minute < 10) { 424 | $minute = '0'.$minute; 425 | } 426 | if($second < 10) { 427 | $second = '0'.$second; 428 | } 429 | $time = $year.'-'.$month.'-'.$day.' '.$hour.':'.$minute.':'.$second; 430 | $microsecond = self::_add_fsp_to_time($column); 431 | if($microsecond) { 432 | $time .='.'.$microsecond; 433 | } 434 | return $time; 435 | } 436 | 437 | private static function _read_binary_slice($binary, $start, $size, $data_length) { 438 | /* 439 | Read a part of binary data and extract a number 440 | binary: the data 441 | start: From which bit (1 to X) 442 | size: How many bits should be read 443 | data_length: data size 444 | */ 445 | $binary = $binary >> $data_length - ($start + $size); 446 | $mask = ((1 << $size) - 1); 447 | return $binary & $mask; 448 | } 449 | 450 | private static function _add_fsp_to_time($column) 451 | { 452 | /*Read and add the fractional part of time 453 | For more details about new date format: 454 | http://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html 455 | */ 456 | $read = 0; 457 | $time = ''; 458 | if( $column['fsp'] == 1 or $column['fsp'] == 2) 459 | $read = 1; 460 | elseif($column['fsp'] == 3 or $column['fsp'] == 4) 461 | $read = 2; 462 | elseif ($column ['fsp'] == 5 or $column['fsp'] == 6) 463 | $read = 3; 464 | if ($read > 0) { 465 | $microsecond = self::$PACK->read_int_be_by_size($read); 466 | if ($column['fsp'] % 2) 467 | $time = (int)($microsecond / 10); 468 | else 469 | $time = $microsecond; 470 | } 471 | return $time; 472 | } 473 | 474 | private static function _read_new_decimal($column) { 475 | #Read MySQL's new decimal format introduced in MySQL 5""" 476 | 477 | # This project was a great source of inspiration for 478 | # understanding this storage format. 479 | # https://github.com/jeremycole/mysql_binlog 480 | 481 | $digits_per_integer = 9; 482 | $compressed_bytes = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4]; 483 | $integral = ($column['precision'] - $column['decimals']); 484 | $uncomp_integral = intval($integral / $digits_per_integer); 485 | $uncomp_fractional = intval($column['decimals'] / $digits_per_integer); 486 | $comp_integral = $integral - ($uncomp_integral * $digits_per_integer); 487 | $comp_fractional = $column['decimals'] - ($uncomp_fractional * $digits_per_integer); 488 | 489 | # Support negative 490 | # The sign is encoded in the high bit of the the byte 491 | # But this bit can also be used in the value 492 | $value = self::$PACK->readUint8(); 493 | if ( ($value & 0x80) != 0) { 494 | $res = ""; 495 | $mask = 0; 496 | }else { 497 | $mask = -1; 498 | $res = "-"; 499 | } 500 | self::$PACK->unread(pack('C', $value ^ 0x80)); 501 | $size = $compressed_bytes[$comp_integral]; 502 | if ($size > 0) { 503 | $f = self::$PACK->read_int_be_by_size($size); 504 | $value = $f ^ $mask; 505 | $res .= (string)$value; 506 | } 507 | 508 | for($i=0;$i<$uncomp_integral;$i++) { 509 | $value = self::$PACK->read_int_be_by_size(4) ^ $mask; 510 | $res .= sprintf('%09d' , $value); 511 | } 512 | 513 | 514 | $res .= "."; 515 | for($i=0;$i<$uncomp_fractional;$i++) { 516 | $value = self::$PACK->read_int_be_by_size(4) ^ $mask; 517 | $res .= sprintf('%09d' , $value); 518 | } 519 | 520 | $size = $compressed_bytes[$comp_fractional]; 521 | if ($size > 0) { 522 | $f = self::$PACK->read_int_be_by_size($size); 523 | $value = $f ^ $mask; 524 | $res.=sprintf('%0'.$comp_fractional.'d' , $value); 525 | } 526 | //todo 超过int范围无法解析此函数 ex:11111111111199876665554567.001 527 | return number_format($res,$comp_fractional,'.',''); 528 | } 529 | 530 | 531 | 532 | private static function _getUpdateRows($result, $len) { 533 | $rows = []; 534 | while(!self::$PACK->isComplete(self::$PACK_SIZE)) { 535 | 536 | $value['before'] = self::_read_column_data($result['bitmap1'], $len); 537 | $value['after'] = self::_read_column_data($result['bitmap2'], $len); 538 | $rows[] = $value['after']; 539 | } 540 | return $rows; 541 | } 542 | 543 | private static function _getDelRows($result, $len) { 544 | $rows = []; 545 | while(!self::$PACK->isComplete(self::$PACK_SIZE)) { 546 | $rows[] = self::_read_column_data($result['bitmap'], $len); 547 | } 548 | return $rows; 549 | } 550 | 551 | private static function _getAddRows($result, $len) { 552 | $rows = []; 553 | 554 | while(!self::$PACK->isComplete(self::$PACK_SIZE)) { 555 | $rows[] = self::_read_column_data($result['bitmap'], $len); 556 | } 557 | return $rows; 558 | } 559 | } 560 | -------------------------------------------------------------------------------- /pack/ServerInfo.php: -------------------------------------------------------------------------------- 1 | 0 68 | $salt_len = ord($pack[$i]); 69 | $i++; 70 | 71 | $salt_len = max(12, $salt_len - 9); 72 | 73 | //填充值 74 | $i = $i + 10; 75 | 76 | //next salt 77 | if ($length >= $i + $salt_len) 78 | { 79 | for($j = $i ;$j < $i + $salt_len;$j++) 80 | { 81 | self::$INFO['salt'] .= $pack[$j]; 82 | } 83 | 84 | } 85 | 86 | self::$INFO['auth_plugin_name'] = ''; 87 | $i = $i + $salt_len + 1; 88 | for($j = $i;$j<$length-1;$j++) { 89 | self::$INFO['auth_plugin_name'] .=$pack[$j]; 90 | } 91 | } 92 | 93 | /** 94 | * @brief 获取salt auth 95 | * @return mixed 96 | */ 97 | public static function getSalt() { 98 | return self::$INFO['salt']; 99 | } 100 | 101 | /** 102 | * @brief 获取编码 103 | * @return mixed 104 | */ 105 | public static function getCharSet() { 106 | return self::$INFO['character_set']; 107 | } 108 | 109 | public static function getVersion() { 110 | return self::$INFO['server_version']; 111 | } 112 | 113 | public static function getInfo() { 114 | return self::$INFO; 115 | } 116 | } -------------------------------------------------------------------------------- /run.php: -------------------------------------------------------------------------------- 1 |