Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B

สวัสดีฮับ ทุกคนที่เคยพบหรือพบปะญาติหรือเพื่อนบนเครื่องบินอาจใช้บริการ Flightradar24 ฟรี นี่เป็นวิธีที่สะดวกมากในการติดตามตำแหน่งของเครื่องบินแบบเรียลไทม์

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B

В ส่วนแรก มีการอธิบายหลักการทำงานของบริการออนไลน์ดังกล่าว ตอนนี้เราจะดำเนินการต่อและพิจารณาว่าข้อมูลใดที่ถูกส่งและรับจากเครื่องบินไปยังสถานีรับและถอดรหัสด้วยตนเองโดยใช้ Python

เรื่องราว

แน่นอนว่าข้อมูลเครื่องบินจะไม่ถูกส่งให้ผู้ใช้ดูบนสมาร์ทโฟนของตน ระบบนี้เรียกว่า ADS-B (การเฝ้าระวังอัตโนมัติที่ขึ้นอยู่กับการออกอากาศ) และใช้ในการส่งข้อมูลเกี่ยวกับเครื่องบินไปยังศูนย์ควบคุมโดยอัตโนมัติ - ตัวระบุ พิกัด ทิศทาง ความเร็ว ระดับความสูง และข้อมูลอื่น ๆ จะถูกส่ง ก่อนหน้านี้ ก่อนการกำเนิดของระบบดังกล่าว ผู้มอบหมายงานสามารถมองเห็นได้เฉพาะจุดบนเรดาร์เท่านั้น นี่ไม่เพียงพออีกต่อไปเมื่อมีเครื่องบินมากเกินไป

ในทางเทคนิคแล้ว ADS-B ประกอบด้วยเครื่องส่งสัญญาณบนเครื่องบินที่ส่งแพ็กเก็ตข้อมูลเป็นระยะที่ความถี่สูงพอสมควรที่ 1090 MHz (มีโหมดอื่น แต่เราไม่สนใจโหมดเหล่านี้มากนักเนื่องจากพิกัดถูกส่งที่นี่เท่านั้น) แน่นอนว่านอกเหนือจากเครื่องส่งสัญญาณแล้ว ยังมีตัวรับสัญญาณอยู่ที่สนามบินด้วย แต่สำหรับเราในฐานะผู้ใช้ ตัวรับสัญญาณของเราเองก็น่าสนใจ

อนึ่ง เพื่อเป็นการเปรียบเทียบ ระบบแรกในลักษณะนี้คือ Airnav Radarbox ซึ่งออกแบบมาสำหรับผู้ใช้ทั่วไป ปรากฏขึ้นในปี 2007 และมีราคาประมาณ 900 ดอลลาร์สหรัฐ และประมาณ 250$ ค่าสมัครใช้บริการเครือข่ายคิดเป็นรายปี

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B

ความคิดเห็นของเจ้าของชาวรัสเซียคนแรกสามารถอ่านได้ในฟอรัม เครื่องสแกนวิทยุ. ขณะนี้เครื่องรับ RTL-SDR มีวางจำหน่ายอย่างแพร่หลายแล้ว อุปกรณ์ที่คล้ายกันสามารถประกอบได้ในราคา 30 เหรียญสหรัฐ (ประมาณ XNUMX บาท) ข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้อยู่ใน ส่วนแรก. มาดูโปรโตคอลกันดีกว่า - มาดูวิธีการทำงานกันดีกว่า

การรับสัญญาณ

ขั้นแรกต้องบันทึกสัญญาณ สัญญาณทั้งหมดมีระยะเวลาเพียง 120 ไมโครวินาที ดังนั้นหากต้องการแยกส่วนประกอบต่างๆ อย่างสะดวกสบาย ตัวรับสัญญาณ SDR ที่มีความถี่สุ่มตัวอย่างอย่างน้อย 5 MHz จึงเป็นที่ต้องการ

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B

