เซิร์ฟเวอร์ล็อกอินระยะไกลของเล่น

สวัสดี! วันก่อน เราได้พูดคุยเกี่ยวกับ สิ่งที่เกิดขึ้นเมื่อคุณกดปุ่มในเทอร์มินัลของคุณ

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

เป้าหมาย: “ssh” ไปยังคอมพิวเตอร์ระยะไกล

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

ความแตกต่างที่ใหญ่ที่สุดระหว่างโปรแกรมนี้กับ SSH ก็คือไม่มีการรักษาความปลอดภัยอย่างแท้จริง (ไม่ใช่แม้แต่รหัสผ่าน) – ใครก็ตามที่สามารถเชื่อมต่อ TCP กับเซิร์ฟเวอร์สามารถรับเชลล์และรันคำสั่งได้

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

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

มาเริ่มกันที่เซิร์ฟเวอร์กัน!

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

เมื่อเซิร์ฟเวอร์ได้รับการเชื่อมต่อใหม่ จะต้อง:

  1. สร้าง pseudoterminal ให้ลูกค้าใช้
  2. เริ่มกระบวนการ bash shell เพื่อให้ไคลเอนต์ใช้
  3. เชื่อมต่อ bash กับ pseudoterminal
  4. คัดลอกข้อมูลไปมาอย่างต่อเนื่องระหว่างการเชื่อมต่อ TCP และ pseudoterminal

ฉันเพิ่งพูดคำว่า “pseudoterminal” ไปบ่อย ๆ ดังนั้นเรามาพูดถึงความหมายกัน

เทอร์มินัลเทียมคืออะไร

เอาล่ะ เทอร์มินัลเทียมคืออะไร?

เทอร์มินอลเทียมเป็นเหมือนไปป์แบบสองทิศทางหรือเต้ารับ – คุณมีปลายทั้งสองข้าง และพวกเขาทั้งสองสามารถส่งและรับข้อมูลได้ คุณสามารถอ่านเพิ่มเติมเกี่ยวกับข้อมูลที่ส่งและรับได้ใน สิ่งที่เกิดขึ้นหากคุณกดปุ่มในเทอร์มินัล

โดยพื้นฐานแล้วแนวคิดก็คือด้านหนึ่ง เรามีการเชื่อมต่อ TCP และอีกด้านหนึ่ง เรามี bash shell ดังนั้นเราจึงต้องขอส่วนหนึ่งของ pseudoterminal กับการเชื่อมต่อ TCP และปลายอีกด้านหนึ่งเพื่อทุบตี

สองส่วนของ pseudoterminal เรียกว่า:

  • “ต้นแบบเทอร์มินัลเทียม” นี่คือจุดสิ้นสุดที่เราจะเชื่อมต่อกับการเชื่อมต่อ TCP
  • “อุปกรณ์เทอร์มินัลเทียมทาส” เราจะตั้งค่า stdout , stderr และ stdin ของ bash shell เป็นสิ่งนี้

เมื่อเชื่อมต่อแล้ว เราสามารถสื่อสารกับ bash ผ่านการเชื่อมต่อ TCP และเราจะมีรีโมตเชลล์!

ทำไมเราถึงต้องการสิ่ง “pseudoterminal” นี้อยู่แล้ว?

คุณอาจกำลังสงสัย – จูเลีย ถ้าเทอร์มินัลปลอมเป็นเหมือนซ็อกเก็ต ทำไมเราไม่สามารถตั้งค่า stdout / stderr / stdin ของเชลล์ทุบตีเป็นซ็อกเก็ต TCP ได้

และคุณสามารถ! เราสามารถเขียนตัวจัดการการเชื่อมต่อ TCP แบบนี้ที่ทำอย่างนั้นได้ ไม่ใช่โค้ดจำนวนมาก ( server-notty.go )

 func handle(conn net.Conn) { tty, _ := conn.(*net.TCPConn).File() // start bash with tcp connection as stdin/stdout/stderr cmd := exec.Command("bash") cmd.Stdin = tty cmd.Stdout = tty cmd.Stderr = tty cmd.Start() }

มันยังใช้งานได้ – ถ้าเราเชื่อมต่อกับ nc localhost 7778 เราสามารถเรียกใช้คำสั่งและดูผลลัพธ์ได้

แต่มีปัญหาเล็กน้อย ฉันจะไม่แสดงรายการทั้งหมด แค่สองรายการ

ปัญหาที่ 1: Ctrl + C ไม่ทำงาน

วิธีการทำงานของ Ctrl + C ในเซสชันการเข้าสู่ระบบระยะไกลคือ

  • คุณกด ctrl + c
  • ที่ได้รับการแปลเป็น 0x03 และส่งผ่านการเชื่อมต่อ TCP
  • เครื่องปลายทางรับ
  • เคอร์เนลลินุกซ์ในส่วนอื่น ๆ “เฮ้นั่นคือ Ctrl + C!”
  • Linux ส่ง SIGINT ไปยังกระบวนการที่เหมาะสม (เพิ่มเติมเกี่ยวกับ “กระบวนการที่เหมาะสม” ในภายหลัง)

หาก “เทอร์มินัล” เป็นเพียงการเชื่อมต่อ TCP สิ่งนี้จะไม่ทำงาน เพราะเมื่อคุณส่ง 0x04 ไปยังการเชื่อมต่อ TCP ลินุกซ์จะไม่ส่ง SIGINT ไปยังกระบวนการใดๆ อย่างน่าอัศจรรย์

ปัญหาที่ 2: top ไม่ทำงาน

เมื่อฉันพยายามเรียกใช้ top ในเชลล์นี้ ฉันได้รับข้อความแสดงข้อผิดพลาด top: failed tty get หากเราลากเส้น เราจะเห็นการเรียกระบบนี้:

 ioctl(2, TCGETS, 0x7ffec4e68d60) = -1 ENOTTY (Inappropriate ioctl for device)

ดังนั้น top กำลังเรียกใช้ ioctl บนตัวอธิบายไฟล์เอาต์พุต (2) เพื่อรับข้อมูลบางอย่างเกี่ยวกับเทอร์มินัล แต่ลีนุกซ์ก็แบบว่า “เฮ้ นี่ไม่ใช่เทอร์มินัล!” และส่งคืนข้อผิดพลาด

มีหลายอย่างผิดพลาด แต่หวังว่า ณ จุดนี้คุณจะมั่นใจว่าเราจำเป็นต้องตั้งค่า stdout/stderr ของ bash ให้เป็นเทอร์มินัล ไม่ใช่อย่างอื่นเช่นซ็อกเก็ต

เรามาเริ่มดูที่รหัสเซิร์ฟเวอร์และดูว่าการสร้าง pseudoterminal เป็นอย่างไร

ขั้นตอนที่ 1: สร้าง pseudoterminal

นี่คือโค้ด Go บางส่วนเพื่อสร้าง pseudoterminal บน Linux สิ่งนี้ถูกคัดลอกจาก github.com/creack/pty แต่ฉันได้ลบการจัดการข้อผิดพลาดบางส่วนเพื่อให้ตรรกะง่ายต่อการติดตามเล็กน้อย:

 pty, _ := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) sname := ptsname(p) unlockpt(p) tty, _ := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)

