Ổ cắm Windows là gì và cách xem chúng Giải quyết vấn đề trạng thái dài hạn trong môi trường đa luồng. Thiết lập kết nối máy khách với máy chủ

Ổ cắm WinSock hoặc Windows là một giao diện lập trình ứng dụng ( API ), được tạo để triển khai các ứng dụng dựa trên giao thức trên mạng TCP/IP . Dùng cho công việc WSOCK 32.DLL . Thư viện này nằm trong thư mục \ Hệ thống 32 danh mục hệ thống Các cửa sổ.

Có hai phiên bản WinSock:

WinSock 1.1 - chỉ hỗ trợ giao thức TCP/IP;

WinSock 2.0 - hỗ trợ phần mềm bổ sung.

WinSock 1.1 tạo động lực phát triển Mạng toàn cầu và được phép truy cập vào Internet dành cho người dùng PC trung bình các cửa sổ . Nếu mục tiêu của phiên bản 1.1 là giải quyết một vấn đề thì mục tiêu WinSock 2.0 - làm môi trường mạng tốt hơn, nhanh hơn và đáng tin cậy hơn. TRONG WinSock 2.0 đã bổ sung hỗ trợ cho các giao thức truyền tải khác và chức năng mới để đảm bảo độ tin cậy của việc trao đổi thông tin mạng. WinSock 2.0 cho phép bạn tạo các ứng dụng độc lập với giao thức truyền tải hoạt động với TCP/IP (Giao thức điều khiển truyền tải/Giao thức Internet), UDP (Giao thức gói dữ liệu người dùng), IPX/SPX (Trao đổi gói Internetwork/Trao đổi gói tuần tự), NetBEUI (Giao diện người dùng mở rộng NetBios ). Hiệu quả cao hơn trong các ứng dụng như vậy đạt được thông qua I/O dùng chung và ổ cắm dùng chung.

Đặc điểm kỹ thuật WinSock chia chức năng thành ba loại:

Chặn và không chặn (chức năng Berkeley);

Thông tin (thu thập thông tin về tên miền, dịch vụ, giao thức Internet);

Khởi tạo và hủy khởi tạo thư viện.

Chức năng chặn là chức năng ngăn chương trình chạy trước khi nó hoàn thành; không chặn là một chức năng được thực hiện song song với chương trình. Danh sách các chức năng chính cần thiết để tạo một ứng dụng được đưa ra trong bảng 1, 2, 3. Tất cả các mô tả chức năng WinSock được đưa ra ở định dạng ngôn ngữ C và các ví dụ về các cuộc gọi của chúng ở dạng Delphi.

Lập trình mạng với ổ cắm các cửa sổ

Các đường dẫn được đặt tên phù hợp để tổ chức giao tiếp giữa các tiến trình trong trường hợp các tiến trình chạy trên cùng một hệ thống và trong trường hợp các tiến trình chạy trên các máy tính được kết nối với nhau bằng mạng cục bộ hoặc toàn cầu. Những khả năng này đã được thể hiện bằng cách sử dụng hệ thống máy khách-máy chủ được phát triển ở Chương 11, bắt đầu từ Chương trình 11.2.

Tuy nhiên, cả đường ống và hộp thư được đặt tên (chúng tôi sẽ sử dụng bên dưới để đơn giản) thuật ngữ chung"ống có tên", nếu sự khác biệt giữa chúng không đóng vai trò quan trọng) sẽ có nhược điểm là chúng không phải là tiêu chuẩn ngành. Tình huống này gây khó khăn cho việc chuyển các chương trình như đã thảo luận trong Chương 11 sang các hệ thống không thuộc họ. các cửa sổ , mặc dù các đường ống được đặt tên là giao thức độc lập và có thể chạy trên nhiều giao thức tiêu chuẩn ngành, ví dụ: TCP/IP.

Khả năng tương tác với các hệ thống khác được cung cấp trong các cửa sổ hỗ trợ ổ cắm (ổ cắm) Ổ cắm Windows ổ cắm analog tương thích và gần như chính xácỔ cắm Berkeley , trên thực tế đóng vai trò của một tiêu chuẩn công nghiệp. Trong chương này, sử dụng API ổ cắm Windows (hoặc "Winsock" ") được hiển thị bằng ví dụ về hệ thống máy khách-máy chủ đã được sửa đổi từ Chương 11. Hệ thống thu được có khả năng hoạt động trong các mạng toàn cầu bằng giao thức TCP/IP , ví dụ: cho phép máy chủ chấp nhận yêu cầu từ khách hàng UNIX hoặc bất kỳ thứ gì khác ngoài Hệ thống Windows.

Bạn đọc đã quen với giao diệnỔ cắm Berkeley , nếu muốn, có thể đi thẳng vào các ví dụ không chỉ sử dụng ổ cắm mà còn giới thiệu các khả năng mới của máy chủ và trình diễn các kỹ thuật bổ sung để làm việc với các thư viện cung cấp hỗ trợ an toàn theo luồng.

Liên quan đến các phương tiện đảm bảo khả năng tương tác giữa các hệ thống không đồng nhất, giao diện hướng tiêu chuẩn Winsock cho phép các lập trình viên truy cập vào các giao thức và ứng dụng cấp cao như ftp, http, RPC và COM, cùng nhau cung cấp một tập hợp phong phú các mô hình cấp cao hỗ trợ cho các quá trình liên tiến trình. tương tác mạng cho các hệ thống có kiến ​​trúc khác nhau.

Trong chương này, hệ thống máy khách-máy chủ được chỉ định được sử dụng như một cơ chế để thể hiện giao diện Winsock và khi máy chủ được sửa đổi, những cái mới sẽ được thêm vào nó cơ hội thú vị. Đặc biệt, lần đầu tiên chúng tôi sẽ sử dụng điểm vào DLL (Chương 5) và các máy chủ đang xử lý DLL . (Những tính năng mới này có thể đã được đưa vào phiên bản gốc của chương trình ở Chương 11, nhưng điều này sẽ khiến bạn không chú ý đến việc phát triển kiến ​​trúc hệ thống cơ bản.) Cuối cùng, các ví dụ bổ sung sẽ chỉ cho bạn cách tạo an toàn, đăng nhập lại, đa luồng thư viện.

Kể từ khi giao diện Winsock phải tuân theo các tiêu chuẩn ngành, quy ước đặt tên và phong cách lập trình của nó hơi khác so với những gì chúng tôi gặp khi làm việc với các hàm được mô tả trước đó Các cửa sổ. Nói đúng ra, API Winsock không phải là một phần Thắng 32/64. Ngoài ra, Winsock cung cấp các chức năng bổ sung không tuân theo tiêu chuẩn; Những chức năng này chỉ được sử dụng khi thực sự cần thiết. Trong số các lợi ích khác được cung cấp Winsock , cần lưu ý khả năng di chuyển của các chương trình kết quả sang các hệ thống khác được cải thiện.

Ổ cắm Windows

API Winsock được phát triển như một phần mở rộng API ổ cắm Berkley cho môi trường Windows và do đó được hỗ trợ bởi tất cả các hệ thống các cửa sổ . Đến lợi ích Winsock có thể quy những điều sau đây:

Di chuyển mã hiện có được viết cho API ổ cắm Berkeley , được thực hiện trực tiếp.

Hệ thống Windows dễ dàng tích hợp vào mạng bằng cách sử dụng cả hai Giao thức TCP/IP IPv4 , và một phiên bản lan rộng dần dần IPv6 6. Ngoài mọi thứ khác, phiên bản IPv6 6 cho phép sử dụng lâu hơn IP -addresses, vượt qua rào cản địa chỉ phiên bản 4 byte hiện có IPv4.

Các ổ cắm có thể được chia sẻ với I/O chồng chéo các cửa sổ (Chương 14), trong số những thứ khác, có thể mở rộng quy mô máy chủ khi số lượng khách hàng đang hoạt động tăng lên.

Ổ cắm có thể được coi là bộ mô tả (chẳng hạn như XỬ LÝ ) khi sử dụng hàm ReadFile và WriteFile và, với một số hạn chế, khi sử dụng các hàm khác, giống như các socket được sử dụng làm bộ mô tả tệp trong UNIX . Tính năng này rất hữu ích khi bạn cần sử dụng các cổng I/O và cổng hoàn thành I/O không đồng bộ.

Ngoài ra còn có các phần mở rộng bổ sung, không di động.

WinSock nó là giao diện lập trình ứng dụng mạng được triển khai trên tất cả các nền tảng Thắng 32, giao diện chính để truy cập các giao thức mạng lõi khác nhau. Giao diện kế thừa rất nhiều từ việc thực hiệnỔ cắm Berkeley (BSD) trên nền tảng UNIX. Trong môi trường Win 32 nó trở nên hoàn toàn độc lập với giao thức, đặc biệt là với việc phát hành WinSock2.

Thuật ngữ ổ cắm ) được sử dụng để biểu thị các bộ mô tả nhà cung cấp dịch vụ vận chuyển. TRONG Thắng Ổ cắm 32 khác với bộ mô tả tệp và do đó được biểu thị bằng một loại riêng biệtỔ CẮM . Từ quan điểm của mô hình tham chiếu Giao diện Winsock OSI nằm ở mức phiên và mức vận chuyển. Được quản lý bởi các cửa sổ Các lớp Ứng dụng, Trình bày và Phiên chủ yếu liên quan đến ứng dụng của bạn. C Có sự khác biệt đáng kể trong việc triển khai socket UNIX và Windows , điều này tạo ra những vấn đề rõ ràng. Thư viện WinSock hỗ trợ hai loại ổ cắm: đồng bộ (chặn) và không đồng bộ (không chặn). Các ổ cắm đồng bộ giữ quyền kiểm soát trong khi hoạt động đang diễn ra, trong khi các ổ cắm không đồng bộ trả lại quyền điều khiển ngay lập tức, tiếp tục thực thi ở chế độ nền và thông báo cho mã gọi khi quá trình này kết thúc.

Windows 3.x kế thừa chỉ hỗ trợ các ổ cắm không đồng bộ, vì trong môi trường đa nhiệm của công ty, việc giành quyền kiểm soát một nhiệm vụ sẽ “tạm dừng” tất cả các nhiệm vụ khác, bao gồm cả chính hệ thống. hệ điều hành Windows 9x và NT /2000/ XP hỗ trợ cả hai loại ổ cắm, tuy nhiên, do thực tế là ổ cắm đồng bộ dễ lập trình hơn ổ cắm không đồng bộ nên ổ cắm không đồng bộ không được sử dụng rộng rãi. Ổ cắm gia đình giao thức TCP/IP được sử dụng để trao đổi dữ liệu giữa các nút Internet.

Ổ cắm được chia thành hai loại: luồng và datagram. Ổ cắm luồng hoạt động trên cơ sở hướng kết nối, cung cấp khả năng nhận dạng mạnh mẽ cho cả hai bên và đảm bảo tính toàn vẹn và thành công của việc phân phối dữ liệu. Ổ cắm gói dữ liệu hoạt động mà không thiết lập kết nối và không cung cấp nhận dạng người gửi cũng như không kiểm soát sự thành công của việc phân phối dữ liệu, nhưng chúng nhanh hơn đáng kể so với ổ cắm phát trực tuyến. Việc lựa chọn một hoặc một loại ổ cắm khác được xác định bởi giao thức truyền tải mà máy chủ vận hành; máy khách không thể thiết lập kết nối trực tuyến với máy chủ datagram.

Mô hình Winsock và OSI

Nhà cung cấp vận tải từ danh mục Winsock được liệt kê bởi WSAEnumProtocols , làm việc ở cấp độ vận chuyển của mô hình OSI , nghĩa là, mỗi người trong số họ cung cấp trao đổi dữ liệu. Tuy nhiên, tất cả chúng đều thuộc về một loại giao thức nào đó và giao thức mạng hoạt động ở cấp độ mạng vì nó xác định cách đánh địa chỉ của từng nút trên mạng. Ví dụ, UDP và TCP đây là những phương tiện vận chuyển, mặc dù cả hai đều thuộc về giao thức IP. Giao diện Winsock nằm giữa lớp phiên và lớp vận chuyển. Winsock cho phép bạn mở, đóng và quản lý phiên cho bất kỳ phương tiện giao thông cụ thể nào. Được quản lý bởi các cửa sổ ba cấp trên: ứng dụng, trình bày và phiên, chủ yếu liên quan đến ứng dụng Winsock . Nói cách khác, ứng dụng Winsock quản lý tất cả các khía cạnh của phiên giao tiếp và, nếu cần, định dạng dữ liệu theo mục tiêu của chương trình.

Ổ cắm Windows

Sẽ rất hữu ích khi xem xét cách các giao thức sẵn có sử dụng các tính năng của Winsock. Giao diện này dựa trên khái niệm về ổ cắm. Ổ cắm là bộ mô tả cho nhà cung cấp dịch vụ vận tải. Trong Win32, ổ cắm khác với bộ mô tả tệp và do đó được biểu thị bằng một loại SOCKET riêng biệt. Ổ cắm được tạo bởi một trong hai chức năng:

// Mã 1.06

