Chỉ thị tiền xử lý:
#include "winsock2.h"
#pragma comment(lib, "Ws2_32.lib")
- Khởi tạo và giải phóng WinSock:
WSADATA wsaData;
WORD wVersion = MAKEWORD(2, 2);
WSAStartup(wVersion, &wsaData);
// ...
WSACleanup();
- Địa chỉ socket được lưu bằng cấu trúc
sockaddr_in
. - Xử lý địa chỉ socket:
- Đổi dạng biểu diễn bằng các hàm
inet_pton()
,inet_ntop()
,htons()
,htonl()
,ntohs()
,ntohl()
. Ví dụ:
in_addr address; inet_pton(AF_INET, "127.0.0.1", &address);
- Phân giải tên miền / địa chỉ:
getaddrinfo()
,gethostbyname()
,gethostbyaddr()
,gethostname()
. Ví dụ:
addrinfo hints; hints.ai_family = AF_INET; memset(&hints, 0, sizeof(hints)); addrinfo *result; getaddrinfo("google.com", NULL, &hints, &result); // ... freeaddrinfo(result);
struct sockaddr_in addr; char hostname[NI_MAXHOST], serverInfo[NI_MAXSERV]; addr.sin_family = AF_INET; inet_pton(AF_INET, "8.8.8.8", &addr.sin_addr); getnameinfo((struct sockaddr *) &addr, sizeof(struct sockaddr), hostname, NI_MAXHOST, serverInfo, NI_MAXSERV, NI_NUMERICSERV);
Ghi nhớ: hàm
getaddrinfo()
trả về kiểu dữ liệustruct sockaddr **
, hàmgetnameinfo()
nhận kiểu dữ liệustruct sockaddr *
. Muốn tương tác với địa chỉ IP ta lại phải dùng trườngsin_addr
của kiểu dữ liệustruct sockaddr_in
(chuyển đổi qua lại bằng ép kiểu). Lưu ý các hàm nhận hoặc trả IP dưới dạng nhị phân; do đó cần dùnginet_ntop()
hayinet_pton()
để chuyển về dạng String. - Đổi dạng biểu diễn bằng các hàm
- Khởi tạo socket: ví dụ:
SOCKET client = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// ...
closesocket(client);
- Gán địa chỉ cho socket bằng hàm
bind()
. Ví dụ:
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
sockaddr_in addr;
addr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
addr.sin_port = htons(5500);
bind(s, (sockaddr *)&addr, sizeof(addr));
- Tùy chọn cho socket (đọc thêm): sử dụng hàm
setsockopt()
vàgetsockopt()
. Ví dụ:
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// (optional) Set time-out for receiving
int tv = 10000; // Time-out interval: 10000ms
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char *)(&tv), sizeof(int));
- Sơ đồ chung:
- Server:
socket()
→bind()
→ {recvfrom()
vàsendto()
} →closesocket()
- Client:
socket()
→ {sendto()
vàrecvfrom()
} →closesocket()
- Server:
- Hàm
sendto()
gửi dữ liệu đến một socket biết trước địa chỉ. Ví dụ:
SOCKET sender = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
sockaddr_in receiverAddr;
// cần phải biết địa chỉ của receiver trước khi thực thi hàm sendto()
sendto(sender, buff, strlen(buff), 0, (sockaddr *)&receiverAddr, sizeof(receiverAddr));
- Hàm
recvfrom()
nhận dữ liệu từ một nguồn nào đó (xác định sau khi hàm thực thi). Ví dụ:
SOCKET receiver = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
sockaddr_in senderAddr;
int senderAddrLen = sizeof(senderAddr);
ret = recvfrom(receiver, buff, BUFF_SIZE, 0, (sockaddr *)&senderAddr, &senderAddrLen);
Lưu ý rằng nếu kích thước thông điệp gửi tới lớn hơn bộ đệm UDP socket bên nhận thì chỉ nhận phần dữ liệu vừa đủ với kích thước bộ đệm, phần còn lại bị bỏ qua và hàm trả về
SOCKET_ERROR
.
- Sơ đồ chung:
- Server:
socket()
→bind()
→listen()
→ {accept()
→ {recv()
vàsend()
} →shutdown()
→closesocket()
→ quay lạiaccept()
} - Client:
socket()
→connect()
→ {send()
vàrecv()
} →shutdown()
→closesocket()
- Server:
- Hàm
listen()
đặt socket sang trạng thái lắng nghe kết nối. Ví dụ:
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(5500);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
bind(listenSock, (sockaddr *)&serverAddr, sizeof(serverAddr));
listen(listenSock, 10);
- Hàm
accept()
khởi tạo một socket gắn với kết nối TCP nằm trong hàng đợi. Ví dụ:
sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
SOCKET connSock = accept(listenSock, (sockaddr *)&clientAddr, &clientAddrLen);
- Hàm
connect()
gửi yêu cầu thiết lập kết nối tới server. Ví dụ:
SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(5500);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
connect(client, (sockaddr *)&serverAddr, sizeof(serverAddr));
- Hàm
send()
gửi dữ liệu bằng socket. Ví dụ:
send(client, buff, strlen(buff), 0);
- Hàm
recv()
nhận dữ liệu bằng socket. Ví dụ:
recv(client, buff, BUFF_SIZE, 0);`
Lưu ý: trên UDP socket có thể sử dụng hàm
connect()
để thiết lập địa chỉ của phía bên kia khi truyền tin. Nếu UDP socket đã dùng hàmconnect()
để kiểm tra, có thể sử dụngsend()
thay chosendto()
hay hàmrecv()
thay chorecvfrom()
.
- Hàm
shutdown()
đóng kết nối trên socket (theo 1 hoặc 2 chiều gửi và nhận). Ví dụ:
shutdown(client, SD_RECEIVE);
- Kích thước bộ đệm TCP socket trên Windows 8.1 là 64kB.
- Hàm
send()
dừng vòng lặp nếu dữ liệu gửi đi lớn hơn kích thước bộ đệm của ứng dụng. - Hàm
recv()
: khi kích thước bộ đệm nhận nhỏ hơn kích thước thông điệp gửi tới, cần sử dụng vòng lặp để đọc được hết dữ liệu.
- Hàm
- Giải pháp truyền theo dòng byte trong TCP:
- Giải pháp 1: sử dụng thông điệp có kích thước cố định.
- Giải pháp 2: sử dụng mẫu ký tự phân tách (delimiter).
- Giải pháp 3: gửi kèm kích thước thông điệp.
Xem trang 30.
- Nhận xét: TCP server chỉ phục vụ được cùng lúc 1 client.
- Các chế độ hoạt động trên WinSock:
- Chế độ chặn dừng, hoặc đồng bộ: là chế độ mặc định (
connect()
,accept()
,send()
,...) - Chế độ không chặn dừng, hoặc bất đồng bộ:
- Các thao tác vào ra trên SOCKET sẽ trở về nơi gọi ngay lập tức và tiếp tục thực thi luồng. Kết quả của thao tác vào ra sẽ được thông báo cho chương trình dưới một cơ chế đồng bộ nào đó.
- Các hàm vào ra bất đồng bộ sẽ trả về mã lỗi WSAEWOULDBLOCK nếu thao tác đó không thể hoàn tất ngay và mất thời gian đáng kể (chấp nhận kết nối, nhận dữ liệu, gửi dữ liệu...)
- Socket chuyển sang chế độ này bằng hàm
ioctlsocket()
:
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); unsigned long ul = 1; ioctlsocket(s, FIONBIO, (unsigned long *) &ul);
- Một số trường hợp trả về WSAEWOULDLOCK: xem trang 5.
- Chế độ chặn dừng, hoặc đồng bộ: là chế độ mặc định (
- Giải quyết vấn đề chặn dừng bằng cách tạo ra các luồng riêng biệt với hàm
_beginthreadex()
. - Sơ đồ luồng chính:
socket()
→bind()
→listen()
→ {accept()
→_beginthreadex()
→ other task → quay lạiaccept()
} - Một số hàm xử lý luồng: trang 8.
- Điều độ luồng sử dụng đoạn găng:
- Khai báo đoạn găng:
CRITICAL_SECTION
- Khởi tạo đoạn găng:
void InitializeCriticalSection(CRITICAL_SECTION *);
- Giải phóng đoạn găng:
void DeleteCriticalSection(CRITICAL_SECTION *);
- Yêu cầu vào đoạn găng:
EnterCriticalSection(CRITICAL_SECTION *);
- Rời khỏi đoạn găng:
LeaveCriticalSection(CRITICAL_SECTION *);
- Khai báo đoạn găng:
- Ví dụ về điều độ luồng sử dụng đoạn găng:
- Luồng chính:
CRITICAL_SECTION criticalSection; int main() { InitializeCriticalSection(&criticalSection); // ... DeleteCriticalSection(&criticalSection); }
- Hàm thực hiện trong luồng con:
unsigned int __stdcall mythread(void *) { EnterCriticalSection(&criticalSection); // ... LeaveCriticalSection(&criticalSection); return 0; }
- Sơ đồ:
socket()
→bind()
→listen()
→ {khởi tạo tập select →select()
→ xử lý sự kiện vào ra} - Sử dụng hàm
select()
:- Thăm dò các trạng thái trên socket (yêu cầu kết nối, kết nối thành công, gửi dữ liệu, nhận dữ liệu,...)
- Có thể xử lý tập trung tất cả các socket trong cùng một thread (tối đa 1024)
- Các socket cần thăm dò được đặt trong cấu trúc
fd_set
:
struct fd_set { u_int fd_count; SOCKET fd_array[FD_SETSIZE]; };
- Hàm
select()
:
/**
* 3 tham số kiểu fd_set * không thể cùng NULL
* @return SOCKET_ERROR (lỗi), 0 (time-out), hoặc tổng số socket có trạng thái sẵn sàng / có lỗi
*/
int select(
int nfds, // 0
fd_set *readfds, // [i/o] tập các socket được thăm dò trạng thái có thể đọc
fd_set *writefds, // [i/o] tập các socket được thăm dò trạng thái có thể ghi
fd_set *exceptfds, // [i/o] tập các socket được thăm dò trạng thái có lỗi
const struct timeval *timeout // thời gian chờ trả về
);
- Kỹ thuật thăm dò:
- Thao tác với
fd_set
qua các macro:FD_CLR()
,FD_SET()
,FD_ISSET()
,FD_ZERO()
. - Kỹ thuật:
- Bước 1: thêm các socket cần thăm dò vào tập
fd_set
. - Bước 2: gọi hàm
select()
. Khi đó các socket không mang trạng thái thăm dò sẽ bị xóa khỏi tậpfd_set
tương ứng. - Bước 3: sử dụng macro FD_ISSET() để sự có mặt của socket trong tập
fd_set
và xử lý.
- Bước 1: thêm các socket cần thăm dò vào tập
- Các trạng thái trên socket được ghi nhận:
- Tập
readfds
:- Có yêu cầu kết nối tới socket đang ở trạng thái lắng nghe (LISTENING).
- Dữ liệu sẵn sàng trên socket để đọc.
- Kết nối bị đóng / reset / hủy.
- Tập
writefds
:- Kết nối thành công khi gọi hàm
connect()
ở chế độ non-blocking. - Sẵn sàng gửi dữ liệu.
- Kết nối thành công khi gọi hàm
- Tập
exceptfds
:- Kết nối thất bại khi gọi hàm
connect()
ở chế độ non-blocking. - Có dữ liệu OOB (out-of-band) để đọc.
- Kết nối thất bại khi gọi hàm
- Tập
- Sơ đồ ở trang 14.
- Thao tác với
Kinh dị và không thi đến (bài duy nhất lập trình với Win32 API). Bỏ qua.
Kinh dị và không thi đến (đùa chứ đã làm bài tập với WSAAsyncSelect()
rồi, cái này là WSAEventSelect()
). Bỏ qua.
- Sử dụng cấu trúc WSAOVERLAPPED chứa thông tin về các thao tác vào ra.
- Socket phải được khởi tạo với cờ điều kiển tương ứng.
- Sử dụng các hàm đặc trưng với kỹ thuật overlapped.
- Hiệu năng cao hơn do có thể gửi đồng thời nhiều yêu cầu tới hệ thống.
- Các phương pháp xử lý kết quả:
- Đợi thông báo từ một sự kiện.
- Thực hiện một thủ tục CALLBACK (completion routine).
- Hàm
WSASocket()
để khởi tạo socket. Để socket ở chế độ overlapped, gán cờ WSA_FLAG_OVERLAPPED cho tham sốdwFlags
. - Hàm
WSASend()
gửi dữ liệu với cơ chế overlapped trên trên nhiều bộ đệm. Trả về 0 hoặc WSA_IO_PENDING. - Hàm
WSARecv()
nhận dữ liệu với cơ chế overlapped trên trên nhiều bộ đệm. Trả về 0 hoặc WSA_IO_PENDING. - Cấu trúc
WSABUF
. - Sơ đồ à các bước sử dụng kỹ thuật overlapped - xử lý qua sự kiện: trang 10.
- Hàm
WSAGetOverlappedResult()
lấy kết quả thực hiện thao tác vào ra trên socket.
- Hệ thống sẽ thông báo cho ứng dụng biết thao tác vào ra kết thúc thông qua hàm
CompletionROUTINE()
. - WinSock sẽ bỏ qua trường
event
trong cấu trúc OVERLAPPED, việc tạo đối tượng event và thăm dò là không cần thiết nữa. - Lưu ý: completion routine không thực hiện được các tác vụ nặng.
- Ứng dụng cần chuyển luồng sang trạng thái alertable ngay sau khi gửi yêu cầu vào ra. Sử dụng hàm
WSAWaitForMultipleEvents()
(hoặcSleepEx()
nếu ứng dụng không có đối tượng event nào). - Sơ đồ: trang 14.
- Completion port tổ chức một hàng đợi cho các luồng và giám sát các sự kiện vào ra trên các socket. Mỗi khi thao tác vào ra hoàn thành trên socket, completion port kích hoạt một luồng để xử lý.
- Hàm
CreateIoCompletionPort()
dùng để tạo một completion port. - Sơ đồ mô tả việc sử dụng completion port: trang 18.
- WorkerThread gọi hàm
GetQueuedCompletionStatus()
đợi thao tác vào ra hoàn thành trên completion port và lấy kết quả thực hiện. Trả về FALSE nếu thao tác vào ra lỗi. Tham số lpCompletionKey và lpOverlapped chứa dữ liệu và kết quả của thao tác vào ra. - Mỗi completion port có thể sử dụng nhiều luồng điều khiển vào ra. Tránh giải phóng cấu trúc OVERLAPPED trên một luồng trong khi đang thực hiện vào ra.
- Gọi hàm
PostQueuedCompletionStatus()
để gửi một packet có kích thước 0 tới completion port trên tất cả các luồng. Gọi hàmCloseHandle()
để đóng completion port.
- Kỹ thuật đa luồng: đơn giản; sử dụng tài nguyên không hiệu quả, không áp dụng cho ứng dụng phục vụ quá nhiều client.
- Kỹ thuật thăm dò: đơn giản; giới hạn bởi cấu trúc fd_set chỉ quản lý được 1024 socket; hàm
select()
không hiệu quả khi quản lý nhiều socket nên không áp dụng cho ứng dụng phục vụ quá nhiều client. - Kỹ thuật vào ra theo thông báo: đơn giản; yêu cầu ứng dụng phải có cửa sổ, nó trở thành nút thắt cổ chai trong ứng dụng nếu phải xử lý quá nhiều kết nối.
- Kỹ thuật vào ra theo sự kiện: đơn giản, không yêu cầu ứng dụng phải có cửa sổ; mỗi luồng chỉ quản lý được 64 bộ nghe sự kiện.
- Kỹ thuật vào ra overlapped theo sự kiện: hiệu năng cao; mỗi luồng chỉ quản lý được 64 bộ nghe sự kiện.
- Kỹ thuật vào ra overlapped, xử lý bằng completon routine: hiệu năng cao, không hạn chế số kết nối có thể xử lý; không thực hiện được các tác vụ nặng.
- Kỹ thuật vào ra overlapped theo completion port: hiệu năng cao, không hạn chế số kết nối có thể xử lý; khó sử dụng.