ในภาษาอังกฤษ สิ่งที่เราทำคือ:

  • เปิด /dev/ptmx เพื่อรับ “pseudoterminal master” อีกครั้งนั่นคือส่วนที่เราจะเชื่อมต่อกับการเชื่อมต่อ TCP
  • รับชื่อไฟล์ของ “slave pseudoterminal device” ซึ่งจะเป็น /dev/pts/13 หรือบางอย่าง
  • “ปลดล็อค” pseudoterminal เพื่อให้เราใช้งานได้ ฉันไม่รู้ว่ามันคืออะไร (ทำไมมันถึงล็อคตั้งแต่เริ่มต้น?) แต่คุณต้องทำด้วยเหตุผลบางอย่าง
  • open /dev/pts/13 (หรือหมายเลขใดก็ตามที่เราได้รับจาก ptsname ) เพื่อรับ “slave pseudoterminal device”

ฟังก์ชั่น unlockpt และ ptsname เหล่านั้นทำอะไรได้บ้าง? พวกเขาเพียงแค่ทำการเรียกระบบ ioctl ไปยังเคอร์เนลลินุกซ์ การสื่อสารทั้งหมดกับเคอร์เนล Linux เกี่ยวกับเทอร์มินัลดูเหมือนจะผ่านการเรียกระบบ ioctl ต่างๆ