หลังจากการบันทึก เราได้รับไฟล์ WAV ที่มีอัตราการสุ่มตัวอย่าง 5000000 ตัวอย่าง/วินาที การบันทึกดังกล่าว 30 วินาที "มีน้ำหนัก" ประมาณ 500MB แน่นอนว่าการฟังด้วยเครื่องเล่นสื่อนั้นไร้ประโยชน์ - ไฟล์ไม่มีเสียง แต่เป็นสัญญาณวิทยุดิจิทัลโดยตรง - นี่คือวิธีการทำงานของ Software Defined Radio

เราจะเปิดและประมวลผลไฟล์โดยใช้ Python ผู้ที่ต้องการทดลองด้วยตนเองสามารถดาวน์โหลดตัวอย่างการบันทึกได้ ลิงค์.

มาดาวน์โหลดไฟล์กันดีกว่าว่ามีอะไรอยู่ข้างใน

from scipy.io import wavfile
import matplotlib.pyplot as plt
import numpy as np

fs, data = wavfile.read("adsb_20190311_191728Z_1090000kHz_RF.wav")
data = data.astype(float)
I, Q = data[:, 0], data[:, 1]
A = np.sqrt(I*I + Q*Q)

plt.plot(A)
plt.show()

ผลลัพธ์: เราเห็น “จังหวะ” ชัดเจนกับเสียงพื้นหลัง

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B

“พัลส์” แต่ละตัวเป็นสัญญาณ โครงสร้างที่มองเห็นได้ชัดเจนหากคุณเพิ่มความละเอียดบนกราฟ

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B

อย่างที่คุณเห็นรูปภาพค่อนข้างสอดคล้องกับคำอธิบายข้างต้น คุณสามารถเริ่มประมวลผลข้อมูลได้

ถอดรหัส

ก่อนอื่นคุณต้องสตรีมสักหน่อย สัญญาณนั้นถูกเข้ารหัสโดยใช้การเข้ารหัสของแมนเชสเตอร์:

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B

จากความแตกต่างของระดับในแทะ เป็นเรื่องง่ายที่จะได้ "0" และ "1" ที่แท้จริง

    bits_str = ""
    for p in range(8):
        pos = start_data + bit_len*p
        p1, p2 = A[pos: pos + bit_len/2], A[pos + bit_len/2: pos + bit_len]
        avg1, avg2 = np.average(p1), np.average(p2)
        if avg1 < avg2:
            bits_str += "0"
        elif avg1 > avg2:
            bits_str += "1"

โครงสร้างของสัญญาณมีดังนี้:

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B

มาดูรายละเอียดเพิ่มเติมที่สาขาต่างๆ

DF (รูปแบบดาวน์ลิงก์ 5 บิต) - กำหนดประเภทของข้อความ มีหลายประเภท:

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B
(แหล่งที่มาของตาราง)

เราสนใจเฉพาะรุ่น DF17 เท่านั้น เพราะ... นี่คือที่มีพิกัดของเครื่องบิน

ICAO (24 บิต) - รหัสเฉพาะระหว่างประเทศของเครื่องบิน คุณสามารถตรวจสอบเครื่องบินด้วยรหัสของมัน ออนไลน์ (น่าเสียดายที่ผู้เขียนหยุดอัปเดตฐานข้อมูลแล้ว แต่ยังมีความเกี่ยวข้องอยู่) ตัวอย่างเช่น สำหรับโค้ด 3c5ee2 เรามีข้อมูลต่อไปนี้:

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B

แก้ไข: ใน ความคิดเห็นต่อบทความ คำอธิบายของรหัส ICAO มีรายละเอียดเพิ่มเติมฉันแนะนำให้ผู้สนใจอ่าน

DATA (56 หรือ 112 บิต) - ข้อมูลจริงที่เราจะถอดรหัส ข้อมูล 5 บิตแรกคือฟิลด์ รหัสประเภทซึ่งมีประเภทย่อยของข้อมูลที่กำลังจัดเก็บ (เพื่อไม่ให้สับสนกับ DF) มีหลายประเภทดังนี้:

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B
(แหล่งที่มาของตาราง)

มาดูตัวอย่างแพ็คเกจกัน

บัตรประจำตัวเครื่องบิน