Ổ cắm WSASocket (

Int af,

kiểu int

Giao thức int

LPWSAPROTOCOL_INFO lpProtocolInfo,

NHÓM g,

DWORD dwCờ

Ổ cắm SOCKET (

Int af,

kiểu int

Giao thức int

Tham số đầu tiên, af, chỉ định họ địa chỉ giao thức. Ví dụ: nếu bạn cần tạo ổ cắm UDP hoặc TCP, bạn cần thay thế hằng số AF_ INET để tham chiếu đến giao thức IP. Tham số thứ hai, type, là loại socket cho giao thức này. Nó có thể là một trong các giá trị sau: SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET, SOCK_RAW và SOCK_RDM. Tham số thứ ba, giao thức, chỉ định một phương thức vận chuyển cụ thể nếu tồn tại nhiều mục nhập cho một họ địa chỉ và loại ổ cắm nhất định. Trong bảng Bảng 1.2 liệt kê các giá trị được sử dụng trong họ địa chỉ, loại ổ cắm và các trường giao thức cho một mạng truyền tải nhất định.

Ba tham số ban đầu để tạo ổ cắm được chia thành ba cấp độ. Đầu tiên và quan trọng nhất là họ địa chỉ. Nó chỉ định giao thức hiện đang được sử dụng và giới hạn việc sử dụng tham số thứ hai và thứ ba. Ví dụ: họ địa chỉ ATM (AF_ATM) chỉ cho phép các ổ cắm đơn giản (SOCK_RAW). Tương tự như vậy, việc lựa chọn họ địa chỉ và loại ổ cắm sẽ hạn chế việc lựa chọn giao thức.

Tuy nhiên, bạn có thể chuyển giá trị 0 trong tham số giao thức. Trong trường hợp này, hệ thống chọn nhà cung cấp dịch vụ vận chuyển dựa trên hai tham số còn lại là af và type. Khi liệt kê các mục nhập thư mục cho các giao thức, bạn nên kiểm tra giá trị của trường dwProviderFlags từ cấu trúc WSAPROTOCOL_INFO. Nếu nó bằng PFL_ MATCHES_PROTOCOL_ZERO thì đây là phương thức vận chuyển tiêu chuẩn được sử dụng nếu giá trị 0 được truyền trong tham số giao thức socket hoặc WSASocket.

Sau khi liệt kê tất cả các giao thức sử dụng WSAEnumProtocols, bạn phải chuyển cấu trúc WSAPROTOCOL_INFO cho hàm WSASocket dưới dạng tham số lpProtocolInfo.

Sau đó, bạn cần chỉ định hằng số FROM_PROTOCOL_INFO trong cả ba tham số (af, loại và giao thức) - các giá trị từ cấu trúc WSAPROTOCOL_INFO đã truyền sẽ được sử dụng cho chúng. Điều này cho biết một mục nhập giao thức cụ thể.

Bây giờ hãy xem hai lá cờ cuối cùng từ WSASocket. Tham số nhóm luôn bằng 0 vì không có phiên bản Winsock nào hỗ trợ nhóm ổ cắm. Tham số dwFlags chỉ định một hoặc nhiều cờ sau:

WSA_FLAG_OVERLAPPED;

WSA_FLAG_MULTIPOINT_C_ROOT;

WSA_FLAG_MULTIPOINT_C_LEAF;

WSA_FLAG_MULTIPOINT_D_ROOT;

WSA_FLAG_MULTIPOINT_D_LEAF.

Cờ đầu tiên, WSA_FLAG_OVERLAPPED, chỉ ra rằng ổ cắm này cho phép I/O chồng chéo, đây là một trong những cơ chế giao tiếp được cung cấp bởi Winsock (xem các chương sau). Khi một ổ cắm được tạo bởi hàm ổ cắm, cờ WSA_FLAG_OVERLAPPED được đặt theo mặc định. Bạn nên luôn đặt cờ này khi sử dụng WSASocket. Bốn cờ cuối cùng áp dụng cho ổ cắm multicast.

Chức năng API máy chủ

Máy chủ là một quá trình chờ đợi khách hàng kết nối để phục vụ các yêu cầu của họ. Máy chủ sẽ lắng nghe các kết nối trên tên tiêu chuẩn. Trong TCP/IP, tên này là địa chỉ IP và số cổng của giao diện cục bộ. Mỗi giao thức có sơ đồ địa chỉ riêng và do đó có đặc điểm đặt tên riêng. Bước đầu tiên trong việc thiết lập kết nối là liên kết ổ cắm của một giao thức nhất định với tên tiêu chuẩn của nó bằng hàm liên kết. Thứ hai là chuyển ổ cắm sang chế độ nghe bằng chức năng nghe. Cuối cùng, máy chủ phải chấp nhận kết nối của máy khách bằng cách sử dụng Accept hoặc WSAAccept.

Sẽ là khôn ngoan khi xem xét từng lệnh gọi API cần thiết để liên kết, lắng nghe và thiết lập kết nối với máy khách. Các cuộc gọi cơ bản mà máy khách và máy chủ phải thực hiện để thiết lập kênh liên lạc là:

chức năng liên kết

Sau khi tạo một socket cho một giao thức cụ thể, bạn nên liên kết nó với một địa chỉ tiêu chuẩn bằng cách gọi hàm liên kết:

// Mã 3.04

Liên kết int(

Ổ cắm,

tên int

Tham số s chỉ định ổ cắm mà các kết nối máy khách được lắng nghe. Tham số thứ hai, với kiểu struct sockaddr, chỉ đơn giản là một bộ đệm có mục đích chung. Trên thực tế, bạn cần đặt một địa chỉ trong bộ đệm này tương ứng với các tiêu chuẩn của giao thức được sử dụng và sau đó khi gọi liên kết truyền nó để gõ struct sockaddr. Tệp tiêu đề Winsock xác định loại SOCKADDR, tương ứng với cấu trúc struct sockaddr. Loại này sẽ được sử dụng để viết ngắn gọn ở phần sau của chương này. Tham số cuối cùng chỉ định kích thước của cấu trúc địa chỉ được truyền đi, tùy thuộc vào giao thức. Ví dụ: đoạn mã sau minh họa ràng buộc trên kết nối TCP:

// Mã 3.05

Ổ cắm;

struct sockaddr_in tcpaddr;

Cổng Int = 5150;

S = ổ cắm (AF_INET, SOCK_STREAM, IPPROTO_TCP);

Tcpaddr.sin_family = AF_INET;

Tcpaddr.sin_port = htons(port);

Tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);

Bind(s, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));

Thông tin chi tiết hơn về cấu trúc sockaddr_in đã được thảo luận trong phần địa chỉ TCP/IP của chương trước. Có một ví dụ về việc tạo một ổ cắm luồng rồi thiết lập cấu trúc địa chỉ TCP/IP để chấp nhận các kết nối máy khách. Trong trường hợp này, ổ cắm trỏ đến giao diện IP mặc định với số cổng 5150. Về mặt hình thức, lệnh gọi Blind liên kết ổ cắm với giao diện IP và cổng.

Nếu xảy ra lỗi, hàm liên kết sẽ trả về SOCKET_ERROR. Lỗi thường gặp nhất khi gọi liên kết WSAEADDRINUSE. Trong trường hợp TCP/IP, điều này có nghĩa là một quy trình khác đã được liên kết với giao diện IP và số cổng cục bộ hoặc chúng ở trạng thái TIME_WAIT. Việc gọi lại liên kết trên ổ cắm đã bị ràng buộc sẽ trả về lỗi WSAEFAULT.

^ Chức năng nghe

Bây giờ bạn cần đặt ổ cắm vào chế độ nghe. Hàm liên kết chỉ liên kết một ổ cắm với một địa chỉ nhất định. Để đặt ổ cắm vào trạng thái chờ kết nối đến, hãy sử dụng chức năng listen API:

// Mã 3.06

Hãy lắng nghe(

Ổ cắm,

Tồn đọng Int

Tham số đầu tiên là ổ cắm liên quan. Tham số tồn đọng xác định độ dài tối đa của hàng đợi kết nối đang chờ xử lý, điều này rất quan trọng khi yêu cầu nhiều kết nối máy chủ. Đặt giá trị của tham số này là 2, khi đó khi nhận được đồng thời ba yêu cầu của máy khách, hai kết nối đầu tiên sẽ được đặt vào hàng đợi và ứng dụng sẽ có thể xử lý chúng. Yêu cầu thứ ba sẽ trả về lỗi WSAECONNREFUSED. Sau khi máy chủ chấp nhận kết nối, yêu cầu sẽ bị xóa khỏi hàng đợi và một yêu cầu khác sẽ thay thế. Ý nghĩa của tồn đọng phụ thuộc vào nhà cung cấp giao thức. Giá trị không hợp lệ được thay thế bằng giá trị hợp lệ gần nhất. Không có cách tiêu chuẩn nào để có được giá trị tồn đọng thực tế.

Các lỗi liên quan đến việc lắng nghe khá đơn giản. Cái phổ biến nhất là WSAEINVAL, thường có nghĩa là hàm liên kết không được gọi trước khi nghe. Đôi khi lỗi WSAEADDRINUSE xảy ra khi gọi listen, nhưng thường xảy ra hơn khi gọi bind.

^ Hàm chấp nhận và WSAAccept

Mọi thứ đã sẵn sàng để chấp nhận kết nối máy khách và bạn có thể gọi hàm chấp nhận hoặc WSAAccept. Nguyên mẫu chấp nhận:

// Mã 3.07

Ổ CẮM chấp nhận(

Ổ cắm,

Cấu trúc sockaddr FAR * addr,

Int FAR * addrlen

Ổ cắm liên quan của tham số ở trạng thái nghe. Tham số thứ hai là địa chỉ của cấu trúc SOCKADDR_IN thực tế và addrlen là tham chiếu đến độ dài của cấu trúc SOCKADDR_IN. Đối với socket của giao thức khác, bạn có thể thay thế SOCKADDR_IN bằng cấu trúc SOCKADDR tương ứng với giao thức đó. Cuộc gọi chấp nhận phục vụ yêu cầu kết nối đầu tiên trong hàng đợi. Sau khi hoàn thành, cấu trúc addr sẽ chứa thông tin về địa chỉ IP của máy khách đã gửi yêu cầu và tham số addrlen sẽ chứa kích thước của cấu trúc.

Ngoài ra, chấp nhận trả về một bộ mô tả ổ cắm mới tương ứng với kết nối máy khách được chấp nhận. Mọi hoạt động tiếp theo với client này đều phải sử dụng socket mới. Ổ cắm nghe ban đầu được sử dụng để chấp nhận các kết nối máy khách khác và vẫn ở chế độ nghe.

Winsock 2 có chức năng WSAAccept có thể thiết lập kết nối tùy thuộc vào kết quả tính toán điều kiện:

// Mã 3.08

SOCKET WSAChấp nhận(

Ổ cắm,

Cấu trúc sockaddr FAR * addr,

địa chỉ LPINT,

LPCONDITIONPROC lpfnĐiều kiện,

DWORD dwCallbackData

Ba tham số đầu tiên giống như chấp nhận cho Winsock 1. Tham số lpfnCondition là một con trỏ tới một hàm được gọi khi máy khách yêu cầu. Nó xác định liệu một kết nối có thể được chấp nhận hay không và có nguyên mẫu sau:

// Mã 3.09

int GỌI LẠI Điều kiệnFunc(

LPWSABUF lpCallerId,

LPWSABUF lpCallerData,

LPQOS lpSQOS,

LPQOS lpGQOS,

LPWSABUF lpCalleeId,

LPWSABUF lpCalleeData,

NHÓM XA*g,

DWORD dwCallbackData

Tham số lpCallerId được truyền theo giá trị chứa địa chỉ của đối tượng kết nối. Cấu trúc WSABUF được nhiều hàm Winsock 2 sử dụng và được định nghĩa như sau:

// Mã 3.10

cấu trúc typedef _WSABUF

U_long len;

Char FAR * buf;

) WSABUF , FAR * LPWSABUF ;

Tùy thuộc vào cách sử dụng của nó, trường len chỉ định kích thước của bộ đệm được tham chiếu bởi trường buf hoặc lượng dữ liệu trong bộ đệm buf.

Đối với lpCallerId, tham số buf cho biết cấu trúc của địa chỉ giao thức mà kết nối được thực hiện. Để truy cập thông tin một cách chính xác, chỉ cần chuyển con trỏ buf sang loại SOCKADDR thích hợp. Khi sử dụng giao thức TCP/IP, đây phải là cấu trúc SOCKADDR_IN chứa địa chỉ IP của máy khách đang kết nối. Số đông giao thức mạng hỗ trợ truy cập từ xa nhận dạng thuê bao ở giai đoạn yêu cầu.

Tham số lpCallerData chứa dữ liệu được máy khách gửi trong yêu cầu kết nối. Nếu dữ liệu này không được chỉ định thì nó là NULL. Hầu hết các giao thức mạng, chẳng hạn như TCP/IP, không sử dụng thông tin kết nối. Để tìm hiểu xem một giao thức có hỗ trợ tính năng này hay không, bạn có thể xem mục thích hợp trong thư mục Winsock bằng cách gọi hàm WSAEnumProtocols (xem Chương 1).

Hai tham số tiếp theo, lpSQOS và lpGQOS, chỉ định mức chất lượng dịch vụ mà khách hàng yêu cầu. Cả hai tham số đều đề cập đến cấu trúc chứa thông tin về các yêu cầu về băng thông truyền và nhận. Nếu máy khách không yêu cầu các tham số chất lượng dịch vụ (QoS) thì chúng là NULL. Sự khác biệt giữa chúng là lpSQOS được sử dụng cho một kết nối duy nhất và IpGQOS được sử dụng cho các nhóm ổ cắm. Các nhóm socket không được triển khai hoặc hỗ trợ trong Winsock 1 và 2. Thông tin thêm về QoS sẽ được trình bày ở các chương sau.

Tham số lpCalleeId là một cấu trúc WSABUF khác chứa địa chỉ cục bộ mà máy khách được kết nối. Trường buf trỏ đến đối tượng SOCKADDR của họ địa chỉ tương ứng. Thông tin này hữu ích nếu máy chủ đang chạy trên máy phát đa hướng. Nếu máy chủ được liên kết với địa chỉ INADDR_ANY, các yêu cầu kết nối sẽ được cung cấp trên bất kỳ giao diện mạng nào và tham số chứa địa chỉ của giao diện đã chấp nhận kết nối.

Tham số lpCalleeData bổ sung cho lpCallerData. Nó đề cập đến cấu trúc WSABUF mà máy chủ có thể sử dụng để gửi dữ liệu đến máy khách trong quá trình thiết lập kết nối. Nếu nhà cung cấp dịch vụ của bạn hỗ trợ tính năng này, trường len sẽ chỉ định số byte tối đa cần gửi. Trong trường hợp này, máy chủ sao chép số byte này vào khối buf của cấu trúc WSABUF và cập nhật trường len để hiển thị số lượng byte đang được truyền. Nếu máy chủ không trả về thông tin kết nối thì trước khi quay lại hàm điều kiện Khi một kết nối được chấp nhận, trường len sẽ được đặt thành 0. Nếu nhà cung cấp không hỗ trợ truyền dữ liệu kết nối, trường len sẽ được đặt thành 0. Hầu hết các giao thức: trên thực tế, tất cả các nền tảng Win32 được hỗ trợ đều không hỗ trợ giao tiếp kết nối .

Sau khi xử lý các tham số được truyền cho hàm điều kiện, máy chủ phải quyết định chấp nhận, từ chối hay trì hoãn yêu cầu kết nối. Nếu kết nối được chấp nhận, hàm điều kiện sẽ trả về CF_ASSERT, nếu bị từ chối CF_REJECT. Nếu vì lý do nào đó mà không thể đưa ra quyết định vào lúc này, CF_DEFER sẽ được trả về.

Khi máy chủ sẵn sàng xử lý yêu cầu, nó sẽ gọi hàm WSAAccept. Hàm có điều kiện chạy trong cùng quy trình với WSAAccept và phải chạy càng nhanh càng tốt. Trong các giao thức được nền tảng Win32 hỗ trợ, yêu cầu của máy khách bị trì hoãn cho đến khi giá trị của hàm điều kiện được đánh giá. Trong hầu hết các trường hợp, ngăn xếp mạng cơ bản có thể chấp nhận kết nối vào thời điểm hàm điều kiện được gọi. Khi CF_REJECT trả về, ngăn xếp chỉ cần đóng nó lại (xem các chương sau).

Nếu xảy ra lỗi, giá trị trả về là INVALID_SOCKET, thường là WSAEWOULDBLOCK. Nó xảy ra khi ổ cắm ở chế độ không đồng bộ hoặc không chặn và không có kết nối để nhận. Nếu hàm điều kiện trả về CF_DEFER, WSAAccept sẽ tạo ra lỗi WSATRY_AGAIN nếu CF_REJECT WSAECONNREFUSED.

Các hàm API máy khách

Phần máy khách đơn giản hơn nhiều và chỉ cần ba bước để thiết lập kết nối: tạo một ổ cắm bằng cách sử dụng hàm socket hoặc WSASocket; giải quyết tên máy chủ (tùy thuộc vào giao thức được sử dụng); bắt đầu kết nối bằng chức năng kết nối hoặc WSAConnect.

Từ Chương 2, bạn đã biết cách tạo một socket và phân giải tên máy chủ IP, vì vậy bước duy nhất còn lại là thiết lập kết nối. Chương 2 cũng xem xét việc phân giải tên cho các họ giao thức khác.

^ Trạng thái TCP

Bạn không cần biết về các trạng thái TCP để sử dụng Winsock, nhưng chúng có thể giúp bạn hiểu rõ hơn những gì đang xảy ra với giao thức khi bạn gọi API Winsock. Ngoài ra, nhiều lập trình viên gặp phải vấn đề tương tự khi đóng socket, trong đó trạng thái TCP là điều thú vị nhất.

Trạng thái ban đầu của bất kỳ ổ cắm nào là ĐÓNG. Khi máy khách bắt đầu kết nối, gói SYN sẽ được gửi đến máy chủ và ổ cắm máy khách sẽ chuyển sang trạng thái SYN_SENT. Khi nhận được gói SYN, máy chủ sẽ gửi gói SYN-and-ACK và máy khách sẽ phản hồi bằng gói ACK. Kể từ thời điểm này, socket máy khách sẽ chuyển sang trạng thái THÀNH LẬP. Nếu máy chủ không gửi gói SYN-ACK, máy khách sẽ hết thời gian chờ và trở về trạng thái ĐÓNG.

Nếu ổ cắm máy chủ được kết nối và lắng nghe trên giao diện và cổng cục bộ thì nó ở trạng thái LISTEN. Khi máy khách cố gắng thiết lập kết nối, máy chủ sẽ nhận được gói SYN và phản hồi bằng gói SYN-ACK. Trạng thái ổ cắm máy chủ thay đổi thành SYN_RCVD. Cuối cùng, sau khi máy khách gửi gói ACK, ổ cắm máy chủ được đặt ở trạng thái THÀNH LẬP.

Có hai cách để đóng một kết nối. Nếu quá trình này được ứng dụng bắt đầu thì việc đóng được gọi là hoạt động, nếu không thì nó được gọi là thụ động. Trong bộ lễ phục. 3.2 cho thấy cả hai kiểu đóng. Khi một kết nối được chủ động đóng, ứng dụng sẽ gửi gói FIN. Nếu ứng dụng gọi closesocket hoặc tắt máy (với đối số thứ hai SD_SEND), nó sẽ gửi gói FIN đến máy chủ và trạng thái socket thay đổi thành FIN_WAIT_1. Thông thường, nút sẽ phản hồi bằng gói ACK và ổ cắm sẽ chuyển sang trạng thái FIN_WAIT_2. Nếu nút cũng đóng kết nối, nó sẽ gửi gói FIN và máy tính sẽ phản hồi bằng gói ACK và đặt ổ cắm ở trạng thái TIME_WAIT.

Trạng thái TIME_WAIT còn được gọi là trạng thái chờ 2*MSL MSL Thời gian tồn tại của phân đoạn tối đa, nói cách khác, thời gian một gói vẫn còn trên mạng trước khi nó bị loại bỏ. Mỗi gói IP có trường thời gian tồn tại (TTL). Nếu bằng 0 thì gói tin có thể bị loại bỏ. Mỗi bộ định tuyến phục vụ gói sẽ giảm giá trị TTL xuống 1 và chuyển gói đi. Khi ứng dụng chuyển sang trạng thái TIME_WAIT, nó sẽ duy trì ở đó trong hai khoảng thời gian bằng MSL. Điều này cho phép TCP, nếu gói ACK cuối cùng bị mất, có thể gửi lại nó, sau đó gửi FIN. Sau 2*MSL, ổ cắm sẽ chuyển sang trạng thái ĐÓNG.

Kết quả của hai phương thức đóng hoạt động còn lại là trạng thái TIME_WAIT. Trong trường hợp trước, chỉ một bên gửi FIN và nhận được phản hồi ACK và nút vẫn được tự do truyền dữ liệu cho đến khi nó bị đóng. Có hai phương pháp khả thi khác ở đây. Trong trường hợp đầu tiên, đóng đồng thời, máy tính và nút đồng thời yêu cầu đóng: máy tính gửi gói FIN đến nút và nhận gói FIN từ nút đó.

Sau đó, để phản hồi gói FIN, máy tính sẽ gửi gói ACK và thay đổi trạng thái ổ cắm thành ĐÓNG. Sau khi máy tính nhận được gói ACK từ máy chủ, ổ cắm sẽ chuyển sang trạng thái TIME_WAIT.

Trường hợp đóng hoạt động thứ hai là một biến thể của đóng đồng thời: ổ cắm chuyển ngay từ trạng thái FIN_WAIT_1 sang trạng thái TIME_WAIT. Điều này xảy ra nếu một ứng dụng gửi gói FIN và ngay sau đó nhận được gói FIN-ACK từ máy chủ. Trong trường hợp này, nút xác nhận gói FIN của ứng dụng bằng cách gửi gói riêng của nó, ứng dụng sẽ phản hồi bằng gói ACK.

Ý nghĩa chính của trạng thái TIME_WAIT là trong khi kết nối đang chờ 2*MSL hết hạn, cặp socket liên quan đến kết nối không thể được sử dụng lại. Một cặp socket là sự kết hợp của các cổng IP cục bộ và từ xa. Một số triển khai TCP không cho phép sử dụng lại bất kỳ cổng nào của cặp ổ cắm ở trạng thái TIME_WAIT. Việc triển khai của Microsoft không có khiếm khuyết này. Nếu bạn cố gắng kết nối với một cặp ổ cắm ở trạng thái TIME_WAIT, lỗi WSAEADDRINUSE sẽ xảy ra. Một giải pháp cho vấn đề (ngoài việc chờ kết thúc trạng thái TIME_WAIT của cặp ổ cắm bằng cách sử dụng cảng địa phương) là sử dụng tùy chọn ổ cắm SO_REUSEADDR. SO_REUSEADDR sẽ được thảo luận chi tiết hơn trong các chương tiếp theo.

Cuối cùng, sẽ rất hữu ích khi xem xét việc đóng cửa thụ động. Trong trường hợp này, ứng dụng nhận được gói FIN từ máy chủ và phản hồi bằng gói ACK. Trong trường hợp này, ổ cắm ứng dụng sẽ chuyển sang trạng thái CLOSE_WAIT. Vì nút đã đóng phía của nó nên nó không thể gửi dữ liệu nữa, nhưng ứng dụng có thể tự do làm điều đó cho đến khi nó đóng phía kết nối của nó. Để đóng phần của nó, ứng dụng sẽ gửi gói FIN, sau đó ổ cắm TCP của ứng dụng được đặt ở trạng thái LAST_ACK. Sau khi nhận được gói ACK từ máy chủ, ổ cắm ứng dụng sẽ trở về trạng thái ĐÓNG.

^ Chức năng kết nối và WSAConnect

Vẫn còn phải thảo luận về việc thiết lập kết nối thực tế. Điều này được thực hiện bằng cách gọi connect hoặc WSAConnect. Đầu tiên chúng ta có thể xem phiên bản Winsock 1 của chức năng này:

// Mã 3.11

Kết nối int(

Ổ cắm

const struct sockaddr FAR * tên,

tên int

Các tham số gần như tự giải thích: đó là ổ cắm TCP thực tế để thiết lập kết nối, đặt tên cho cấu trúc địa chỉ ổ cắm (SOCKADDR_IN) cho TCP, mô tả máy chủ sẽ kết nối, đặt tên cho độ dài của biến đường dẫn. Phiên bản Winsock 2 của chức năng này được định nghĩa như sau:

// Mã 3.12

Int WSAConnect(

Ổ cắm,

const struct sockaddr FAR * tên,

Tên int

LPWSABUF lpCallerData,

LPWSABUF lpCalleeData,

LPQOS lpSQOS,

LPQOS lpGQOS

Ba tham số đầu tiên giống như trong hàm kết nối. Hai phần tiếp theo: lpCallerData và lpCalleeData, là bộ đệm chuỗi được sử dụng để nhận và gửi dữ liệu tại thời điểm thiết lập kết nối. Tham số lpCallerData trỏ đến bộ đệm chứa dữ liệu được máy khách gửi đến máy chủ cùng với yêu cầu kết nối; lpCallerData vào bộ đệm có dữ liệu được máy chủ trả về trong quá trình thiết lập kết nối. Cả hai biến đều là cấu trúc WSABUF và đối với lpCallerData, trường len phải cho biết độ dài dữ liệu của bộ đệm buf được truyền. Trong trường hợp lpCalleeData, trường len xác định kích thước của bộ đệm buf nơi dữ liệu được nhận từ máy chủ. Hai tham số cuối cùng, lpSQOS và lpGQOS, đề cập đến cấu trúc QoS xác định các yêu cầu về băng thông gửi và nhận của kết nối đã thiết lập. Tham số lpSQOS chỉ định các yêu cầu đối với các ổ cắm và lpGQOS cho một nhóm ổ cắm. Các nhóm ổ cắm không được hỗ trợ đầy đủ tại thời điểm này. Giá trị lpSQOS bằng 0 có nghĩa là ứng dụng không có yêu cầu về chất lượng dịch vụ.

Nếu máy tính mà bạn đang kết nối không có tiến trình đang chạy nghe trên cổng này thì chức năng kết nối sẽ trả về lỗi WSAECONNREFUSED. Một lỗi khác, WSAETIMEDOUT, xảy ra khi không thể truy cập được đích được gọi, ví dụ: do thiết bị liên lạc bị lỗi trên đường đến nút hoặc nút không khả dụng trên mạng.

Truyền dữ liệu

Điều quan trọng nhất trong lập trình mạng là khả năng gửi và nhận dữ liệu. Các hàm gửi và WSASend được sử dụng để gửi dữ liệu qua ổ cắm. Tương tự, các hàm recv và WSARecv tồn tại để nhận dữ liệu.

Tất cả các bộ đệm được sử dụng khi gửi và nhận dữ liệu đều bao gồm các phần tử char. Nghĩa là, các chức năng này không được thiết kế để hoạt động với mã hóa UNICODE. Điều này đặc biệt quan trọng đối với Windows CE vì nó sử dụng UNICODE theo mặc định. Có hai cách để gửi chuỗi ký tự UNICODE: ở dạng ban đầu hoặc chuyển sang kiểu char. Sắc thái là khi chỉ định số lượng ký tự được gửi hoặc nhận, kết quả của hàm xác định độ dài của chuỗi phải được nhân với 2, vì mỗi ký tự UNICODE chiếm 2 byte của mảng chuỗi. Một cách khác: trước tiên hãy chuyển đổi chuỗi từ UNICODE sang ASCII bằng hàm WideCharToMultiByte.

Tất cả các chức năng nhận và gửi dữ liệu đều trả về mã SOCKET_ERROR khi xảy ra lỗi. Bạn có thể gọi hàm WSAGetLastError để có được thông tin chi tiết hơn về lỗi. Các lỗi phổ biến nhất là WSAECONNABORTED và WSAECONNRESET. Cả hai đều xảy ra khi kết nối bị đóng, do hết thời gian hoặc do nút ngang hàng đóng kết nối. Một lỗi phổ biến khác là WSAEWOULDBLOCK, thường xảy ra khi sử dụng ổ cắm không chặn hoặc không đồng bộ. Về cơ bản, điều đó có nghĩa là chức năng này không thể được thực thi vào lúc này. Chương tiếp theo sẽ mô tả các phương pháp I/O Winsock khác nhau giúp bạn tránh được những lỗi này.

^ Chức năng gửi và WSASend

Hàm gửi API để gửi dữ liệu qua ổ cắm được xác định như sau:

// Mã 3.13

Int gửi(

Ổ cắm,

Const char FAR * buf,

Ý nghĩa,

cờ int

Tham số s chỉ định ổ cắm để gửi dữ liệu tới. Tham số thứ hai, buf, trỏ đến bộ đệm ký tự chứa dữ liệu được gửi. Len thứ ba, chỉ định số lượng ký tự được gửi từ bộ đệm. Và tham số cuối cùng, cờ, có thể lấy các giá trị 0, MSG_DONTROUTE, MSG_OOB hoặc kết quả của logic OR trên bất kỳ tham số nào trong số này. Khi cờ MSG_DONTROUTE được chỉ định, việc vận chuyển sẽ không định tuyến các gói gửi đi. Việc xử lý yêu cầu này tùy thuộc vào quyết định của giao thức cơ bản (ví dụ: nếu việc vận chuyển không hỗ trợ tùy chọn này thì yêu cầu sẽ bị bỏ qua). Cờ MSG_OOB chỉ ra rằng dữ liệu cần được gửi ngoài băng tần, nghĩa là khẩn cấp.

Nếu thành công, hàm gửi sẽ trả về số byte được truyền, nếu không sẽ trả về lỗi SOCKET_ERROR. Một trong những lỗi điển hình WSAECONNABORTED, xảy ra khi kết nối ảo bị chấm dứt do lỗi giao thức hoặc hết thời gian chờ. Trong trường hợp này, ổ cắm phải được đóng lại vì không thể sử dụng được nữa. Lỗi WSAECONNRESET xảy ra nếu một ứng dụng trên máy chủ từ xa, sau khi thực hiện tắt phần cứng, đặt lại kết nối ảo hoặc chấm dứt đột ngột hoặc máy chủ từ xa khởi động lại. Trong tình huống này, ổ cắm cũng phải được đóng lại. Một lỗi khác là WSAETIMEDOUT, thường xảy ra khi mất kết nối do lỗi mạng hoặc hệ thống từ xa bị lỗi mà không có cảnh báo.

Hàm WSASend của Winsock phiên bản 2, tương tự như chức năng gửi, được định nghĩa như sau:

// Mã 3.14

Int WSASend(

Ổ cắm,

Bộ đệm LPWSABUF,

DWORD dwBufferCount,

DWORD dwFlags,

Ổ cắm là bộ mô tả phiên kết nối hợp lệ. Tham số thứ hai trỏ đến cấu trúc WSABUF hoặc một mảng các cấu trúc này. Cái thứ ba chỉ định số lượng cấu trúc WSABUF được truyền đi. Cấu trúc WSABUF bao gồm chính bộ đệm ký tự và độ dài của nó. Câu hỏi có thể nảy sinh: tại sao bạn cần gửi nhiều bộ đệm cùng một lúc? Điều này được gọi là đầu vào-đầu ra phức tạp (I/O phân tán-thu thập). Điều này sẽ được thảo luận chi tiết hơn sau, nhưng hiện tại có thể lưu ý rằng khi sử dụng nhiều bộ đệm để gửi dữ liệu trên ổ cắm kết nối, một mảng bộ đệm sẽ được gửi bắt đầu từ bộ đệm đầu tiên và kết thúc bằng cấu trúc WSABUF cuối cùng.

Tham số lpNumberOfBytesSent là một con trỏ tới DWORD, sau khi gọi WSASend, chứa tổng số byte được gửi. Tham số flags dwFlags giống như trong hàm gửi. Hai con trỏ cuối cùng, 1pOverlapped và lpCompletionROUTINE, được sử dụng cho I/O chồng chéo, một trong những mô hình I/O không đồng bộ được Winsock hỗ trợ (xem thêm chương tiếp theo).

WSASend đặt tham số lpNumberOfBytesSent thành số byte được ghi. Nếu thành công, hàm trả về 0, nếu không thì SOCKET_ERROR. Các lỗi tương tự như đối với chức năng gửi.

^ Chức năng WSASendDisconnect

Chức năng chuyên biệt này hiếm khi được sử dụng. Cô ấyđược xác định như thế này:

// Mã 3.15

Int WSASendDisconnect(

Ổ cắm,

LPWSABUF lpOUT bị ràng buộcDisconnectData

^ Dữ liệu khẩn cấp

Nếu một ứng dụng cần gửi thông tin có mức độ ưu tiên cao hơn qua ổ cắm luồng, thì ứng dụng đó có thể chỉ định thông tin này là dữ liệu ngoài băng tần (OOB). Ứng dụng ở phía bên kia của kết nối sẽ nhận và xử lý dữ liệu OOB thông qua một kênh logic riêng biệt, độc lập về mặt khái niệm với luồng dữ liệu.

Trong TCP, việc truyền dữ liệu OOB được triển khai bằng cách thêm điểm đánh dấu 1 bit (được gọi là URG) và con trỏ 16 bit trong tiêu đề phân đoạn TCP, cho phép làm nổi bật các byte quan trọng trong lưu lượng cơ bản. Hiện tại có hai cách để TCP phân bổ dữ liệu khẩn cấp. RFC 793, mô tả TCP và khái niệm dữ liệu khẩn cấp, nêu rõ rằng chỉ báo mức độ khẩn cấp trong tiêu đề TCP là độ lệch dương của byte theo sau byte dữ liệu khẩn cấp. Tuy nhiên, RFC 1122 coi phần bù này là một con trỏ tới chính byte khẩn cấp.

Trong đặc tả Winsock, thuật ngữ OOB đề cập đến cả dữ liệu OOB độc lập với giao thức và việc triển khai cơ chế truyền dữ liệu khẩn cấp trong TCP. Để kiểm tra xem có dữ liệu khẩn cấp trong hàng đợi hay không, hàm ioctlsocket được gọi với tham số SIOCATMARK. Thông tin chi tiết hơn về chức năng này trong các chương tiếp theo.

Winsock cung cấp một số cách để truyền dữ liệu khẩn cấp. Bạn có thể nhúng chúng vào luồng thông thường hoặc bằng cách tắt tính năng này, hãy gọi một hàm riêng biệt chỉ trả về dữ liệu khẩn cấp. Tham số SO_OOBINLINE kiểm soát hành vi của dữ liệu OOB (điều này sẽ được thảo luận chi tiết trong các chương tiếp theo).

Trong một số trường hợp, dữ liệu khẩn cấp được các chương trình Telnet và Rlogin sử dụng. Trừ khi bạn định viết phiên bản của riêng mình cho các chương trình này, bạn nên tránh sử dụng dữ liệu khẩn cấp - chúng không được chuẩn hóa và có thể có cách triển khai khác nhau trên các nền tảng khác ngoài Win32. Nếu thỉnh thoảng bạn cần chuyển gấp một số thông tin, bạn có thể tạo một ổ cắm điều khiển riêng cho dữ liệu khẩn cấp và cung cấp kết nối chính để truyền dữ liệu thông thường.

Hàm WSASendDisconnect bắt đầu quá trình đóng socket và gửi dữ liệu thích hợp. Nó chỉ khả dụng cho các giao thức hỗ trợ đóng dần và truyền dữ liệu khi nó xảy ra. Hiện tại không có nhà cung cấp dịch vụ vận tải nào hỗ trợ việc truyền dữ liệu đóng kết nối. Hàm WSASendDisconnect hoạt động tương tự như tắt máy với tham số SD_SEND nhưng cũng gửi dữ liệu có trong tham sốboundDisconnectData. Sau khi gọi nó, không thể gửi dữ liệu qua socket. Nếu thất bại, WSASendDisconnect trả về SOCKET_ERROR. Các lỗi gặp phải khi chạy chức năng cũng tương tự như lỗi gửi.

^ Hàm recv và WSARecv

Hàm recv là công cụ chính để nhận dữ liệu qua ổ cắm. Nó được định nghĩa như thế này:

// Mã 3.16

Int recv(

Ổ cắm

char FAR * buf,

Ý nghĩa,

cờ int

Tham số s chỉ định ổ cắm để nhận dữ liệu. Tham số thứ hai, buf, là bộ đệm ký tự cho dữ liệu đã nhận và len chỉ định số byte cần nhận hoặc kích thước của bộ đệm buf. Tham số cuối cùng, cờ, có thể là 0, MSG_PEEK, MSG_OOB hoặc kết quả của OR logic của bất kỳ tham số nào trong số này. Tất nhiên, 0 có nghĩa là không có hành động đặc biệt nào. Cờ MSG_PEEK chỉ định rằng dữ liệu có sẵn sẽ được sao chép vào bộ đệm nhận trong khi vẫn còn trong bộ đệm hệ thống. Khi hoàn thành, hàm cũng trả về số byte đang chờ xử lý.

Không nên đọc tin nhắn theo cách này. Không chỉ vậy, vì có hai lệnh gọi hệ thống (một để đọc dữ liệu và một không có cờ MSG_PEEK để xóa dữ liệu) nên hiệu suất bị giảm. Trong một số trường hợp, phương pháp này đơn giản là không đáng tin cậy. Lượng dữ liệu được trả về có thể không tương ứng với tổng lượng dữ liệu có sẵn. Ngoài ra, bằng cách lưu trữ dữ liệu trong bộ đệm hệ thống, hệ thống ngày càng để lại ít bộ nhớ hơn để chứa dữ liệu đến. Điều này làm giảm kích thước cửa sổ TCP cho tất cả người gửi, ngăn ứng dụng đạt được hiệu suất tối đa. Tốt nhất là sao chép tất cả dữ liệu vào bộ đệm của riêng bạn và xử lý nó ở đó. Cờ MSG_OOB đã được thảo luận trước đó khi thảo luận về việc gửi dữ liệu.

Việc sử dụng recv trên các socket định hướng thông điệp hoặc datagram có một số hàm ý. Khi gọi recv, nếu kích thước của dữ liệu đang chờ xử lý lớn hơn bộ đệm được cung cấp thì lỗi WSAEMSGSIZE sẽ xảy ra sau khi bộ đệm được lấp đầy hoàn toàn. Lỗi vượt quá kích thước tin nhắn chỉ xảy ra khi sử dụng các giao thức hướng tin nhắn. Giao thức truyền phát đệm dữ liệu đến và cung cấp đầy đủ khi ứng dụng yêu cầu, ngay cả khi có nhiều dữ liệu đang chờ xử lý hơn kích thước bộ đệm. Do đó, lỗi WSAEMSGSIZE không thể xảy ra khi làm việc với các giao thức phát trực tuyến.

Hàm WSARecv có các khả năng bổ sung so với recv: nó hỗ trợ các thông báo I/O chồng chéo và các gói dữ liệu bị phân mảnh.

// Mã 3.17

int WSARecv(

Ổ cắm,

Bộ đệm LPWSABUF,

DWORD dwBufferCount,

LPOWORD lpNumberOfBytesRecvd,

Cờ LPDWORD,

LPWSAOVERLAPPED lpOveflapped,

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE

Ổ cắm kết nối của tham số. Tham số thứ hai và thứ ba xác định bộ đệm để nhận dữ liệu. Con trỏ lpBuffers tham chiếu đến một mảng các cấu trúc WSABUF và dwBufferCount chỉ định số lượng các cấu trúc như vậy trong mảng. Tham số lpNumberOfBytesReceured, nếu thao tác truy xuất dữ liệu hoàn tất ngay lập tức, cho biết số byte mà lệnh gọi này nhận được. Tham số lpFlags có thể là MSG_PEEK, MSG_OOB, MSG_PARTIAL hoặc kết quả của OR logic của bất kỳ tham số nào trong số này.

Cờ MSG_PARTIAL có thể có ý nghĩa và ý nghĩa khác nhau tùy thuộc vào cách sử dụng. Đối với các giao thức hướng thông báo, cờ này được đặt sau khi gọi WSARecv (nếu không thể trả về toàn bộ thông báo do không đủ dung lượng bộ đệm). Trong trường hợp này, mỗi cuộc gọi tiếp theo tới WSARecv sẽ đặt cờ MSG_PARTIAL cho đến khi toàn bộ tin nhắn được đọc. Nếu cờ này được chuyển làm tham số đầu vào thì thao tác nhận dữ liệu phải hoàn tất ngay khi có dữ liệu, ngay cả khi nó chỉ là một phần của thông báo. Cờ MSG_PARTIAL chỉ được sử dụng với các giao thức hướng thông điệp. Mỗi mục giao thức trong thư mục Winsock chứa một cờ biểu thị sự hỗ trợ cho tính năng này (xem thêm các chương tiếp theo). Các tùy chọn lpOverlapped và lpCompletionROUTINE được sử dụng trong các hoạt động I/O chồng chéo (được thảo luận trong chương tiếp theo).

^ Hàm WSARecvDisconnect

Hàm này nghịch đảo của WSASendDisconnect và được định nghĩa như sau:

// Mã 3.18

Int WSARecvDisconnect(

Ổ cắm,

LPWSABUF lpInboundDisconnectData

Giống như WSASendDisconnect, các tham số của nó là một tay cầm socket kết nối và một cấu trúc WSABUF thực tế để nhận dữ liệu. Hàm này chỉ chấp nhận dữ liệu đóng kết nối được gửi từ phía bên kia bởi hàm WSASendDisconnect, không thể sử dụng để nhận dữ liệu thông thường. Ngoài ra, ngay sau khi nhận dữ liệu thì nó sẽ dừng nhận từ phía xa, tương đương với việc gọi tắt máy với tham số SD_RECV.

^ Hàm WSARecvEx

Hàm này là một phần mở rộng đặc biệt của Microsoft dành cho Winsock 1. Nó giống hệt recv ngoại trừ tham số flags được truyền bằng tham chiếu. Điều này cho phép nhà cung cấp cơ bản đặt cờ MSG_PARTLAL.

// Mã 3.19

Int PASCAL FAR WSAREcvEx(

Ổ cắm

char FAR * buf,

Ý nghĩa,

cờ int *

Nếu dữ liệu nhận được không tạo thành một thông báo hoàn chỉnh, cờ MSG_PARTIAL sẽ được trả về trong tham số flags. Nó chỉ được sử dụng với các giao thức hướng thông điệp. Khi cờ MSG_PARTIAL được chuyển như một phần của tham số cờ khi nhận được thông báo không đầy đủ, hàm sẽ thoát ngay lập tức, trả về dữ liệu đã nhận. Nếu không có đủ dung lượng bộ đệm để chấp nhận toàn bộ tin nhắn, WSARecvEx sẽ trả về lỗi WSAEMSGSIZE và dữ liệu còn lại sẽ bị loại bỏ. Có sự khác biệt giữa cờ MSG_PARTIAL và lỗi WSAEMSGSIZE: trong trường hợp xảy ra lỗi, toàn bộ tin nhắn đã đến nhưng bộ đệm tương ứng quá nhỏ để có thể nhận được. Cờ MSG_PEEK và MSG_OOB cũng có thể được sử dụng trong WSARecvEx.

Kết thúc một phiên

Khi bạn sử dụng xong ổ cắm, bạn phải đóng kết nối và giải phóng tất cả tài nguyên được liên kết với tay cầm ổ cắm bằng cách gọi hàm liều lượng. Việc sử dụng không đúng cách có thể dẫn đến mất dữ liệu, vì vậy trước khi gọi liều lượng, phiên phải được kết thúc đúng cách bằng chức năng tắt máy.

^ Chức năng tắt máy

Một ứng dụng được viết đúng cách sẽ thông báo cho người nhận khi dữ liệu đã được gửi. Nút cũng nên làm như vậy. Hành vi này được gọi là kết thúc phiên một cách duyên dáng và được thực hiện bằng cách sử dụng hàm Shudoum:

// Mã 3.23

Tắt máy Int(

Ổ cắm,

Int như thế nào

Tham số cung có thể là SD_RECEIVE, SD_SEND hoặc SD_BOTH. Giá trị SD_RECEIVE cấm tất cả các cuộc gọi tiếp theo đến bất kỳ chức năng nhận dữ liệu nào; điều này không ảnh hưởng đến các giao thức cấp thấp hơn. Nếu có dữ liệu trong hàng đợi ổ cắm TCP hoặc nếu nó đến muộn hơn thì kết nối sẽ được đặt lại. Ổ cắm UDP trong tình huống tương tự tiếp tục nhận dữ liệu và xếp hàng. SD_SEND vô hiệu hóa tất cả các lệnh gọi tiếp theo tới chức năng gửi dữ liệu. Trong trường hợp ổ cắm TCP, sau khi người nhận xác nhận đã nhận được tất cả dữ liệu đã gửi, gói FIN sẽ được gửi. Cuối cùng, SD_BOTH vô hiệu hóa cả việc nhận và gửi.

^ Chức năng closesocket

Chức năng này đóng ổ cắm. Nó được định nghĩa như thế này:

// Mã 3.24

int closesocket(SOCKET s);

Việc gọi closesocket sẽ giải phóng tay cầm ổ cắm và tất cả các thao tác tiếp theo trên ổ cắm sẽ dẫn đến lỗi WSAENOTSOCK. Nếu không có tham chiếu nào khác đến ổ cắm, tất cả tài nguyên được liên kết với tay cầm sẽ được giải phóng, bao gồm cả dữ liệu được xếp hàng đợi.

Các cuộc gọi không đồng bộ đang chờ xử lý bắt nguồn từ bất kỳ luồng nào trong quá trình này sẽ bị hủy âm thầm. Các hoạt động I/O chồng chéo đang chờ xử lý cũng bị loại bỏ. Tất cả các sự kiện, thủ tục và cổng chấm dứt đang chạy liên quan đến I/O bị chặn sẽ không thành công với lỗi WSA_OPERATION_ABORTED. (Các mô hình I/O không đồng bộ và không chặn sẽ được thảo luận chi tiết hơn trong chương tiếp theo.) Một yếu tố khác ảnh hưởng đến hoạt động của hàm closesocket là giá trị của tham số socket SO_Linger (được mô tả đầy đủ trong các chương sau).

Giao thức không kết nối

Nguyên lý hoạt động của các giao thức như vậy là khác nhau vì chúng sử dụng các phương pháp khác để gửi và nhận dữ liệu. Trước tiên hãy thảo luận về bộ thu (hoặc máy chủ), vì bộ thu không có kết nối không khác nhiều so với các máy chủ yêu cầu kết nối.

Người nhận

Quá trình nhận dữ liệu trên ổ cắm không kết nối rất đơn giản. Đầu tiên, một socket được tạo bằng cách sử dụng hàm socket hoặc WSASocket. Sau đó, socket được liên kết với giao diện nơi dữ liệu sẽ được nhận bằng cách sử dụng chức năng liên kết (như trong trường hợp các giao thức hướng phiên). Sự khác biệt là bạn không thể gọi nghe hoặc chấp nhận: thay vào đó bạn chỉ cần đợi nhận dữ liệu đến. Vì không có kết nối trong trường hợp này nên socket nhận có thể nhận datagram từ bất kỳ máy nào trên mạng. Điều đơn giản nhất nhận chức năng recvform.

// Mã 3.28

Int nhận được từ(

Ổ cắm,

Char FAR* buf,

Ý nghĩa,

Cờ int,

Cấu trúc sockaddr FAR * từ,

int FAR * fromlen

Bốn tham số đầu tiên giống như đối với hàm recv, bao gồm các giá trị hợp lệ cho cờ-. MSG_OOB và MSG_PEEK. Tham số from là cấu trúc SOCKADDR cho giao thức đã cho của socket nghe, kích thước của cấu trúc địa chỉ được tham chiếu bởi fromlen. Khi cuộc gọi quay trở lại, cấu trúc SOCKADDR sẽ chứa địa chỉ của máy trạm đang gửi dữ liệu.

Winsock 2 sử dụng một phiên bản khác của recvform WSARecvForm:

// Mã 3.29

Int WSARecvFrom(

Ổ cắm,

Bộ đệm LPWSABUF,

DWORD dwBufferCount,

LPDWORD lpNumberOfBytesRecvd,

Cờ LPDWORD,

Cấu trúc sockaddr FAR * lpFrom,

LPINT lpFromlen,

LPWSAOVERLAPPED lpChồng chéo,

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE

Sự khác biệt giữa các phiên bản là việc sử dụng cấu trúc WSABUF để lấy dữ liệu. Bạn có thể cung cấp một hoặc nhiều bộ đệm WSABUF bằng cách chỉ định số lượng của chúng trong divBufferCount trong trường hợp này có thể thực hiện được I/O phức tạp. Tổng số byte đã đọc được chuyển tới lpNumberOfBytesRecvd. Khi gọi hàm WSARecvFrom, lpFlags có thể nhận các giá trị sau: 0 (nếu không có tham số), MSG_OOB, MSG_PEEK hoặc MSG_PARTIAL. Những cờ này có thể được kết hợp bằng phép toán logic OR. Nếu cờ MSG_PARTlAL được chỉ định khi gọi hàm, nhà cung cấp sẽ chuyển tiếp dữ liệu ngay cả khi chỉ nhận được một phần tin nhắn. Khi trả về, cờ chỉ được đặt thành MSG_PARTIAL nếu tin nhắn được nhận một phần. Khi trả về, WSARecvFrom sẽ đặt tham số lpFrom (con trỏ tới cấu trúc SOCKADDR) thành địa chỉ của máy tính gửi. Một lần nữa, lpFromLen trỏ đến kích thước của cấu trúc SACKADDR, nhưng trong hàm này, nó là một con trỏ tới DWORD. Hai tham số cuối cùng, lpOverlapped và lpCompletionROUTINE, được sử dụng cho I/O chồng chéo (xem chương tiếp theo).

Một cách khác để nhận (gửi) dữ liệu trên các ổ cắm không kết nối là thiết lập kết nối (mặc dù điều này nghe có vẻ lạ). Sau khi tạo socket, bạn có thể gọi connect hoặc WSAConnect, đặt tham số SOCKADDR thành địa chỉ máy tính điều khiển từ xa ai có nhu cầu liên hệ. Trong thực tế, không có kết nối nào xảy ra. Địa chỉ ổ cắm được truyền cho hàm kết nối được liên kết với ổ cắm để có thể sử dụng các hàm recv và WSARecv thay vì recvfrom hoặc WSARecvFrom (vì đã biết nguồn dữ liệu). Nếu ứng dụng của bạn chỉ cần giao tiếp với một điểm cuối tại một thời điểm, bạn có thể sử dụng khả năng kết nối ổ cắm datagram.

Người gửi

Có hai cách để gửi dữ liệu qua ổ cắm không kết nối. Cách đầu tiên và dễ dàng nhất là tạo một socket và gọi hàm sendto hoặc WSASendTo. Trước tiên, hãy xem xét hàm sendto:

// Mã 3.30

Int gửi tới(

Ổ cắm,

const char FAR * buf,

Ý nghĩa,

Cờ int,

Const struct sockaddr FAR * tới,

int tolen

Các tham số của hàm này giống như recvfrom, ngoại trừ buf bộ đệm dữ liệu cần gửi và len cho biết số byte cần gửi. Tham số to là một con trỏ tới cấu trúc SOCKADDR với địa chỉ của máy trạm nhận.

Bạn cũng có thể sử dụng hàm WSASendTo từ Winsock 2:

// Mã 3.31

Int WSASendTo(

Ổ cắm,

Bộ đệm LPWSABUF,

DWORD dwBufferCount,

LPDWORD lpNumberOfBytesĐã gửi,

DWORD dwFlags,

Const struct sockaddr FAR * lpTo,

Int iToLen,

LPWSAOVERLAPPED lpChồng chéo,

LPWSAOVERLAPPED_COMPLETION ROUTINE lpCompletionROUTINE

Một lần nữa, hàm WSASendTo tương tự như hàm tiền thân của nó. Nó chấp nhận một con trỏ tới một hoặc nhiều cấu trúc WSABUF có dữ liệu để gửi đến người nhận dưới dạng tham số lpBuffers và divBufferCount chỉ định số lượng cấu trúc. Đối với I/O phức tạp, nhiều cấu trúc WSABUF có thể được gửi. Trước khi thoát, WSASendTo gán tham số thứ tư lpNumberOfBytesSent cho số byte thực sự được gửi đến người nhận. Cấu trúc tham số lpTo SOCKADDR cho giao thức này với địa chỉ người nhận. Tham số độ dài iToLen của cấu trúc SOCKADDR. Hai tham số cuối cùng, lpOverlapped và lpCompletionROUTINE, được sử dụng cho I/O chồng chéo (xem thêm chương tiếp theo).

Giống như việc nhận dữ liệu, ổ cắm không kết nối có thể được kết nối với địa chỉ điểm cuối và gửi dữ liệu bằng chức năng gửi và WSASend. Sau khi tạo liên kết này, bạn không thể sử dụng các hàm sendto hoặc WSASendTo để trao đổi dữ liệu với một địa chỉ khác; lỗi WSAEISCONN sẽ được tạo ra. Cách duy nhất để hủy liên kết một ổ cắm là gọi hàm closesocket bằng tay cầm ổ cắm rồi tạo một ổ cắm mới.

Mô hình đầu vào-đầu ralần lượt xác định cách ứng dụng sẽ xử lý các hoạt động I/O cho một socket cụ thể.

Winsock cung cấp hai chế độ socket: chặn và không chặn, cũng như một số chế độ mô hình thú vị Các cổng I/O giúp ứng dụng quản lý đồng thời các hoạt động I/O trên nhiều ổ cắm theo cách không đồng bộ: chặn, chọn, WSAAsyncSelect, WSAEventSelect, I/O chồng chéo và cổng hoàn thành. Tất cả các nền tảng Windows đều cung cấp chế độ hoạt động chặn và không chặn cho socket. Tuy nhiên, không phải tất cả các mô hình I/O đều có sẵn trên mọi nền tảng. Bảng sau đây cho thấy tính khả dụng của các kiểu máy trên các nền tảng Windows khác nhau.

Đặc tả Winsock 2.0 cho phép các mô hình cơ bản sau thực hiện các thao tác I/O trên ổ cắm:

chặn I/O,

ghép kênh I/O bằng cách sử dụng select() trên ổ cắm chặn hoặc không chặn,

I/O không chặn không đồng bộ bằng cách sử dụng thông báo sự kiện mạng Windows WSAAsyncSelect(),

I/O không chặn với các sự kiện mạng không đồng bộ WSAEventSelect(),

đầu vào/đầu ra kết hợp (hoặc I/O chồng chéo),

cổng hoàn thiện.

Chế độ ổ cắm

Như chúng tôi đã đề cập, các socket Windows có thể thực hiện các thao tác I/O ở hai chế độ: chặn và không chặn. Trong chế độ chặn, các lệnh gọi đến các hàm Winsock thực hiện các thao tác I/O, chẳng hạn như gửi và nhận, hãy đợi cho đến khi thao tác hoàn tất trước khi trả lại quyền điều khiển cho ứng dụng. Ở chế độ Winsock không chặn, các chức năng sẽ cung cấp quyền kiểm soát ứng dụng ngay lập tức. Các ứng dụng chạy trên nền tảng Windows CE và Windows 95 (Winsock 1), chỉ hỗ trợ một số kiểu đầu vào/đầu ra nhất định, buộc phải thực hiện các hành động cụ thể với các ổ cắm chặn và không chặn để xử lý chính xác các tình huống khác nhau.

1.1. Chế độ chặn

Việc chặn ổ cắm tạo ra một số bất tiện vì lệnh gọi tới bất kỳ chức năng API Winsock nào đều bị chặn trong một thời gian. Hầu hết các ứng dụng Winsock đều tuân theo mô hình nhà sản xuất-người tiêu dùng, trong đó ứng dụng đọc hoặc ghi một số byte nhất định và xử lý chúng. Đoạn mã sau minh họa mô hình này:

Ổ cắm tất;

Bộ đệm than;

int xong = 0,

Ờ;

Trong khi(! xong)

// nhận dữ liệu

Err = recv(sock, buffer, sizeof (bộ đệm));

Nếu (err == SOCKET_ERROR)

// xử lý lỗi tiếp nhận

Printf("recv thất bại với lỗi %dn",

WSAGetLastError());

Trở lại;

// xử lí dữ liệu

ProcessReceuredData (bộ đệm);

Vấn đề với đoạn mã trên là hàm recv có thể không bao giờ cấp quyền kiểm soát cho ứng dụng trừ khi một số dữ liệu đến ổ cắm nhất định. Một số lập trình viên kiểm tra dữ liệu đang chờ xử lý trên ổ cắm bằng cách gọi recv bằng cờ MSG_PEEK hoặc ioctlsocket bằng tùy chọn FIONREAD. Việc kiểm tra dữ liệu đang chờ xử lý trên ổ cắm mà không chấp nhận nó được coi là cách lập trình không tốt và cần tránh mọi hoạt động đọc dữ liệu có giá trị từ bộ đệm hệ thống. Để tránh phương pháp này, chúng ta phải ngăn ứng dụng ngừng hoạt động hoàn toàn do thiếu dữ liệu đang chờ xử lý mà không gọi kiểm tra xem nó có tồn tại hay không. Giải pháp cho vấn đề này là chia ứng dụng thành hai luồng: một đọc và một xử lý dữ liệu, cả hai đều chia sẻ bộ đệm dữ liệu chung. Được đối tượng đồng bộ hóa truy cập dưới dạng sự kiện hoặc mutex. Nhiệm vụ của luồng đọc là đọc dữ liệu đến từ mạng vào ổ cắm vào bộ đệm dùng chung. Khi chủ đề đọc được coi là tối thiểu khối lượng bắt buộc dữ liệu dành cho luồng xử lý, nó sẽ chuyển sự kiện sang trạng thái được báo hiệu, do đó cho phép luồng xử lý biết rằng có dữ liệu trong bộ đệm dùng chung để xử lý. Luồng xử lý lần lượt lấy dữ liệu từ bộ đệm và xử lý nó.

Đoạn mã sau đây cho thấy cách triển khai phương pháp này, triển khai hai chức năng: một chức năng cung cấp khả năng đọc dữ liệu từ mạng (ReadingThread), chức năng còn lại xử lý dữ liệu (ProcessingThread):

#define MAX_BUFFER_SIZE 4096

// Khởi tạo phần quan trọng

// và tự động đặt lại các sự kiện trước khi các luồng được khởi tạo

dữ liệu CRITICAL_SECTION;

XỬ LÝ sự kiện;

Ổ cắm tất;

bộ đệm CHAR;

// tạo ổ cắm đọc

// đọc chủ đề

Void ReadingThread(void)

Int nTotal = 0,

NĐọc = 0,

NTrái = 0,

NByte = 0;

Trong khi (đúng)

NTổng cộng = 0;

NLeft = NUM_BYTES_REQUIRED;

Trong khi(nTotal< NUM_BYTES_REQUIRED)

NRead = recv(sock, &(buffer), nLeft, 0);

Nếu (nRead == -1)

Printf("lỗi");

ExitThread();

NTotal += nĐọc;

NLeft -= nRead;

NBytes += nRead;

SetEvent(hEvent);

// xử lý luồng

Xử lý VoidThread(void)

Trong khi (đúng)

// chờ dữ liệu

WaitForSingleObject(hEvent);

EnterCriticalSection(&data);

DoSomeComputingOnData(bộ đệm);

// xóa dữ liệu đã xử lý khỏi bộ đệm

nByte -= NUM_BYTES_REQUIRED;

LeaveCriticalSection(&data);

Khó khăn chính trong việc lập trình các socket chặn là việc hỗ trợ truyền và nhận dữ liệu cho nhiều socket. Sử dụng cách triển khai trước đó, ứng dụng phải được sửa đổi để có một cặp luồng đọc và xử lý trên mỗi ổ cắm. Điều này thêm một số công việc thường ngày cho người lập trình và làm phức tạp mã. Hạn chế duy nhất là ứng dụng không có khả năng mở rộng tốt với số lượng lớn ổ cắm.

1.2. Chế độ không chặn

Một giải pháp thay thế cho ổ cắm chặn là không chặn. Ổ cắm không chặn có triển vọng hơn, nhưng lợi thế của chúng so với ổ cắm chặn là không lớn. Ví dụ sau đây cho thấy cách tạo ổ cắm và chuyển nó sang chế độ không chặn:

Ổ cắm tất;

Không dấu dài nb = 1;

Lỗi int;

Sock = socket(AF_INET, SOCK_STREAM, 0);

Err = ioctlsocket(sock, FIONBIO, (dài không dấu *) &nb);

nếu (err == SOCKET_ERROR)

//lỗi khi chuyển ổ cắm sang chế độ không chặn

Sau khi chuyển socket sang chế độ non-blocking, các lệnh gọi API Winsock liên quan đến nhận, truyền dữ liệu hoặc quản lý kết nối sẽ ngay lập tức trả lại quyền điều khiển cho ứng dụng mà không cần đợi thao tác hiện tại hoàn tất. Trong hầu hết các trường hợp, các cuộc gọi này trả về lỗi loại WSAEWOULDBLOCK, có nghĩa là thao tác không có thời gian để hoàn thành trong suốt thời gian gọi hàm. Ví dụ: hàm recv sẽ trả về WSAEWOULDBLOCK nếu không có dữ liệu đang chờ xử lý trong bộ đệm hệ thống cho một ổ cắm nhất định. Thông thường, các lệnh gọi hàm bổ sung là cần thiết cho đến khi nó trả về một thông báo cho biết thao tác đã hoàn tất thành công.

Bởi vì hầu hết các cuộc gọi chức năng không chặn đều thất bại với lỗi WSAEWOULDBLOCK, bạn nên kiểm tra tất cả các mã trả về và chuẩn bị cho cuộc gọi không thành công bất kỳ lúc nào. Nhiều lập trình viên mắc sai lầm lớn khi gọi hàm liên tục cho đến khi nó trả về mã trả về thành công. Ví dụ: liên tục gọi recv trong một vòng lặp trong khi chờ đọc 100 byte dữ liệu cũng không tốt hơn gọi recv ở chế độ chặn với tham số MSG_PEEK. Các mô hình I/O của Winsock có thể giúp ứng dụng xác định khi nào ổ cắm sẵn sàng đọc hoặc truyền dữ liệu.

Mỗi chế độ - chặn và không chặn - đều có những nhược điểm và ưu điểm riêng. Ổ cắm chặn dễ sử dụng hơn theo quan điểm khái niệm nhưng có thể khó quản lý khi có số lượng lớn kết nối hoặc khi dữ liệu được truyền với số lượng khác nhau và trong các khoảng thời gian khác nhau. Mặt khác, các ổ cắm không chặn phức tạp hơn vì cần phải viết mã phức tạp hơn để quản lý khả năng nhận mã trả về thuộc loại WSAEWOULDBLOCK với mỗi lệnh gọi đến các hàm API Winsock. Các mô hình I/O socket giúp ứng dụng quản lý việc truyền dữ liệu trên một hoặc nhiều kết nối đồng thời theo cách không đồng bộ.

Mô hình cổng kết cuối

Mô hình I/O cuối cùng mà chúng ta xem xét là mô hình cổng hoàn thiện. Cổng hoàn thành là một cơ chế đặc biệt trong HĐH, qua đó ứng dụng sử dụng nhóm nhiều luồng cho mục đích duy nhất là xử lý các hoạt động I/O không đồng bộ chồng chéo.

Các ứng dụng buộc phải xử lý nhiều yêu cầu không đồng bộ (chẳng hạn như chúng ta đang nói về hàng trăm, hàng nghìn yêu cầu đến đồng thời trên các công cụ tìm kiếm hoặc các máy chủ phổ biến như www.microsoft.com), việc sử dụng cơ chế này có thể xử lý các yêu cầu I/O nhanh hơn nhiều và hiệu quả hơn là chỉ bắt đầu một luồng mới để xử lý yêu cầu đến. Hỗ trợ cho cơ chế này được bao gồm trong Windows NT, Windows 2000, Windows XP và Windows Server 2003 và đặc biệt hiệu quả trên các hệ thống đa bộ xử lý. Do đó, mã demo được xuất bản trên MSDN được thiết kế cho nền tảng phần cứng 16 bộ xử lý.

Để mô hình này hoạt động, cần phải tạo một đối tượng phần mềm đặc biệt của nhân hệ thống, được gọi là "cổng hoàn thành". Điều này được thực hiện bằng cách sử dụng hàm CreateIoCompletionPort(), hàm này sẽ liên kết đối tượng này với một hoặc nhiều bộ mô tả tệp (socket) (xem ví dụ bên dưới trong phần 4.5.1.1) và sẽ quản lý các hoạt động I/O chồng chéo, sử dụng một số luồng nhất định để phục vụ các yêu cầu đã hoàn thành.

Đầu tiên, chúng ta cần tạo một đối tượng phần mềm - một cổng hoàn thành I/O, sẽ được sử dụng để quản lý nhiều yêu cầu I/O cho bất kỳ số lượng bộ mô tả socket nào. Điều này được thực hiện bằng cách gọi hàm CreateIoCompletionPort(), được định nghĩa như sau:

XỬ LÝ CreateIoCompletionPort(

XỬ LÝ FileHandle,

XỬ LÝCổng hoàn thành hiện có,

Khóa hoàn thành DWORD,

Số DWORDOfConcurrentThreads

Trước khi xem xét chi tiết các tham số, cần lưu ý rằng chức năng này thực sự được sử dụng cho hai mục đích khác nhau:

Để tạo một đối tượng cổng kết thúc

Liên kết một tay cầm với một cổng hoàn thành

Khi bạn tạo một đối tượng cổng hoàn thành lần đầu tiên, tham số duy nhất cần quan tâm là NumberOfConcurrentThreads; ba tham số đầu tiên không đáng kể. Tham số NumberOfConcurrentThreads là cụ thể vì nó chỉ định số lượng luồng được phép thực thi đồng thời trên cổng hoàn thành. Về lý thuyết, chúng tôi chỉ cần một luồng cho mỗi bộ xử lý riêng lẻ để phục vụ cổng hoàn thành và tránh chuyển đổi ngữ cảnh của luồng. Đặt tham số này thành 0 sẽ yêu cầu hệ thống cho phép số lượng luồng bằng số bộ xử lý trên hệ thống. Kế tiếp cuộc gọi tạo một cổng hoàn thành I/O:

CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL, 0, 0);

Kết quả là, hàm sẽ trả về một tay cầm được sử dụng để xác định cổng kết thúc khi một tay cầm ổ cắm được gán cho nó.

Mô hình I/O

Lựa chọn

Mô hình này cung cấp một phương pháp chặn được kiểm soát tốt hơn. Mặc dù nó cho phép bạn làm việc với các ổ cắm chặn nhưng tôi sẽ tập trung vào chế độ không chặn. Nguyên lý của mô hình này sẽ trở nên rõ ràng nếu bạn nhìn vào hình minh họa:

Đoạn hội thoại giữa chương trình và WinSock sẽ như sau:

Chương trình: “Được rồi, hãy cho tôi biết khi nào là thời điểm tốt nhất để thử lại.”

WinSock: "Chắc chắn rồi, đợi một chút"

"Thử lại!"

Chương trình: “Gửi dữ liệu này”

WinSock: “Xong!”

Bạn có thể nhận thấy rằng mô hình này trông giống như một ổ cắm chặn. Điều này là do việc chọn chặn. Cuộc gọi đầu tiên cố gắng thực hiện thao tác WinSock. Trong trường hợp này, thao tác sẽ chặn việc thực thi quy trình chính, nhưng chức năng không thể được thực thi và nó bị lỗi. Sau đó, điều khiển được chuyển đến luồng chương trình chính, luồng này gọi phương thức chọn (tức là chương trình gọi mô hình để xác định thời điểm thích hợp để thử lại). Nó sẽ chờ thời điểm tốt nhất để lặp lại chức năng WinSock.

Nhưng ở đây có thể nảy sinh một câu hỏi hoàn toàn công bằng: nếu mô hình này chặn thì tại sao chúng ta lại sử dụng nó cho các ổ cắm không chặn? Thực tế là phương pháp này có thể “chờ” nhiều sự kiện. Dưới đây là nguyên mẫu của hàm select:

chọn (nfds:DWORD, readfds:DWORD, writefds:DWORD, ngoại trừfds:DWORD, hết thời gian chờ:DWORD)

Chọn xác định trạng thái của một hoặc nhiều ổ cắm, cung cấp đồng bộ hóa I/O nếu cần. Tham số đầu tiên bị bỏ qua, tham số cuối cùng được sử dụng để xác định thời gian “chờ” tối ưu cho hàm. Các tham số còn lại xác định bộ ổ cắm:

readfds là một tập hợp các ổ cắm sẽ được kiểm tra khả năng đọc.

writefds - một bộ ổ cắm sẽ được kiểm tra khả năng ghi.

ngoại trừfds - một bộ ổ cắm sẽ được kiểm tra lỗi.

“Có thể đọc được” có nghĩa là dữ liệu đã đến ổ cắm và việc đọc sau khi chọn cũng giống như việc nhận dữ liệu. “Có thể ghi” có nghĩa là bây giờ là thời điểm thích hợp để truyền dữ liệu vì... người nhận có thể sẵn sàng chấp nhận chúng. Ngoại trừ được sử dụng để bắt lỗi từ các kết nối không chặn.

WSAASyncChọn

Hầu hết các chương trình cửa sổ đều sử dụng đặc biệt những hộp thoại, để lấy thông tin từ người dùng hoặc ngược lại. WinSock cung cấp một cách để các thông báo sự kiện mạng tương tác với quá trình xử lý thông báo của Windows. Chức năng WSAAsyncSelect cho phép bạn đăng ký thông báo cho một sự kiện mạng cụ thể dưới dạng tin nhắn Windows quen thuộc.

WSAAsyncSelect (s:DWORD, hWnd:DWORD, wMsg:DWORD, lEvent:DWORD)

Tính năng này yêu cầu một thông báo đặc biệt (wMsg) do người dùng chọn. Và thủ tục cửa sổ phải xử lý chính thông báo này. lEvent là một bitmask chỉ định sự kiện sẽ được báo cáo. Bản vẽ cho mô hình này có thể được thực hiện như thế này:

Giả sử tin nhắn đầu tiên muốn gửi một số dữ liệu đến socket bằng cách sử dụng send. Vì ổ cắm không bị chặn nên chức năng sẽ hoàn thành ngay lập tức. Cuộc gọi hàm có thể thành công, nhưng điều này không xảy ra ở đây. Giả sử rằng WSAAsyncSelect đã được định cấu hình để thông báo cho chúng tôi về sự kiện FD_WRITE, cuối cùng chúng tôi sẽ nhận được thông báo từ WinSock cho chúng tôi biết rằng sự kiện đã xảy ra. Trong trường hợp này, đây là sự kiện FD_WRITE, có nghĩa là “Tôi đã sẵn sàng, hãy thử gửi dữ liệu của bạn”. Do đó, trong trình xử lý tin nhắn, chương trình sẽ cố gắng gửi dữ liệu và nỗ lực đó đã thành công.

Cuộc hội thoại giữa chương trình và WinSock tương tự như mô hình select, điểm khác biệt duy nhất là phương thức thông báo: một thông báo cửa sổ thay vì lệnh gọi đồng bộ tới select. Trong khi select chặn quá trình chính chờ sự kiện xảy ra, một chương trình sử dụng WSAAsyncSelect có thể tiếp tục xử lý các thông báo Windows cho đến khi không có sự kiện nào xảy ra:

Chương trình đăng ký thông báo sự kiện mạng qua tin nhắn cửa sổ

Chương trình: “Gửi dữ liệu này”

WinSock: "Tôi không thể làm điều này bây giờ"

Chương trình xử lý một số tin nhắn

Chương trình xử lý một tin nhắn khác

Chương trình nhận được tin nhắn thông báo từ WinSock

Chương trình: “Gửi dữ liệu này”

WinSock: “Xong! »

WSAAsyncSelect cung cấp cách thông báo "Windows" hơn và khá dễ sử dụng. Đối với các máy chủ có băng thông thấp (dưới 1000 kết nối) phương pháp này khá tốt. Nhược điểm là bản thân các thông báo trong cửa sổ không nhanh lắm và các cửa sổ cũng bắt buộc phải sử dụng mô hình này (tức là chương trình phải là GUI).

WSAESự kiệnChọn

Lưu ý: “đối tượng sự kiện” sẽ được hiểu thêm là một sự kiện mạng cụ thể. Thực tế là ở đây sự kiện được coi là một lớp =).

WSAEventSelect có thể được gọi là họ hàng của WSAAsyncSelect, hoạt động theo cách rất giống nhưng sử dụng các đối tượng sự kiện thay vì thông báo cửa sổ. Điều này có một số lợi thế nhất định, một trong số đó là tính hiệu quả (đối tượng sự kiện nhanh hơn thông báo cửa sổ). Việc giải thích bằng đồ họa của mô hình này có vẻ phức tạp hơn một chút so với mô hình trước, nhưng trên thực tế thì không phải vậy:

Chương trình đăng ký thông báo sự kiện mạng thông qua đối tượng sự kiện

Chương trình: “Gửi dữ liệu này”

WinSock: "Tôi không thể làm điều này bây giờ"

Chương trình chờ đợi một sự kiện để báo hiệu cho nó

Chương trình: “Gửi dữ liệu này”

WinSock: “Xong! »

Thật khó để vẽ một bức tranh cho chức năng này vì các đối tượng sự kiện là một cơ chế rất mạnh có thể được sử dụng những cách khác. Tôi đã chọn một ví dụ sử dụng đơn giản ở đây. Theo tôi, từ hình vẽ và lời thoại, bản chất của mô hình này đã quá rõ ràng.

Lúc đầu, mô hình này tương tự như mô hình chặn: bạn đợi một sự kiện mà bạn sẽ được thông báo. Điều này đúng, nhưng đồng thời bạn có thể tạo đối tượng sự kiện của riêng mình. Tất cả các đối tượng sự kiện là một phần của WinAPI mà WinSock sử dụng. WinSock có một số hàm để tạo đối tượng, nhưng thực chất đây là các hàm API trong gói WinSock.

Tất cả những gì WinSock thực hiện trong mô hình này là báo hiệu một đối tượng sự kiện khi sự kiện đó sắp xảy ra.

Hàm đăng ký sự kiện mạng WSAEventSelect:

WSAEventSelect (s:DWORD, hEventObject:DWORD, lNetworkEvents:DWORD)

WSAAsyncSelect sẽ gửi cho bạn một tin nhắn khi một sự kiện mạng xảy ra (FD_READ, FD_WRITE, v.v.) Không giống như WSAAsyncSelect, WSAEventSelect chỉ có một cách thông báo: báo hiệu một đối tượng sự kiện. Điều này cho phép mô hình này được sử dụng cả trong ứng dụng GUI và trong bảng điều khiển. Bạn có thể tìm thấy những sự kiện nào đã xảy ra bằng WSAEnumNetworkEvents.

Giới thiệu về I/O chồng chéo

I/O bị ghi đè rất hiệu quả, đặc biệt khi được triển khai tốt (cho phép bạn xử lý nhiều kết nối). Điều này đặc biệt đúng khi kết hợp I/O chồng chéo với các cổng kết cuối. Như tôi đã nói trước đó, trong hầu hết các trường hợp, việc sử dụng I/O chồng chéo là không cần thiết, nhưng tôi vẫn sẽ cố gắng đề cập đến chủ đề này, ít nhất là cho sự phát triển chung.

Trong các mô hình đã thảo luận trước đó, một số thông báo đã được gửi (chẳng hạn như “dữ liệu có sẵn” hoặc “sẵn sàng gửi dữ liệu”, v.v.) khi xảy ra một số sự kiện mạng nhất định. Các mô hình chồng chéo cũng gửi thông báo, nhưng không phải về sự xuất hiện của các sự kiện mạng mà về việc hoàn thành chúng. Khi hàm WinSock được gọi, nó có thể thành công hoặc thất bại với mã lỗi WSA_IO_PENDING. Khi sử dụng các mô hình chồng chéo, bạn sẽ được thông báo khi thao tác hoàn tất. Điều này có nghĩa là bạn chỉ cần đợi cho đến khi thao tác hoàn tất.

Chi phí của phương pháp hiệu quả này là khó thực hiện. Nếu bạn không yêu cầu hiệu quả thực sự tốt thì tốt hơn nên sử dụng các mô hình được mô tả trước đó. Ngoài ra, hệ điều hành Windows 9x/ME không hỗ trợ đầy đủ các mô hình I/O chồng chéo.

Giống như các mô hình thông báo sự kiện mạng, mô hình chồng chéo cũng có thể được triển khai theo nhiều cách khác nhau. Chúng khác nhau ở cách chúng được thông báo: chặn, bỏ phiếu, quy trình chấm dứt và cổng chấm dứt.

I/O bị ghi đè: Chặn

Mô hình I/O chồng chéo đầu tiên mà tôi sẽ nói đến sử dụng một đối tượng sự kiện để báo hiệu sự hoàn thành. Mô hình này tương tự như WSAEventSelect về nhiều mặt, nhưng điểm khác biệt là đối tượng được đặt ở trạng thái được báo hiệu khi hoạt động WinSock hoàn tất, thay vì khi một số sự kiện mạng xảy ra.

Chương trình: “Gửi dữ liệu này”

WinSock: "Được rồi, nhưng tôi không thể gửi chúng ngay bây giờ"

Chương trình chờ tín hiệu từ đối tượng sự kiện cho biết chức năng đã hoàn thành

Từ hình ảnh và lời thoại có thể thấy rõ bản chất công việc của mô hình này. Như bạn có thể thấy, hàm WinSock xảy ra đồng thời với main thread của chương trình (trong ví dụ này, main thread đang chờ tín hiệu từ sự kiện). Khi một sự kiện nhận được tín hiệu từ hàm WinSock (đường đứt nét trong hình), nó sẽ chuyển sang trạng thái được báo hiệu và gửi tín hiệu đến luồng chính mà hàm đó đã hoàn thành, còn luồng chính sẽ xử lý tín hiệu nhận được và chuyển sang thực hiện lệnh tiếp theo.

I/O bị ghi đè: bỏ phiếu

Giống như trong mô hình thăm dò đã đề cập trước đó, trong mô hình này cũng có thể truy vấn trạng thái thực thi của một thao tác (mặc dù trong mô hình thăm dò được mô tả trước đó, chúng tôi không yêu cầu trạng thái thực thi mà chỉ nhận được dữ liệu về việc hoàn thành hàm không thành công. . Nhưng luồng chính của chương trình biết khi nào chức năng bị lỗi và khi nào thì ngược lại). Bạn có thể sử dụng hàm WSAGetOverlappedResult để tìm hiểu trạng thái của một thao tác đang diễn ra. Việc giải thích đồ họa của việc bỏ phiếu chồng chéo rất giống với việc bỏ phiếu thông thường, ngoại trừ chức năng WinSock được thực thi cùng lúc với chương trình đang thăm dò để chức năng này hoàn thành.

Chương trình: “Gửi dữ liệu này”

Chương trình: “Bạn đã gửi nó chưa?”

WinSock: "Không"

Chương trình: “Bạn đã gửi nó chưa?”

WinSock: "Không"

Chương trình: “Bạn đã gửi nó chưa?”

WinSock: "Không"

Chương trình: “Bạn đã gửi nó chưa?”

WinSock: “Đúng vậy! »

Và ở đây tôi nhắc lại: mô hình này không tốt lắm, vì nó khiến bộ xử lý bị hoảng loạn. Vì vậy, tôi không khuyên bạn nên sử dụng mô hình này.

I/O bị ghi đè: Quy trình hoàn thành

Các thủ tục hoàn thành là các thủ tục gọi lại (nghĩa là được gọi để đáp lại một hành động cụ thể. Từ bây giờ tôi sẽ gọi các thủ tục này là thủ tục gọi lại) được gọi khi một thao tác hoàn thành. Mọi thứ ở đây có vẻ đơn giản, nhưng có một thủ thuật: các thủ tục này được gọi trong ngữ cảnh của luồng đã bắt đầu thao tác. Nó có nghĩa là gì? Hãy tưởng tượng một luồng đã yêu cầu thao tác ghi chồng chéo. WinSock thực hiện thao tác này trong khi luồng của bạn cũng đang chạy. Vì vậy WinSock có luồng riêng cho thao tác này. Khi thao tác hoàn tất, WinSock phải gọi thủ tục thu hồi. Nếu điều này xảy ra, thủ tục được gọi sẽ được thực thi trong ngữ cảnh của luồng WinSock. Điều này có nghĩa là luồng gọi thao tác ghi sẽ thực thi cùng lúc với thủ tục gọi. Vấn đề là không có sự đồng bộ hóa với luồng đang gọi và nó không biết liệu thao tác đã hoàn thành hay chưa (trừ khi được một luồng song song thông báo như vậy).

Để tránh điều này, WinSock đảm bảo rằng quy trình thu hồi diễn ra trên cùng một luồng mà yêu cầu bắt nguồn từ đó. Việc này được thực hiện bằng cách sử dụng APC (Cuộc gọi thủ tục không đồng bộ), một cơ chế được tích hợp trong Windows. Điều này có thể được coi là "đưa" một thủ tục vào luồng chính của việc thực thi chương trình. Do đó, luồng đầu tiên sẽ thực thi thủ tục và sau đó sẽ thực hiện những gì nó đã làm trước khi được “triển khai”. Đương nhiên, hệ thống không thể ra lệnh cho chuỗi: “Dừng làm mọi việc bạn đang làm và xử lý quy trình này trước”.

Để đảm bảo việc “đưa” vào đúng vị trí, cơ chế APC yêu cầu luồng ở trạng thái được gọi là trạng thái chờ thông báo. Mỗi luồng có hàng đợi APC riêng trong đó các thủ tục chờ được gọi. Khi một luồng chuyển sang trạng thái chờ thông báo, nó cho biết rằng nó đã sẵn sàng phục vụ hàng đợi APC.

I/O bị ghi đè với các quy trình hoàn thành sử dụng APC để thông báo khi một thao tác đã hoàn tất.

Chương trình: “Gửi dữ liệu này”

WinSock: "Được rồi, nhưng tôi không thể gửi chúng bây giờ"

Chương trình chuyển sang trạng thái chờ thông báo

Chức năng đã kết thúc

Trạng thái chờ nhận được tín hiệu rằng chức năng đã hoàn thành

Chức năng thu hồi được thực thi và điều khiển được chuyển đến chương trình

APC có thể hơi khó hiểu nhưng không sao. Đây chỉ là phần giới thiệu ngắn gọn. Thông thường, luồng sẽ đợi cho đến khi một thủ tục gọi lại được gọi để xử lý sự kiện (hàm WinSock đã hoàn thành) và trả lại quyền điều khiển cho chương trình. Sau đó, luồng chính thực hiện các thao tác cần thiết (nếu có) và quay lại trạng thái chờ.

I/O bị ghi đè: Cổng kết thúc

Cuối cùng chúng ta đến với mô hình I/O cuối cùng và có lẽ là hiệu quả nhất: I/O chồng chéo với các cổng hoàn thành. Cổng kết thúc là một cơ chế có sẵn trong nhân NT (9x không hỗ trợ chúng) cho phép quản lý luồng hiệu quả. Không giống như tất cả các mô hình được xem xét, “các cổng hoàn thiện” có khả năng kiểm soát luồng riêng. Như bạn có thể nhận thấy, tất cả các hình minh họa trước đây đều giống như đồ thị theo thời gian. Đối với mô hình này, tôi không tạo biểu đồ và đối thoại chương trình như vậy với WinSock, bởi vì điều này khó có thể giúp làm sáng tỏ tình hình. Thay vào đó, tôi đã vẽ một hình ảnh của chính cơ chế này, điều này mang lại ý tưởng hay về những gì đang diễn ra:

Ý tưởng là thế này: khi một cổng hoàn chỉnh được tạo, các ổ cắm có thể được liên kết với nó. Từ quan điểm này, khi thao tác I/O chồng chéo hoàn tất, một thông báo tương ứng sẽ được gửi đến cổng hoàn thành. Có các luồng công nhân tương tự trên cổng bị chặn. Khi có thông báo đến, cổng sẽ lấy một luồng từ hàng đợi luồng không hoạt động và kích hoạt nó. Chuỗi này xử lý sự kiện đã hoàn thành sắp đến và chặn trên cổng.

Có một giới hạn nhất định về số lượng luồng trong cổng hoàn thành, nhưng thường không phải tất cả chúng đều hoạt động cùng lúc, điều này cho phép giảm hàng đợi luồng. Bằng cách tạo một cổng như thế này, bạn có thể chỉ định số lượng luồng sẽ hoạt động.

Trong mô hình này không có sự kết nối giữa luồng và kết nối. Mỗi luồng có thể tương tác với một sự kiện đến cổng. Mô hình này không dễ thực hiện nhưng một khi đã triển khai, bạn có thể làm việc với hàng nghìn kết nối.

Hướng dẫn chơi trên WINSOCK

ổ cắm(ổ cắm) là một giao diện hợp nhất cấp cao để tương tác với các giao thức viễn thông. Trong tài liệu kỹ thuật có nhiều cách dịch khác nhau của từ này - chúng được gọi là ổ cắm, đầu nối, hộp mực, ống dẫn, v.v. Do thiếu thuật ngữ tiếng Nga đã được thiết lập nên trong bài viết này, ổ cắm sẽ được gọi là ổ cắm và không có gì khác.

Bản thân việc lập trình các socket không khó nhưng được mô tả khá hời hợt trong các tài liệu có sẵn và Windows Sockets SDK có rất nhiều lỗi cả trong tài liệu kỹ thuật và trong các bản demo đi kèm. Ngoài ra, có những khác biệt đáng kể trong việc triển khai các socket trong UNIX và trong Windows, điều này tạo ra những vấn đề rõ ràng.

Tác giả đã cố gắng đưa ra mô tả đầy đủ và mạch lạc nhất, không chỉ bao gồm những điểm chính mà còn cả một số điểm tinh tế mà các lập trình viên bình thường không biết đến. Phạm vi hạn chế của bài báo không cho phép chúng tôi nói về mọi thứ, do đó, chúng tôi chỉ tập trung vào một cách triển khai socket - thư viện Winsock 2, một ngôn ngữ lập trình - C/C++(mặc dù những gì đã nói hầu hết được chấp nhận đối với Delphi, Perl, v.v.) và một loại ổ cắm - chặn ổ cắm đồng bộ.

VẬT LIỆU ALMA

Sự trợ giúp chính trong các socket học tập là SDK Windows Socket 2. SDK là tài liệu, một tập hợp các tệp tiêu đề và công cụ dành cho nhà phát triển. Tài liệu này không hay lắm - nhưng nó vẫn được viết khá thành thạo và cho phép, mặc dù không gặp khó khăn, làm chủ các ổ cắm ngay cả khi không có sự trợ giúp của bất kỳ tài liệu nào khác. Hơn nữa, hầu hết các cuốn sách hiện có trên thị trường rõ ràng đều thua kém Microsoft về tính đầy đủ và chu đáo trong phần mô tả. Hạn chế duy nhất của SDK là nó hoàn toàn bằng tiếng Anh (đối với một số người, điều này rất quan trọng).

Trong số các công cụ có trong SDK, trước hết tôi muốn nêu bật tiện ích sockeye.exe - đây là một “băng ghế thử nghiệm” thực sự dành cho nhà phát triển. Nó cho phép bạn gọi các hàm socket khác nhau một cách tương tác và thao tác chúng theo ý muốn của bạn.

Thật không may, các chương trình trình diễn không phải là không có lỗi, đôi khi khá thô thiển và mang tính gợi ý - những ví dụ này đã được thử nghiệm chưa? (Ví dụ: trong mã nguồn của chương trình simples.c, khi gọi hàm send và sendto, sizeof được sử dụng thay cho strlen) Đồng thời, tất cả các ví dụ đều chứa nhiều nhận xét chi tiết và tiết lộ các kỹ thuật khá thú vị về lập trình phi truyền thống, vì vậy bạn vẫn nên làm quen với chúng.

Trong số các tài nguyên WEB dành cho các socket lập trình và mọi thứ được kết nối với chúng, trước hết tôi muốn lưu ý đến ba tài nguyên sau: sockaddr.com; www.winsock.com và www.sockets.com.

Tổng quan về ổ cắm

Thư viện Winsock hỗ trợ hai loại ổ cắm - đồng bộ (có thể chặn được) Và không đồng bộ (không chặn). Các ổ cắm đồng bộ giữ quyền kiểm soát trong khi hoạt động đang diễn ra, trong khi các ổ cắm không đồng bộ trả lại quyền điều khiển ngay lập tức, tiếp tục thực thi ở chế độ nền và thông báo cho mã gọi khi quá trình này kết thúc.

Windows 3.x chỉ hỗ trợ các ổ cắm không đồng bộ, vì trong môi trường đa nhiệm của công ty, việc giành quyền kiểm soát một tác vụ sẽ "treo" tất cả các tác vụ khác, bao gồm cả chính hệ thống. Windows 9x\NT hỗ trợ cả hai loại socket, tuy nhiên, do thực tế là socket đồng bộ dễ lập trình hơn so với socket không đồng bộ nên loại socket không đồng bộ không được sử dụng rộng rãi. Bài viết này được dành riêng cho các ổ cắm đồng bộ (không đồng bộ là một chủ đề để thảo luận riêng).

Ổ cắm cho phép bạn làm việc với nhiều giao thức khác nhau và là phương tiện giao tiếp liên bộ xử lý thuận tiện, nhưng trong bài viết này, chúng tôi sẽ chỉ nói về các ổ cắm thuộc họ giao thức TCP/IP, được sử dụng để trao đổi dữ liệu giữa các nút Internet. Tất cả các giao thức khác, chẳng hạn như IPX/SPX, NetBIOS, sẽ không được xem xét do phạm vi giới hạn của bài báo.

Bất kể loại nào, ổ cắm được chia thành hai loại - phát trực tuyến gói dữ liệu . Ổ cắm luồng hoạt động trên cơ sở từng kết nối, cung cấp khả năng nhận dạng mạnh mẽ cho cả hai bên và đảm bảo tính toàn vẹn và thành công của việc phân phối dữ liệu. Ổ cắm gói dữ liệu hoạt động mà không thiết lập kết nối và không cung cấp nhận dạng người gửi cũng như không kiểm soát sự thành công của việc phân phối dữ liệu, nhưng chúng nhanh hơn đáng kể so với ổ cắm phát trực tuyến.

Việc lựa chọn một hoặc một loại ổ cắm khác được xác định bởi giao thức truyền tải mà máy chủ vận hành - máy khách không thể tùy ý thiết lập kết nối truyền phát với máy chủ datagram.

Bình luận : Ổ cắm datagram dựa trên giao thức UDP, trong khi ổ cắm phát trực tuyến dựa trên TCP.

Bước đầu tiên, bước thứ hai, bước thứ ba

Để làm việc với thư viện Winsock 2.x, bạn phải thêm lệnh " #bao gồm ", và trong dòng lệnh trình liên kết chỉ định "ws2_32.lib". Trong môi trường phát triển Microsoft Visual Studio, tất cả những gì bạn phải làm là nhấp vào<Alt-F7>, chuyển đến tab "Liên kết" và đến danh sách các thư viện được liệt kê trong dòng "Mô-đun đối tượng/Thư viện", thêm "ws2_32.lib", tách nó khỏi phần còn lại bằng ký tự khoảng trắng.

Trước khi có thể bắt đầu sử dụng các chức năng của thư viện Winsock, bạn cần chuẩn bị nó để làm việc bằng cách gọi hàm " intWSAKhởi động (WORD wVersionRequested, LPWSADATA lpWSAData)" bằng cách truyền byte cao của từ wPhiên bản được yêu cầu số của phiên bản được yêu cầu và số phụ - số phiên bản lật đổ.

Lý lẽ lpWSAData phải trỏ đến cấu trúc WSADATA, khi khởi tạo thành công, sẽ chứa thông tin về nhà sản xuất thư viện. Nó không được quan tâm đặc biệt và ứng dụng có thể bỏ qua nó. Nếu khởi tạo không thành công, hàm sẽ trả về giá trị khác 0.

Bước thứ hai– tạo ra một đối tượng “socket”. Việc này được thực hiện bởi hàm " Ổ CẮMổ cắm (int af, kiểu int, giao thức int)". Đối số đầu tiên bên trái cho biết họ giao thức được sử dụng. Đối với các ứng dụng Internet, đối số này phải được đặt thành AF_INET.

Đối số sau chỉ định loại ổ cắm được tạo - phát trực tuyến (SOCK_STREAM) hoặc gói dữ liệu (SOCK_DGRAM) (ổ cắm thô cũng tồn tại, nhưng chúng không được Windows hỗ trợ - xem phần "Ổ cắm thô").

Đối số cuối cùng chỉ định giao thức vận chuyển nào sẽ được sử dụng. Giá trị 0 tương ứng với lựa chọn mặc định: TCP cho ổ cắm luồng và UDP cho ổ cắm datagram. Trong hầu hết các trường hợp, không có ích gì khi thiết lập giao thức theo cách thủ công và người ta thường dựa vào lựa chọn mặc định tự động.

Nếu hàm thành công, nó sẽ trả về tay cầm ổ cắm, nếu không thì INVALID_SOCKET.

Các bước tiếp theo phụ thuộc vào việc ứng dụng là máy chủ hay máy khách. Dưới đây hai trường hợp này sẽ được mô tả riêng biệt.

Khách hàng: bước ba -Để thiết lập kết nối với máy chủ từ xa, ổ cắm luồng phải gọi hàm "intkết nối (SOCKET s, const struct sockaddr FAR* name, int namelen)". Ổ cắm datagram hoạt động mà không cần kết nối, vì vậy thường xuyên không gọi chức năng kết nối.

Ghi chú: đằng sau từ “thường” là một thủ thuật lập trình phức tạp - cuộc gọi kết nối cho phép ổ cắm gói dữ liệu trao đổi dữ liệu với nút không chỉ bằng các chức năng sendto, recvfrom mà còn với chức năng gửi và recv thuận tiện và nhỏ gọn hơn. Sự tinh tế này được mô tả trong Winsocket SDK và được sử dụng rộng rãi bởi chính Microsoft và các nhà phát triển bên thứ ba. Vì vậy, việc sử dụng nó là hoàn toàn an toàn.

Đối số đầu tiên ở bên trái là bộ mô tả ổ cắm được hàm ổ cắm trả về; thứ hai là một con trỏ tới cấu trúc " sockaddr", chứa địa chỉ và cổng của nút từ xa mà kết nối đang được thiết lập. Cấu trúc sockaddr được nhiều hàm sử dụng, do đó mô tả của nó được bao gồm trong một phần riêng "Địa chỉ một, địa chỉ hai". Đối số cuối cùng cho biết chức năng kích thước của cấu trúc sockaddr.

Sau đó kết nối cuộc gọi hệ thống cố gắng thiết lập kết nối với nút được chỉ định. Nếu vì lý do nào đó mà điều này không thể thực hiện được (địa chỉ được đặt không chính xác, nút không tồn tại hoặc bị treo, máy tính không có mạng), hàm sẽ trả về giá trị khác 0.

Máy chủ: bước ba– Trước khi máy chủ có thể sử dụng socket, nó phải liên kết socket đó với một địa chỉ cục bộ. Địa chỉ cục bộ, giống như bất kỳ địa chỉ Internet nào khác, bao gồm địa chỉ IP máy chủ và số cổng. Nếu máy chủ có nhiều địa chỉ IP thì ổ cắm có thể được liên kết với tất cả chúng cùng một lúc (để thực hiện việc này, thay vì địa chỉ IP, bạn nên chỉ định hằng số INADDR_ANY bằng 0) hoặc với bất kỳ địa chỉ cụ thể nào.

Việc liên kết được thực hiện bằng cách gọi hàm " inttrói buộc (SOCKET s, const struct sockaddr FAR* name, int namelen)".Đối số đầu tiên bên trái là bộ mô tả socket được hàm socket trả về, theo sau là một con trỏ tới cấu trúc sockaddr và độ dài của nó (xem phần " Địa chỉ một, địa chỉ hai").

Nói đúng ra, máy khách cũng phải liên kết ổ cắm với một địa chỉ cục bộ trước khi sử dụng nó, tuy nhiên, chức năng kết nối thực hiện điều này cho nó, liên kết ổ cắm với một trong các cổng được chọn ngẫu nhiên trong phạm vi 1024-5000. Máy chủ phải “ngồi” trên một cổng được xác định trước, ví dụ: 21 cho FTP, 23 cho telnet, 25 cho SMTP, 80 cho WEB, 110 cho POP3, v.v. Vì vậy, anh phải thực hiện việc ràng buộc “thủ công”.

Hàm trả về 0 nếu thành công, khác 0 nếu ngược lại.

Máy chủ: bước bốn - Sau khi hoàn tất việc liên kết, máy chủ phát trực tuyến sẽ chuyển sang chế độ chờ kết nối, gọi hàm " intNghe (SOCKET s, int tồn đọng)", Ở đâu S– mô tả ổ cắm, và tồn đọng– kích thước tối đa được phép của hàng đợi tin nhắn.

Kích thước hàng đợi giới hạn số lượng kết nối được xử lý đồng thời, vì vậy bạn nên chọn nó một cách khôn ngoan. Nếu hàng đợi đầy hoàn toàn, máy khách tiếp theo sẽ nhận được thông báo từ chối khi cố gắng thiết lập kết nối (gói TCP có đặt cờ RST). Đồng thời, số lượng kết nối tối đa hợp lý được xác định bởi hiệu suất máy chủ, dung lượng RAM, v.v.

Máy chủ datagram không gọi chức năng nghe vì hoạt động mà không cần thiết lập kết nối và có thể gọi ngay recvfrom để đọc tin nhắn đến ngay sau khi liên kết, bỏ qua bước thứ tư và thứ năm.

Máy chủ: bước năm– các yêu cầu kết nối được lấy từ hàng đợi bằng hàm " Ổ CẮMchấp nhận (SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen)", tự động tạo một ổ cắm mới, thực hiện liên kết và trả về tay cầm của nó và đưa vào cấu trúc sockaddr ghi lại thông tin về máy khách được kết nối (địa chỉ IP và cổng). Nếu hàng đợi trống khi chấp nhận được gọi, hàm sẽ không trả lại quyền điều khiển cho đến khi có ít nhất một kết nối được thiết lập với máy chủ. Nếu xảy ra lỗi, hàm sẽ trả về giá trị âm.

Để làm việc song song với một số máy khách, ngay sau khi xóa yêu cầu khỏi hàng đợi, hãy tạo ra một luồng (quy trình) mới, chuyển cho nó bộ mô tả ổ cắm được tạo bởi hàm chấp nhận, sau đó xóa lại yêu cầu tiếp theo khỏi hàng đợi, v.v. Nếu không, cho đến khi một máy khách hoàn thành, máy chủ sẽ không thể phục vụ tất cả những máy khách khác.

cùng nhau - Sau khi kết nối được thiết lập, các ổ cắm luồng có thể giao tiếp với máy chủ từ xa bằng cách gọi các hàm " intgửi (SOCKET s, const char FAR * buf, int len,int cờ)" Và " intrecv (SOCKET s, char FAR* buf, int len, cờ int)" để gửi và nhận dữ liệu tương ứng.

Chức năng gửi trả lại quyền kiểm soát ngay sau khi thực thi, bất kể bên nhận có nhận được dữ liệu của chúng tôi hay không. Khi hoàn thành thành công, hàm trả về số lượng truyền đi (không được truyền đi!) dữ liệu - tức là việc hoàn thành thành công không có nghĩa là đã phân phối thành công! Nói chung, TCP (mà các ổ cắm luồng dựa vào) đảm bảo phân phối dữ liệu thành công đến người nhận, nhưng chỉ khi kết nối không bị chấm dứt sớm. Nếu kết nối bị gián đoạn trước khi kết thúc quá trình truyền, dữ liệu sẽ vẫn chưa được truyền nhưng mã gọi điện sẽ không nhận được bất kỳ thông báo nào về việc này! Và lỗi chỉ được trả về nếu kết nối bị hỏng trước gọi hàm gửi!

Chức năng là recv chỉ trả lại quyền kiểm soát sau khi nhận được ít nhất một byte. Chính xác hơn, cô mong đợi sự xuất hiện của một gói dữ liệu. Một datagram là một tập hợp của một hoặc nhiều gói IP được gửi bằng một cuộc gọi gửi. Nói một cách đơn giản, mỗi cuộc gọi recv cùng một lúc sẽ nhận được số byte bằng với số byte được gửi bởi hàm gửi. Điều này giả định rằng hàm recv được cung cấp một bộ đệm đủ lớn, nếu không nó sẽ phải được gọi nhiều lần. Tuy nhiên, đối với tất cả các cuộc gọi tiếp theo, dữ liệu sẽ được lấy từ bộ đệm cục bộ và không được nhận từ mạng, bởi vì Nhà cung cấp TCP không thể nhận được một “phần” của datagram mà chỉ nhận được toàn bộ nó.

Cả hai chức năng đều có thể được điều khiển bằng cách sử dụng cờ, được truyền vào một biến kiểu int làm đối số thứ ba tính từ bên trái. Biến này có thể nhận một trong hai giá trị: bột ngọt _PEEKbột ngọt _OOB.

Cờ MSG_PEEK khiến hàm recv xem dữ liệu thay vì đọc dữ liệu. Việc xem, không giống như đọc, không hủy dữ liệu đang được xem. Một số nguồn cho biết rằng khi cờ MSG_PEEK được đặt, hàm recv không trì hoãn việc điều khiển nếu không có dữ liệu trong bộ đệm cục bộ để nhận ngay. Đây không phải là sự thật! Tương tự, đôi khi bạn gặp một tuyên bố sai hoàn toàn rằng hàm gửi với bộ cờ MSG_PEEK trả về số byte đã được truyền (lệnh gọi gửi không chặn điều khiển). Trong thực tế, chức năng gửi bỏ qua cờ này!

Cờ MSG_OOB dùng để truyền và nhận cấp bách (Ngoài ban nhạc) dữ liệu. Dữ liệu khẩn cấp không có lợi thế hơn những dữ liệu khác khi được gửi qua mạng mà chỉ cho phép bạn tách khách hàng khỏi quá trình xử lý thông thường đối với luồng dữ liệu thông thường và cung cấp cho anh ta thông tin “khẩn cấp”. Nếu dữ liệu được truyền bằng hàm gửi với cờ MSG_OOB được đặt thì để đọc dữ liệu đó, cờ MSG_OOB của hàm recv cũng phải được đặt.

Bình luận: Bạn nên hạn chế sử dụng dữ liệu nhạy cảm về thời gian trong ứng dụng của mình. Thứ nhất, chúng hoàn toàn không bắt buộc - thay vào đó, việc tạo một kết nối TCP riêng sẽ đơn giản hơn, đáng tin cậy hơn và thanh lịch hơn nhiều. Thứ hai, không có sự đồng thuận về việc thực hiện chúng và cách giải thích của các nhà sản xuất khác nhau rất khác nhau. Do đó, các nhà phát triển vẫn chưa đi đến thỏa thuận cuối cùng về vị trí mà con trỏ khẩn cấp sẽ trỏ: đến byte cuối cùng của dữ liệu khẩn cấp hoặc đến byte theo sau byte dữ liệu khẩn cấp cuối cùng. Kết quả là người gửi không bao giờ có thể chắc chắn rằng người nhận có thể hiểu chính xác yêu cầu của mình hay không.

Ngoài ra còn có cờ MSG_DONTROUTE, hướng dẫn dữ liệu được truyền mà không cần định tuyến, nhưng nó không được Winsock hỗ trợ và do đó không được thảo luận ở đây.

Ổ cắm datagram cũng có thể sử dụng các chức năng gửi và recv nếu lần đầu tiên nó gọi kết nối (xem phần " Khách hàng: bước chân ngày thứ ba"), nhưng nó cũng có các chức năng "cá nhân" riêng: " intgửi đến (SOCKET s, const char FAR * buf, int len,int flags, const struct sockaddr FAR * to, int tolen)" Và " intnhận được từ (SOCKET s, char FAR* buf, int len, int flags, struct sockaddr FAR* from, int FAR* fromlen)".

Chúng rất giống với send và recv, điểm khác biệt duy nhất là sendto và recvfrom yêu cầu chỉ báo rõ ràng về địa chỉ của nút để nhận hoặc truyền dữ liệu. Cuộc gọi recvfrom không yêu cầu cài đặt sơ bộ địa chỉ của nút gửi - chức năng chấp nhận tất cả các gói đến cổng UDP được chỉ định từ tất cả các địa chỉ IP và cổng. Ngược lại, người gửi phải trả lời cùng một cổng mà tin nhắn được gửi đến. Vì hàm recvfrom ghi địa chỉ IP và số cổng của máy khách sau khi nhận được tin nhắn từ nó, nên lập trình viên thực sự không cần phải làm gì khác ngoài việc chuyển send tới cùng một con trỏ tới cấu trúc sockaddr mà trước đó đã được chuyển cho hàm recvfrem. đã nhận được tin nhắn từ khách hàng.

Một chi tiết nữa– giao thức truyền tải UDP, mà các ổ cắm datagram dựa vào, không đảm bảo việc gửi tin nhắn thành công và nhiệm vụ này thuộc về chính nhà phát triển. Ví dụ, điều này có thể được giải quyết bằng cách khách hàng gửi xác nhận rằng dữ liệu đã được nhận thành công. Đúng, khách hàng cũng không thể chắc chắn rằng xác nhận sẽ đến máy chủ và sẽ không bị thất lạc ở đâu đó trên đường đi. Việc xác nhận đã nhận được xác nhận là vô nghĩa vì nó không thể quyết định được theo cách đệ quy. Tốt hơn hết là không nên sử dụng ổ cắm datagram trên các kênh không đáng tin cậy.

Về tất cả các khía cạnh khác, cả hai cặp hàm đều hoàn toàn giống nhau và hoạt động với cùng một cờ - MSG_PEEK và MSG_OOB.

Tất cả bốn hàm đều trả về SOCKET_ERROR (== -1) khi xảy ra lỗi.

Ghi chú: Trong UNIX, các socket có thể được xử lý giống hệt như các tệp thông thường, đặc biệt, chúng có thể được ghi và đọc bằng các hàm ghi và đọc. Windows 3.1 không hỗ trợ tính năng này, vì vậy khi chuyển các ứng dụng UNIX sang Windows, tất cả các lệnh gọi ghi và đọc phải được thay thế bằng gửi và recv tương ứng. Trong Windows 95 có cài đặt Windows 2.x, thiếu sót này đã được sửa chữa - giờ đây các bộ mô tả socket có thể được chuyển tới các hàm ReadFil, WriteFile, DuplicateHandle, v.v.

Bước sáu, cuối cùng– chức năng " nhằm mục đích đóng kết nối và hủy ổ cắm intổ cắm kín (Ổ cắm)", trả về giá trị null nếu thao tác hoàn tất thành công.

Trước khi thoát khỏi chương trình, bạn phải gọi hàm " intWSADọn dẹp (vô hiệu)" để khởi tạo lại thư viện WINSOCK và giải phóng các tài nguyên được ứng dụng này sử dụng. Chú ý : Việc chấm dứt một tiến trình bằng ExitProcess không tự động giải phóng tài nguyên socket!

Lưu ý: Các kỹ thuật nâng cao hơn để đóng kết nối là Giao thức TCP cho phép bạn đóng kết nối của một trong hai bên một cách có chọn lọc trong khi vẫn để bên kia hoạt động. Ví dụ: một máy khách có thể thông báo với máy chủ rằng nó sẽ không gửi bất kỳ dữ liệu nào cho nó nữa và đóng kết nối máy khách (máy chủ) đó, tuy nhiên, nó sẵn sàng tiếp tục nhận dữ liệu từ nó miễn là máy chủ gửi nó, tức là muốn rời khỏi kết nối "máy khách (máy chủ" mở.

Để làm điều này bạn cần gọi hàm "int tắt(SOCKET s ,int way)", chuyển vào đối số way một trong các giá trị sau: SD_RECEIVE để đóng kênh máy chủ (máy khách), SD_SEND để đóng kênh máy khách (máy chủ) và cuối cùng là SD_BOTH để đóng cả hai kênh.

Tùy chọn thứ hai so sánh thuận lợi với closesocket bằng cách đóng kết nối "mềm" - một thông báo sẽ được gửi đến nút từ xa rằng nó muốn đóng kết nối, nhưng mong muốn này sẽ không được thực hiện cho đến khi nút đó trả về xác nhận của nó. Bằng cách này, bạn không phải lo lắng rằng kết nối sẽ bị đóng vào thời điểm không thích hợp nhất.

Chú ý: gọi tắt máy không làm giảm nhu cầu đóng ổ cắm bằng chức năng closesocket!

Cây gọi

Để thể hiện rõ hơn mối quan hệ giữa các hàm socket với nhau, bên dưới là cây cuộc gọi hiển thị thứ tự các lệnh gọi hàm sẽ tuân theo tùy thuộc vào loại socket (streaming hoặc datagram) và loại xử lý yêu cầu (máy khách hoặc máy chủ).

máy chủ khách hàng

kết nối |-sendto TCP UDP

| |-recvfrom | |

|-gửi nghe |

|-gửi |-sendto

|-recv |-recvform

Liệt kê 29 Trình tự gọi đến các hàm socket cho các hoạt động khác nhau

Địa chỉ một, địa chỉ hai

Địa chỉ là nơi dễ gây nhầm lẫn nhất và sẽ không có hại gì nếu bạn làm rõ một chút. Trước hết, cấu trúc sockaddr được định nghĩa như sau:

u_short sa_family; // họ giao thức

// (thường là AF_INET)

char sa_data; // Địa chỉ IP và cổng máy chủ

Liệt kê 30 Định nghĩa cấu trúc sockaddr không được dùng nữa

Hiện không được dùng nữa, Winsock 2.x đã thay thế nó bằng cấu trúc sockaddr_in, được định nghĩa như sau:

cấu trúc sockaddr_in

sin_family ngắn; // họ giao thức

// (thường là AF_INET)

u_ngắn sin_port; // Hải cảng

cấu trúc in_addr sin_addr; // Địa chỉ IP

char sin_zero; // đuôi

Liệt kê 31 Định nghĩa hiện đại của cấu trúc sockaddr_in

Nói chung, không có gì thay đổi (và có đáng để rào vườn không?), việc thay thế một số nguyên ngắn không dấu bằng một số nguyên ngắn có dấu để biểu thị một họ giao thức không mang lại kết quả gì. Nhưng bây giờ địa chỉ nút được trình bày dưới dạng ba trường - sin_port (số cổng), sin_addr (địa chỉ IP của nút) và một “đuôi” gồm tám byte 0, còn lại từ mảng mười bốn ký tự sa_data . Nó dùng để làm gì? Thực tế là cấu trúc sockaddr không bị ràng buộc cụ thể với Internet và có thể hoạt động với các mạng khác. Địa chỉ của một số mạng yêu cầu nhiều hơn bốn byte để thể hiện chúng, vì vậy bạn phải dự trữ chúng!

Cấu trúc in_addr được định nghĩa như sau:

cấu trúc in_addr(

cấu trúc ( u_char s_b1,s_b2,s_b3,s_b4; ) S_un_b;

// Địa chỉ IP

cấu trúc ( u_short s_w1,s_w2; ) S_un_w;

// Địa chỉ IP

u_long S_addr; // Địa chỉ IP

Liệt kê 32 Xác định cấu trúc in_addr

Như bạn có thể thấy, nó bao gồm một địa chỉ IP được viết bằng ba “đội lốt” - một chuỗi bốn byte (S_un_b), một cặp từ hai byte (S_un_W) và một số nguyên dài (S_addr) - hãy chọn để nếm thử Nhưng đó là không phải là đơn giản! Nhiều chương trình, hướng dẫn kỹ thuật và thậm chí cả các bản demo đi kèm với Winsock SDK đề cập đến một thành viên "bí ẩn" của cấu trúc s_addr không được mô tả rõ ràng trong SDK! Ví dụ: đây là một dòng trong tệp "Simples.h": "local.sin_addr. s_addr= (!interface)?INADDR_ANY:inet_addr(interface);"

Nó là gì?! Nhìn vào tệp "winsock2.h", bạn có thể tìm thấy thông tin sau: "#define s_addr S_un.S_addr". Đúng, nhưng cái này tương đương với s_addr, tức là Địa chỉ IP được viết dưới dạng số nguyên dài!

Trong thực tế, bạn có thể sử dụng cả sockaddr “lỗi thời” và sockaddr_in “mới” với mức độ thành công như nhau. Tuy nhiên, do nguyên mẫu của các hàm khác không thay đổi nên khi sử dụng sockaddr_in, bạn sẽ phải liên tục thực hiện các chuyển đổi rõ ràng, ví dụ như thế này: " sockaddr_in dest_addr; kết nối (mysocket, (cấu trúc sockaddr* ) &dest_addr, sizeof(dest_addr)".

Để chuyển đổi địa chỉ IP được viết dưới dạng chuỗi ký tự như "127.0.0.1" thành chuỗi số 4 byte, hãy sử dụng " dài không dấuinet_addr (const char FAR * cp)". Nó nhận một con trỏ tới một chuỗi ký tự và nếu thành công, sẽ chuyển đổi nó thành địa chỉ IP 4 byte hoặc -1 nếu điều này là không thể. Kết quả mà hàm trả về có thể được gán cho một phần tử của cấu trúc sockaddr_in như sau: " cấu trúc sockaddr_in dest_addr; dest_addr.sin_addr.S_addr=inet_addr("195.161.42.222");". Sử dụng cấu trúc sockaddr nó sẽ trông như thế này: " cấu trúc sockaddr Dest_addr; ((unsigned int *)(&dest_addr.sa_data+2)) = inet_addr("195.161.42.222");"

Nỗ lực chuyển inet_addr một tên miền máy chủ không thành công. Bạn có thể tìm ra địa chỉ IP của tên miền đó bằng cách sử dụng chức năng " cấu trúc máy chủ FAR *gethostbyname (const char FAR * tên);". Hàm truy cập DNS và trả về phản hồi của nó trong cấu trúc máy chủ hoặc null nếu máy chủ DNS không thể xác định địa chỉ IP của miền này.

Cấu trúc máy chủ trông như thế này:

char FAR * h_name; // tên nút chính thức

char FAR * FAR * h_aliases; // Tên khác

// nút (mảng chuỗi)

h_addrtype ngắn; // kiểu địa chỉ

h_length ngắn; // độ dài địa chỉ

// (thường là AF_INET)

char XA * XA * h_addr_list; // danh sách con trỏ

// tới địa chỉ IP

// số 0 là cuối danh sách

Liệt kê 33 Xác định cấu trúc máy chủ

Giống như in_addr, nhiều chương trình và ví dụ đi kèm với Winsock SDK sử dụng rộng rãi trường cấu trúc h_addr không có giấy tờ. Ví dụ: đây là một dòng trong tệp "simplec.c" "memcpy(&(server.sin_addr),hp->h_addr ,hp->h_length);" Nhìn vào "winsock2.h" bạn có thể tìm thấy ý nghĩa của nó: " #define h_addr h_addr_list".

Bây giờ điều này thật thú vị! Thực tế là với một số Tên miền Một số địa chỉ IP được liên kết cùng một lúc. Nếu một nút bị lỗi, khách hàng có thể thử kết nối với nút khác hoặc chỉ cần chọn một nút có tỷ giá hối đoái cao nhất. Nhưng trong ví dụ trên, máy khách chỉ sử dụng địa chỉ IP đầu tiên trong danh sách và bỏ qua tất cả những địa chỉ khác! Tất nhiên, điều này không gây tử vong, nhưng sẽ tốt hơn nếu trong các chương trình của bạn, bạn tính đến khả năng kết nối với các địa chỉ IP khác nếu không thể thiết lập kết nối với địa chỉ đầu tiên.

Hàm gethostbyname mong đợi đầu vào chỉ một tên miền, nhưng không phải địa chỉ IP kỹ thuật số. Trong khi đó, các quy tắc về “hình thức tốt” yêu cầu khách hàng có cơ hội chỉ định cả tên miền và địa chỉ IP kỹ thuật số.

Giải pháp như sau - cần phải phân tích chuỗi được gửi bởi máy khách - nếu đó là địa chỉ IP thì chuyển nó đến hàm inet_addr, nếu không - gethostbyaddr, giả sử rằng đó là một tên miền. Để phân biệt địa chỉ IP với tên miền, nhiều lập trình viên sử dụng một mẹo đơn giản: nếu ký tự đầu tiên của dòng là số thì đó là địa chỉ IP, nếu không thì là tên miền. Tuy nhiên, thủ thuật này không hoàn toàn trung thực - tên miền có thể bắt đầu bằng một số, ví dụ: “666.ru”, chúng cũng có thể kết thúc bằng một số, ví dụ: các thành viên của tên miền phụ “666” có thể đánh địa chỉ nút “666 .ru” là “666” ". Điều buồn cười là (về lý thuyết) có thể có những tên miền không thể phân biệt được về mặt cú pháp với địa chỉ IP! Vì vậy, theo ý kiến ​​của tác giả bài viết, tốt nhất nên hành động theo cách này: chúng ta chuyển chuỗi do người dùng nhập vào vào hàm inet_addr, nếu nó trả về lỗi thì chúng ta gọi gethostbyaddr.

Để giải bài toán ngược – xác định tên miền theo địa chỉ IP, hàm “ struct HOSTENT FAR *gethostbyaddr (const char FAR * addr, int len, kiểu int)", hoàn toàn giống với gethostbyname, ngoại trừ đối số của nó không phải là một con trỏ tới một chuỗi chứa tên mà là một con trỏ tới địa chỉ IP bốn byte. Hai đối số nữa chỉ định độ dài và loại của nó (tương ứng là 4 và AF_INET ).

Việc xác định tên máy chủ theo địa chỉ của nó có thể hữu ích cho các máy chủ muốn biết trực tiếp về khách hàng của họ.

Để chuyển đổi địa chỉ IP được viết ở định dạng mạng thành chuỗi ký tự, hàm " ký tự FAR *inet _ ntoa (cấu trúc in_addr)", lấy cấu trúc in_addr làm đầu vào và trả về một con trỏ tới một chuỗi nếu chuyển đổi thành công và bằng 0 nếu ngược lại.

Thứ tự byte mạng

Winsock dành cho mọi người (phần 1)

Vậy Winsock là gì và nó ăn với cái gì? Tóm lại, Winsock là một giao diện giúp đơn giản hóa việc phát triển các ứng dụng mạng trong Windows. Tất cả những gì chúng ta cần biết là Winsock là giao diện giữa ứng dụng và giao thức truyền tải thực hiện việc truyền dữ liệu.

Chúng ta đừng đi vào chi tiết kiến trúc nội thất, bởi vì chúng tôi không quan tâm đến cách nó được cấu trúc bên trong mà quan tâm đến cách sử dụng các chức năng mà Winsock cung cấp cho người dùng cho công việc. Nhiệm vụ của chúng ta là tìm hiểu cơ chế hoạt động của WinsockAPI bằng các ví dụ cụ thể. "Cái này có thể dùng để làm gì? Chắc chắn có những thư viện giúp đơn giản hóa việc làm việc với mạng và có giao diện đơn giản?" - bạn hỏi. Tôi một phần đồng ý với nhận định này, nhưng theo tôi, không thể tồn tại những thư viện hoàn toàn phổ quát hướng đến mọi nhiệm vụ. Và bên cạnh đó, sẽ dễ chịu hơn nhiều khi bạn tự mình tìm ra mọi thứ mà không cảm thấy lúng túng trước một “hộp đen” mà bạn không hiểu nguyên lý hoạt động mà chỉ sử dụng như một công cụ :) Tất cả tài liệu đều được thiết kế cho người mới bắt đầu. Tôi nghĩ sẽ không có vấn đề gì khi làm chủ nó. Nếu bạn vẫn còn thắc mắc, hãy viết thư cho [email được bảo vệ]. Tôi sẽ trả lời mọi người. Để minh họa các ví dụ, chúng tôi sẽ sử dụng các đoạn mã Microsoft VC++. Vậy hãy bắt đầu!

Winsock - bắt đầu từ đâu?

Vì vậy, câu hỏi đầu tiên là - nếu có Winsock thì sử dụng nó như thế nào? Trong thực tế, mọi thứ không quá phức tạp. Giai đoạn một - kết nối thư viện và tiêu đề.

#include "winsock.h" hoặc #include "winsock2.h" - tùy thuộc vào phiên bản Winsock bạn sẽ sử dụng
Ngoài ra, tất cả các tệp lib tương ứng (Ws2_32.lib hoặc Wsock32.lib) phải được đưa vào dự án

Bước 2 - khởi tạo.

Bây giờ chúng ta có thể sử dụng các hàm WinsockAPI một cách an toàn. ( danh sách đầy đủ các hàm có thể được tìm thấy trong các phần có liên quan của MSDN).

Để khởi tạo Winsock, hãy gọi hàm WSAStartup

int WSAStartup(WORD wVersionRequested, (in) LPWSADATA lpWSAData (out));


Tham số WORD wVersionRequested - byte thấp - phiên bản, byte cao - lật đổ, giao diện Winsock. Các phiên bản có thể là 1.0, 1.1, 2.0, 2.2... Để “lắp ráp” tham số này chúng ta sử dụng macro MAKEWORD. Ví dụ: MAKEWORD (1, 1) - phiên bản 1.1. Các phiên bản sau này được phân biệt bởi sự hiện diện của các chức năng mới và cơ chế mở rộng. Tham số lpWSAData là một con trỏ tới cấu trúc WSADATA. Khi trả về từ một hàm, cấu trúc này chứa thông tin về phiên bản WinsockAPI mà chúng ta đã khởi tạo. Về nguyên tắc thì bạn có thể bỏ qua, nhưng nếu ai quan tâm đến nội dung bên trong thì đừng lười biếng, hãy mở tài liệu ra;)

Đây là những gì nó trông giống như trong thực tế:

WSADATA ws;
//...
if (THẤT BẠI (WSAStartup (MAKEWORD(1, 1), &ws)))
{
// Lỗi...
lỗi = WSAGetLastError();
//...
}


Trong trường hợp này, bạn có thể nhận thông tin lỗi mở rộng bằng cách gọi WSAGetLastError(). Hàm này trả về mã lỗi (kiểu int)

Bước 3 - tạo một ổ cắm.

Vì vậy, chúng ta có thể chuyển sang giai đoạn tiếp theo - tạo phương tiện liên lạc chính trong Winsock - ổ cắm. Từ góc độ WinsockAPI, ổ cắm là một tay cầm có thể nhận hoặc gửi dữ liệu. Trong thực tế, mọi thứ trông như thế này: chúng ta tạo một ổ cắm với các thuộc tính nhất định và sử dụng nó để kết nối, nhận/truyền dữ liệu, v.v. Bây giờ chúng ta hãy thực hiện một sự lạc đề nhỏ... Vì vậy, khi tạo một ổ cắm, chúng ta phải chỉ định các tham số của nó: ổ cắm sử dụng giao thức TCP/IP hoặc IPX (nếu là TCP/IP thì loại nào, v.v.). Vì các phần sau của bài viết này sẽ tập trung vào giao thức TCP/IP nên chúng tôi sẽ tập trung vào các tính năng của socket sử dụng giao thức này. Chúng ta có thể tạo hai loại ổ cắm chính hoạt động bằng giao thức TCP/IP - SOCK_STREAM và SOCK_DGRAM (hiện tại chúng ta sẽ để lại ổ cắm RAW :)). Điểm khác biệt là đối với loại socket đầu tiên (chúng còn được gọi là TCP hoặc socket dựa trên kết nối), để gửi dữ liệu, socket phải liên tục duy trì kết nối với người nhận, trong khi việc truyền gói tin đến người nhận được đảm bảo. . Trong trường hợp thứ hai, không cần kết nối cố định, nhưng không thể lấy được thông tin về việc gói có đến hay không (được gọi là UDP hoặc ổ cắm không kết nối). Cả loại ổ cắm thứ nhất và thứ hai đều có những ứng dụng thực tế. Hãy bắt đầu làm quen với các ổ cắm có ổ cắm TCP (dựa trên kết nối).

Đầu tiên chúng ta hãy khai báo nó:

Bạn có thể tạo một ổ cắm bằng chức năng ổ cắm

Ổ cắm SOCKET (int af (in), // giao thức (TCP/IP, IPX...)
kiểu int (trong), // kiểu ổ cắm (SOCK_STREAM/SOCK_DGRAM)
giao thức int (trong) // cho Ứng dụng Windows có lẽ 0
);


Ví dụ:

if (INVALID_SOCKET == (s = ổ cắm (AF_INET, SOCK_STREAM, 0)))
{
// Lỗi...
lỗi = WSAGetLastError();
// ...
}


Nếu có lỗi, hàm trả về INVALID_SOCKET. Trong trường hợp này, bạn có thể nhận thông tin lỗi mở rộng bằng cách gọi WSAGetLastError().

Bước 4 - thiết lập kết nối.

Trong ví dụ trước, chúng ta đã tạo một socket. Chúng ta nên làm gì với nó bây giờ? :) Bây giờ chúng ta có thể sử dụng ổ cắm này để trao đổi dữ liệu với các máy khách winock khác và hơn thế nữa. Để thiết lập kết nối với máy khác, bạn cần biết địa chỉ IP và cổng của nó. Máy từ xa phải "lắng nghe" cổng này để biết các kết nối đến (tức là nó hoạt động như một máy chủ). Trong trường hợp này, ứng dụng của chúng tôi là một máy khách.

Để thiết lập kết nối, hãy sử dụng chức năng kết nối.

int connect(SOCKET s, // socket (socket của chúng ta)
const struct sockaddr FAR *name, // địa chỉ
int namelen // độ dài địa chỉ
);


Ví dụ:

//Khai báo một biến để lưu trữ địa chỉ
sockaddr_in s_addr;

// Điền vào:
ZeorMemory(&s_addr, sizeof(s_addr));
// loại địa chỉ (TCP/IP)
s_addr.sin_family = AF_INET;
//địa chỉ máy chủ. Bởi vì TCP/IP biểu thị địa chỉ ở dạng số, sau đó được dịch
// các địa chỉ sử dụng hàm inet_addr.
s_addr.sin_addr.S_un.S_addr = inet_addr("193.108.128.226");
// Hải cảng. Chúng tôi sử dụng hàm htons để chuyển đổi số cổng từ cổng thông thường sang biểu diễn //TCP/IP.
s_addr.sin_port = htons(1234);


Nếu có lỗi, hàm trả về SOCKET_ERROR.
Bây giờ socket s đã được liên kết với máy từ xa và chỉ có thể gửi/nhận dữ liệu từ nó.

Bước 5 - gửi dữ liệu.

Để gửi dữ liệu chúng ta sử dụng chức năng gửi

int send(SOCKET s, // ổ cắm gửi
const char FAR *buf, // con trỏ tới bộ đệm chứa dữ liệu
int len, // độ dài dữ liệu
);


Ví dụ về việc sử dụng chức năng này:

if (SOCKET_ERROR == (gửi (s, (char*) & buff), 512, 0))
{
// Lỗi...
lỗi = WSAGetLastError();
// ...
}


Nếu có lỗi, hàm trả về SOCKET_ERROR.
Độ dài của gói dữ liệu bị giới hạn bởi chính giao thức. Chúng ta sẽ xem cách tìm ra độ dài tối đa của gói dữ liệu vào lần tới. Hàm không trả về cho đến khi dữ liệu được gửi.

Bước 6 - chấp nhận dữ liệu.

Chức năng recv cho phép chúng tôi nhận dữ liệu từ máy mà chúng tôi đã thiết lập kết nối trước đó.

int recv(SOCKET s, // ổ cắm người nhận
char FAR *buf, // địa chỉ bộ đệm để nhận dữ liệu
int len, // độ dài vùng đệm nhận dữ liệu
cờ int // cờ (có thể là 0)
);


Nếu bạn không biết trước kích thước của dữ liệu đến thì độ dài của bộ đệm nhận không được nhỏ hơn Kích thước tối đa gói, nếu không tin nhắn có thể không vừa với gói đó và sẽ bị cắt bỏ. Trong trường hợp này, hàm trả về lỗi.
Ví dụ:

int thực tế_len = 0;

Nếu (SOCKET_ERROR == (actual_len = recv (s, (char*) & buff), max_packet_size, 0))
{
// Lỗi...
lỗi = WSAGetLastError();
// ...
}


Nếu dữ liệu được nhận thì hàm trả về kích thước của gói dữ liệu đã nhận (trong ví dụ -actual_len). Nếu có lỗi, hàm sẽ trả về SOCKET_ERROR. Lưu ý rằng các chức năng gửi/recv sẽ đợi cho đến khi hết thời gian chờ hoặc gói dữ liệu được gửi/nhận. Điều này dẫn đến sự chậm trễ trong hoạt động của chương trình. Đọc cách tránh điều này trong các số tiếp theo.

Bước 6 - đóng kết nối.

Quy trình đóng một kết nối đang hoạt động xảy ra bằng cách sử dụng chức năng tắt và đóng ổ cắm. Có hai loại đóng kết nối: hủy bỏ và duyên dáng. Loại thứ nhất là đóng ổ cắm khẩn cấp (closesocket). Trong trường hợp này, kết nối bị hỏng ngay lập tức. Việc gọi closesocket có tác dụng ngay lập tức. Sau khi gọi closesocket, ổ cắm không thể truy cập được nữa. Cách đóng ổ cắm bằng cách sử dụng tắt máy/closesocket sẽ được đọc trong các số báo sau, vì chủ đề này yêu cầu kiến ​​thức đầy đủ hơn về Winsock.

intshutdown(SOCKET s, // Ổ cắm cần đóng
int thế nào // Phương thức đóng
);


int closesocket(SOCKET s // Ổ cắm đã đóng
);


Ví dụ:

(các) ổ cắm gần;

Như bạn có thể thấy, cơ chế trao đổi dữ liệu Winsock mà chúng tôi xem xét rất đơn giản. Lập trình viên chỉ được yêu cầu phát triển “giao thức” của riêng mình để liên lạc giữa các máy từ xa và triển khai nó bằng các chức năng này. Tất nhiên, các ví dụ chúng tôi xem xét không phản ánh tất cả các khả năng của Winsock. Trong các bài viết của chúng tôi, chúng tôi sẽ cố gắng xem xét các tính năng quan trọng nhất khi làm việc với Winsock. Giữ nguyên. :)
Đọc ở số tiếp theo:

  • Chúng tôi đang viết một ứng dụng winock đơn giản.
  • Ổ cắm UDP - nhận/phân phối các gói không bảo đảm
  • Chúng tôi giải quyết vấn đề "chặn" ổ cắm.

Lý thuyết đủ rồi, cho tôi WinSock!

Vì vậy, hiện nay có hai phiên bản WinSock: 1.1 và 2. Tôi khuyên bạn nên sử dụng phiên bản thứ hai. Chúng ta sẽ làm gì tiếp theo? Chúng tôi sẽ viết phiên bản máy khách-máy chủ của trò chơi "Rock-Paper-Scissors". Máy chủ sẽ là một ứng dụng bảng điều khiển đa luồng, máy khách sẽ được viết bằng DirectX.

Để bắt đầu, tôi sẽ chỉ cho bạn cách tổ chức WinSock, giải thích các cách khác nhau để lập trình socket và mô tả các chức năng được sử dụng để thực hiện việc đó. Sau khi xử lý xong WinSock, chúng ta sẽ dùng nó để viết món đồ chơi mà tôi đã đề cập ở trên.

Để sử dụng WinSock, bạn phải bao gồm tệp tiêu đề thích hợp và thêm ws2_32.libđến dự án của bạn. Bây giờ mọi thứ đã sẵn sàng để lập trình trong WinSock. Nhưng nó hoạt động như thế nào và bắt đầu từ đâu?

Có một số cách để lập trình socket. bạn có thể dùng chức năng cơ bản UNIX/Berkley hoặc các chức năng dành riêng cho Microsoft Windows hoặc sử dụng phiên bản ổ cắm MFC hướng đối tượng. Lúc đầu, tôi muốn sử dụng phiên bản OO của ổ cắm, bởi vì... Tôi nghĩ các lớp làm cho API dễ hiểu hơn. Nhưng đây không chỉ là các lớp, đây còn là MFC. “Làm càng khó càng tốt” là khẩu hiệu của họ. Không, MFC rất tuyệt, nhưng việc bạn phải tạo một ứng dụng Win32 và xử lý các thông báo ổ cắm Windows được gửi tới chương trình của bạn thật là khó chịu, đặc biệt là trong trường hợp máy chủ. Tại sao chúng ta cần tạo máy chủ dưới dạng ứng dụng Win32? Nó là vô nghĩa. Để làm mọi việc dễ dàng hơn, chúng ta sẽ sử dụng các chức năng máy chủ UNIX/Berkley cơ bản nhất.

Loại dữ liệu.

Sockaddr_in(sockaddr)

Sự miêu tả: kiểu sockaddr_inđược sử dụng để mô tả một kết nối qua ổ cắm. Nó cũng chứa địa chỉ IP và số cổng. Đây là phiên bản hướng TCP sockaddr. Chúng tôi sẽ sử dụng sockaddr_inđể tạo socket.

struct sockaddr_in (viết tắt sin_family; // loại giao thức (phải là AF_INET) u_ngắn sin_port; // Số cổng ổ cắm cấu trúc in_addr sin_addr; // Địa chỉ IP char sin_zero[ 8 ] ; // không được sử dụng };

Sự miêu tả: Dữ liệu WSAđược sử dụng khi bạn tải và khởi tạo thư viện ws2_32.dll. Loại này được sử dụng làm giá trị trả về của hàm WSAStartup(). Sử dụng nó để xác định phiên bản WinSock trên máy tính của bạn.

Sự miêu tả: Kiểu dữ liệu này được sử dụng để lưu trữ bộ mô tả ổ cắm. Những mô tả này được sử dụng để xác định ổ cắm. Thực ra SOCKET chỉ là một int không dấu.

Nào, hãy bắt đầu lập trình

Đầu tiên bạn cần tải ws2_32.dll:

// Mã này phải có trong bất kỳ chương trình nào sử dụng WinSock WSADATA w; // dùng để lưu trữ thông tin phiên bản socket lỗi int = WSAStartup (0x0202, &w); // điền w if (lỗi) ( // một số lỗi trở lại ; ) if (w.wVersion != 0x0202 ) (// phiên bản sai của ổ cắm! WSACleanup();// dỡ bỏ ws2_32.dll

trở lại ;

)

Có lẽ bạn có câu hỏi - 0x0202 nghĩa là gì? Điều này có nghĩa là phiên bản 2.2. Nếu cần phiên bản 1.1 thì số này phải được đổi thành 0x0101. WSAStartup() điền biến WSADATA và tải thư viện ổ cắm động. WSACleanup(), theo đó, sẽ dỡ nó xuống.

Tạo một ổ cắm trói buộc) với một số cổng khi bạn muốn trực tiếp bắt đầu làm việc với nó. Hằng số AF_INET được xác định ở đâu đó trong winock2.h. Nếu một số chức năng yêu cầu bạn thực hiện một số việc như họ địa chỉ ( địa chỉ gia đình) hoặc int af, chỉ cần chỉ định AF_INET. Hằng số SOCK_STREAM là cần thiết để tạo ổ cắm phát trực tuyến (TCP/IP). Bạn cũng có thể tạo ổ cắm CẬP NHẬT, nhưng như đã nêu ở trên, nó không đáng tin cậy bằng TCP. Giá trị của tham số cuối cùng sẽ bằng 0. Điều này chỉ có nghĩa là giao thức đúng sẽ được tự động chọn cho bạn (phải là TCP/IP).

Chúng tôi bổ nhiệm cổng mong muốn vào một ổ cắm (chúng tôi liên kết một cổng và một ổ cắm):

// Nhớ! Bạn chỉ nên liên kết ổ cắm máy chủ, không phải máy khách // Hàm WSAStartup được gọi sockaddr_in addr; // biến cho socket TCP addr.sin_family = AF_INET; // Họ địa chỉ - Internet addr.sin_port = htons(5001); // Gán cổng 5001 cho socket addr.sin_addr.s_addr = htonl (INADDR_ANY) ; // Không có địa chỉ cụ thể if (bind(s, (LPSOCKADDR) &addr, sizeof (addr) ) == SOCKET_ERROR) ( // Lỗi WSACleanup () ; // dỡ tải WinSock return ; )

Mã này có thể có vẻ khó hiểu, nhưng không phải vậy. địa chỉ mô tả một ổ cắm, chỉ định cổng. Nhưng câu hỏi có thể được đặt ra: “Còn địa chỉ IP thì sao?” Chúng tôi đặt nó là INADDR_ANY. Điều này sẽ cho phép chúng tôi không phải lo lắng về một địa chỉ cụ thể. Chúng tôi chỉ cần cho biết số cổng mà chúng tôi muốn sử dụng để kết nối. Tại sao chúng ta sử dụng htons() và htonl()? Các hàm này chuyển đổi các biến loại ngắn và dài tương ứng thành định dạng mà mạng có thể hiểu được. Ví dụ: nếu số cổng là 7134 (một số ngắn), thì bạn cần gọi hàm htons(7134). Đối với địa chỉ IP, chúng ta phải sử dụng htonl(). Nhưng nếu chúng ta thực sự muốn đặt địa chỉ IP thì sao? Chúng ta phải sử dụng hàm inet_addr(). Ví dụ: inet_addr("129.42.12.241"). Hàm này chuyển đổi một chuỗi địa chỉ, loại bỏ các dấu chấm khỏi nó và chuyển nó thành một kiểu dài.

Nghe cổng liên quan ( nghe cổng)

// WSAStartup() được gọi // SOCKET s đã trỏ tới socket được tạo if (nghe(s,5 ) ==SOCKET_ERROR) ( // lỗi! Nghe là không thể WSACleanup(); trở lại ; ) //Nghe:

Ở đây chúng tôi đã chấp nhận kết nối từ một khách hàng muốn tham gia trò chơi. Có một cái gì đó thú vị khác trong dòng Nghe(SOCKET s, int tồn đọng). Chuyện gì đã xảy ra vậy tồn đọng? Tồn đọng là số lượng máy khách có thể kết nối trong khi chúng tôi đang sử dụng ổ cắm, tức là. những máy khách này sẽ phải đợi trong khi máy chủ đàm phán kết nối với các máy khách khác. Ví dụ: nếu bạn chỉ định 5 là tồn đọng và 7 người cố gắng tham gia thì 2 người cuối cùng sẽ nhận được thông báo lỗi và sẽ buộc phải thiết lập kết nối sau đó. Thông thường, thông số này dao động từ 2 đến 10, tùy thuộc vào dung lượng tối đa của máy chủ.

Đang cố gắng kết nối với ổ cắm ( thử và kết nối ổ cắm)

// WSAStartup() được gọi // SOCKET s đã trỏ tới socket được tạo // s được liên kết với cổng bằng sockaddr_in. mục tiêu sockaddr_in; target.sin_family = AF_INET; // họ địa chỉ - Internet target.sin_port = htons(5001); // cổng máy chủ target.sin_addr.s_addr = inet_addr ("52 .123 .72 .251 ") ; // Địa chỉ IP máy chủ if (kết nối, mục tiêu, sizeof (đích) ) == SOCKET_ERROR) ( // Lỗi kết nối WSACleanup(); trở lại ; )

Về cơ bản, đó là tất cả những gì cần có đối với một yêu cầu kết nối. Biến đổi mục tiêu biểu thị ổ cắm mà chúng tôi đang cố gắng kết nối với (máy chủ). Hàm connect() yêu cầu một socket (phía máy khách), mô tả về socket kết nối (phía máy chủ) và kích thước của biến mô tả đó. Chức năng này chỉ đơn giản là gửi yêu cầu kết nối và chờ phản hồi từ máy chủ, đồng thời báo cáo bất kỳ lỗi nào xảy ra.

Nhận kết nối ( chấp nhận kết nối)

// WSAStartup() được gọi // SOCKET s đã trỏ tới socket được tạo // s được liên kết với cổng bằng sockaddr_in. // socket s đang lắng nghe#define MAX_CLIENTS 5 ; // chỉ để cho rõ ràng int number_of_clients = 0 ; Máy khách SOCKET [ MAX_CLIENTS] ; // ổ cắm máy khách sockaddr client_sock[ MAX_CLIENTS] ; // mô tả socket client trong khi (number_of_clients< MAX_CLIENTS) // MAX_CLIENTS máy khách có được kết nối không?( khách hàng[ number_of_clients] = // chấp nhận yêu cầu kết nối chấp nhận (s, client_sock[ number_of_clients] , &addr_size) ; if (client[ number_of_clients] == INVALID_SOCKET) ( // Lỗi kết nối WSACleanup(); trở lại ; ) khác ( // khách hàng đã tham gia thành công// bắt đầu một thread để giao tiếp với client

startThread (client[ number_of_clients] ); số_of_clients++; ) ) Nói chung, mọi thứ đều rõ ràng ở đây. Số lượng khách hàng không cần phải được chỉ định bằng biểu thức MAX_CLIENTS. Nó chỉ được sử dụng ở đây để cải thiện sự rõ ràng và dễ hiểu của mã này. số_of_khách hàng sockaddr, chứa thông tin về loại kết nối, số cổng, v.v. Thông thường, chúng ta không thực sự cần nó, mặc dù một số chức năng yêu cầu mô tả kết nối dưới dạng tham số. Vòng lặp chính chỉ chờ yêu cầu kết nối, sau đó chấp nhận nó và bắt đầu một luồng để giao tiếp với máy khách.

Đang gửi dữ liệu ( viết hoặc gửi)

// SOCKET s được khởi tạo bộ đệm char[11] ; // bộ đệm gồm 11 ký tự sprintf(buffer, "Sao cũng được:"); gửi (s, bộ đệm, sizeof (bộ đệm), 0);

Tham số thứ hai của hàm send() là một biến kiểu char FAR *buf, là một con trỏ tới bộ đệm chứa dữ liệu mà chúng ta muốn gửi. Tham số thứ ba là kích thước của bộ đệm được gửi. Tham số cuối cùng là để thiết lập các cờ khác nhau. Chúng tôi sẽ không sử dụng nó và sẽ để nó ở mức 0.

Nhận dữ liệu ( đọc hoặc nhận)

// SOCKET s được khởi tạo bộ đệm char[80] ; // bộ đệm 80 ký tự recv(s, buffer, sizeof(buffer) , 0 );

recv(), phần lớn tương tự như gửi(), ngoại trừ việc chúng tôi không truyền dữ liệu mà nhận dữ liệu.

Chuyển đổi địa chỉ chuỗi (địa chỉ IP hoặc tên máy chủ) thành địa chỉ số được sử dụng khi kết nối ( giải quyết địa chỉ IP).

Thư viện mạng Winsock

Làm việc với mạng thông qua các thành phần delphi rất thuận tiện và khá đơn giản, nhưng lại quá chậm. Điều này có thể được khắc phục bằng cách truy cập trực tiếp vào thư viện cửa sổ mạng - winock. Hôm nay chúng ta sẽ làm quen với những điều cơ bản của nó.

winock là gì

Thư viện winock chỉ bao gồm một tệp, winock.dll. Nó rất phù hợp để tạo các ứng dụng đơn giản vì nó cung cấp tất cả các chức năng cần thiết để tạo kết nối và nhận/truyền tệp. Nhưng thậm chí đừng cố gắng tạo ra một sniffer. Không có gì trong winock để truy cập các tiêu đề gói. ms đã hứa sẽ tích hợp những thứ cần thiết này cho người có trình độ cao vào winock2, nhưng, như mọi khi, nó khiến chúng tôi phải vất vả giấy nhám và nói, được rồi, chúng ta sẽ vượt qua được. Điều tuyệt vời ở thư viện này là tất cả các chức năng của nó đều giống nhau trên nhiều nền tảng và ngôn ngữ lập trình. Vì vậy, ví dụ, nếu chúng ta viết một trình quét cổng, nó có thể dễ dàng được chuyển sang ngôn ngữ C/C++ và thậm chí được viết một cái gì đó tương tự trong *nix, bởi vì ở đó các chức năng mạng được gọi giống nhau và có các tham số gần như giống nhau. Sự khác biệt giữa mạng thư viện cửa sổ và linux là tối thiểu, mặc dù nó tồn tại. Nhưng lẽ ra phải như thế này, bởi vì Bill không thể hành động như một con người, và anh ấy nhất định phải thể hiện. Tôi sẽ cảnh báo ngay với bạn rằng chúng ta sẽ nghiên cứu winock2 và delphi chỉ hỗ trợ phiên bản đầu tiên. Để anh ấy có thể nhìn thấy cái thứ hai, anh ấy cần tải xuống tập tin tiêu đềđối với phiên bản 2, chúng có thể được tìm thấy trên Internet. Toàn bộ công việc của thư viện mạng được xây dựng xoay quanh khái niệm socket - đây giống như một kênh mạng ảo. Để kết nối với máy chủ, bạn phải chuẩn bị một kênh như vậy để làm việc và sau đó bạn có thể kết nối với bất kỳ cổng nào của tủ bên. Tất cả điều này được thấy rõ nhất trong thực tế, nhưng bây giờ tôi sẽ cố gắng cung cấp cho bạn một thuật toán chung để làm việc với socket:
1. Khởi tạo thư viện winock.
2. Khởi tạo ổ cắm (kênh liên lạc). Sau khi khởi tạo, chúng ta sẽ có một biến trỏ đến kênh mới. Có thể nói, ổ cắm được tạo là một cổng mở trên máy tính của bạn. Có các cổng không chỉ trên tủ bên mà còn trên cổng của bạn và khi quá trình truyền dữ liệu diễn ra giữa các máy tính, nó sẽ xảy ra giữa các cổng mạng.
3. Bạn có thể tham gia máy chủ. Trong mỗi chức năng làm việc với mạng, tham số đầu tiên phải là biến chỉ ra kênh đã tạo mà qua đó kết nối sẽ diễn ra.

Hãy bắt đầu winock

Điều đầu tiên bạn cần làm là khởi động thư viện (đối với Unixoid thì không cần phải làm điều này). Để làm điều này, bạn cần gọi hàm wsastartup. Nó có hai tham số:
- Phiên bản winock chúng ta muốn bắt đầu. Đối với phiên bản 1.0, bạn cần chỉ định makeword(1.0), nhưng chúng ta cần cái thứ hai, nghĩa là chúng ta sẽ chỉ định makeword(2.0).
- Cấu trúc kiểu twsadata, trong đó thông tin về winock tìm thấy sẽ được trả về.
Bây giờ chúng ta sẽ học cách đóng thư viện. Để thực hiện việc này, bạn cần gọi hàm wsacleanup, hàm này không có tham số. Về nguyên tắc, nếu bạn không đóng winock thì sẽ không có gì nghiêm trọng xảy ra. Sau khi thoát khỏi chương trình, mọi thứ sẽ tự đóng lại; chỉ cần loại bỏ những thứ không cần thiết ngay sau khi sử dụng là một cách tốt trong việc viết mã.

Ví dụ đầu tiên

Hãy viết ngay một ví dụ khởi tạo winock và hiển thị thông tin về nó. Tạo một dự án mới trong delphi. Bây giờ bạn cần kết nối các tệp tiêu đề winock phiên bản 2 với nó. Để thực hiện việc này, hãy chuyển đến phần sử dụng và thêm mô-đun winock2 vào đó. Nếu bạn cố gắng biên dịch dự án trống này ngay bây giờ, delphi sẽ phàn nàn về mô-đun được thêm vào. Điều này là do nó không thể tự tìm thấy các tập tin. Nếu bạn đã tải xuống tệp tiêu đề winock2 thì bạn có thể thực hiện theo hai cách:
1. Lưu dự án mới vào một thư mục nào đó và thả các tệp winock2.pas, ws2tcpip.inc, wsipx.inc, wsnwlink.inc và wsnetbs.inc vào đó. Điều bất tiện của phương pháp này là các tệp tiêu đề phải được thêm vào từng dự án sử dụng winock2.
2. Bạn có thể ném những tệp này vào thư mục delphilib và sau đó bất kỳ dự án nào chắc chắn sẽ tìm thấy chúng.

Shkodim

Bây giờ hãy tạo một biểu mẫu có nút và trường đầu ra. Sau đó, tạo trình xử lý sự kiện onclick cho nút và viết văn bản sau vào đó:
thủ tục tform1.button1click(người gửi: tobject);
var
thông tin:twsadata;
bắt đầu
wsastartup(makeword(2,0), info);
versionedit.text:=inttostr(info.wversion);
descriptionedit.text:=info.szdescription;
systemstatusedit.text:=info.szsystemstatus;
wsacleanup;
kết thúc;

Lúc đầu tôi khởi động winock bằng wsastartup. Trong đó tôi yêu cầu phiên bản thứ 2 và thông tin về trạng thái hiện tại sẽ được trả về cho tôi trong cấu trúc thông tin. Sau đó, tôi hiển thị thông tin nhận được để công chúng xem. Tôi gặp một chút vấn đề khi xuất thông tin phiên bản vì thuộc tính wversion của cấu trúc thông tin là kiểu số và tôi cần chuyển đổi thành chuỗi để xuất ra. Để làm điều này tôi thực hiện chuyển đổi bằng inttostr.

Chuẩn bị đầu nối

Trước khi thực hiện kết nối tới máy chủ, bạn cũng phải chuẩn bị socket để làm việc. Đây là những gì chúng tôi sẽ làm. Để chuẩn bị, bạn cần thực thi hàm socket, hàm này có ba tham số:
1. Loại địa chỉ được sử dụng. Chúng tôi quan tâm đến Internet, vì vậy chúng tôi sẽ chỉ ra pf_inet hoặc af_inet. Như bạn có thể thấy, cả hai giá trị đều rất giống nhau và hiển thị cùng một địa chỉ, chỉ trong trường hợp đầu tiên, thao tác sẽ đồng bộ và trong trường hợp thứ hai là không đồng bộ.
2. Giao thức cơ bản. Ở đây chúng ta phải chỉ ra trên cơ sở giao thức nào công việc sẽ diễn ra. Bạn nên biết rằng có hai giao thức cơ bản - tcp (với kết nối đáng tin cậy) và udp (không tạo kết nối mà chỉ truyền dữ liệu vào cổng). Đối với tcp, bạn cần chỉ định sock_stream trong tham số này và nếu bạn cần udp thì hãy chỉ định sock_dgram.
3. Ở đây chúng tôi có thể chỉ ra giao thức cụ thể nào chúng tôi quan tâm. Có vô số giá trị có thể có ở đây (ví dụ: ipproto_ip, ipport_echo, ipport_ftp, v.v.). Nếu bạn muốn xem mọi thứ, hãy mở tệp winock2.pas và chạy tìm kiếm ipport_, và tất cả những gì bạn sẽ tìm thấy là các giao thức có thể có.

Đồng bộ/không đồng bộ

Bây giờ tôi muốn giới thiệu cho các bạn hoạt động của cổng đồng bộ và không đồng bộ. Sự khác biệt ở hai chế độ này như sau. Công việc đồng bộ: khi bạn gọi một hàm, chương trình sẽ dừng và đợi nó thực thi xong. Giả sử bạn đã yêu cầu kết nối đến máy chủ. Chương trình ngay lập tức chạy chậm lại và đợi cho đến khi xảy ra kết nối hoặc lỗi. Công việc không đồng bộ: Ở chế độ này, chương trình không gặp phải mọi chức năng mạng. Giả sử bạn đã yêu cầu kết nối tương tự với máy chủ. Chương trình của bạn gửi yêu cầu kết nối và ngay lập tức tiếp tục thực hiện các hành động sau mà không cần chờ tiếp xúc vật lý với tủ bên. Điều này rất thuận tiện (nhưng khó viết mã) vì bạn có thể sử dụng thời gian cho đến khi liên hệ xảy ra cho mục đích riêng của mình. Điều duy nhất bạn không thể làm là gọi các chức năng mạng cho đến khi xảy ra tiếp xúc vật lý thực sự. Điểm bất lợi là bản thân bạn phải theo dõi khi nào chức năng kết thúc và bạn có thể tiếp tục làm việc với mạng.

Kết nối đầy đủ

Ổ cắm đã sẵn sàng, nghĩa là bạn có thể kết nối với máy chủ. Với mục đích này, thư viện winock có chức năng kết nối. Hàm này có ba tham số:
1. Biến socket mà chúng ta nhận được sau khi gọi hàm socket.
2. Cấu trúc của kiểu tsockaddr.
3. Kích thước của cấu trúc được chỉ định trong tham số thứ hai. Để tìm ra kích thước, bạn có thể sử dụng hàm sizeof và chỉ định cấu trúc làm tham số.
Cấu trúc của tsockaddr rất phức tạp và không có ích gì khi mô tả nó một cách đầy đủ. Sẽ tốt hơn nếu chúng ta làm quen với nó trong thực tế, nhưng bây giờ tôi sẽ chỉ hiển thị những trường chính cần điền.
sin_family - họ địa chỉ được sử dụng. Ở đây bạn cần chỉ định điều tương tự đã được chỉ định trong tham số đầu tiên khi tạo ổ cắm (đối với chúng tôi đây là Pf_inet hoặc af_inet).
sin_addr là địa chỉ của máy chủ mà chúng tôi muốn tham gia.
sin_port - cổng chúng tôi muốn kết nối.
Trong thực tế nó sẽ trông như thế này:

var
địa chỉ: tsockaddr;
bắt đầu
addr.sin_family:= af_inet;
addr.sin_addr:= tên máy chủ;
addr.sin_port:= htons(21);
connect(fsocket, @addr, sizeof(addr));
kết thúc;

tắt

Và cuối cùng - chức năng đóng kết nối - closesocket. Bạn cần chỉ định một biến socket làm tham số.