nguyendang95
Thành viên hoạt động



- Tham gia
- 25/5/22
- Bài viết
- 171
- Được thích
- 161
Mặc dù ngôn ngữ lập trình VBA tích hợp trong Excel cho phép người dùng bổ sung thêm tính năng cho Excel, tuy vậy những hạn chế của ngôn ngữ lập trình này khiến cho khả năng hiện thực hóa ý tưởng của người dùng bị thu hẹp lại đáng kể. WebSocket là giao thức kết nối thời gian thực cho phép thiết lập cơ chế trao đổi dữ liệu hai chiều giữa máy chủ và máy khách, tuy nhiên với việc VBA chỉ hỗ trợ lập trình đơn luồng (STA), việc viết code trở nên bất khả thi, cho nên người dùng sẽ cần phải tìm đến ngôn ngữ lập trình khác để giải quyết vấn đề này.
Trong bài viết này sử dụng ngôn ngữ lập trình Visual C++ để giải quyết bài toán trên, Excel sẽ làm việc thông qua COM add-in mà người dùng thiết kế. Để kết nối với máy chủ WebSocket, người dùng có thể sử dụng nhiều thư viện khác nhau, tuy nhiên trong bài viết này sử dụng WinHTTP API bởi vì bộ API này có sẵn trong Windows (từ Windows 8 trở đi), gọn nhẹ dễ sử dụng.
Để tiện cho việc trình bày, bài viết này sẽ trình bày code tạo một ứng dụng dạng console, trong đó có sử dụng WinHTTP API để kết nối đến máy chủ WebSocket trong chế độ đồng bộ (synchronous mode) và chế độ bất đồng bộ (asynchronous mode).
Chế độ đồng bộ:
Trong chế độ này, chương trình sẽ xử lý code lần lượt từ trên xuống dưới, khi gọi các hàm nhận/gửi, những hàm này chỉ trả về khi chương trình nhận được dữ liệu từ máy chủ/hàm gửi đã gửi xong dữ liệu đến máy chủ, với nhược điểm này thread chạy code sẽ bị chặn cho đến khi hàm được gọi trả về, tệ hơn nữa khi máy chủ WebSocket vì một lý do nào đó (đường truyền gặp sự cố hoặc máy chủ có vấn đề, v.v.,) khiến cho máy chủ không thể gửi yêu cầu đóng kết nối khiến cho chương trình vẫn lầm tưởng kết nối vẫn đang được duy trì khiến cho hàm gọi bị treo vĩnh viễn không thể trả về, khiến cho chương trình bị treo hoàn toàn.

