Trong bài học này chúng ta sẽ xem xét vấn đề trung tâm của lập trình mạng: socket.
Trong các bài học trước chúng ta đã xem xét chi tiết các vấn đề của ứng dụng mạng. Tất cả các vấn đề đó giúp chúng ta định hình vị trí của ứng dụng mạng trong toàn hệ thống.
Để tạo ra ứng dụng mạng chúng ta phải sử dụng đến socket giúp ứng dụng tương tác với dịch vụ mạng của hệ thống.
Khái niệm Socket
Lập trình với socket là nền tảng cho việc phát triển tất cả các loại phần mềm có sử dụng dịch vụ truyền thông mạng, cho phép chúng ta sử dụng các dịch vụ truyền thông mạng mà hệ thống cung cấp. Trong phần này chúng ta sẽ xem xét chi tiết khái niệm và vị trí của socket.
Như đã học trong các bài trước, các ứng dụng mạng đều bao gồm các cặp tiến trình và quá trình truyền thông giữa chúng. Bất kỳ thông điệp nào truyền đi từ một tiến trình tới tiến trình còn lại phải đi qua mạng. Dữ liệu từ tiến trình tới dịch vụ truyền thông phải đi qua một đối tượng trung gian gọi là socket.
Góc nhìn truyền thông
Khi nhìn nhận từ khía cạnh truyền thông (đường đi của thông tin), socket có thể được hình dung như là một cánh cửa ngăn cách giữa chương trình ứng dụng (thuộc toàn quyền của người lập trình) và ngăn xếp giao thức mạng (thuộc quyền quản lý của hệ điều hành).
Dữ liệu do chương trình tạo ra đi xuyên qua cánh cửa này để đến thế giới mạng, nơi nó sẽ được truyền tới nơi cần đến. Ở chiều ngược lại, dữ liệu từ mạng có thể đi xuyên qua cánh cửa này để tới được chương trình, nơi nó sẽ được xử lý.
Một khi dữ liệu đã đi xuyên qua cánh cửa này, người lập trình sẽ không kiểm soát được nữa mà hoàn toàn do các giao thức mạng nhào nặn để làm sao có thể truyền đi qua mạng một cách tốt nhất. Người lập trình chỉ có khả năng lựa chọn loại dịch vụ vận chuyển dữ liệu theo nhu cầu. Cụ thể hơn người lập trình chỉ có thể lựa chọn dịch vụ TCP hoặc dịch vụ UDP, đồng thời cung cấp tham số để các dịch vụ này phục vụ việc vận chuyển dữ liệu theo yêu cầu của mình.
Góc nhìn mô hình mạng
Nhìn từ ứng dụng, socket là giao diện giữa ứng dụng và dịch vụ tầng giao vận trên mỗi máy. Trong mô hình mạng TCP/IP, socket có thể xem như giao diện giữa tầng ứng dụng và tầng chuyển vận.
Người phát triển ứng dụng có toàn quyền kiểm soát phía tầng ứng dụng của socket nhưng không thể kiểm soát phía tầng giao vận của socket. Ở phía tầng giao vận, người phát triển ứng dụng chỉ có thể lựa chọn giao thức của tầng này và điều chỉnh một vài tham số (kích thước tối đa của bộ nhớ đệm, kích thước tối đa của các segment dữ liệu).
Trong hai cách nhìn nhận trên, socket đóng vai trò điểm đầu và điểm cuối của quá trình truyền thông mạng, cũng như phân tách giữa tiến trình (chương trình ứng dụng) và dịch vụ vận chuyển của mạng.
So sánh một cách hình tượng, mỗi tiến trình có thể so sánh với một ngôi nhà, còn socket chính là cửa của ngôi nhà đó, và môi trường mạng truyền dữ liệu tương tự như một dịch vụ chuyển phát nhanh đặt ở ngay cửa của ngôi nhà.
Góc nhìn lập trình
Đối với người lập trình ứng dụng, socket có thể được hình dung là một giao diện lập trình ứng dụng (API) để gọi tới các chương trình con của hệ điều hành.
Trong các bài trước chúng ta đã nhắc tới giao thức TCP hay giao thức UDP. Về bản chất, TCP hay UDP đều là những chương trình được tích hợp sẵn trong các hệ điều hành hiện đại.
Tương tự như các chương trình hệ thống khác, TCP hay UDP (và cả IP) cũng cung cấp API để người lập trình có thể sử dụng được các chương trình này.
Khi học lập trình socket, chúng ta sẽ nhìn nhận khái niệm socket chủ yếu theo góc nhìn này. Việc học lập trình socket khi đó thực chất là việc học làm việc với các API của hệ thống để gọi các dịch vụ truyền thông.
Socket API đầu tiên được xây dựng bởi đại học Berkeley cho hệ điều hành BSD nên thường được gọi là BSD socket hay Berkeley socket. Sau đó Microsoft tham khảo và tạo ra các socket API dành cho hệ điều hành windows, gọi là windows socket, hay thường gọi tắt là winsock.
Winsock là bộ API tiêu chuẩn để lập trình mạng trong windows. Cũng vì lý do này mà các hàm socket API trên các hệ điều hành hiện nay đều có tên gọi gần giống nhau. Nắm bắt được cách lập trình với socket API trên một nền tảng này có thể dễ dàng tiếp cận với các nền tảng khác.
Phân loại socket
Do có 2 loại dịch vụ hỗ trợ trao đổi dữ liệu qua mạng như đã nói, socket cũng được chia làm 2 loại chủ yếu:
- Socket để sử dụng dịch vụ TCP, gọi là TCP socket:
Bởi vì TCP cung cấp dịch vụ truyền dữ liệu theo một liên kết ảo giữa hai tiến trình, TCP socket còn được gọi là socket hướng kết nối (connection-oriented socket). Do dữ liệu truyền theo liên kết TCP được hình dung như một chuỗi byte liên tục, loại socket này còn có một tên gọi khác là socket hướng dòng (stream socket). - Socket để sử dụng dịch vụ UDP, gọi là UDP socket:
Do UDP không tạo liên kết mà truyền dữ liệu theo các gói (datagram) độc lập, UDP socket được còn được gọi là socket phi liên kết (connectionless socket) hay dgram socket.
Đối với người phát triển ứng dụng hệ thống còn một loại socket nữa, gọi là socket thô (raw socket). Loại socket này cho phép gọi thẳng đến chương trình IP mà bỏ qua TCP hoặc UDP.
Bình thường, dữ liệu sau khi đến chương trình IP sẽ được chuyển tiếp lên chương trình TCP hay UDP, sau đó mới được chuyển tới ứng dụng. Khi sử dụng socket thô, ta có thể bỏ qua TCP/UDP mà trực tiếp nhận gói tin “thô” từ IP. Loại socket này giúp chúng ta phát triển một số ứng dụng đặc biệt như ping, trace route.
Lập trình socket
Các hệ điều hành đầu tiên của Microsoft (như MS-DOS và các phiên bản đầu của Microsoft Windows) có cung cấp khả năng kết nối mạng nhưng rất hạn chế, chủ yếu dựa trên NetBIOS. Trên thực tế, tại thời điểm đó, Microsoft không cung cấp hỗ trợ cho ngăn xếp giao thức TCP / IP.
Một số nhóm và nhà cung cấp thương mại (như nhóm PC / IP tại MIT, FTP Software, Sun Microsystems, Ungermann-Bass và Excelan) đã giới thiệu các sản phẩm TCP / IP cho MS-DOS, ở dạng một bộ phận của gói phần cứng / phần mềm . Khi Windows 2.0 được phát hành, một số nhóm khác (như Distinct và NetManage) cũng tham gia cung cấp TCP / IP cho Windows.
Một vấn đề rất lớn gặp phải là mỗi nhà cung cấp lại sử dụng bộ API của riêng mình mà không có một mô hình lập trình tiêu chuẩn thống nhất. Đến năm 1991, Martin Hall của JSB Software (sau này đổi tên thành Stardust Technologies) đề xuất Windows Sockets API và trở thành tiêu chuẩn cho lập trình ứng dụng mạng trong Windows.
Winsock
Windows Sockets API (WSA), sau rút gọn thành Winsock, là một đặc tả kỹ thuật xác định cách phần mềm mạng trong Windows truy cập các dịch vụ mạng, đặc biệt là TCP / IP. Winsock định nghĩa giao diện chuẩn giữa ứng dụng (ví dụ chương trình FTP client hoặc trình duyệt web) và ngăn xếp giao thức TCP / IP bên dưới. Winsock API được đặt trong file winsock.dll (16 bit) hoặc wsock32.dll (32 bit) trong thư mục hệ thống.
Windows Sockets định nghĩa hai giao diện:
- API cho các nhà phát triển phần mềm ứng dụng, và SPI cho các nhà phát triển phần mềm hệ thống (để thêm các giao thức mới vào hệ thống). API đảm bảo cho ứng dụng có thể hoạt động với cài đặt của giao thức (protocol implementation) từ bất kỳ nhà cung cấp phần mềm mạng nào.
- SPI đảm bảo cho một cài đặt của giao thức phù hợp có thể được thêm vào hệ thống Windows, và nếu một ứng dụng tuân thủ API thì có thể sử dụng được giao thức mới. Tuy nhiên, hiện nay SPI rất hiếm được sử dụng do trong tất cả các phiên bản Windows gần đây, Microsoft đã hỗ trợ đầy đủ ngăn xếp giao thức TCP / IP và không có nhiều người quan tâm xây dựng các giao thức khác với TCP / IP.
Code và thiết kế của Windows Socket dựa trên tham khảo BSD Socket và cung cấp chức năng bổ sung để API tuân thủ theo mô hình lập trình Windows. Windows Sockets API bao gồm gần như tất cả các tính năng của BSD Socket API. Do đó, khi nắm được kỹ thuật lập trình với một bộ socket API có thể dễ dàng tiếp cận với các bộ socket API khác.
Ngoài ra, nhiều công cụ phát triển phần mềm ứng dụng hiện đại cũng tạo ra các “vỏ bọc” (wrapper) riêng xung quanh bộ socket API để đơn giản hóa và nâng cao hiệu quả trong phát triển các phần mềm ứng dụng mạng.
Hạn chế của lập trình winsock
Như trên vừa trình bày, winsock là bộ API tiêu chuẩn để lập trình mạng trong windows. Tuy nhiên, việc lập trình ứng dụng với winsock có những khó khăn nhất định.
Thứ nhất, các API hệ thống thường rất phức tạp với rất nhiều tham số gây khó khăn cho việc lập trình. Để đảm bảo tính linh hoạt, mỗi API đều chứa rất nhiều tham số, sử dụng nhiều kiểu dữ liệu hỗ trợ, cũng như có rất nhiều loại “magic constant”. Lập trình với socket API cũng không ngoại lệ.
Thư hai, việc gọi đến các API của hệ thống thường chỉ phù hợp khi lập trình với một số ngôn ngữ và công nghệ nhất định. Ví dụ khi sử dụng C/C++/Delphi xây dựng ứng dụng native cho windows sẽ dễ dàng truy cập các API này hơn. Tuy nhiên, sử dụng các ngôn ngữ và công cụ bậc “không cao” như C/C++ làm tăng thời gian phát triển ứng dụng (giảm năng suất).
Các công nghệ phát triển ứng dụng hiện đại thường hạn chế việc truy xuất trực tiếp đến các API của hệ thống. Thay vào đó, các công nghệ này thường tạo ra các “vỏ bọc” (wrapper) để giúp người lập trình gọi đến các API của hệ thống một cách dễ dàng hơn.
Ví dụ, trong công nghệ windows form của .Net framework, thay vì để người dùng trực tiếp gọi tới các API để vẽ ra giao diện đồ họa, .NET tạo ra các wrapper xung quanh các API này, .NET sau đó sẽ giúp người dùng gọi các API tương ứng để vẽ ra giao diện đồ họa.
Lập trình socket trong .NET framework
Đối với socket API, .NET framework cũng tạo ra các lớp wrapper để giúp người lập trình gọi các hàm của TCP hay UDP mà không cần tiếp xúc trực tiếp với socket API. Qua đó giúp người lập trình tiếp tục sử dụng mô hình lập trình mạnh, đơn giản, hiệu quả của .NET framework trong việc lập trình truyền thông.
.NET framework cũng có những hỗ trợ khác (không riêng) cho lập trình mạng, bao gồm: giao diện luồng dữ liệu (stream), trình tự hóa dữ liệu (serialization), biến đổi dữ liệu (data conversion), lập trình bất đồng bộ (asynchronous programming), lập trình đa luồng (multi-threading programming), tạo bộ đệm (caching), bảo mật (socket security, crypto-stream).
Các hỗ trợ này đóng vai trò đặc biệt quan trọng khi xây dựng thành phần server và cài đặt giao thức. Tất cả các vấn đề này sẽ lần lượt được trình bày chi tiết trong các bài học tương ứng của tài liệu.
Trong khuôn khổ bài giảng này, chúng ta sẽ chỉ nghiên cứu cách lập trình socket trên .NET framework với ngôn ngữ C#. Các nguyên tắc cơ bản của lập trình socket là tương tự nhau mặc dù sử dụng các công cụ khác nhau. Nắm bắt được cách lập trình socket trên .NET có thể hoàn toàn dễ dàng tiếp cập lập trình socket, ví dụ, trên Java, hay Python, Rubi, v.v..
Lưu ý rằng đây là một tài liệu chuyên về lập trình mạng với .NET framework, không phải là một tài liệu về lập trình C#. Chúng ta sẽ không đề cập đến các vấn đề cơ bản của lập trình C# ở đây. Để có thể theo dõi các ví dụ của bài giảng, các bạn cần nắm vững ngôn ngữ lập trình C#, kỹ thuật lập trình hướng đối tượng trong C#, cách lập trình với các thư viện của .NET framework, cũng như một số kỹ thuật lập trình .NET nâng cao.
Một số vấn đề trong lập trình mạng
Khi nào cần lập trình socket?
Mặc dù lập trình socket là nền tảng trong phát triển ứng dụng mạng, sử dụng nó cũng có những nhược điểm nhất định.
Khi sử dụng socket, bạn đang làm việc với dịch vụ Tcp hoặc Udp của hệ thống để gửi và nhận dữ liệu. Để đạt được hiệu quả, bạn bắt buộc phải xây dựng một giao thức riêng của mình (được tính là giao thức thuộc tầng ứng dụng). Tạo ra các giao thức tầng ứng dụng không phải là một công việc đơn giản, đặc biệt nếu ứng dụng của bạn có những yêu cầu cao về mặt truyền thông (ví dụ, đối với game online).
Khi lập trình socket, bạn được (và phải) kiểm soát hoàn toàn quy trình truyền thông và dữ liệu. Đây là một con dao hai lưỡi với người lập trình, nhất là khi bạn không có nhiều kiến thức về mạng máy tính và truyền thông.
Lập trình socket được sử dụng nếu ứng dụng của bạn có những yêu cầu đặc biệt về truyền thông mà không có các giao thức hoặc công cụ cấp cao hỗ trợ.
Khi bạn cần lập trình phần mềm để nhận dữ liệu từ các thiết bị cảm biến, hoặc lập trình để điều khiển thiết bị, v.v., đây là lúc phải dùng đến lập trình socket. Khi bạn cần xây dựng một ứng dụng mạng đặc biệt của riêng mình, ví dụ, một chương trình chat đơn giản hoạt động trong LAN, lập trình socket là một công cụ tốt.
Các công nghệ hỗ trợ phát triển ứng dụng mạng
Để đơn giản hóa việc phát triển ứng dụng mạng, nhiều công nghệ lập trình hỗ trợ làm việc với các giao thức tầng ứng dụng phổ biến như HTTP, POP, SMTP, IMAP, FTP. Các công cụ cấp cao này hoàn toàn ẩn đi các chi tiết liên quan tới lập trình mạng cấp thấp với socket. Các bạn không cần phải biết tới các quy tắc làm việc với socket, mặc dù socket là người chống lưng cho các công nghệ này.
Trên .NET framework hiện có một số công nghệ hỗ trợ phát triển ứng dụng mạng: WCF (Windows Communication Foundation), ASP.NET Web API, ASP.NET MVC.
Ví dụ, nếu cần phát triển ứng dụng quản lý theo mô hình hướng dịch vụ (SOA – service oriented application) có thể sử dụng công nghệ WCF hay ASP.NET Web API.
Khi sử dụng các công cụ này, bạn được cung cấp sẵn các công cụ để thực hiện việc truyền dữ liệu giữa client và server qua giao thức HTTP. Bạn sẽ không cần quan tâm đến cách thức truyền dữ liệu (cũng không cần quan tâm đến HTTP) nữa mà chỉ cần quan tâm đến bản thân dữ liệu đó thôi.
Hầu hết các công nghệ/công cụ hỗ trợ phát triển ứng dụng cấp cao đều hỗ trợ đầy đủ việc truyền dữ liệu giữa client và server qua HTTP. Trên thực tế HTTP là một sự lựa chọn đặc biệt phổ biến hiện nay để phát triển các ứng dụng mạng, nhất là các ứng dụng quản lý, đồng thời nó cũng được các công nghệ phát triển ứng dụng hỗ trợ rất tốt.
HTTP không phải là giải pháp tốt cho nhiều loại ứng dụng. Ví dụ, game client và server không thể sử dụng HTTP để trao đổi dữ liệu, vì HTTP rất chậm và không duy trì trạng thái qua mỗi phiên truy vấn/phản hồi.
Theo dõi gói tin
Các ứng dụng mạng mặc dù cho phép chúng ta nhìn thấy kết quả thực trên giao diện nhưng chúng ta không thực sự nhìn thấy những gì diễn ra trên mạng trong quá trình các ứng dụng trao đổi thông tin. Trong quá trình phát triển ứng dụng, ngay cả khi đặt đánh dấu dừng chương trình để xem stack, chúng ta cũng không thể nhìn thấy được dữ liệu đã đóng gói và truyền đi trên mạng như thế nào.
Việc không nhìn thấy được dữ liệu “chạy” trên mạng gây ra rất nhiều khó khăn cho việc debug ứng dụng hoặc khi thiết kế giao thức truyền thông mới. Việc này cũng gây khó khăn cho việc hiểu được thực chất của quá trình truyền thông mạng.
Có một số công cụ giúp chúng ta bắt và phân tích các gói tin chạy trên mạng nhằm phục vụ cho quá trình phát triển ứng dụng mạng.
Nổi tiếng và được sử dụng rộng rãi nhất là WireShark. Đây được đánh giá là công cụ mạnh mẽ hàng đầu trong lĩnh vực này. Bạn có thể tải về để cài đặt hoặc đọc thêm tài liệu về WireShark.
Một công cụ khác do Microsoft phát triển có tên gọi Microsoft Message Analyzer (MSA) cũng có chức năng tương tự WireShark. Ứng dụng này ra đời để thay thế cho Microsoft Network Monitoring đã được sử dụng từ lâu bởi các quản trị viên Windows Server. MSA đặc biệt phù hợp với môi trường windows và sẽ được sử dụng chủ yếu trong loạt bài này. Có thể tải MSA ở đây.