การอัปโหลดวิดีโอขนาดสองกิกะไบต์ผ่าน WiFi ในร้านกาแฟคือการเดิมพันด้วยความหวัง คุณเฝ้ามองแถบความคืบหน้าที่ค่อยๆ ขยับไปถึงเจ็ดสิบเปอร์เซ็นต์ แล้วจู่ๆ การเชื่อมต่อก็ติดๆ ดับๆ การอัปโหลดล้มเหลว เมื่อคุณพยายามอีกครั้ง เซิร์ฟเวอร์กลับคาดหวังไฟล์ใหม่เอี่ยม คุณต้องเริ่มนับหนึ่งใหม่จากศูนย์ สำหรับใครก็ตามที่กำลังสร้างแอปพลิเคชันที่จัดการวิดีโอที่ผู้ใช้สร้างขึ้น (user-generated video) นี่ไม่ใช่กรณีที่เกิดขึ้นได้ยาก แต่มันคือเรื่องปกติ โปรโตคอล tus ถูกสร้างขึ้นมาเพื่อกำจัดปัญหานี้โดยเฉพาะ มันเป็นมาตรฐานเปิดที่ทำงานบน HTTP ทั่วไป และทำให้การอัปโหลด "มีความจำ" เมื่อเครือข่ายกลับมาใช้งานได้ การถ่ายโอนไฟล์จะดำเนินการต่อจากจุดเดิมที่ค้างไว้พอดี
ทำไมการอัปโหลดแบบมาตรฐานถึงล้มเหลว
การอัปโหลดไฟล์แบบ multipart มาตรฐานจะปฏิบัติกับ payload ทั้งหมดเสมือนเป็นธุรกรรมเดียว เบราว์เซอร์หรือแอปจะเปิดการเชื่อมต่อ TCP ส่งข้อมูลแบบสตรีมไบต์ และรอรับสถานะ 200 OK เมื่อเสร็จสิ้น หากการเชื่อมต่อถูกรีเซ็ตเพราะผู้ใช้สลับจาก LTE เป็น WiFi หรือเพราะแล็ปท็อปเข้าสู่โหมดสลีป เซิร์ฟเวอร์จะไม่มีทางรู้เลยว่ามีไบต์ไหนบ้างที่ส่งมาถึงอย่างปลอดภัย เฟรมเวิร์กส่วนใหญ่จะทิ้งข้อมูลที่ส่งมาเพียงบางส่วนนั้นไปเลย ผู้ใช้จะเหลือเพียงฟอร์มที่ส่งไม่สำเร็จและความรู้สึกแย่ๆ ไฟล์วิดีโอจะยิ่งทำให้ปัญหานี้รุนแรงขึ้น เพราะไฟล์มีขนาดใหญ่ มักถูกอัปโหลดจากอุปกรณ์เคลื่อนที่ผ่านเครือข่ายที่ไม่เสถียร และมักจะถูกเข้ารหัสในรูปแบบที่ไม่สามารถประมวลผลได้จนกว่าไฟล์ทั้งหมดจะสมบูรณ์
Tus เปลี่ยนเกมนี้อย่างไร
Tus เปลี่ยนแนวคิดการอัปโหลดจากการส่งข้อมูลเพียงครั้งเดียว (one-shot delivery) ให้เป็นการสนทนาที่มีสถานะ (stateful conversation) แทนที่จะส่งไฟล์ด้วยการพ่นไบต์ออกมาอย่างต่อเนื่องเพียงครั้งเดียว ไคลเอนต์จะแบ่ง payload ออกเป็นส่วนๆ (chunks) ที่สำคัญกว่านั้น เซิร์ฟเวอร์จะจดจำค่า offset หรือจำนวนไบต์ที่ได้รับมาแล้วอย่างแม่นยำ การรักษาความต่อเนื่องของสถานะนี้เองที่ทำให้การอัปโหลดต่อ (resumption) เป็นไปได้ โปรโตคอลนี้ถูกออกแบบมาให้เรียบง่ายโดยตั้งใจ ไม่จำเป็นต้องใช้ WebSockets, gRPC หรือ SDK เฉพาะทาง แต่มันใช้ HTTP methods ที่คุณคุ้นเคยอยู่แล้ว
การร้องขอ (Requests) 3 รูปแบบที่เป็นหัวใจของการอัปโหลดต่อ
ทุกการอัปโหลดด้วย tus จะมีขั้นตอนที่คาดเดาได้
ขั้นแรก ไคลเอนต์จะส่งคำขอ POST ไปยัง tus endpoint ที่กำหนดไว้ ส่วนหัว (headers) จะระบุรายละเอียดของไฟล์ Header Upload-Length จะระบุขนาดสุดท้ายในหน่วยไบต์ และ metadata ที่ไม่บังคับ เช่น ชื่อไฟล์หรือ content type จะถูกส่งผ่าน Header Upload-Metadata ในรูปแบบรายการ key-value ที่เข้ารหัสแบบ base64 และคั่นด้วยเครื่องหมายจุลภาค เซิร์ฟเวอร์จะสร้างทรัพยากรการอัปโหลดที่ว่างเปล่าขึ้นมา และตอบกลับด้วยสถานะ 201 Created พร้อมกับ Location header ที่ชี้ไปยัง URL เฉพาะ URL นี้จะเป็นที่อยู่สำหรับการอัปโหลดนี้ไปตลอด
ต่อมาคือคำขอ PATCH โดยไคลเอนต์จะส่งข้อมูลไบนารีดิบ (raw binary data) เป็นส่วนๆ (chunks) ไปยัง URL เฉพาะนั้น จะมี Header สำคัญสองตัวที่มาพร้อมกับ payload โดย Content-Type ต้องเป็น application/offset+octet-stream และ Upload-Offset ต้องตรงกับตำแหน่งไบต์ที่ chunk นี้เริ่มต้น เซิร์ฟเวอร์จะตรวจสอบว่า offset ตรงกับบันทึกของตนเองหรือไม่ หากไม่ตรง เซิร์ฟเวอร์จะส่ง 409 Conflict กลับมา เพื่อป้องกันสถานะข้อมูลที่ผิดพลาด หากทุกอย่างถูกต้อง เซิร์ฟเวอร์จะเพิ่มไบต์ต่อท้าย อัปเดต offset ภายใน และตอบกลับด้วย 204 No Content พร้อมกับ Upload-Offset ใหม่ ขนาดของ chunk สามารถกำหนดค่าได้ แต่โดยทั่วไปมักจะอยู่ระหว่างไม่กี่ร้อยกิโลไบต์ไปจนถึงสองสามเมกะไบต์ Chunk ขนาดเล็กจะสร้าง HTTP overhead มากกว่าแต่จะกู้คืนจากข้อผิดพลาดได้เร็วกว่า ส่วน Chunk ขนาดใหญ่จะช่วยลดจำนวนรอบการรับส่งข้อมูล (round trips) แต่จะสิ้นเปลืองแบนด์วิดท์มากกว่าหากการส่งล้มเหลวระหว่างทาง
สุดท้ายคือคำขอ HEAD ซึ่งทำหน้าที่เป็นตาข่ายนิรภัย (safety net) หาก PATCH ล้มเหลวกลางคัน เช่น หลังจากส่งไปได้ 3 เมกะไบต์จากทั้งหมด 5 เมกะไบต์ แล้วการเชื่อมต่อหลุดไป เมื่อไคลเอนต์กลับมาเชื่อมต่อเครือข่ายได้อีกครั้ง มันจะส่งคำขอ HEAD ไปยัง URL ของการอัปโหลด เซิร์ฟเวอร์จะตอบกลับด้วย Upload-Offset ปัจจุบัน ไคลเอนต์จะเปรียบเทียบ
