<?php
// ============================================================
// 웹소켓 타격기 : stock_total_striker_ws.php
// 환경기가 준 $unit_packs를 순차로 박고 퇴근
// ============================================================
if (empty($unit_packs) || empty($WS_APPROVAL_KEY)) return;
if (!function_exists('ws_send_frame')) {
function ws_send_frame($fp, $payload) {
$len = strlen($payload);
$mask = random_bytes(4);
$masked = '';
for ($i = 0; $i < $len; $i++) { $masked .= $payload[$i] ^ $mask[$i % 4]; }
return @fwrite($fp, pack('CCn', 0x81, 0xFE, $len) . $mask . $masked);
}
}
$total_received = 0;
foreach ($unit_packs as $index => $target_symbols) {
echo "[타격기] 세트 #" . ($index + 1) . " 시작 (종목수: " . count($target_symbols) . ")\n";
// 1. 소켓 열기 (세트마다 새로 열기)
$fp = @stream_socket_client($WS_URL, $errno, $errstr, 10);
if (!$fp) {
error_log("[WS] 연결 실패: $errstr ($errno)");
sleep(1);
continue;
}
stream_set_timeout($fp, 10);
// 2. 핸드쉐이크
$ws_key = base64_encode(random_bytes(16));
$header = "GET / HTTP/1.1\r\nHost: ops.koreainvestment.com:21000\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {$ws_key}\r\nSec-WebSocket-Version: 13\r\n\r\n";
fwrite($fp, $header);
fread($fp, 2048);
// 3. 종목 구독
foreach ($target_symbols as $sym) {
$sub = json_encode([
"header" => ["approval_key" => $WS_APPROVAL_KEY, "custtype" => "P", "tr_type" => "1", "content-type" => "utf-8"],
"body" => ["input" => ["tr_id" => "H0STCNT0", "tr_key" => $sym]]
]);
ws_send_frame($fp, $sub);
usleep(50000);
}
// 4. 수신 및 DB 박기
$received = 0;
$target = count($target_symbols);
$deadline = time() + 5;
while ($received < $target && time() < $deadline) {
$head = @fread($fp, 2);
if (!$head || strlen($head) < 2) continue;
$payload_len = ord($head[1]) & 0x7F;
if ($payload_len == 126) $payload_len = unpack('n', fread($fp, 2))[1];
elseif ($payload_len == 127) $payload_len = unpack('J', fread($fp, 8))[1];
$payload = @fread($fp, $payload_len);
if (empty($payload) || strpos($payload, 'H0STCNT0') === false) continue;
$parts = explode('|', $payload);
if (count($parts) < 4) continue;
$data = explode('^', $parts[3]);
// 아까 모니터로 확인한 실제 웹소켓 인덱스 기준
$code = $data[0] ?? ''; // 종목코드
$mksc_shrn_iscd = $data[0] ?? ''; // 0번
$stck_cntg_hour = $data[1] ?? ''; // 1번
$stck_prpr = $data[2] ?? ''; // 2번
$prdy_vrss_sign = $data[3] ?? ''; // 3번
$prdy_vrss = $data[4] ?? ''; // 4번
$prdy_ctrt = $data[5] ?? ''; // 5번
$wghn_avrg_prce = $data[6] ?? ''; // 6번
$stck_oprc = $data[7] ?? ''; // 7번
$stck_hgpr = $data[8] ?? ''; // 8번
$stck_lwpr = $data[9] ?? ''; // 9번
$askp1 = $data[10] ?? ''; // 10번
$bidp1 = $data[11] ?? ''; // 11번
$cntg_vol = $data[12] ?? ''; // 12번
$acml_vol = $data[13] ?? ''; // 13번
$acml_tr_pbmn = $data[14] ?? ''; // 14번
$seln_cntg_smtn = $data[15] ?? ''; // 15번
$buyt_cntg_smtn = $data[16] ?? ''; // 16번
$ctg_vol_intn = $data[17] ?? ''; // 17번
$seln_dgre_rate = $data[18] ?? ''; // 18번
$stck_sdpr = $data[19] ?? ''; // 19번
$hour_cls_code = $data[20] ?? ''; // 20번
$virt_cntg_qty = $data[21] ?? ''; // 21번
$vi_stnd_prce = $data[22] ?? ''; // 22번
$prdy_vol = $data[23] ?? ''; // 23번
if ($code && $stck_prpr && !empty($pdo)) {
try {
$stmt = $pdo->prepare("
UPDATE daemon_stock_Ticker_0100
SET mksc_shrn_iscd = :mksc_shrn_iscd,
stck_cntg_hour = :stck_cntg_hour,
stck_prpr = :stck_prpr,
prdy_vrss_sign = :prdy_vrss_sign,
prdy_vrss = :prdy_vrss,
prdy_ctrt = :prdy_ctrt,
wghn_avrg_prce = :wghn_avrg_prce,
stck_oprc = :stck_oprc,
stck_hgpr = :stck_hgpr,
stck_lwpr = :stck_lwpr,
askp1 = :askp1,
bidp1 = :bidp1,
cntg_vol = :cntg_vol,
acml_vol = :acml_vol,
acml_tr_pbmn = :acml_tr_pbmn,
seln_cntg_smtn = :seln_cntg_smtn,
buyt_cntg_smtn = :buyt_cntg_smtn,
ctg_vol_intn = :ctg_vol_intn,
seln_dgre_rate = :seln_dgre_rate,
stck_sdpr = :stck_sdpr,
hour_cls_code = :hour_cls_code,
virt_cntg_qty = :virt_cntg_qty,
vi_stnd_prce = :vi_stnd_prce,
prdy_vol = :prdy_vol
WHERE stck_shrn_iscd = :code
");
$stmt->execute([
':mksc_shrn_iscd' => $mksc_shrn_iscd,
':stck_cntg_hour' => $stck_cntg_hour,
':stck_prpr' => $stck_prpr,
':prdy_vrss_sign' => $prdy_vrss_sign,
':prdy_vrss' => $prdy_vrss,
':prdy_ctrt' => $prdy_ctrt,
':wghn_avrg_prce' => $wghn_avrg_prce,
':stck_oprc' => $stck_oprc,
':stck_hgpr' => $stck_hgpr,
':stck_lwpr' => $stck_lwpr,
':askp1' => $askp1,
':bidp1' => $bidp1,
':cntg_vol' => $cntg_vol,
':acml_vol' => $acml_vol,
':acml_tr_pbmn' => $acml_tr_pbmn,
':seln_cntg_smtn' => $seln_cntg_smtn,
':buyt_cntg_smtn' => $buyt_cntg_smtn,
':ctg_vol_intn' => $ctg_vol_intn,
':seln_dgre_rate' => $seln_dgre_rate,
':stck_sdpr' => $stck_sdpr,
':hour_cls_code' => $hour_cls_code,
':virt_cntg_qty' => $virt_cntg_qty,
':vi_stnd_prce' => $vi_stnd_prce,
':prdy_vol' => $prdy_vol,
':code' => $code,
]);
$received++;
$total_received++;
echo "[" . date('H:i:s') . "] {$code} | {$stck_prpr}\n";
} catch (Throwable $e) {
error_log("[WS] DB 에러 ({$code}): " . $e->getMessage());
}
}
}
// 5. 소켓 닫기
fclose($fp);
echo "[타격기] 세트 #" . ($index + 1) . " 완료. 1초 휴식...\n";
sleep(1);
}
echo "[결과] 총 {$total_received}개 종목 업데이트 완료. 퇴근!\n";