ตัวอย่างในรูปแบบไบนารี:

00100 011 000101 010111 000111 110111 110001 111000

เขตข้อมูล:

+------+------+------+------+------+------+------+------+------+------+
| TC,5 | EC,3 | C1,6 | C2,6 | C3,6 | C4,6 | C5,6 | C6,6 | C7,6 | C8,6 |
+------+------+------+------+------+------+------+------+------+------+

TC = 00100b = 4 อักขระแต่ละตัว C1-C8 มีรหัสที่สอดคล้องกับดัชนีในบรรทัด:
#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789#######

โดยการถอดรหัสสตริงทำให้ง่ายต่อการรับรหัสเครื่องบิน: EWG7184

symbols = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
code_str = ""
for p in range(8):
     c = int(bits_str[8 + 6*p:8 + 6*(p + 1)], 2)
     code_str += symbols[c]
print("Aircraft Identification:", code_str.replace('#', ''))

ตำแหน่งทางอากาศ

ถ้าชื่อง่าย พิกัดก็จะซับซ้อนกว่า พวกมันจะถูกส่งในรูปแบบของ 2 เฟรมคู่และเฟรมคี่ รหัสฟิลด์ TC = 01011b = 11

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B

ตัวอย่างของแพ็กเก็ตคู่และคี่:

01011 000 000101110110 00 10111000111001000 10000110101111001
01011 000 000110010000 01 10010011110000110 10000011110001000

การคำนวณพิกัดนั้นเกิดขึ้นตามสูตรที่ค่อนข้างยุ่งยาก:

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B
(แหล่ง)

ฉันไม่ใช่ผู้เชี่ยวชาญด้าน GIS ดังนั้นฉันจึงไม่รู้ว่ามันมาจากไหน ใครจะรู้เขียนในความคิดเห็น

ความสูงถือว่าง่ายกว่า - ขึ้นอยู่กับบิตที่ระบุ ซึ่งสามารถแสดงเป็นผลคูณของ 25 หรือ 100 ฟุตได้

ความเร็วทางอากาศ

แพ็คเกจพร้อม TC=19 สิ่งที่น่าสนใจที่นี่คือความเร็วสามารถแม่นยำ สัมพันธ์กับพื้นดิน (ความเร็วพื้นดิน) หรือในอากาศ วัดโดยเซ็นเซอร์เครื่องบิน (ความเร็วลม) นอกจากนี้ยังมีการถ่ายทอดสาขาต่างๆ มากมาย:

Flightradar24 - มันทำงานอย่างไร? ส่วนที่ 2 โปรโตคอล ADS-B
(แหล่ง)

ข้อสรุป

อย่างที่คุณเห็น เทคโนโลยี ADS-B ได้กลายเป็นการเชื่อมโยงกันที่น่าสนใจ เมื่อมาตรฐานมีประโยชน์ไม่เฉพาะกับมืออาชีพเท่านั้น แต่ยังรวมถึงผู้ใช้ทั่วไปด้วย แต่แน่นอนว่าเทคโนโลยีที่ถูกกว่าของตัวรับ SDR แบบดิจิทัลมีบทบาทสำคัญในเรื่องนี้ซึ่งช่วยให้อุปกรณ์รับสัญญาณที่มีความถี่สูงกว่ากิกะเฮิรตซ์ได้อย่างแท้จริง "สำหรับเพนนี"

แน่นอนว่ายังมีอะไรอีกมากมายในมาตรฐาน ผู้สนใจสามารถดู PDF ได้ในเพจ ICAO หรือเยี่ยมชมที่กล่าวมาข้างต้น สถาน.

ไม่น่าเป็นไปได้ที่สิ่งที่กล่าวมาทั้งหมดจะเป็นประโยชน์กับหลาย ๆ คน แต่ฉันหวังว่าจะยังคงมีแนวคิดทั่วไปเกี่ยวกับวิธีการทำงานอยู่

