แชร์วิธีการทำโปรแกรมดาวน์โหลดไฟล์แบบง่ายๆ

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

โปรแกรมที่ผมทำมีชื่อว่า TirkxDownloader เป็นโปรแกรมจัดการการดาวน์โหลด(ดาวน์โหลดไฟล์,ทำคิวการดาวน์โหลด)

โปรแกรมนี้เขียนด้วย C# เกือบทั้งหมด มี Javascript บ้างตอนเขียน Extension สำหรับ Chrome ตัว GUI ใช้ WPF ซึ่งผมคิดว่าดีกว่า platform ใหม่ที่ Microsoft เปิดตัวใหม่อย่าง UWP(Universal windows plateform) ซะอีก(จริงๆ มันคนละเป้าหมายล่ะนะ)

รายละเอียดการเขียน

MVVM pattern

เป็น pattern การเขียนโปรแกรมของ WPF หรือแม้กระทั่ง platform อื่นที่สามารถนำไปประยุกต์ได้ รายละเอียดมีประมาณนี้

Model เป็นตัวข้อมูลที่จะถูกแสดงใน View ซึ่งส่วนใหญ่ผม implement INotifyPropertyChanged ไว้เพื่อทำ data binding ผ่าน ViewModel ตรงๆ เมื่อข้อมูลใน Model เปลี่ยนก็จะมีการแจ้งเตือนและทำให้ View อัพเดทข้อมูลตามไปด้วย นอกจากนี้ยังมี event OnDownloadComplete อีกเพื่อใช้สำหรับการจัดคิวต่อ

ViewModel เป็นส่วนที่ช่วยแสดงผล Model ใน View ซึ่ง 1 ViewModel จะมี 1 View ที่ตรงกันอยู่(บางอันมี 2) ที่ส่วนนี้ก็จะมีพวก event handler อยู่ด้วยซึ่งใช้ระบบ event ในแบบของ framework ที่ผมใช้อยู่

View เป็นส่วน GUI ที่เราเห็นจริงๆ ตรงนี้ผมเขียนแต่ xaml ล้วนๆ

MVVM Infrastructure framework ที่ ผมใช้คือ Caliburn Micro ซึ่งใช้งานง่ายมาก แต่การที่เน้น convention มากกว่า configuration อาจจะทำให้คนที่ไม่รู้เรื่อง WPF เลยแล้วมาเริ่มต้นงงซักเลยน้อย(ผมก็ด้วย) ดังนั้นถ้าจะใช้ WPF อย่างน้อยผมขอให้อ่านบทความ XAML Overview (WPF) หรือหาหนังสือสอน WPF มาอ่านก่อน

การโหลดไฟล์

ส่ง http request file ไปที่ server และ อ่าน response stream แบบ asynchronous จากนั้นก็เขียนลงเครื่องโดยตรง

[c-sharp] var request = (HttpWebRequest)HttpRequest.Create(url);
var response = await request.GetResponseAsync();
var inStream = response.GetResponseStream();
byte[] buffer = new byte[102400];
int readbyte;