Trong bài viết này sử dụng ngôn ngữ lập trình Visual C++ để giải quyết bài toán trên, Excel sẽ làm việc thông qua COM add-in mà người dùng thiết kế. Để kết nối với máy chủ WebSocket, người dùng có thể sử dụng nhiều thư viện khác nhau, tuy nhiên trong bài viết này sử dụng WinHTTP API bởi vì bộ API này có sẵn trong Windows (từ Windows 8 trở đi), gọn nhẹ dễ sử dụng.
Để tiện cho việc trình bày, bài viết này sẽ trình bày code tạo một ứng dụng dạng console, trong đó có sử dụng WinHTTP API để kết nối đến máy chủ WebSocket trong chế độ đồng bộ (synchronous mode) và chế độ bất đồng bộ (asynchronous mode).
Chế độ đồng bộ:
Trong chế độ này, chương trình sẽ xử lý code lần lượt từ trên xuống dưới, khi gọi các hàm nhận/gửi, những hàm này chỉ trả về khi chương trình nhận được dữ liệu từ máy chủ/hàm gửi đã gửi xong dữ liệu đến máy chủ, với nhược điểm này thread chạy code sẽ bị chặn cho đến khi hàm được gọi trả về, tệ hơn nữa khi máy chủ WebSocket vì một lý do nào đó (đường truyền gặp sự cố hoặc máy chủ có vấn đề, v.v.,) khiến cho máy chủ không thể gửi yêu cầu đóng kết nối khiến cho chương trình vẫn lầm tưởng kết nối vẫn đang được duy trì khiến cho hàm gọi bị treo vĩnh viễn không thể trả về, khiến cho chương trình bị treo hoàn toàn.
C++:
#include <stdio.h>
#include <Windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")
#include <string>
#include <iostream>
void ConnectWebSocketSynchronously();
DWORD WINAPI WebSocketSendThreadProc(LPVOID lpParam);
typedef struct tagWEBSOCKETDATA {
HINTERNET hRequest = NULL;
HINTERNET hWebSocket = NULL;
BOOL bConnected = FALSE;
BYTE buffer[8192]{};
HANDLE hEvent = NULL;
CRITICAL_SECTION cs;
DWORD dwBytesRead = 0;
DWORD dwTotalBytesRead = 0;
DWORD dwBufferLength = 8192;
BYTE* pbCurrentBufferPointer = 0;
}WEBSOCKETDATA, *LPWEBSOCKETDATA;
int main()
{
//ConnectWebSocketAsynchronously();
ConnectWebSocketSynchronously();
return 0;
}
void ConnectWebSocketSynchronously() {
//Khởi tạo phiên làm việc WinHTTP, không kết nối qua máy chủ proxy, chạy đồng bộ
HINTERNET hSession = WinHttpOpen(L"WinHttpWebSocket", WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
DWORD dwError = NO_ERROR;
if (!hSession) {
dwError = GetLastError();
wprintf_s(L"WinHttpOpen failed with error %u", dwError);
return;
}
//Chuẩn bị thiết lập kết nối
HINTERNET hConnect = WinHttpConnect(hSession, L"echo.websocket.org", INTERNET_DEFAULT_HTTPS_PORT, 0);
if (!hConnect) {
dwError = GetLastError();
wprintf_s(L"WinHttpConnect failed with error %u", dwError);
WinHttpCloseHandle(hSession);
return;
}
//Chuẩn bị những tham số kết nối cần thiết
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", L"/", NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
if (!hRequest) {
dwError = GetLastError();
wprintf_s(L"WinHttpOpenRequest failed with error %u", dwError);
WinHttpCloseHandle(hSession);
WinHttpCloseHandle(hConnect);
return;
}
//Thiết lập tùy chọn nâng cấp kết nối HTTP thành kết nối WebSocket
if (!WinHttpSetOption(hRequest, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, NULL, 0)) {
dwError = GetLastError();
wprintf_s(L"WinHttpSetOption failed with error %u", dwError);
WinHttpCloseHandle(hSession);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hRequest);
return;
}
//Gửi yêu cầu đến máy chủ
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, NULL)) {
dwError = GetLastError();
wprintf_s(L"WinHttpSendRequest failed with error %u", dwError);
WinHttpCloseHandle(hSession);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hRequest);
return;
}
//Nhận phản hồi từ máy chủ
if (!WinHttpReceiveResponse(hRequest, NULL)) {
dwError = GetLastError();
wprintf_s(L"WinHttpReceiveResponse failed with error %u", dwError);
WinHttpCloseHandle(hSession);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hRequest);
return;
}
//Lấy mã status
DWORD dwStatusCode = 0, dwStatusCodeLength = sizeof(DWORD);
if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &dwStatusCode, &dwStatusCodeLength, WINHTTP_NO_HEADER_INDEX)) {
dwError = GetLastError();
wprintf_s(L"WinHttpQueryHeaders failed with error %u", dwError);
WinHttpCloseHandle(hSession);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hRequest);
return;
}
//Nếu mã status trả về khác 101 thì nghĩa là máy chủ không hỗ trợ kết nối WebSocket
if (dwStatusCode != 101) {
wprintf_s(L"%s", L"This server does not support WebSocket connection");
WinHttpCloseHandle(hSession);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hRequest);
return;
}
//Hoàn tất thiết lập kết nối WebSocket
HINTERNET hWebSocket = NULL;
if (!(hWebSocket = WinHttpWebSocketCompleteUpgrade(hRequest, NULL))) {
dwError = GetLastError();
wprintf_s(L"WinHttpWebSocketCompleteUpgrade failed with error %u", dwError);
WinHttpCloseHandle(hSession);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hRequest);
return;
}
//Handle quản lý việc gửi yêu cầu HTTP không còn cần thiết nữa, nên đóng lại để giải phóng bộ nhớ
WinHttpCloseHandle(hRequest);
//Chuẩn bị các dữ liệu cần thiết để làm việc với kết nối WebSocket
WEBSOCKETDATA wsData = { 0 };
wsData.hWebSocket = hWebSocket;
wsData.bConnected = TRUE;
wprintf_s(L"%s\n", L"Successfully connected to the server");
//Tạo một thread mới dùng để nhận nhập liệu từ người dùng và gửi dữ liệu đến máy chủ WebSocket
HANDLE hThread = CreateThread(NULL, 0, WebSocketSendThreadProc, (LPVOID)&wsData, 0, NULL);
if (!hThread) {
wprintf_s(L"CreateThread failed with error %u", GetLastError());
WinHttpCloseHandle(hSession);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hWebSocket);
return;
}
DWORD dwBufferLength = 8192;
/*
Liên tục nhận dữ liệu từ máy chủ cho đến khi máy chủ đóng kết nối
Thiết lập bộ đệm (buffer) đủ để chứa phản hồi từ máy chủ
*/
while (wsData.bConnected) {
WINHTTP_WEB_SOCKET_BUFFER_TYPE bufferType;
DWORD dwTotalBytesRead = 0, dwBytesRead = 0;
BYTE* pbCurrentBufferPointer = wsData.buffer;
do {
//Nhận phản hồi từ máy chủ
dwError = WinHttpWebSocketReceive(hWebSocket, pbCurrentBufferPointer, dwBufferLength, &dwBytesRead, &bufferType);
if (dwError != NO_ERROR) {
wsData.bConnected = FALSE;
WinHttpWebSocketClose(hWebSocket, WINHTTP_WEB_SOCKET_ABORTED_CLOSE_STATUS, NULL, 0);
wprintf_s(L"WinHttpWebSocketReceive failed with error %u\n", dwError);
break;
}
pbCurrentBufferPointer += dwBytesRead;
dwTotalBytesRead += dwBytesRead;
dwBufferLength -= dwBytesRead;
//Kiểm tra nội dung kiểu bộ đệm nhận được từ máy chủ
switch (bufferType) {
/*
Trường hợp bộ đệm mang thông điệp mã hóa UTF-8
Kiểu bộ đệm này nghĩa là máy chủ gửi thông điệp hoàn chỉnh
Hoặc, đây là phần thông điệp cuối cùng tạo thành thông điệp hoàn chỉnh
*/
case WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE:
{
//Cấp phát bộ nhớ động để chứa nội dung hữu ích từ bộ đệm
char* pszResponse = new char[dwTotalBytesRead + 1];
if (!pszResponse) {
wsData.bConnected = FALSE;
WinHttpWebSocketClose(hWebSocket, WINHTTP_WEB_SOCKET_ABORTED_CLOSE_STATUS, NULL, 0);
wprintf_s(L"%s\n", L"Out of memory");
break;
}
//Sao chép nội dung hữu ích từ bộ đệm sang vùng nhớ vừa cấp phát động
strcpy_s(pszResponse, static_cast<rsize_t>(dwTotalBytesRead) + 1, (char*)wsData.buffer);
//Xóa thông tin trong bộ đệm
memset(wsData.buffer, 0, dwBufferLength);
printf_s("Response from the server: %s\n", pszResponse);
//Dọn dẹp bộ nhớ sau khi sử dụng xong
delete[] pszResponse;
break;
}
//Trường hợp bộ đệm chứa thông điệp đóng kết nối
case WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE:
{
//Gửi yêu cầu đóng kết nối
dwError = WinHttpWebSocketClose(hWebSocket, WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, NULL, 0);
//Thiết lập điều kiện để thoát khỏi vòng lặp
wsData.bConnected = FALSE;
if (dwError != NO_ERROR) {
wprintf_s(L"WinHttpWebSocketClose failed with error %u\n", dwError);
break;
}
wprintf_s(L"%s\n", L"Disconnected from the server");
break;
}
}
if (!dwBufferLength) break;
/*
Khi gặp kiểu bộ đệm này, tức là máy chủ chia thông điệp thành nhiều phần rồi gửi
Nên cần tiếp tục nhận phản hồi từ máy chủ cho đến khi kiểu bộ đệm trả về WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE
*/
} while (bufferType == WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE);
}
//Chờ cho đến khi thread kết thúc
WaitForSingleObject(hThread, INFINITE);
//Dọn dẹp bộ nhớ
CloseHandle(hThread);
WinHttpCloseHandle(hSession);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hWebSocket);
};
//Thread chịu trách nhiệm nhận nhập liệu từ người dùng và gửi dữ liệu đến máy chủ
DWORD WINAPI WebSocketSendThreadProc(LPVOID lpParam) {
if (!lpParam) return 1;
LPWEBSOCKETDATA pWsData = (LPWEBSOCKETDATA)lpParam;
HINTERNET hWebSocket = pWsData->hWebSocket;
std::string strRequest{};
DWORD dwError = NO_ERROR;
//Lập lại hành động cho đến khi đóng kết nối
while (pWsData->bConnected) {
//Nhận nhập liệu từ người dùng
std::cin >> strRequest;
if (!strRequest.length()) continue;
//Nếu nhận được "exit" thì gửi yêu cầu đóng kết nối và kết thúc thread
else if (!strcmp("exit", strRequest.c_str())) {
DWORD dwError = WinHttpWebSocketClose(hWebSocket, WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, NULL, 0);
pWsData->bConnected = FALSE;
if (dwError != NO_ERROR) {
wprintf_s(L"WinHttpWebSocketClose failed with error %u\n", dwError);
return 1;
}
wprintf_s(L"%s\n", L"Disconnected from the server");
break;
}
//Gửi thông tin đến máy chủ
dwError = WinHttpWebSocketSend(hWebSocket, WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE, (LPVOID)strRequest.c_str(), static_cast<DWORD>(strRequest.size()));
if (dwError != NO_ERROR) {
wprintf_s(L"Failed to send message to the server with error %u\n", dwError);
continue;
}
}
return 0;
}