อย่างไรก็ตาม มีตัวถอดรหัสสำเร็จรูปใน Python อยู่แล้ว คุณสามารถศึกษาได้ ที่นี่. และเจ้าของเครื่องรับ SDR ก็สามารถประกอบและเปิดใช้ตัวถอดรหัส ADS-B สำเร็จรูปได้ จากหน้าเรื่องนี้ถูกกล่าวถึงในรายละเอียดเพิ่มเติมใน ส่วนแรก.

ซอร์สโค้ดของ parser ที่อธิบายไว้ในบทความมีระบุไว้ด้านล่างของการตัด นี่คือตัวอย่างทดสอบที่ไม่ได้แกล้งทำเป็นว่าเป็นการใช้งานจริง แต่มีบางอย่างใช้งานได้ และสามารถใช้เพื่อแยกวิเคราะห์ไฟล์ที่บันทึกไว้ด้านบน
ซอร์สโค้ด (หลาม)

from __future__ import print_function

from scipy.io import wavfile
from scipy import signal
import matplotlib.pyplot as plt
import numpy as np
import math
import sys


def parse_message(data, start, bit_len):
    max_len = bit_len*128
    A = data[start:start + max_len]
    A = signal.resample(A, 10*max_len)
    bits = np.zeros(10*max_len)
    bit_len *= 10
    start_data = bit_len*8

    # Parse first 8 bits
    bits_str = ""
    for p in range(8):
        pos = start_data + bit_len*p
        p1, p2 = A[pos: pos + bit_len/2], A[pos + bit_len/2: pos + bit_len]
        avg1, avg2 = np.average(p1), np.average(p2)
        if avg1 < avg2:
            bits_str += "0"
        elif avg1 > avg2:
            bits_str += "1"

    df = int(bits_str[0:5], 2)

    # Aircraft address (db - https://junzis.com/adb/?q=3b1c5c )
    bits_str = ""
    for p in range(8, 32):
        pos = start_data + bit_len * p
        p1, p2 = A[pos: pos + bit_len / 2], A[pos + bit_len / 2: pos + bit_len]
        avg1, avg2 = np.average(p1), np.average(p2)
        if avg1 < avg2:
            bits_str += "0"
        elif avg1 > avg2:
            bits_str += "1"
    # print "Aircraft address:", bits_str, hex(int(bits_str, 2))
    address = hex(int(bits_str, 2))

    # Filter specific aircraft (optional)
    # if address != "0x3c5ee2":
    #    return

    if df == 16 or df == 17 or df == 18 or df == 19 or df == 20 or df == 21:
        # print "Pos:", start, "DF:", msg_type

        # Data (56bit)
        bits_str = ""
        for p in range(32, 88):
            pos = start_data + bit_len*p
            p1, p2 = A[pos: pos + bit_len/2], A[pos + bit_len/2: pos + bit_len]
            avg1, avg2 = np.average(p1), np.average(p2)
            if avg1 < avg2:
                bits_str += "0"
                # bits[pos + bit_len / 2] = 50
            elif avg1 > avg2:
                bits_str += "1"

        # http://www.lll.lu/~edward/edward/adsb/DecodingADSBposition.html
        # print "Data:"
        # print bits_str[:8], bits_str[8:20],  bits_str[20:22], bits_str[22:22+17], bits_str[39:39+17]
        # Type Code:
        tc, ec = int(bits_str[:5], 2), int(bits_str[5:8], 2)
        # print("DF:", df, "TC:", tc)
        
        # 1 - 4  Aircraft identification
        # 5 - 8  Surface position
        # 9 - 18  Airborne position (w/ Baro Altitude)
        # 19  Airborne velocities

        if tc >= 1 and tc <= 4: # and (df == 17 or df == 18):
            print("Aircraft address:", address)
            print("Data:")
            print(bits_str[:8], bits_str[8:14],  bits_str[14:20], bits_str[20:26], bits_str[26:32], bits_str[32:38], bits_str[38:44])

            symbols = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
            code_str = ""
            for p in range(8):
                c = int(bits_str[8 + 6*p:8 + 6*(p + 1)], 2)
                code_str += symbols[c]
            print("Aircraft Identification:", code_str.replace('#', ''))
            print()
        if tc == 11:
            print("Aircraft address:", address)
            print("Data: (11)")
            print(bits_str[:8], bits_str[8:20],  bits_str[20:22], bits_str[22:22+17], bits_str[39:39+17])

            # Bit 22 contains the F flag which indicates which CPR format is used (odd or even)
            # First frame has F flag = 0 so is even and the second frame has F flag = 1 so odd
            # f = bits_str[21:22]
            # print("F:", int(f, 2))

            # Altitude
            alt1b = bits_str[8:20]
            if alt1b[-5] == '1':
                bits = alt1b[:-5] + alt1b[-4:]
                n = int(bits, 2)
                alt_ft = n*25 - 1000
                print("Alt (ft)", alt_ft)

            # lat_dec = int(bits_str[22:22+17], 2)
            # lon_dec = int(bits_str[39:39+17], 2)
            # print("Lat/Lon:", lat_dec, lon_dec)

            # http://airmetar.main.jp/radio/ADS-B%20Decoding%20Guide.pdf
            print()
        if tc == 19:
            print("Aircraft address:", address)
            print("Data:")
            # print(bits_str)
            print(bits_str[:5], bits_str[5:8], bits_str[8:10], bits_str[10:13], bits_str[13] ,bits_str[14:24], bits_str[24], bits_str[25:35], bits_str[35:36], bits_str[36:65])

            subtype = int(bits_str[5:8], 2)
            # https://mode-s.org/decode/adsb/airborne-velocity.html
            spd, hdg, rocd = -1, -1, -1
            if subtype == 1 or subtype == 2:
                print("Velocity Subtype 1: Ground speed")
            
                v_ew_sign = int(bits_str[13], 2)
                v_ew = int(bits_str[14:24], 2) - 1       # east-west velocity
                
                v_ns_sign = int(bits_str[24], 2)
                v_ns = int(bits_str[25:35], 2) - 1       # north-south velocity
                
                v_we = -1*v_ew if v_ew_sign else v_ew
                v_sn = -1*v_ns if v_ns_sign else v_ns
                
                spd = math.sqrt(v_sn*v_sn + v_we*v_we)  # unit in kts
                
                hdg = math.atan2(v_we, v_sn)
                hdg = math.degrees(hdg)                 # convert to degrees
                hdg = hdg if hdg >= 0 else hdg + 360    # no negative val
            if subtype == 3:
                print("Subtype Subtype 3: Airspeed")
                hdg = int(bits_str[14:24], 2)/1024.0*360.0
                spd = int(bits_str[25:35], 2)
            
            vr_sign = int(bits_str[36], 2)
            vr = int(bits_str[36:45], 2)
            rocd = -1*vr if vr_sign else vr         # rate of climb/descend
            print("Speed (kts):", spd, "Rate:", rocd, "Heading:", hdg)
            print()

        # print()

