Hỏi đáp về Mini-Web-Server

Q: Mini-Web-Server là gì?

A: Mini-Web-Server (gọi tắt là Mini) là một web server mã nguồn mở viết trên C#, .NET 7, hỗ trợ HTTP/1.1, HTTPS, Multi-Host…

Q: Mini có hỗ trợ nội dung động không?

A: Có, vì không có nội dung động thì sẽ không cần dùng đến các method như POST, PUT… Tuy nhiên hiện tại tôi chưa quyết định cụ thể sẽ hỗ trợ như thế nào, ban đầu dự định hỗ trợ thông qua CGI (https://www.hostgator.com/help/article/what-is-cgi-bin), thông qua đó sẽ dễ dàng tích hợp các ngôn ngữ script như JS hay PHP, nhưng CGI đã quá cũ và tiềm ẩn rất nhiều rủi ro về bảo mật nên có thể tôi sẽ tự thiết kế một engine riêng. (Ở đây các bạn có thể thấy sức mạnh của abstraction, khi tôi có thể thiết kế web server trước khi implement phần cung cấp nội dung).

Q: Mini được viết ra để làm gì?

A: Mini được viết ra như một ứng dụng minh họa cho những khái niệm/công nghệ mà tôi đã/đang/định/sẽ nói đến. Do vậy một trong nhưng tiêu chí khi thiết kế và implement là code phải dễ đọc dễ hiểu nhất.

Tôi không thích lắm khi nói về OOP cứ phải lấy ví dụ về Animal với Dog với Cat, hoặc là mấy cái hình vuông với hình chữ nhật, nghe thì dễ hiểu nhưng người học chẳng biết dùng vào việc gì cả.

Các bạn sẽ được nhìn thấy các phần đã học được áp dụng trong thực tế thế nào. Có những khái niệm khá mơ hồ và khó hiểu (ai học OOP thì chắc cũng biết), nhưng vì máy tính là khoa học, không phải nghệ thuật, nên dù có khó thì cũng chỉ có một cách hiểu duy nhất.

Thông qua việc viết ra một ứng dụng hoàn chỉnh, chúng ta sẽ hiểu rõ các khái niệm này một cách chính xác, bằng cách so sánh các cách áp dụng khác nhau (vì hiểu theo những cách khác nhau) sẽ dẫn đến những kết quả nào.

Q: Tại sao không phải là ứng dụng khác mà lại là một Web server?

A: Một web server hoàn chỉnh dù nhỏ cũng phải chứa khá nhiều tính năng khác nhau, và phải sử dụng nhiều kỹ thuật, thiết kế, kiến trúc. Ngoài ra bạn sẽ phải đi sâu vào giao thức HTTP, nền tảng cho mọi thứ trên web, kể cả các web app hay API. Do vậy theo tôi nó là hoàn hảo để trình bày các kỹ thuật khác nhau khi viết chương trình.

Q: Mini có thể thay thế các web server phổ biến hiện tại không?

A: Về mặt chức năng: Có. Nó sẽ có khả năng phục vụ để tạo ra các website, API, (và

có thể cả websocket).

Về hiệu quả sử dụng: Không (hoặc chí ít là chưa). Bạn không nên mạo hiểm đưa mini vào sử dụng production 😅. Một dự án nho nhỏ làm trong lúc rảnh rỗi thì không thể so được với dự án được hàng trăm chuyên gia làm trong nhiều (chục) năm.

Tuy nhiên, sử dụng Mini làm backend server và một web server khác (nginx chẳng hạn) làm reverse proxy cũng có thể là một ý hay 🙂.

Q: Tại sao không sử dụng các web server mã nguồn mở để học?

A: Hầu hết các web server phổ biến nhất (trừ IIS) đều là mã nguồn mở. Bạn hoàn toàn có thể đọc code để xem họ đã làm như thế nào. Tuy nhiên bạn chỉ nên làm điều này khi muốn tham gia vào dự án, bởi sau nhiều năm phát triển chúng đã quá phức tạp để hiểu, đặc biệt với người chưa có kinh nghiệm.

Minix – hệ điều hành còn già hơn Linux (Minix chính là cảm hứng để Linus Torvalds viết lên Linux) vẫn còn được dùng để giảng dạy môn Hệ điều hành trong nhiều trường đại học (https://wiki.minix3.org/doku.php?id=courses) bởi sự đơn giản của nó (Linux hiện đã có hơn 30 triệu dòng code).

Q: Mini có hỗ trợ HTTP/2, HTTP/3 không?

A: Có (nhưng khi nào thì chưa biết 😅). HTTP/2 và HTTP/3 chủ yếu khác HTTP/1.1 ở phương thức truyền dữ liệu giúp tăng cường hiệu năng, cấu trúc các request và response và các tính năng bên dưới hầu như không có thay đổi. Do vậy việc implement hai phiên bản này không phải là một yêu cầu quan trọng với một máy chủ web “hiệu năng thấp” như Mini.

Q: Tính năng Multi-Host là gì?

A: Multi-Host cho phép bạn chạy nhiều website trên cùng server, tùy thuộc vào domain name.

Q: Làm sao tôi hiểu được Mini được thiết kế và viết như thế nào?

A: Tôi sẽ có bài viết/video hướng dẫn, hiện nay chưa làm là vì LƯỜI!

Q: Nếu một lúc nào đó ứng dụng được hoàn thành, vậy khi đó nó lại trở nên quá phức tạp để học?

A: Khi đã implement các tính năng ở một mức độ hoàn chỉnh cho phép, tôi sẽ tạo các tag để lưu lại source code ở thời điểm đó (https://github.com/daohainam/mini-web-server/tags). Các bài hướng dẫn sẽ dựa trên các tag này, tránh việc khi source code thay đổi thì nội dung bài viết trở nên không chính xác. Khi nói về vấn đề nào, tôi sẽ cố gắng sử dụng tag đầu tiên mà tính năng đó xuất hiện, giúp chúng ta dễ dàng theo dõi nhất.

Q: Viết ra Mini có khó không?

A: Khó. Đặc biệt là phần xử lý giao thức HTTP. Dù tôi đã làm với HTTP từ rất lâu (có thể từ trước khi một số độc giả ở đây ra đời 😉) nhưng tôi vẫn thấy khó, nguyên nhân là vì phải đọc và implement TOÀN BỘ giao thức, theo đúng các RFC của nó (https://datatracker.ietf.org/doc/html/rfc9112), không được bỏ sót kẽ hở nào (công nhận ông này cũng rảnh thật). Ngoài ra việc xử lý các dữ liệu đó trên nhiều stream khác nhau một cách không đồng bộ cũng gây ra khó khăn, bởi sẽ xảy ra vô số trường hợp cần xử lý.

Q: Để hiểu thì có cần biết kiến thức gì trước không?

A: Tất nhiên là có. Bạn sẽ cần biết về lập trình, tốt nhất là với C#, nhưng nếu chỉ biết một ngôn ngữ khác tương tự cũng không sao (Java, C++), nếu chỉ biết JS thì hơi khó nhưng nếu đã viết app NodeJS thì chắc cũng được.

Bạn sẽ cần biết về networking (TCP/IP, socket), OOP, multi-thread programming, các cấu trúc dữ liệu thông dụng… Những thứ này các bạn có thể đọc các bài viết trên trang https://www.daohainam.com hoặc hỏi chị Google. Khi đi vào phần code, sẽ có nhiều pattern (Mini được viết để minh họa cho các pattern này), sẽ dễ hiểu hơn nếu các bạn đã từng học về chúng.

Ngoại trừ phần ngôn ngữ lập trình, các phần còn lại sẽ có bài viết/video giải thích. Hiện nay vì sao chưa có chắc các bạn cũng biết rồi đấy! Vì LƯỜI! 😁

Giới thiệu project mới

Trong bài viết “NHỮNG MÔN HỌC CẦN THIẾT NẾU BẠN MUỐN TRỞ THÀNH SENIOR DEVELOPER“, tôi đã nói về những môn học giúp bạn có kiến thức vững chắc, và tự tin tham gia vào bất kỳ dự án nào. Tuy nhiên sau khi đọc xong, có nhiều bạn nhắn với tôi là “anh ơi, em biết là phải học những môn đó, nhưng em lại không biết học như thế nào”. Thật sự điều này cũng tương đối khó, bởi có quá nhiều thứ để học, bạn không biết được mình phải bắt đầu từ đâu, lộ trình thế nào, làm sao đạt được hiệu quả tốt nhất, thời gian ngắn nhất.

Một trong những cách học hiệu quả nhất là bắt tay vào làm một dự án thực tế (tôi cũng có nói về điều này trong bài Một số lời khuyên cho người học IT), nhưng làm cái gì lại là một câu hỏi khác. Một trong những sai lầm của các bạn khi chọn đề tài là các bạn chọn một dự án mà bạn đã biết cách làm, điều này làm bạn mất thời gian nhưng không đạt hiệu quả. Ví dụ khi chọn đề tài là viết một hệ thống quản lý điểm sinh viên, bạn sẽ nhận ra rằng có rất nhiều thao tác trong đó chỉ là CRUD (tạo/xem/xóa/sửa) dữ liệu trong database, và thao tác bạn làm nhiều nhất đôi khi lại là cắt/dán.

Sau một hồi suy nghĩ về điều này, tôi đã chọn ra một đề tài có thể giúp các bạn vừa học OOP, design pattern (Abstract Factory, DI, IoC, Decorator, caching patterns, và nhiều nhiều nữa…), vừa liên quan đến networking (TCP/IP, HTTP), lại có cả lập trình multithread.

Đó là viết ra hẳn một Web Server.

Continue reading “Giới thiệu project mới”

Những dấu hiệu cho thấy code của bạn có vấn đề (2) – Data Clump

Nếu có nhiều dữ liệu liên quan với nhau và thường phải làm việc cùng nhau, hãy gom chung vào một lớp thay vì lưu trong nhiều biến riêng biệt.

Một ví dụ là các tham số cho một cơ sở dữ liệu, bao gồm: địa chỉ server, user name, password, port. Mỗi khi kết nối, bạn đều phải cần cả 4 tham số này, do vậy thay vì lưu chúng trong 4 biến khác nhau, hãy gom chúng vào chung một lớp. Nếu có một function dùng các tham số này, ví dụ:

bool Connect(string serverAddress, int port, string user, string password);

Hãy sử dụng lớp bạn vừa tạo:

bool Connect(DatabaseConnectionInfo connectionInfo);

Sau khi refactor, code của bạn sẽ được tổ chức gọn gàng, dễ hiểu hơn. Tuy nhiên là khi tạo một lớp mới, cần lưu ý không để tăng sự phụ thuộc giữa các lớp hoặc thư viện.

Những dấu hiệu cho thấy code của bạn có vấn đề – Bài 1

Với một coder, viết code nhanh, đúng, chưa đủ mà bạn còn phải viết đẹp và dễ đọc nữa, vì nó sẽ giúp code dễ bảo trì hơn. Trong công việc hàng ngày, đôi khi việc tìm và sửa lỗi còn mất thời gian hơn viết mới nữa, vì bạn có thể sẽ phải đọc lại những đoạn code do người khác viết, hoặc thậm chí do chính bạn viết, từ trước đó rất lâu (thậm chí có thể trước khi bạn biết dùng máy tính :D).

Hãy thử đọc lại một chương trình bạn viết khi mới vào nghề xem, nếu nó cũ hơn 3 năm, tôi tin chắc 99% bạn sẽ thấy nó rất … buồn cười :D.

Lời khuyên đầu tiên của tôi là: Ngay từ ngày đầu tiên, hãy học cách viết code rõ ràng, sáng sủa, kể cả khi bạn biết chắc sẽ không ai đọc nó. Hãy biến nó thành thói quen và phải cảm thấy khó chịu khi nhìn vào một một đoạn “dirty code”.

Trong bài này, tôi sẽ nói qua những dấu hiệu cần tránh, giúp chương trình của bạn sẽ đọc, dễ bảo trì hơn. Có rất nhiều dấu hiệu như vậy, do đó tôi sẽ chia chúng ra nhiều bài ngắn giúp bạn dễ theo dõi.

Continue reading “Những dấu hiệu cho thấy code của bạn có vấn đề – Bài 1”

Con trỏ gần, con trỏ xa

Sau khi đọc bài: https://daohainam.com/2021/08/13/cau-chuyen-ve-con-tro-pointer, bạn đã biết biến con trỏ là một số nguyên chứa địa chỉ của một vùng nhớ. Tuy nhiên đôi khi bạn còn nghe về khái niệm con trỏ gần và con trỏ xa (near pointer và far pointer), vậy chúng là gì?

Với các CPU đời cũ thời 16bit (đọc tiếp: 64 bit? 32 bit?), để định vị được đến tất cả các địa chỉ trong bộ nhớ, CPU dùng cơ chế segment:offset. Như đã đọc trong bài con trỏ, nếu thanh ghi địa chỉ có kích thước 16 bit, nó chỉ có thể chứa một địa chỉ từ 0-65535 (64KB), một biến con trỏ khi đó cũng chỉ chứa được một địa chỉ trong phạm vi tương tự.

Tuy nhiên, các CPU này lại có tới 20 đường địa chỉ, tức là giữa CPU và MCU (Memory Control Unit) có tới 20 “sợi dây điện” (gọi vậy cho dễ hình dung :D, đặt tên là A0-A19), do vậy CPU có thể gửi 1 con số lớn tới 20 bit đến MCU mỗi khi nó cần đọc/ghi 1 giá trị trong bộ nhớ. Vì một thanh ghi chỉ có kích thước 16 bit, do vậy nếu muốn lưu lại một địa chỉ, CPU phải kết hợp 2 thanh ghi lại với nhau. Người ta chia bộ nhớ thành từng phân đoạn (segment), mỗi segment sẽ bắt đầu tại một địa chỉ cách nhau 16 byte (1). Như vậy, segment 0 bắt đầu từ địa chỉ 0, segment 1 bắt đầu từ địa chỉ 16, segment 2 bắt đầu từ 32… và cứ như vậy.

Continue reading “Con trỏ gần, con trỏ xa”

Bài 12: Tổng kết

Chúng tôi có một số phiên bản trong kỷ nguyên .NET hiện đại, gần đây đã phát hành .NET 7. Chúng tôi nghĩ rằng sẽ hữu ích nếu chúng tôi tóm tắt những gì đang cố gắng xây dựng — ở mức thấp nhất của nền tảng — kể từ .NET Core 1.0 . Mặc dù rõ ràng chúng tôi đã giữ nguyên tinh thần của .NET gốc, nhưng kết quả là một nền tảng mới tạo ra một con đường mới và mang lại nhiều giá trị mới hơn đáng kể cho các nhà phát triển.

Hãy kết thúc nơi chúng ta bắt đầu. .NET là đại diện cho bốn giá trị: Năng suất, Hiệu suất, Bảo mật và Độ tin cậy. Chúng tôi rất tin tưởng rằng các nhà phát triển sẽ được phục vụ tốt nhất khi các nền tảng ngôn ngữ khác nhau cung cấp các cách tiếp cận khác nhau. Với tư cách là một nhóm, chúng tôi tìm cách mang lại năng suất cao nhất cho các nhà phát triển .NET đồng thời cung cấp một nền tảng dẫn đầu về hiệu suất, bảo mật và độ tin cậy.

Chúng tôi dự định thêm nhiều bài viết trong loạt bài này. Những chủ đề nào bạn muốn xem giải quyết đầu tiên? Hãy cho chúng tôi biết trong các ý kiến. Bạn có muốn xem thêm nội dung về “bức tranh lớn” này không?

Nếu muốn biết thêm nội dung này, bạn có thể xem phần Giới thiệu về Common Language Runtime (CLR).

Bài đăng này được viết bởi Jan Kotas, Rich Lander, Maoni Stephens và Stephen Toub, với thông tin chi tiết và đánh giá của các đồng nghiệp trong nhóm .NET.

(Kết thúc bài viết)

Bài viết gốc tại địa chỉ: https://devblogs.microsoft.com/dotnet/why-dotnet/

Bài 11: Các bản phân phối

Nhóm .NET tại Microsoft duy trì một số bản phân phối, gần đây nhất đã hỗ trợ Android, iOS, và WebAssembly. Nhóm này dùng một số kỹ thuật khác nhau để duy trì hỗ trợ cho các yêu cầu riêng biệt của từng môi trường. Hầu hết các nền tảng được viết bằng C#, cho phép khi chuyển đổi chỉ cần làm việc trên một tập tương đối nhỏ các thành phần.

Cộng đồng cũng duy trì một tập các bản phân phối khác, chủ yếu tập trung cho Linux. Ví dụ, .NET được bao gồm trong Alpine LinuxFedoraRed Hat Enterprise Linux, và Ubuntu.

Cộng đồng cũng đã mở rộng .NET để chạy trên các nền tảng khác. Samsung đã chuyển .NET lên nền tảng Tizen cho ArmRed Hat và IBM cũng chuyển .NET lên LinuxONE/s390xLoongson Technology chuyển .NET lên LoongArch. Chúng tôi hi vọng và mong muốn sẽ có thêm nhiều đối tác nữa chuyển .NET lên các môi trường khác.

Continue reading “Bài 11: Các bản phân phối”

Bài 10: Interop

.NET đã được thiết kế rõ ràng để tương tác với các thư viện native với chi phí thấp. Các chương trình và thư viện .NET có thể gọi các API hệ điều hành cấp thấp một cách liền mạch hoặc khai thác hệ sinh thái rộng lớn của các thư viện C/C++. Thời gian chạy .NET hiện đại tập trung vào việc cung cấp các khối xây dựng tương tác cấp thấp, chẳng hạn như khả năng gọi các native method thông qua các con trỏ hàm, xuất các phương thức .NET dưới dạng unmanaged callbacks hoặc customized interface casting. .NET cũng liên tục phát triển trong lĩnh vực này và trong .NET 7 đã phát hành các giải pháp tạo mã nguồn giúp giảm thêm chi phí hoạt động và thân thiện với AOT.

Đoạn mã sau minh họa hiệu quả của các con trỏ hàm C#.

// Using a function pointer avoids a delegate allocation.
// Equivalent to `void (*fptr)(int) = &Callback;` in C
delegate* unmanaged<int, void> fptr = &Callback;
RegisterCallback(fptr);

[UnmanagedCallersOnly]
static void Callback(int a) => Console.WriteLine($"Callback:  {a}");

[LibraryImport("...", EntryPoint = "RegisterCallback")]
static partial void RegisterCallback(delegate* unmanaged<int, void> fptr);

Ví dụ này sử dụng trình tạo mã nguồn LibraryImport được giới thiệu trong .NET 7. Nó dựa trên DllImport hoặc P/Invoke hiện có.

Continue reading “Bài 10: Interop”

Bài 9: Khả năng sinh code

Mã bytecode .NET không phải là định dạng có thể thực thi trực tiếp bởi máy tính, mà nó cần phải được xử lý bằng một số dạng trình tạo code. Điều này có thể đạt được bằng cách biên dịch sẵn(AOT), biên dịch, phiên dịch hoặc biên dịch ngay lúc chạy (JIT). Trên thực tế, hiện nay tất cả cách cách này đều được sử dụng trong các tình huống khác nhau.

.NET được biết đến nhiều nhất với trình biên dịch JIT. Các JIT biên dịch các phương thức (và các thành phần khác) thành native code trong khi ứng dụng đang chạy và chỉ khi chúng cần thiết, do đó có tên “just in time” (đúng lúc). Ví dụ: một chương trình có thể chỉ gọi một trong số các phương thức trên một kiểu khi chạy. Một JIT cũng có thể tận dụng thông tin chỉ có sẵn trong thời gian chạy, như giá trị của các biến tĩnh chỉ đọc đã được khởi tạo hoặc mô hình CPU chính xác mà chương trình đang chạy và có thể biên dịch cùng một phương thức nhiều lần để tối ưu hóa mỗi lần cho các mục đích khác nhau với khả năng tối ưu code dựa trên các bài học từ các lần biên dịch trước đó.

Continue reading “Bài 9: Khả năng sinh code”

Bài 8: Định dạng được biên dịch của mã nhị phân

Các ứng dụng và thư viện được biên dịch thành mã bytecode đa nền tảng được tiêu chuẩn hóa ở định dạng PE/COFF. Bản phân phối ở dạng nhị phân là một tính năng được thiết cho hiệu suất. Nó cho phép các ứng dụng mở rộng quy mô cho số lượng dự án ngày càng lớn hơn. Mỗi thư viện bao gồm một tập dữ liệu gồm danh sách các kiểu được import và export, còn được gọi là metadata, đóng vai trò quan trọng cho cả hoạt động phát triển và chạy ứng dụng.

Các bản dữ liệu nhị phân được biên dịch bao gồm hai phần chính:

  • Mã bytecode — định dạng ngắn gọn và thông thường cho phép bỏ qua nhu cầu phân tích cú pháp từ mã nguồn dạng văn bản sau khi biên dịch bởi một trình biên dịch ngôn ngữ cấp cao (như C#).
  • Metadata — mô tả các kiểu được import và export, bao gồm vị trí của mã bytecode cho một phương thức nhất định.
Continue reading “Bài 8: Định dạng được biên dịch của mã nhị phân”