using (FileStream outStream = new FileStream(_currentItem.FullName, FileMode.Open, FileAccess.Write, FileShare.None, 65536))
{
do {
readbyte = await inStream.ReadAsync(buffer, 0, buffer.Length);
await outStream.WriteAsync(buffer, 0, readbyte);
while {
}

[/c-sharp]

ซึ่งวิธีนี้เราจะได้ Stream มาซึ่งทำให้เราสามารถวัดความเร็วการดาวน์โหลด, ดาวน์โหลดต่อจากที่หยุดไว้และที่สำคัญที่สุดคือจำกัดความเร็วได้
วิธีที่ง่ายกว่านี้คือใช้ WebClient ซึ่งมี method DownloadFile ให้อยู่แล้ว วิธีนี้เราจะได้เปอเซนต์ที่เราดาวน์โหลดไปแล้ว, จำนวนข้อมูลที่เรารับมา(รับมาแล้วกี่ byte)และจำนวนข้อมูลทั้งหมดที่จะถูกดาวน์โหลดผ่านทาง event DownloadProgressChanged ตัวอย่าง

[c-sharp] public Task DownloadFile(string uri, string locationAndFileName)
{
DownloadProgressChanged += DownloadProgressCallback;
return await DownloadFileAsync(uri, locationAndFileName);
}

private static void DownloadProgressCallback(object sender, DownloadProgressChangedEventArgs e)
{
// Displays the operation identifier, and the transfer progress.
Console.WriteLine("{0} downloaded {1} of {2} bytes. {3} % complete…",
(string)e.UserState,
e.BytesReceived,
e.TotalBytesToReceive,
e.ProgressPercentage);
}
[/c-sharp]

event นี้จะใช้ได้ก็ต่อเมื่อเราใช้ asynchronous api รายละเอียดตามนี้ครับ WebClient.DownloadProgressChanged

อีกเรื่องของการดาวน์โหลดคือการดาวน์โหลดแบบพร้อมกัน ถ้าไม่เคยรู้จัก asynchronous มาก่อนอาจคิดว่าต้องใช้ Thread แต่ในความเป็นจริงแล้วเราใช้ asynchronous api ได้ โดยไม่ต้องใช้ Thread ให้ยุ่งยากเลยดังตัวอย่างด้านบน การใช้ Thread จะเริ่มยุ่งยากขึ้นเมื่อเราต้องการหยุดการทำงานของ Thread นั้น เนื่องจากจะมีบัคตามมาขึ้นอยู่กับว่าเราใช้ api อะไรไปบ้าง ดังนั้นผมไม่แนะนำให้ใช้ Thread ตรงๆ ยกเว้นแต่ต้องใช้จริงๆ เช่น ต้องการตั้ง ApartmentState(มันคืออะไร – -)

ทำ Queue

ใช้ Queue(เป็น collection แบบหนึ่ง) เก็บ model ที่ต้องการดาวน์โหลดเอาไว้ และมี method ตัวหนึ่งทำหน้าที่เป็นตัวสั่งการดาวน์โหลดเองในตอนต้นและเป็น event handler ให้กับ model ที่การดาวน์โหลดเสร็จแล้วในภายหลัง(ไม่ว่าจะเสร็จสมบูรณ์, มี error และอื่นๆ) model ภายใน Queue จะถูกถึงออกมาดาวน์โหลดเรื่อยๆ จนกว่าจะหมดและจะไม่เพิ่มการดาวน์โหลดหากถึงจำนวนสูงสุดที่อนุญาติให้ดาวน์โหลดแล้ว
รายละเอียดโค้ดตรงนี้ยาวมากๆ ขอให้ไปดูรายละเอียดที่ Downloader เลยครับ

เก็บ Credential(username กับ password ของ website)

ผมเก็บไว้ใน Credential Manager ของ Windows  ซึ่งผมใช้ CredentialManagement  library ในการเข้าใช้งาน Credential Manager ตัวอย่างโค้ดก็ตามนี้ครับ AuthorizationManager และอีกส่วนที่ใช้ในการหาว่า url นี้จะใช้ credential อันไหนก็ตามนี้เลยครับ FileHostingUtil (อยู่ใน method FillCredential)

ในตอนนี้นั้นผมยังไม่ได้ทำให้โปรแกรมปลอดภัย วิธีทำให้ปลอยภัยขึ้นอีกก็ไม่ยาก เวลาเราจะให้ผู้ใช้ใส่รหัส ให้เราใช้ PasswordBox แทน TextBox ซึ่ง PasswordBox นั้นจะมี property ชื่อ SecurePassword อยู่ซึ่งเป็น SecureString จากนั้นเวลาจะสร้าง Credential หรือเก็บลงเครื่อง ให้ใช้ method หรือ constructor ที่รับ password เป็น SecureString เท่านั้น ห้ามเปลี่ยน SecureString เป็น string โดยเด็ดขาด ไม่อย่างงั้นก็จบเกม

สาเหตุที่เป็นชื่อนี้เป็นเพราะว่าตอนแรกนั้นต้องการจะทำโปรแกรมที่ใช้เปิดหาอนิเมะบนเว็บ Tirkx ได้โดยตรงโดยไม่ต้องเปิด web browser และไม่ใช้ API จากเว็บ Tirkx เองด้วย เพราะช่วงนั้นอยากลองใช้ html agility pack ในการอ่านหน้าเว็บไซต์และนำข้อมูลมาแสดง ซึ่งท้ายที่สุดก็ต้องยอมแพ้เนื่องจากส่วนที่แสดงไฟล์ให้ดาวน์โหลดนั้น เป็น javascript ซึ่งจำเป็นต้องใช้ web browser มา interpret มันออกมา ซึ่งช้าและใช้ทรัพยากรเครื่องมากเกินไป ท้ายที่สุดจึงทำออกมาเป็นโปรแกรม download manager ไว้ใช้ดาวน์โหลดไฟล์ทั้งหมดซะเลย