def calc_coordinates():
    def _cprN(lat, is_odd):
        nl = _cprNL(lat) - is_odd
        return nl if nl > 1 else 1

    def _cprNL(lat):
        try:
            nz = 15
            a = 1 - math.cos(math.pi / (2 * nz))
            b = math.cos(math.pi / 180.0 * abs(lat)) ** 2
            nl = 2 * math.pi / (math.acos(1 - a/b))
            return int(math.floor(nl))
        except:
            # happens when latitude is +/-90 degree
            return 1
    
    def floor_(x):
        return int(math.floor(x))
  
    lat1b, lon1b, alt1b = "10111000111010011", "10000110111111000", "000101111001"
    lat2b, lon2b, alt2b = "10010011101011100", "10000011000011011", "000101110111"
    lat1, lon1, alt1 = int(lat1b, 2), int(lon1b, 2), int(alt1b, 2)
    lat2, lon2, alt2 = int(lat2b, 2), int(lon2b, 2), int(alt2b, 2)
    
    # 131072 is 2^17, since CPR lat and lon are 17 bits each
    cprlat_even, cprlon_even = lat1/131072.0, lon1/131072.0
    cprlat_odd, cprlon_odd = lat2/131072.0, lon2/131072.0
    print(cprlat_even, cprlon_even)

    j = floor_(59*cprlat_even - 60*cprlat_odd)
    print(j)

    air_d_lat_even = 360.0 / 60
    air_d_lat_odd = 360.0 / 59

    # Lat
    lat_even = float(air_d_lat_even * (j % 60 + cprlat_even))
    lat_odd = float(air_d_lat_odd * (j % 59 + cprlat_odd))
    if lat_even >= 270:
        lat_even = lat_even - 360
    if lat_odd >= 270:
        lat_odd = lat_odd - 360

    # Lon
    ni = _cprN(lat_even, 0)
    m = floor_(cprlon_even * (_cprNL(lat_even)-1) - cprlon_odd * _cprNL(lat_even) + 0.5)
    lon = (360.0 / ni) * (m % ni + cprlon_even)
    print("Lat", lat_even, "Lon", lon)

    # Altitude
    # Q-bit (bit 48) indicates whether the altitude is encoded in multiples of 25 or 100 ft (0: 100 ft, 1: 25 ft)
    # The value can represent altitudes from -1000 to +50175 ft.
    if alt1b[-5] == '1':
        bits = alt1b[:-5] + alt1b[-4:]
        n = int(bits, 2)
        alt_ft = n*25 - 1000
        print("Alt (ft)", alt_ft)


