[Lưu ý: Bài viết này được viết với .NET 7 và MiniWebServer 0.3.1, từ phiên bản 0.3.3 bạn sẽ cần dùng .NET 8]
Nếu theo dõi blog này chắc có lẽ bạn không lạ gì Mini-Web-Server, một máy chủ web 6 tháng tuổi, cung cấp nhiều tính năng với lượng code tối thiểu, giúp bạn có thể đọc và hiểu dễ dàng.
Tuy mục đích ban đầu là cung cấp một tập code mẫu trực quan cho một số môn học như OOP, phân tích thiết kế, lập trình mạng, hệ điều hành, lập trình hệ thống, .NET… nhưng sau một thời gian, Mini-Web-Server đã trở nên ổn định và nhiều tính năng hơn: Session, Authentication, Authorization, HTTPS, Caching, và thậm chí là cả một bộ MVC. Tất nhiên không thể so sánh được với các máy chủ web lớn phổ biến, nhưng nó hoàn toàn phục vụ được cho các web đơn giản, và sẽ rất thú vị khi bạn theo dõi những bước nó làm để phục vụ cho một yêu cầu từ người dùng: đọc các gói tin, phân tích thành một HTTP request, phân tích các thành phần trong request, tìm kiếm và chạy một trình xử lý cho request đó, lấy dữ liệu từ response và đẩy về client…
Một trong những ưu điểm giúp Mini-Web-Server dễ mở rộng là phần lõi được giữ tối thiểu, hầu hết các thành phần đều được thiết kế như các module mở rộng, ngay cả những thành phần quan trọng nhất như trình phân tích luồng dữ liệu cũng không nằm trong lõi, và nó hoàn toàn dễ dàng bị thay thế khi ta viết thêm các trình phân tích để hỗ trợ HTTP/2. Hầu hết tính năng nó có đều được cung cấp thông qua hệ thống Middleware, do vậy bạn chỉ cần cài những phần cần dùng, ở mức tối thiểu, nó chỉ chiếm vài MB bộ nhớ. Với định hướng trở thành một máy chủ web nhúng (chạy bên trong để mở rộng tính năng cho các ứng dụng khác), có lẽ chúng ta sẽ còn có thể tối ưu nó thêm nhiều nữa và cho phép nó chạy trên các thiết bị rất hạn chế tài nguyên.
Trong bài viết này, tôi sẽ hướng dẫn các bạn viết một máy chủ web đơn giản với Mini-Web-Server. Sau khi hoàn thành, bạn có thể gắn nó vào một chương trình sẵn có của bạn để cho phép người dùng truy cập đến thông qua giao diện web.
Giờ ta đã có các business entity quan trọng nhất, cũng như định nghĩa ISolver, một “bản hướng dẫn” cách giải quyết vấn đề. Đúng vậy, ISolver là một interface, tự nó không thể giải được bài toán, nhưng nó đóng vai là bản cam kết, là một “contract” cho việc giải bài toán PTB1. Và đó cũng là nhiệm vụ của bất kỳ interface nào. Nguyên tắc đơn giản là: tôi không biết anh là ai, nhưng nếu anh implement ISolver, vậy chắc chắn anh có thể giải được bài toán này, anh có thể dùng đầu óc tính toán thông qua các phép cộng trừ nhân chia để tìm ra kết quả, hoặc như trong trường hợp của DistributedSolver (xem phần 2), anh phân phối lại các yêu cầu cho các máy tính khác và tổng hợp lại.
Vậy ta sẽ cần ít nhất một lớp hiện thực hóa ISolver, ta đặt tên nó là LocalCpuSolver (đặt tên hoành tráng một chút mới bán được nhiều tiền):
public interface ISolver {
Result Solve(Constants constants);
}
public class LocalCpuSolver implements ISolver {
Result Solve(Constants constants) {
return new Result() {
...
};
}
}
Tới đây, ta đã có một ISolver cụ thể (chính là LocalCpuSolver), hoàn toàn độc lập, bạn có thể viết các unit test và gắn vào để chạy, việc LocalCpuSolver chỉ phụ thuộc duy nhất vào ISolver giúp chúng ta dễ dàng kiểm thử, dễ dàng thay thế các instance của nó với instance của một class khác. Một thiết kế có càng ít mối phụ thuộc càng dễ bảo trì, chỉnh sửa, thay đổi… vậy nên, khi thiết kế, hãy cố gắng làm mọi thứ thật đơn giản.
Tương tự, ta sẽ xây dựng hai lớp ConsoleResultWriter và ConsoleConstantsReader, cho các interface IResultWriter và ConstantsReader (xem lại bài 2).
Đó là câu hỏi thường trực trong đầu mình kể từ ngày bắt đầu học máy tính.
Làm thế nào mình gõ vài dòng code mà máy tính lại hiểu được mình muốn làm gì?
Khi mình gõ một nút trên bàn phím, họ làm thế nào để biết mình gõ chữ gì và làm sao đọc nó vào được?
Làm sao chuỗi ký tự từ câu lệnh printf lại chạy ra được màn hình?
Khi mình bấm nút nguồn thì quá trình khởi động bắt đầu, mà quá trình khởi động này nó làm gì nhỉ?
Làm sao khi mình gõ một địa chỉ thì nó biết được cần phải kết nối với máy tính nào? Và làm sao gửi được yêu cầu đến đúng máy đó mà không phải máy khác?
Một trong những vấn đề bảo mật được nhắc đến nhiều nhất gần đây là HTTP/2 Rapid Reset Attack, được dùng để tạo ra các cuộc tấn công DDOS, với báo cáo lớn nhất là 394 triệu yêu cầu được gửi đến mỗi giây.
Tui nhân dịp này tranh thủ giải thích về lỗi bảo mật này, giúp các bạn hiểu thêm về giao thức HTTP, và hiểu về code của dự án Mini-Web-Server dễ dàng hơn.
Cách một request được xử lý trong HTTP/1.1
Khi một client gửi request đến một HTTP server, các bước thực hiện sẽ như sau:
– Mở kết nối TCP/IP đến HTTP server.
– Gửi request.
– Server phân tích request.
– Server xử lý request và trả về response.
– Client đọc response và xử lý kết quả.
Mỗi một vòng như vậy gọi là 1 round trip, client sẽ phải chờ cho đến khi server hoàn thành việc xử lý và trả về kết quả. Trong lúc server xử lý request, ví dụ như đọc file, cập nhật database… thì các phần khác cũng phải dừng lại. Client muốn gửi request kế tiếp thì phải chờ cho đến khi request hiện tại xử lý xong, do vậy không tận dụng hết tài nguyên của cả client và server.
Trước hết, vấn đề chúng ta cần phải giải quyết là gì? Đó là tìm ra nghiệm của một phương trình bậc 1 với đầu vào là a và b cho trước, kết quả có thể là vô nghiệm, vô số nghiệm hoặc một x cụ thể nào đó.
Điều tôi muốn nhấn mạnh ở đây chính là mục đích mà chương trình được tạo ra. Thực chất khách hàng của bạn có quan tâm tới việc dữ liệu đầu vào đến từ bàn phím hay từ file không? Không! Họ có quan tâm dữ liệu sẽ được đưa ra màn hình, file PDF hay Excel không? Cũng không! Việc tìm nghiệm mới là mục đích tồn tại của chương trình này, mọi thứ khác diễn ra xung quanh và chỉ nhằm phục vụ cho mục đích này mà thôi.
Khi nhìn vào một bài toán, chúng ta sẽ phải tìm ra những thành phần tham gia vào hệ thống, cái nào quan trọng hơn cái nào, cái nào thường xuyên và cái nào không bao giờ thay đổi… Xác định đúng các thành phần này là bước quan trọng sống còn trong thiết kế, bởi chúng sẽ định hình sự phụ thuộc sau này.
OCP là một trong những nguyên tắc quan trọng trong thiết kế phần mềm, mọi người thường nhớ đến nó như một trong 5 nguyên tắc SOLID. Tuy nhiên thực tế nó đã xuất hiện từ trước đó, được nhiều người thảo luận và sử dụng, ở nhiều cấp độ khác hơn là chỉ các class và interface… Trong bài này chúng ta sẽ cùng tìm hiểu OCP nhé.
Trước tiên, bạn hãy nhìn vào chương trình giải phương trình bậc 1 trong hình, đây là một chương trình rất đơn giản mà ai biết lập trình đều có thể hiểu được (kể cả khi bạn không biết C#). Chương trình này sẽ đọc các số a và b, sau đó trả về kết quả. Giờ ta hãy thử đưa chương trình này cho khách hàng, sau vài năm triển khai, liệu có thể có thêm những yêu cầu gì mới?
Đây là bài viết giới thiệu về .NET, một platform mạnh mẽ, hỗ trợ đa nền tảng, nhiều công cụ hỗ trợ, và cũng rất phổ biến trong thị trường lao động.
Một chút lịch sử:
– Microsoft đã từng hỗ trợ Java. Bộ Visual Studio 6.0 đã từng có một công cụ tên Visual J++ dùng để viết app Java, sau này còn có trình biên dịch Visual J# để dịch ứng dụng viết bằng Java trên .NET.
– Microsoft bắt đầu phát triển .NET (hay còn gọi là .NET Framework) như một nền tảng thay thế Java khi mối quan hệ với Sun Microsystems (chủ sở hữu Java lúc đó) rạn nứt.
– Nhiều ứng dụng của Microsoft đã được viết lại trên .NET, bao gồm cả IDE Visual Studio và CSDL SQL Server, đồng thời trình runtime .NET cũng được tích hợp sẵn vào Windows luôn.
– Tuy về mặt lý thuyết .NET được thiết kế để chạy trên mọi nền tảng, tuy nhiên Microsoft chỉ phát triển bộ runtime cho Windows, do vậy các app .NET chỉ có thể chạy trên Windows.
– Mono project được cộng đồng mã nguồn mở phát triển để xây dựng trình runtime cho .NET trên Linux, và sau này mở rộng ra cả các nền tảng khác. Mono không hỗ trợ đầy đủ các công nghệ như .NET Framework chạy trên Windows.
– Sau một hồi mua đi bán lại thì công ty chủ quản tạo ra Mono có ý đồ dẹp tiệm dự án này, anh chàng LTV đã tạo nên Mono bèn lập ra một công ty mới là Xamarin, tiếp tục phát triển các công nghệ hỗ trợ xây dựng trình runtime cho .NET trên các nền tảng ngoài Windows, thậm chí mở rộng ra cho cả iOS và Android.
– Xamarin phát hành bộ công cụ xây dựng ứng dụng .NET cho đa nền tảng tên là Xamarin Studio.
– Microsoft mua lại Xamarin và mang các công nghệ từ Xamarin Studio tích hợp vào Visual Studio. Một trang sử mới mở ra khi các công cụ này được cung cấp miễn phí thay vì trả phí khá đắt đỏ, người người nhà nhà thi nhau viết app .NET cho smart phone, hơn 1 tỷ developer đã chuyển từ Java/C/C++/Swift sang C# (*).
– Nhận thấy .NET Framework đã trở nên cũ kỹ, đồng thời nhu cầu phát triển ứng dụng trên các nền tảng ngoài Windows (Linux, Mac, iOS, Android…) rất cao. Việc hỗ trợ đồng thời nhiều trình runtime tương thích .NET Framework khác nhau vừa tốn kém chi phí vừa bất tiện, Microsoft phát triển một nền tảng mới gọi là .NET Core.
– .NET Core là một bản .NET được thiết kế và xây dựng lại, với mục tiêu đa nền tảng ngay từ đầu.
– Có rất nhiều dự án vốn viết trên .NET Framework nay muốn chuyển sang .NET Core, tuy nhiên rất khó và cũng rất rủi ro khi chuyển toàn bộ từ nền tảng cũ sang nền tảng mới, vậy nên Microsoft đưa ra thêm một thứ gọi là .NET Standard.
– .NET Standard chỉ là các chuẩn tương thích, không phải là một trình runtime hay framework. Ví dụ .NET Framework 4.6.1+ và .NET Core 2.0+ tương thích với .NET Standard 2.0, do vậy bạn hoàn toàn có thể sử dụng các thư viện được viết ra cho .NET Standard 2.0 trong các chương trình viết cho Framework 4.6.1 lẫn .NET Core 2.0. Nhờ vậy các phần mềm cũ có thể được chuyển đổi dần mà không cần chuyển hoàn toàn sang .NET Core.
– Phiên bản chính cuối cùng của .NET Framework là 4, được phát hành vào 2010, phiên bản hiện tại là 4.8.1, đây cũng sẽ là bản .NET Framework cuối cùng. (**)
– .NET Core có các phiên bản 1, 2 và 3, sau bản 3.0 thì nhảy luôn lên 5. .NET Core không có phiên bản 4 vì sợ nhầm lẫn với .NET Framework 4, vốn là phiên bản phổ biến đến mức trước đây nhiều người hay gọi là .NET 4.0.
– Từ phiên bản 5.0, .NET Core cũng bỏ luôn chữ Core và ta chỉ còn gọi là .NET. Vì không có phiên bản .NET Framework nào >= 5 nên người ta không còn sợ nhầm lẫn nữa.
– Phiên bản chính thức hiện tại là .NET 7, phiên bản 8 sẽ ra mắt vào cuối năm nay. Theo kế hoạch thì cứ một năm sẽ có một phiên bản mới.
– Vì .NET Framework đã ngừng phát triển nên từ giờ trở đi khi nói về .NET, ta sẽ mặc nhiên là nói về .NET mới.
Vậy .NET làm được gì?
– .NET có thể được dùng để viết gần như tất cả các loại ứng dụng, ngoại trừ các ứng dụng hệ thống.
– Bạn có thể viết web, mobile app, ứng dụng desktop, chạy trên local, chạy trên cloud, viết game… Nói tóm lại là trừ khi viết firmware, OS hay driver… còn lại thì bạn đều có thể làm được với .NET.
Có gì lưu ý khi học .NET?
– .NET chỉ là một framework, bạn có thể lập trình bằng bất kỳ ngôn ngữ nào miễn sao nó có trình biên dịch tương thích. Tuy nhiên C# là ngôn ngữ được Microsoft hỗ trợ mạnh nhất và vẫn liên tục được cập nhật, vì vậy học viết app .NET bằng C# được coi là chuẩn mực.
– C# có cấu trúc giống với Java, vậy nên ai đã từng học C++/Java chuyển sang sẽ thấy rất quen thuộc (đó là lý do tại sao tôi luôn khuyên những người mới học nên bắt đầu với C++ – khổ trước sướng sau). Tuy nhiên để nắm chắc C# sẽ mất khá nhiều thời gian vì nó có nhiều thành phần hỗ trợ runtime (async, lock, LINQ…).
– Ứng dụng .NET có thể được dịch trên một nền tảng và chạy trên một nền tảng khác. Chương trình của bạn sẽ được dịch sang mã IL, sau đó sẽ được dịch tiếp một lần nữa sang mã máy khi chạy (JiT), các bản .NET mới còn cho phép dịch sẵn sang mã máy khi cài đặt ứng dụng (AoT), nhờ đó tốc độ khởi động ứng dụng sẽ nhanh hơn.
– Bộ nhớ trong .NET được quản lý tự động, bạn không cần giải phóng vùng nhớ đã cấp phát.
– Tôi khuyên dùng Visual Studio bản mới nhất nếu có thể, bạn sẽ mất một thời gian làm quen nhưng khi đã quen rồi thì bạn sẽ hiểu vì sao nó là IDE hàng đầu thế giới. Dù gì sẽ có lúc bạn phải làm quen với nó nếu theo .NET.
– .NET (Core) là mã nguồn mở, .NET Framework cũng mở mã nguồn nhưng chỉ cho mục đích tham khảo.
Có nhiều công ty tuyển dụng .NET không?
Bạn tự vào các trang tuyển dụng tìm hiểu đi! Ở đây không trả lời mấy câu này!
Model-View-Controller là một mẫu thiết kế cho phép ta phân tách các thành phần theo 3 chức năng:
Các thực thể (entity) chứa dữ liệu, hay còn gọi là Model.
Các View có nhiệm vụ hiển thị dữ liệu.
Các Controller có nhiệm vụ điều phối Model đến View.
Việc phân tách này giúp chúng ta tách biệt các thành phần với các chức năng hoàn toàn độc lập, và mang lại những lợi ích sau:
Dễ thay đổi: Một thay đổi nào đó trên giao diện chỉ ảnh hưởng đến View. Giả sử chương trình của bạn cần cung cấp các trang hiển thị thông tin sản phẩm khác nhau: cho người dùng web, cho người dùng trên các thiết bị màn hình nhỏ, cho máy in… thì chỉ cần tạo ra các view khác nhau, tất cả những gì trong Model hoặc code tải thông tin sản phẩm trong Controller đều không cần thay đổi.
Dễ test: bạn hoàn toàn có thể viết các unit test riêng biệt cho Controller (chứa business logic) hoặc View (để test giao diện), một tester cũng có thể test các view thông qua các Fake/Mock object mà không cần chờ Controller hoàn thiện.
Loạt bài này sẽ nói về MVC design pattern, cách chúng ta thiết kế các thành phần để hỗ trợ MVC trong Mini-Web-Server.
Mô hình MVC, lưu ý hướng các mũi tên, các bạn có thể sẽ nhìn thấy nhiều hình minh họa khác với số lượng/hướng mũi tên khác nhau. Tôi sẽ giải thích kỹ hơn về điều này khi chúng ta đi vào chi tiết.
Nếu theo dõi blog này và trang Facebook Nam.NET chắc mọi người không lạ gì project Mini-Web-Server (trang github), một web server đơn giản để thông qua đó giới thiệu về những công nghệ, mẫu thiết kế, kiến trúc phổ biến, giúp các bạn hiểu sâu hơn về những gì các bạn đang sử dụng hàng ngày. Hiện tại chúng ta đã có một mini server gọn nhẹ, chạy nhanh và hỗ trợ đầy đủ các method, hỗ trợ HTTPS, các nội dung tĩnh, cho phép viết các hàm phục vụ người dùng giúp phát triển các API, cung cấp đầy đủ session, authentication/authorization thông qua JWT và cookie…
Nhưng điều quan trọng nhất mà chúng ta đạt được (cũng như tôi muốn nhấn mạnh) khi viết Mini-Web-Server không phải là nó đã hỗ trợ những gì, mà lại là việc thêm vào những tính năng mới dễ dàng như thế nào. Thông qua một kiến trúc rõ ràng và nhiều design pattern, chúng ta có một ứng dụng với các module độc lập với nhau, gắn kết với nhau thông qua các policy. Khi thay thế module phân tích các request từ sử dụng Regular Expression sang việc phân tích trực tiếp mảng byte, chuyển từ mô hình sync sang async…, chúng ta hầu như không phải làm gì khác ngoài viết module mới và thay thế với module cũ, tất cả các phần khác đều giữ nguyên. Thông qua một loạt các cơ chế dựa trên “Dependency Inversion” như Callable, Callable filters, middleware… ta có thể mở rộng ra tới chừng nào ta muốn. Và đó là thứ quan trọng nhất mà tôi muốn mang đến cho các bạn thông qua project này.
Tất cả các ứng dụng trong thực tế đều sẽ thay đổi theo thời gian, lớn hơn, nhiều yêu cầu mới phát sinh, mô hình triển khai thay đổi và thậm chí các chức năng cũng thay đổi, do vậy việc quan trọng nhất đối với một người thiết kế đó là làm ra một ứng dụng có thể dễ dàng chỉnh sửa và bảo trì, hơn là làm cho nó chạy đúng. Điều này nghe có vẻ buồn cười nhưng thử nghĩ xem, nếu chỉ để một chương trình chạy đúng (vào một thời điểm cụ thể) thì bạn đâu cần phải thiết kế? Bạn không cần các interface, không cần đa hình, khi thêm một chức năng mới, bạn chỉ việc copy từ một chức năng tương tự đã có và sửa lại một chút… Bạn sẽ có những file mã nguồn, những function hàng ngàn dòng mà đến người viết còn không hiểu nó chạy thế nào, và khi có một yêu cầu mới, bạn chỉ mong ước có thể đập ra làm lại từ đầu. Một chương trình dễ bảo trì sẽ dễ dàng được làm cho chạy đúng, nhưng một chương trình chạy đúng với một thiết kế tồi lại không chắc sẽ chạy đúng mãi (sau khi thêm các chức năng mới hoặc thay đổi trong tương lai).
Quay lại với Mini-Web-Server, chúng ta đã hỗ trợ nhiều tính năng (như quảng cáo :D), nhưng những gì các bạn nhìn thấy chỉ là một trang web đơn giản, với vài tính năng demo. Thực sự mà nói, chừng đó cũng đã đủ để bạn viết một ứng dụng web, với nội dung tĩnh trong các trang HTML, và dữ liệu động được cung cấp bởi các API. Các tính năng cơ bản này cho phép bạn viết các ứng dụng SPA (single page app) dễ dàng, hoặc cung cấp backend API cho một mobile app. Nhưng nếu muốn viết một web app bình thường với nhiều trang thì sao? Có lẽ chúng ta cần thêm một cơ chế khác nữa.
Một lựa chọn phổ biến với người viết web .NET là ASP.NET MVC, một framework cực kỳ mạnh mẽ, hỗ trợ nhiều tính năng và cực kỳ mềm dẻo (flexible), cho phép chúng ta phát triển các ứng dụng với code .NET, render nội dung với Razor, các tham số được tiền xử lý và gửi đến các action, người lập trình hầu như chỉ cần tập trung vào việc viết code xử lý, các request/response sẽ được xử lý tự động thông qua các policy được ASP.NET hỗ trợ… Vì vậy nếu Mini-Web-Server cũng hỗ trợ một mô hình như vậy thì các nhà phát triển sẽ dễ dàng xây dựng các ứng dụng dựa trên “Mini MVC” thay vì ASP.NET MVC :D. Và có lẽ đây sẽ là tính năng lớn nhất mà Mini cung cấp, là một hành trình mất nhiều tháng để hoàn thành, và tôi muốn chia sẻ cùng các bạn hành trình này, từ các bước chúng ta thiết kế, cho đến việc implement, và thậm chí những sai sót khi thiết kế đã dẫn đến những vấn đề gì. A ha! Bạn nghĩ đúng rồi đó! Tôi vẫn thường xuyên sai sót khi thiết kế và cả khi code nữa!
Nhưng trước tiên chúng ta phải hiểu một cách chính xác MVC là gì và nó hoạt động như thế nào.