นี่คือรหัส มันค่อนข้างสั้น: (ฉันเพิ่งคัดลอกมาจาก creack/pty อีกครั้ง)

 func ptsname(f *os.File) string { var n uint32 ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))) return "/dev/pts/" + strconv.Itoa(int(n)) } func unlockpt(f *os.File) { var u int32 // use TIOCSPTLCK with a pointer to zero to clear the lock ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))) }

ขั้นตอนที่ 2: ขอ pseudoterminal ขึ้นไป bash

สิ่งต่อไปที่เราต้องทำคือเชื่อมต่อ pseudoterminal กับ bash โชคดีที่ง่ายมาก นี่คือรหัส Go สำหรับมัน! เราแค่ต้องเริ่มกระบวนการใหม่และตั้งค่า stdin, stdout และ stderr เป็น tty

 cmd := exec.Command("bash") cmd.Stdin = tty cmd.Stdout = tty cmd.Stderr = tty cmd.SysProcAttr = &syscall.SysProcAttr{ Setsid: true, } cmd.Start()

ง่าย! แม้ว่า – ทำไมเราต้องการ Setsid: true คุณอาจถาม? ฉันพยายามแสดงความคิดเห็นโค้ดนั้นเพื่อดูว่ามีอะไรผิดพลาด ปรากฎว่าสิ่งที่ผิดพลาดคือ – Ctrl + C ใช้งานไม่ได้อีกต่อไป!

Setsid: true สร้าง เซสชัน ใหม่สำหรับกระบวนการทุบตีใหม่ แต่ทำไมถึงทำให้ Ctrl + C ใช้งานได้ Linux รู้ได้อย่างไรว่ากระบวนการใดที่จะส่ง SIGINT ไปเมื่อคุณกด Ctrl + C และเกี่ยวข้องกับเซสชันอย่างไร

Linux รู้ได้อย่างไรว่าจะส่ง Ctrl + C ไปที่กระบวนการใด

ฉันพบว่ามันค่อนข้างสับสน ดังนั้นฉันจึงไปหาหนังสือเล่มโปรดเพื่อเรียนรู้เกี่ยวกับสิ่งนี้: อินเทอร์เฟซการเขียนโปรแกรม linux โดยเฉพาะบทที่ 34 เกี่ยวกับกลุ่มกระบวนการและเซสชัน

บทนั้นมีข้อเท็จจริงสำคัญบางประการ: (#3, #4 และ #5 เป็นคำพูดโดยตรงจากหนังสือ)

  1. ทุกกระบวนการมี รหัสเซสชันและรหัส กลุ่มกระบวนการ (ซึ่งอาจหรืออาจไม่เหมือนกับ PID)
  2. เซสชันประกอบด้วยกลุ่มกระบวนการหลายกลุ่ม
  3. กระบวนการทั้งหมดในเซสชันใช้เทอร์มินัลควบคุมเดียว
  4. เทอร์มินัลอาจเป็นเทอร์มินัลควบคุมได้ไม่เกินหนึ่งเซสชัน
  5. ในช่วงเวลาใดๆ กลุ่มกระบวนการหนึ่งในเซสชันคือ กลุ่มกระบวนการเบื้องหน้า สำหรับเทอร์มินัล และกลุ่มอื่นคือกลุ่มกระบวนการเบื้องหลัง
  6. เมื่อคุณกด Ctrl+C ในเทอร์มินัล SIGINT จะถูกส่งไปยังกระบวนการทั้งหมดในกลุ่มกระบวนการเบื้องหน้า

กลุ่มกระบวนการคืออะไร? ความเข้าใจของฉันคือ:

  • กระบวนการในไพพ์เดียวกัน x | y | z อยู่ในกลุ่มกระบวนการเดียวกัน
  • กระบวนการที่คุณเริ่มต้นบนเชลล์ไลน์เดียวกัน ( x && y && z ) อยู่ในกลุ่มกระบวนการเดียวกัน
  • กระบวนการลูกเป็นค่าเริ่มต้นในกลุ่มกระบวนการเดียวกัน เว้นแต่คุณจะตัดสินใจเป็นอย่างอื่นอย่างชัดเจน

ฉันไม่รู้สิ่งนี้ส่วนใหญ่ (ฉันไม่รู้ว่ากระบวนการมี ID เซสชัน!) ดังนั้นนี่เป็นเรื่องที่ต้องสนใจมาก ฉันพยายามวาดไดอะแกรมศิลปะ ASCII คร่าวๆ ของสถานการณ์

 (maybe) terminal --- session --- process group --- process | |- process | |- process |- process group | |- process group

ดังนั้นเมื่อเรากด Ctrl+C ในเทอร์มินัล นี่คือสิ่งที่ฉันคิดว่าเกิดขึ้น:

  • \x04 ถูกเขียนไปยัง “pseudotermimal master” ของเทอร์มินัล
  • Linux ค้นหา เซสชัน สำหรับเทอร์มินัลนั้น (ถ้ามี)
  • Linux ค้นหา กลุ่มกระบวนการเบื้องหน้า สำหรับเซสชันนั้น
  • Linux ส่ง SIGINT

หากเราไม่สร้างเซสชันใหม่สำหรับกระบวนการทุบตีใหม่ของเรา เทอร์มินัลเทียมใหม่ของเราจะ ไม่มี เซสชันที่เกี่ยวข้อง ดังนั้นจึงไม่มีอะไรเกิดขึ้นเมื่อเรากด Ctrl+C แต่ถ้าเราสร้างเซสชันใหม่ pseudoterminal ใหม่จะมีเซสชันใหม่ที่เกี่ยวข้องด้วย

วิธีรับรายการเซสชันทั้งหมดของคุณ

นอกจากนี้ หากคุณต้องการรับรายการเซสชันทั้งหมดบนเครื่อง Linux ของคุณ โดยจัดกลุ่มตามเซสชัน คุณสามารถเรียกใช้:

 $ ps -eo user,pid,pgid,sess,cmd | sort -k3

ซึ่งรวมถึง PID, ID กลุ่มกระบวนการ และ ID เซสชัน ตัวอย่างของผลลัพธ์ ต่อไปนี้คือสองกระบวนการในไปป์ไลน์:

 bork 58080 58080 57922 ps -eo user,pid,pgid,sess,cmd bork 58081 58080 57922 sort -k3

คุณจะเห็นว่าพวกเขาแชร์ ID กลุ่มกระบวนการและ ID เซสชันเดียวกัน แต่แน่นอนว่าพวกเขามี PID ต่างกัน

นั่นเป็นจำนวนมาก แต่นั่นคือทั้งหมดที่เราจะพูดเกี่ยวกับเซสชันและกลุ่มกระบวนการในโพสต์นี้ ไปกันต่อเถอะ!

ขั้นตอนที่ 3: กำหนดขนาดหน้าต่าง

ต้องบอกเทอร์มินอลว่าใหญ่แค่ไหน!

อีกครั้งฉันเพิ่งคัดลอกมาจาก creack/pty ฉันตัดสินใจฮาร์ดโค้ดขนาดเป็น 80×24

 Setsize(tty, &Winsize{ Cols: 80, Rows: 24, })

เช่นเดียวกับการรับชื่อไฟล์ pts ของเทอร์มินัลและปลดล็อก การตั้งค่าขนาดเป็นเพียงการเรียกระบบ ioctl หนึ่งครั้ง:

 func Setsize(t *os.File, ws *Winsize) { ioctl(t.Fd(), syscall.TIOCSWINSZ, uintptr(unsafe.Pointer(ws))) }

ค่อนข้างง่าย! เราสามารถทำอะไรที่ฉลาดกว่านี้และได้ขนาดหน้าต่างจริง แต่ฉันขี้เกียจเกินไป

ขั้นตอนที่ 4: คัดลอกข้อมูลระหว่างการเชื่อมต่อ TCP และ pseudoterminal

เพื่อเป็นการเตือนความจำ ขั้นตอนคร่าวๆ ของเราในการตั้งค่าเซิร์ฟเวอร์การเข้าสู่ระบบระยะไกลนี้คือ:

  1. สร้าง pseudoterminal ให้ลูกค้าใช้
  2. เริ่มกระบวนการ bash shell
  3. เชื่อมต่อ bash กับ pseudoterminal
  4. คัดลอกข้อมูลไปมาอย่างต่อเนื่องระหว่างการเชื่อมต่อ TCP และ pseudoterminal

เราได้ทำ 1, 2 และ 3 แล้ว ตอนนี้เราแค่ต้องการส่งข้อมูลระหว่างการเชื่อมต่อ TCP และ pseudoterminal

มีการเรียก io.Copy สองครั้ง การโทรหนึ่งครั้งเพื่อคัดลอกอินพุต จาก การเชื่อมต่อ tcp และอีกรายการหนึ่งเพื่อคัดลอกเอาต์พุต ไปยัง การเชื่อมต่อ TCP นี่คือลักษณะของรหัส:

 go func() { io.Copy(pty, conn) }() io.Copy(conn, pty)

อันแรกอยู่ใน goroutine เพื่อให้ทั้งคู่วิ่งขนานกัน

ค่อนข้างง่าย!

ขั้นตอนที่ 5: ออกเมื่อเราทำเสร็จแล้ว

ฉันยังเพิ่มโค้ดเล็กน้อยเพื่อปิดการเชื่อมต่อ TCP เมื่อคำสั่งออก

 go func() { cmd.Wait() conn.Close() }()

และนั่นคือทั้งหมดสำหรับเซิร์ฟเวอร์! คุณสามารถดูรหัส Go ทั้งหมดได้ที่นี่: server.go

ถัดไป: เขียนลูกค้า

ต่อไปเราต้องเขียนลูกค้า นี่เป็นมากกว่าเซิร์ฟเวอร์เพราะเราไม่ต้องตั้งค่าเทอร์มินัลมากนัก มีเพียง 3 ขั้นตอน:

  1. ใส่เทอร์มินัลลงในโหมดดิบ
  2. คัดลอก stdin/stdout ไปยังการเชื่อมต่อ TCP
  3. รีเซ็ตเทอร์มินัล

ไคลเอ็นต์ขั้นตอนที่ 1: วางเทอร์มินัลลงในโหมด “ดิบ”

เราจำเป็นต้องตั้งค่าเทอร์มินัลไคลเอ็นต์ให้อยู่ในโหมด “ดิบ” เพื่อให้ทุกครั้งที่คุณกดปุ่ม คีย์จะถูกส่งไปยังการเชื่อมต่อ TCP ทันที หากเราไม่ทำเช่นนี้ ทุกอย่างจะถูกส่งเมื่อคุณกด Enter

“โหมด Raw” ไม่ใช่สิ่งเดียว แต่เป็นแฟล็กจำนวนมากที่คุณต้องการปิด มีบทช่วยสอนที่ดีที่อธิบายแฟล็กทั้งหมดที่เราต้องปิดที่เรียกว่า Entering raw mode

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

ฉันหาวิธีทำสิ่งนี้ใน Go โดยไปที่ https://grep.app แล้วพิมพ์ syscall.TCSETS เพื่อค้นหาโค้ด Go อื่นที่ทำสิ่งเดียวกัน

 func MakeRaw(fd uintptr) syscall.Termios { // from https://github.com/getlantern/lantern/blob/devel/archive/src/golang.org/x/crypto/ssh/terminal/util.go var oldState syscall.Termios ioctl(fd, syscall.TCGETS, uintptr(unsafe.Pointer(&oldState))) newState := oldState newState.Iflag &^= syscall.ISTRIP | syscall.INLCR | syscall.ICRNL | syscall.IGNCR | syscall.IXON | syscall.IXOFF newState.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.ISIG ioctl(fd, syscall.TCSETS, uintptr(unsafe.Pointer(&newState))) return oldState }

ไคลเอ็นต์ขั้นตอนที่ 2: คัดลอก stdin/stdout ไปยังการเชื่อมต่อ TCP

นี้เหมือนกับสิ่งที่เราทำกับเซิร์ฟเวอร์ มันเป็นรหัสที่น้อยมาก:

 go func() { io.Copy(conn, os.Stdin) }() io.Copy(os.Stdout, conn)

ไคลเอ็นต์ขั้นตอนที่ 3: คืนค่าสถานะของเทอร์มินัล

เราสามารถใส่เทอร์มินัลกลับเข้าสู่โหมดที่เริ่มต้นในลักษณะนี้ (อีก ioctl !):

 func Restore(fd uintptr, oldState syscall.Termios) { ioctl(fd, syscall.TCSETS, uintptr(unsafe.Pointer(&oldState))) }

เราทำได้!

เราได้เขียนเซิร์ฟเวอร์การเข้าสู่ระบบระยะไกลขนาดเล็กที่ช่วยให้ทุกคนเข้าสู่ระบบได้! ไชโย!

เห็นได้ชัดว่าสิ่งนี้ไม่มีความปลอดภัย ดังนั้นฉันจะไม่พูดถึงแง่มุมนั้น

มันทำงานบนอินเทอร์เน็ตสาธารณะ! คุณสามารถลอง!

ประมาณสัปดาห์หน้า ฉันจะสาธิตสิ่งนี้บนอินเทอร์เน็ตที่ tetris.jvns.ca มันรัน tetris แทนที่จะเป็นเชลล์เพราะฉันต้องการหลีกเลี่ยงการละเมิด แต่ถ้าคุณต้องการลองใช้กับเชลล์ คุณสามารถรันบนคอมพิวเตอร์ของคุณเองได้ 🙂

หากคุณต้องการทดลองใช้ คุณสามารถใช้ netcat เป็นไคลเอนต์แทนโปรแกรมไคลเอนต์ Go แบบกำหนดเองที่เราเขียน เนื่องจากการคัดลอกข้อมูลไปยัง/จากการเชื่อมต่อ TCP เป็นสิ่งที่ netcat ทำ นี่คือวิธี:

 stty raw -echo && nc tetris.jvns.ca 7777 && stty sane

นี้จะช่วยให้คุณเล่นเกมเทตริสเทอร์มินัลที่เรียกว่า tint

คุณยังสามารถใช้ โปรแกรม client.go และเรียกใช้ go run client.go tetris.jvns.ca 7777

นี่ไม่ใช่โปรโตคอลที่ดี

โปรโตคอลนี้ที่เราเพิ่งคัดลอกไบต์จากการเชื่อมต่อ TCP ไปยังเทอร์มินัลและไม่มีอะไรอื่นไม่ดีเพราะไม่อนุญาตให้เราส่งข้อมูล เช่น เทอร์มินัลหรือขนาดหน้าต่างจริงของเทอร์มินัล

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

มันจะทำให้เทอร์มินัลของคุณเลอะเล็กน้อย

คำเตือน: การใช้เซิร์ฟเวอร์นี้เพื่อเล่น tetris อาจทำให้เทอร์มินัลของคุณเสียหายเล็กน้อย เพราะมันกำหนดขนาดหน้าต่างเป็น 80×24 เพื่อแก้ไขว่าฉันเพิ่งปิดแท็บเทอร์มินัลหลังจากรันคำสั่งนั้น

หากเราต้องการแก้ไขปัญหานี้จริง เราจะต้องคืนค่าขนาดหน้าต่างหลังจากที่เราทำเสร็จแล้ว แต่เราจำเป็นต้องมีโปรโตคอลจริงมากกว่า “แค่คัดลอกไบต์ไปมาด้วย TCP สุ่มสี่สุ่มห้า” และฉันไม่ได้ ไม่รู้สึกอยากทำอย่างนั้น

นอกจากนี้ บางครั้งอาจใช้เวลาสักครู่ในการยกเลิกการเชื่อมต่อหลังจากโปรแกรมออกด้วยเหตุผลบางอย่าง ฉันไม่แน่ใจว่าทำไมถึงเป็นเช่นนั้น

โครงการเล็กๆ อื่นๆ

นั่นคือทั้งหมด! มีการใช้งานของเล่นที่คล้ายกันอื่น ๆ สองสามโปรแกรมที่ฉันเขียนไว้ที่นี่:

จะเกิดอะไรขึ้นเมื่อคุณกดปุ่มในเทอร์มินัลของคุณ?

ฉันสับสนเกี่ยวกับสิ่งที่เกิดขึ้นกับเทอร์มินัลมาเป็นเวลานาน

แต่สัปดาห์ที่ผ่านมานี้ ฉันใช้ xterm.js เพื่อแสดงเทอร์มินัลแบบโต้ตอบในเบราว์เซอร์ และในที่สุดฉันก็คิดว่าจะถามคำถามพื้นฐานที่ดี: เมื่อคุณกดปุ่มบนแป้นพิมพ์ในเทอร์มินัล (เช่น Delete หรือ Escape หรือ a ) ไบต์ใดที่ส่ง

ตามปกติเราจะตอบคำถามนั้นโดยทำการทดลองและดูว่าเกิดอะไรขึ้น 🙂

ขั้วต่อระยะไกลเป็นเทคโนโลยีที่เก่ามาก

ก่อนอื่น ฉันต้องการจะบอกว่าการแสดงเทอร์มินัลในเบราว์เซอร์ด้วย xterm.js อาจดูเหมือนเป็นสิ่งใหม่ แต่จริงๆ แล้วไม่ใช่ ในยุค 70 คอมพิวเตอร์มีราคาแพง พนักงานจำนวนมากในสถาบันแห่งหนึ่งใช้คอมพิวเตอร์เครื่องเดียวร่วมกัน และแต่ละคนสามารถมี “เทอร์มินัล” ของตนเองกับคอมพิวเตอร์เครื่องนั้นได้

ตัวอย่างเช่น นี่คือภาพถ่ายของเทอร์มินัล VT100 จากยุค 70 หรือ 80 ดูเหมือนว่าอาจเป็นคอมพิวเตอร์ (ค่อนข้างใหญ่!) แต่ก็ไม่ใช่ เพียงแสดงข้อมูลใดก็ตามที่คอมพิวเตอร์จริงส่งมาให้

ขั้วต่อ DEC VT100

แน่นอนว่าในยุค 70 พวกเขาไม่ได้ใช้ websockets สำหรับสิ่งนี้ แต่ข้อมูลที่ถูกส่งกลับไปกลับมานั้นเหมือนเดิมไม่มากก็น้อย

(เทอร์มินัลในรูปนั้นมาจาก Living Computer Museum ในซีแอตเทิล ซึ่งฉันเคยไปเยี่ยมชมครั้งหนึ่ง และเขียน FizzBuzz ในภาษา ed บนระบบ Unix ที่เก่ามาก เป็นไปได้ว่าฉันเคยใช้เครื่องนั้นจริง ๆ หรือหนึ่งในพี่น้องของมัน! ฉัน หวังว่า Living Computer Museum จะเปิดขึ้นอีกครั้ง เป็นการดีที่จะได้เล่นกับคอมพิวเตอร์เครื่องเก่า)

ข้อมูลอะไรถูกส่ง?

เห็นได้ชัดว่าหากคุณต้องการเชื่อมต่อกับคอมพิวเตอร์ระยะไกล (ด้วย ssh หรือใช้ xterm.js และ websocket หรืออย่างอื่น) จำเป็นต้องส่งข้อมูลบางอย่างระหว่างไคลเอนต์และเซิร์ฟเวอร์

โดยเฉพาะ:

  • ลูกค้า ต้องส่งการกดแป้นที่ผู้ใช้พิมพ์ (เช่น ls -l )
  • เซิร์ฟเวอร์ ต้องบอกลูกค้าว่าจะแสดงอะไรบนหน้าจอ

มาดูโปรแกรมจริงที่รันเทอร์มินัลระยะไกลในเบราว์เซอร์และดูว่าข้อมูลใดบ้างที่ถูกส่งกลับไปกลับมา!

เราจะใช้ goterm เพื่อทดลอง

ฉันพบโปรแกรมเล็กๆ นี้บน GitHub ชื่อ goterm ที่รันเซิร์ฟเวอร์ Go ที่ให้คุณโต้ตอบกับเทอร์มินัลในเบราว์เซอร์ได้โดยใช้ xterm.js โปรแกรมนี้ไม่ปลอดภัยมาก แต่ใช้งานได้ง่ายและยอดเยี่ยมสำหรับการเรียนรู้

ฉัน แยกมัน เพื่อให้ใช้งานได้กับ xterm.js ล่าสุด เนื่องจากอัปเดตล่าสุดเมื่อ 6 ปีที่แล้ว จากนั้นฉันได้เพิ่มคำสั่งการบันทึกเพื่อพิมพ์ทุกครั้งที่มีการส่ง / รับไบต์ผ่าน websocket

มาดูการส่งและรับระหว่างการโต้ตอบกับเทอร์มินัลที่แตกต่างกันเล็กน้อย!

ตัวอย่าง: ls

ก่อนอื่นให้เรียกใช้ ls นี่คือสิ่งที่ฉันเห็นในเทอร์มินัล xterm.js :

 [email protected]:/play$ ls file [email protected]:/play$

และนี่คือสิ่งที่รับส่ง: (ในรหัสของฉัน ฉันบันทึก sent: [bytes] ทุกครั้งที่ไคลเอนต์ส่งไบต์และ recv: [bytes] ทุกครั้งที่ได้รับไบต์จากเซิร์ฟเวอร์)

 sent: "l" recv: "l" sent: "s" recv: "s" sent: "\r" recv: "\r\n\x1b[?2004l\r" recv: "file\r\n" recv: "\x1b[[email protected]:/play$ "

ฉันสังเกตเห็น 3 สิ่งในผลลัพธ์นี้:

  1. Echoing: ลูกค้าส่ง l แล้วได้รับ l ที่ส่งกลับทันที ฉันเดาว่าความคิดที่นี่คือลูกค้าโง่จริงๆ – ไม่รู้ว่าเมื่อฉันพิมพ์ l ฉันต้องการให้ l ถูกสะท้อนกลับไปที่หน้าจอ จะต้องมีการบอกอย่างชัดเจนโดยกระบวนการของเซิร์ฟเวอร์เพื่อแสดง
  2. การขึ้นบรรทัดใหม่: เมื่อฉันกด Enter มันจะส่งสัญลักษณ์ \r (การขึ้นบรรทัดใหม่) ไม่ใช่ \n (ขึ้นบรรทัดใหม่)
  3. ลำดับการหลบหนี: \x1b เป็นอักขระหลีก ASCII ดังนั้น \x1b[?2004h จึงบอกให้เทอร์มินัลแสดงบางอย่างหรืออย่างอื่น ฉันคิดว่านี่เป็นลำดับสี แต่ฉันไม่แน่ใจ เราจะพูดถึงลำดับการหลบหนีเพิ่มเติมอีกเล็กน้อยในภายหลัง

เอาล่ะ เรามาทำสิ่งที่ซับซ้อนกว่านี้กันเล็กน้อย

ตัวอย่าง: Ctrl+C

ต่อไป มาดูกันว่าจะเกิดอะไรขึ้นเมื่อเราขัดจังหวะกระบวนการด้วย Ctrl+C นี่คือสิ่งที่ฉันเห็นในเทอร์มินัลของฉัน:

 [email protected]:/play$ cat ^C [email protected]:/play$

และนี่คือสิ่งที่ลูกค้าส่งและรับ

 sent: "c" recv: "c" sent: "a" recv: "a" sent: "t" recv: "t" sent: "\r" recv: "\r\n\x1b[?2004l\r" sent: "\x03" recv: "^C" recv: "\r\n" recv: "\x1b[?2004h" recv: "[email protected]:/play$ "

เมื่อฉันกด Ctrl+C ลูกค้าส่ง \x03 ถ้าฉันค้นหาตาราง ASCII \x03 คือ “สิ้นสุดข้อความ” ซึ่งดูสมเหตุสมผล ฉันคิดว่ามันเจ๋งมากเพราะฉันเคยสับสนเล็กน้อยเกี่ยวกับวิธีการทำงานของ Ctrl+C – เป็นการดีที่รู้ว่ามันแค่ส่งอักขระ \x03

ฉันเชื่อว่าเหตุผลที่ cat ถูกขัดจังหวะเมื่อเรากด Ctrl+C คือเคอร์เนล Linux บนฝั่งเซิร์ฟเวอร์ได้รับอักขระ \x03 นี้ ตระหนักว่ามันหมายถึง “ขัดจังหวะ” จากนั้นจึงส่ง SIGINT ไปยังกระบวนการที่เป็นเจ้าของกลุ่มกระบวนการของ pseudoterminal . ดังนั้นมันจึงถูกจัดการในเคอร์เนลและไม่ใช่ใน userspace

ตัวอย่าง: Ctrl+D

ลองทำสิ่งเดียวกัน ยกเว้น Ctrl+D นี่คือสิ่งที่ฉันเห็นในเทอร์มินัลของฉัน:

 [email protected]:/play$ cat [email protected]:/play$

และนี่คือสิ่งที่ส่งและรับ:

 sent: "c" recv: "c" sent: "a" recv: "a" sent: "t" recv: "t" sent: "\r" recv: "\r\n\x1b[?2004l\r" sent: "\x04" recv: "\x1b[?2004h" recv: "[email protected]:/play$ "

คล้ายกับ Ctrl+C มาก ยกเว้นว่าส่ง \x04 แทน \x03 เย็น! \x04 สอดคล้องกับ ASCII “สิ้นสุดการส่ง”

แล้ว Ctrl + อีกตัวอักษรล่ะ?

ต่อไปฉันสงสัยเกี่ยวกับ – ถ้าฉันส่ง Ctrl+e ไบต์ใดจะถูกส่งไป

ปรากฎว่ามันเป็นเพียงตัวเลขของตัวอักษรนั้นในตัวอักษรเช่นนี้:

  • Ctrl+a => 1
  • Ctrl+b => 2
  • Ctrl+c => 3
  • Ctrl+d => 4
  • Ctrl+z => 26

นอกจากนี้ Ctrl+Shift+b ยังทำสิ่งเดียวกันกับ Ctrl+b (มันเขียน 0x2 )

แล้วปุ่มอื่นๆ บนคีย์บอร์ดล่ะ? นี่คือสิ่งที่พวกเขาแมปไปยัง:

  • Tab -> 0x9 (เหมือนกับ Ctrl+I เนื่องจากฉันเป็นตัวอักษรตัวที่ 9)
  • หนี -> \x1b
  • Backspace -> \x7f
  • หน้าแรก -> \x1b[H
  • สิ้นสุด: \x1b[F
  • พิมพ์หน้าจอ: \x1b\x5b\x31\x3b\x35\x41
  • แทรก: \x1b\x5b\x32\x7e
  • ลบ -> \x1b\x5b\x33\x7e
  • คีย์ Meta ของฉันไม่ทำอะไรเลย

อัลท์ล่ะ? จากการทดลองของฉัน (และ Googling บางส่วน) ดูเหมือนว่า Alt จะเหมือนกับ “Escape” อย่างแท้จริง เว้นแต่ว่าการกด Alt ด้วยตัวเองจะไม่ส่งอักขระใดๆ ไปยังเทอร์มินัล และการกด Escape ด้วยตัวเองก็ทำได้ ดังนั้น:

  • alt + d => \x1bd (และเหมือนกันทุกตัวอักษร)
  • alt + shift + d => \x1bD (และเหมือนกันทุกตัวอักษร)
  • etcetera

ลองดูอีกตัวอย่างหนึ่ง!

ตัวอย่าง: nano

นี่คือสิ่งที่ส่งและรับเมื่อฉันเรียกใช้โปรแกรมแก้ไขข้อความ nano :

 recv: "\r\x1b[[email protected]:/play$ " sent: "n" [[]byte{0x6e}] recv: "n" sent: "a" [[]byte{0x61}] recv: "a" sent: "n" [[]byte{0x6e}] recv: "n" sent: "o" [[]byte{0x6f}] recv: "o" sent: "\r" [[]byte{0xd}] recv: "\r\n\x1b[?2004l\r" recv: "\x1b[?2004h" recv: "\x1b[?1049h\x1b[22;0;0t\x1b[1;16r\x1b(B\x1b[m\x1b[4l\x1b[?7h\x1b[39;49m\x1b[?1h\x1b=\x1b[?1h\x1b=\x1b[?25l" recv: "\x1b[39;49m\x1b(B\x1b[m\x1b[H\x1b[2J" recv: "\x1b(B\x1b[0;7m GNU nano 6.2 \x1b[44bNew Buffer \x1b[53b \x1b[1;123H\x1b(B\x1b[m\x1b[14;38H\x1b(B\x1b[0;7m[ Welcome to nano. For basic help, type Ctrl+G. ]\x1b(B\x1b[m\r\x1b[15d\x1b(B\x1b[0;7m^G\x1b(B\x1b[m Help\x1b[15;16H\x1b(B\x1b[0;7m^O\x1b(B\x1b[m Write Out \x1b(B\x1b[0;7m^W\x1b(B\x1b[m Where Is \x1b(B\x1b[0;7m^K\x1b(B\x1b[m Cut\x1b[15;61H"

คุณสามารถเห็นข้อความบางส่วนจาก UI ในนั้น เช่น “GNU nano 6.2” และ \x1b[27m สิ่งเหล่านี้เป็น Escape Sequence มาพูดถึงฉากหลบหนีกันสักหน่อย!

ลำดับการหลบหนี ANSI

\x1b[ สิ่งที่อยู่เหนือ vim ที่ส่งไปยังไคลเอนต์เรียกว่า “ลำดับการหลบหนี” หรือ “รหัสหลบหนี” นี่เป็นเพราะพวกเขาทั้งหมดเริ่มต้นด้วย \x1b อักขระ “escape” . เปลี่ยนตำแหน่งของเคอร์เซอร์ ทำให้ข้อความเป็นตัวหนาหรือขีดเส้นใต้ เปลี่ยนสี ฯลฯ Wikipedia มีประวัติบางส่วน หากคุณสนใจ

ยกตัวอย่างง่ายๆ: ถ้าคุณเรียกใช้

 echo -e '\e[0;31mhi\e[0m there'

ในเทอร์มินัลของคุณ มันจะพิมพ์คำว่า “hi there” โดยที่ “hi” เป็นสีแดงและ “there” เป็นสีดำ หน้านี้ มีตัวอย่างที่ดีของ Escape Code สำหรับสีและการจัดรูปแบบ

ฉันคิดว่ามีมาตรฐานที่แตกต่างกันเล็กน้อยสำหรับรหัส Escape แต่ความเข้าใจของฉันคือชุดรหัส Escape ทั่วไปที่ผู้คนใช้บน Unix มาจาก VT100 (เทอร์มินัลเก่านั้นในรูปภาพที่ด้านบนสุดของโพสต์บล็อก) และ ไม่ได้เปลี่ยนแปลงมากนักในช่วง 40 ปีที่ผ่านมา

Escape Code คือสาเหตุที่เทอร์มินัลของคุณอาจเกิดความยุ่งเหยิงได้ หากคุณ cat ไบนารีจำนวนมากมาที่หน้าจอของคุณ โดยปกติคุณจะต้องพิมพ์รหัส Escape แบบสุ่มจำนวนหนึ่งโดยไม่ได้ตั้งใจ ซึ่งจะทำให้เทอร์มินัลของคุณยุ่งเหยิง โดยจะต้องมีไบต์ 0x1b อยู่ใน มีที่ไหนสักแห่งถ้าคุณ cat ไบนารีเพียงพอไปยังเทอร์มินัลของคุณ

คุณสามารถพิมพ์ Escape Sequence ด้วยตนเองได้หรือไม่?

ย้อนกลับไปบางส่วน เราได้พูดถึงวิธีที่คีย์ Home จับคู่กับ \x1b[H 3 ไบต์เหล่านั้นคือ Escape + [ + H (เพราะ Escape คือ \x1b )

และถ้าฉันพิมพ์ Escape ด้วยตนเอง จากนั้น [ จากนั้น H ในเทอร์มินัล xterm.js ฉันจะลงเอยที่จุดเริ่มต้นของบรรทัด เหมือนกับว่าฉันกด Home

ฉันสังเกตเห็นว่าสิ่งนี้ใช้ไม่ได้กับ fish บนคอมพิวเตอร์ของฉัน – ถ้าฉันพิมพ์ Escape แล้ว [ มันก็จะพิมพ์ออกมา [ แทนที่จะปล่อยให้ฉันดำเนินการลำดับการหลบหนีต่อไป ฉันถามเพื่อนของฉัน Jesse ที่เขียน โค้ด Terminal ของ Rust เกี่ยวกับเรื่องนี้ และ Jesse บอกฉันว่าโปรแกรมจำนวนมากใช้การ หมดเวลา สำหรับรหัส Escape – ถ้าคุณไม่กดปุ่มอื่นหลังจากเวลาขั้นต่ำผ่านไป ตัดสินใจว่าจริง ๆ แล้วไม่ใช่ Escape Code อีกต่อไป

เห็นได้ชัดว่าสิ่งนี้สามารถกำหนดค่าได้ในปลาด้วย fish_escape_delay_ms ดังนั้นฉันจึงรัน set fish_escape_delay_ms 1000 จากนั้นฉันก็สามารถพิมพ์รหัสการหลบหนีด้วยมือได้ เย็น!

การเข้ารหัสเทอร์มินัลค่อนข้างแปลก

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

  • Ctrl + a ทำสิ่งเดียวกันกับ Ctrl + Shift + a
  • Alt เหมือนกับ Escape
  • ลำดับการควบคุม (เช่น สี / การเลื่อนเคอร์เซอร์ไปรอบๆ) ใช้ไบต์เดียวกับปุ่ม Escape ดังนั้นคุณต้องอาศัยจังหวะเวลาในการพิจารณาว่ามันเป็นลำดับการควบคุมของผู้ใช้เพียงแค่กด Escape หรือไม่

แต่ทั้งหมดนี้ได้รับการออกแบบในยุค 70 หรือ 80 หรือบางอย่าง และจำเป็นต้องคงสภาพเดิมตลอดไปเพื่อความเข้ากันได้แบบย้อนหลัง นั่นคือสิ่งที่เราได้รับ 🙂

เปลี่ยนขนาดหน้าต่าง

ไม่ใช่ทุกสิ่งที่คุณสามารถทำได้ในเทอร์มินัลโดยส่งไบต์ไปมา ตัวอย่างเช่น เมื่อเทอร์มินัลถูกปรับขนาด เราต้องบอก Linux ว่าขนาดหน้าต่างเปลี่ยนไปในทางที่ต่างออกไป

นี่คือสิ่งที่ Go code ใน goterm ต้องทำ:

 syscall.Syscall( syscall.SYS_IOCTL, tty.Fd(), syscall.TIOCSWINSZ, uintptr(unsafe.Pointer(&resizeMessage)), )

นี่คือการใช้การเรียกระบบ ioctl ความเข้าใจของฉันเกี่ยวกับ ioctl คือการเรียกใช้ระบบสำหรับสิ่งสุ่มจำนวนมากที่ไม่ครอบคลุมโดยการเรียกระบบอื่น ๆ ซึ่งโดยทั่วไปเกี่ยวข้องกับ IO ฉันเดา

syscall.TIOCSWINSZ เป็นค่าคงที่จำนวนเต็มซึ่งบอก ioctl ว่าเราต้องการสิ่งใดในกรณีนี้ (เปลี่ยนขนาดหน้าต่างของเทอร์มินัล)

นี่เป็นวิธีการทำงานของ xterm เช่นกัน

ในโพสต์นี้ เรากำลังพูดถึงเทอร์มินัลระยะไกล โดยที่ไคลเอนต์และเซิร์ฟเวอร์อยู่ในคอมพิวเตอร์เครื่องอื่น แต่ที่จริงแล้ว ถ้าคุณใช้เทอร์มินัลอีมูเลเตอร์ เช่น xterm ทั้งหมดนี้ทำงานในลักษณะเดียวกัน มันสังเกตได้ยากกว่าเพราะว่าไบต์ไม่ได้ถูกส่งผ่านการเชื่อมต่อเครือข่าย

นั่นคือทั้งหมดที่สำหรับตอนนี้!

มีอะไรให้รู้อีกมากเกี่ยวกับเทอร์มินัล (เราสามารถพูดคุยเกี่ยวกับสีหรือโหมดดิบเทียบกับปรุงสุกหรือการสนับสนุน Unicode หรืออินเทอร์เฟซ pseudoterminal ของ Linux) แต่ฉันจะหยุดที่นี่เพราะเวลา 22.00 น. มันค่อนข้างจะยาว และฉันคิดว่าสมองของฉันไม่สามารถจัดการกับข้อมูลใหม่ ๆ เกี่ยวกับเทอร์มินัลได้ในวันนี้

ขอบคุณ Jesse Luehrs ที่ตอบคำถามนับพันล้านข้อเกี่ยวกับเทอร์มินัล ข้อผิดพลาดทั้งหมดเป็นของฉัน 🙂

ตรวจสอบบริการเว็บขนาดเล็ก

สวัสดี! ฉันเพิ่งเริ่มใช้เซิร์ฟเวอร์เพิ่มอีกสองสามเครื่องเมื่อเร็ว ๆ นี้ ( nginx playground , ยุ่งกับ dns , dns lookup ) ดังนั้นฉันจึงคิดเกี่ยวกับการตรวจสอบ

ตอนแรกฉันตรวจสอบเว็บไซต์เหล่านี้ไม่ชัดเจนนัก ดังนั้นฉันจึงต้องการเขียนอย่างรวดเร็วว่าฉันทำอย่างไร

ฉันจะไม่พูดถึงวิธีการตรวจสอบเว็บไซต์ Big Serious Mission Critical เลย เฉพาะเว็บไซต์เล็กๆ ที่ไม่สำคัญเท่านั้น

เป้าหมาย: ใช้เวลาดำเนินการประมาณ 0 ครั้ง

ฉันต้องการให้ไซต์ทำงานเป็นส่วนใหญ่ แต่ฉันก็ต้องการใช้เวลาประมาณ 0% ของเวลาของฉันในการดำเนินงานที่กำลังดำเนินอยู่

ตอนแรกฉันระมัดระวังการใช้เซิร์ฟเวอร์มากเพราะในงานสุดท้ายของฉัน ฉันอยู่ในการหมุนเวียน 24 ถึง 7 วัน สำหรับบริการที่สำคัญบางอย่าง และในใจของฉัน “ความรับผิดชอบต่อเซิร์ฟเวอร์” หมายถึง “ตื่นนอนตอนตีสองเพื่อซ่อมเซิร์ฟเวอร์ ” และ “มีแดชบอร์ดที่ซับซ้อนมากมาย”

ดังนั้นในขณะที่ฉันสร้างเว็บไซต์แบบคงที่เพื่อที่ฉันจะได้ไม่ต้องคิดถึงเซิร์ฟเวอร์

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

ไม่มีการติดตามห่วยๆ

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

ขั้นตอนที่ 1: ตัวตรวจสอบสถานะการออนไลน์

ขั้นตอนแรกคือการตั้งค่าตัวตรวจสอบเวลาทำงาน มีสิ่งเหล่านี้มากมายที่ฉันใช้อยู่ตอนนี้คือ updown.io และ uptime robot ฉันชอบส่วนต่อประสานผู้ใช้ของ updown มากกว่า แต่หุ่นยนต์ uptime มีระดับฟรีที่ใจกว้างมากกว่า

เหล่านี้

  1. ตรวจสอบว่าไซต์ขึ้น
  2. ถ้ามันลงไปมันจะส่งอีเมลถึงฉัน

ฉันพบว่าการแจ้งเตือนทางอีเมลเป็นระดับที่ดีสำหรับฉัน ฉันจะรู้ได้อย่างรวดเร็วว่าไซต์หยุดทำงานหรือไม่ แต่มันไม่ปลุกฉันหรืออะไรก็ตาม

ขั้นตอนที่ 2: การตรวจสุขภาพแบบ end-to-end

ต่อไป มาพูดถึงความหมายของคำว่า “ตรวจสอบว่าไซต์พร้อมใช้งาน” จริงๆ แล้ว

ตอนแรกฉันเพิ่งสร้างหนึ่งในจุดสิ้นสุดการตรวจสอบสุขภาพของฉันเป็นฟังก์ชันที่ส่งคืน 200 OK ไม่ว่าจะเกิดอะไรขึ้น

สิ่งนี้มีประโยชน์ – มันบอกฉันว่าเซิร์ฟเวอร์เปิดอยู่!

แต่ไม่น่าแปลกใจเลยที่ฉันพบปัญหาเพราะไม่ได้ตรวจสอบว่า API นั้นใช้ งาน ได้จริง – บางครั้งการตรวจสุขภาพก็สำเร็จแม้ว่าบริการที่เหลือจะเข้าสู่สถานะไม่ดีจริงๆ

ดังนั้นฉันจึงอัปเดตเพื่อสร้างคำขอ API จริงและทำให้แน่ใจว่าสำเร็จ

บริการทั้งหมดของฉันทำสิ่งเล็กๆ น้อยๆ (สนามเด็กเล่น nginx มีเพียง 1 ปลายทาง) ดังนั้นจึงค่อนข้างง่ายในการตั้งค่าการตรวจสุขภาพที่ดำเนินการจริงส่วนใหญ่ที่บริการควรทำ

นี่คือสิ่งที่ตัวจัดการการตรวจสุขภาพแบบ end-to-end สำหรับสนามเด็กเล่น nginx ดูเหมือน เป็นพื้นฐานมาก: มันแค่ส่งคำขอ POST อีกครั้ง (สำหรับตัวมันเอง) และตรวจสอบว่าคำขอนั้นสำเร็จหรือล้มเหลว

 func healthHandler(w http.ResponseWriter, r *http.Request) { // make a request to localhost:8080 with `healthcheckJSON` as the body // if it works, return 200 // if it doesn't, return 500 client := http.Client{} resp, err := client.Post("http://localhost:8080/", "application/json", strings.NewReader(healthcheckJSON)) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } if resp.StatusCode != http.StatusOK { log.Println(resp.StatusCode) w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) }

ความถี่ในการตรวจสุขภาพ: รายชั่วโมง

ตอนนี้ฉันกำลังตรวจสุขภาพเป็นส่วนใหญ่ทุกชั่วโมง และบางรายการทุก 30 นาที

ฉันเรียกใช้ทุกชั่วโมงเพราะราคาของ updown.io ต่อการตรวจสุขภาพ ฉันกำลังตรวจสอบ URL ที่แตกต่างกัน 18 รายการ และฉันต้องการรักษางบประมาณการตรวจสุขภาพของฉันให้น้อยที่สุดที่ $5/ปี

ฉันใช้เวลาหนึ่งชั่วโมงกว่าจะพบว่าเว็บไซต์ใดเว็บไซต์หนึ่งหยุดทำงาน หากมีปัญหา ก็ไม่รับประกันว่าฉันจะแก้ไขปัญหาทั้งหมดได้อย่างรวดเร็ว

ถ้ามันเป็นอิสระที่จะเรียกใช้พวกเขาบ่อยขึ้น ฉันอาจจะเรียกใช้พวกเขาทุก 5-10 นาทีแทน

ขั้นตอนที่ 3: รีสตาร์ทโดยอัตโนมัติหากการตรวจสุขภาพล้มเหลว

บางเว็บไซต์ของฉันใช้ fly.io และ fly มีคุณสมบัติมาตรฐานที่ค่อนข้างดี ซึ่งฉันสามารถกำหนดค่า HTTP healthcheck สำหรับบริการและเริ่มต้นบริการใหม่ได้หากการตรวจสุขภาพเริ่มล้มเหลว

“รีสตาร์ทบ่อยมาก” เป็นกลยุทธ์ที่มีประโยชน์มากในการเขียนทับจุดบกพร่องที่ฉันยังไม่ได้แก้ไข – ในขณะที่สนามเด็กเล่น nginx มีการรั่วไหลของกระบวนการซึ่งกระบวนการ nginx ไม่ได้ถูกยกเลิก ดังนั้นเซิร์ฟเวอร์จึงทำงานจนหมด ของแรม

ด้วยการตรวจสุขภาพ ผลลัพธ์ของสิ่งนี้ก็คือ ทุกๆ วัน สิ่งนี้จะเกิดขึ้น:

  • เซิร์ฟเวอร์หมด RAM
  • การตรวจสุขภาพเริ่มล้มเหลว
  • มันเริ่มต้นใหม่
  • ทุกอย่างเรียบร้อยดีอีกแล้ว
  • ทำซ้ำเรื่องราวทั้งหมดอีกครั้งในอีกไม่กี่ชั่วโมงต่อมา

ในที่สุดฉันก็ได้แก้ไขการรั่วไหลของกระบวนการจริง ๆ แต่ก็ดีที่มีวิธีแก้ปัญหาที่สามารถทำให้สิ่งต่าง ๆ ทำงานต่อไปได้ในขณะที่ฉันผัดวันประกันพรุ่งแก้ไขข้อผิดพลาด

การตรวจสอบสภาพเหล่านี้เพื่อตัดสินใจว่าจะเริ่มต้นบริการใหม่ให้ทำงานบ่อยขึ้นหรือไม่: ทุกๆ 5 นาทีหรือมากกว่านั้น

นี่ไม่ใช่วิธีที่ดีที่สุดในการตรวจสอบ Big Services

สิ่งนี้อาจชัดเจนและฉันได้พูดไปแล้วในตอนเริ่มต้น แต่ “เขียน HTTP healthcheck หนึ่งรายการ” ไม่ใช่วิธีที่ดีที่สุดสำหรับการตรวจสอบบริการที่ซับซ้อนขนาดใหญ่ แต่ฉันจะไม่พูดถึงเรื่องนั้นเพราะนั่นไม่ใช่สิ่งที่โพสต์นี้เกี่ยวกับ

มันทำงานได้ดีจนถึงตอนนี้!

ตอนแรกฉันเขียนโพสต์นี้เมื่อ 3 เดือนที่แล้วในเดือนเมษายน แต่ฉันรอจนถึงตอนนี้เพื่อเผยแพร่เพื่อให้แน่ใจว่าการตั้งค่าทั้งหมดใช้งานได้

มันสร้างความแตกต่างได้ค่อนข้างมาก – ก่อนที่ฉันจะมีปัญหาเรื่องการหยุดทำงานที่งี่เง่า และตอนนี้ในช่วงสองสามเดือนที่ผ่านมา ไซต์เพิ่มขึ้น 99.95% ของเวลาทั้งหมด!