fs, data = wavfile.read("adsb_20190311_191728Z_1090000kHz_RF.wav")
T = 1/fs

print("Sample rate %f MS/s" % (fs / 1e6))
print("Cnt samples %d" % len(data))
print("Duration: %f s" % (T * len(data)))

data = data.astype(float)

cnt = data.shape[0]
# Processing only part on file (faster):
# cnt = 10000000
# data = data[:cnt]
print("Processing I/Q...")
I, Q = data[:, 0], data[:, 1]
A = np.sqrt(I*I + Q*Q)

bits = np.zeros(cnt)

# To see scope without any processing, uncomment
# plt.plot(A)
# plt.show()
# sys.exit(0)

print("Extracting signals...")

pos = 0
avg = 200
msg_start = 0
# Find beginning of each signal
while pos < cnt - 16*1024:
    # P1 - message start
    while pos < cnt - 16*1024:
        if A[pos] < avg and A[pos+1] > avg and pos - msg_start > 1000:
            msg_start = pos
            bits[pos] = 100
            pos += 4
            break
        pos += 1

    start1, start2, start3, start4 = msg_start, 0, 0, 0
    # P2
    while pos < cnt - 16*1024:
        if A[pos] < avg and A[pos+1] > avg:
            start2 = pos
            bits[pos] = 90
            pos += 1
            break
        pos += 1
    # P3
    while pos < cnt - 16*1024:
        if A[pos] < avg and A[pos+1] > avg:
            start3 = pos
            bits[pos] = 80
            pos += 1
            break
        pos += 1
    # P4
    while pos < cnt - 16*1024:
        if A[pos] < avg and A[pos+1] > avg:
            start4 = pos
            bits[pos] = 70
            pos += 1
            break
        pos += 1


    sig_diff = start4 - start1
    if 20 < sig_diff < 25:
        bits[msg_start] = 500
        bit_len = int((start4 - start1) / 4.5)
        # print(pos, start1, start4, ' - ', bit_len)
        # start = start1 + 8*bit_len
        parse_message(A, msg_start, bit_len)

        pos += 450

# For debugging: check signal start
# plt.plot(A)
# plt.plot(bits)
# plt.show()

ฉันหวังว่าจะมีคนสนใจ ขอบคุณสำหรับความสนใจของคุณ

ที่มา: will.com

ซื้อโฮสติ้งที่เชื่อถือได้สำหรับไซต์ที่มีการป้องกัน DDoS เซิร์ฟเวอร์ VPS VDS 🔥 ซื้อบริการเว็บโฮสติ้งที่เชื่อถือได้ พร้อมระบบป้องกัน DDoS และเซิร์ฟเวอร์ VPS/VDS | ProHoster