Lập Trình Mạng Với Thư Viện Winsock trên VC++ (Part 3)

V. DEMO MINI WEBSERVER

Thật ra ở đây mình không có tham vọng làm 1 Web Server thực sự như IIS hay Apache đâu? Mình chỉ muốn dùng Winsock để làm 1 chương trình có thể biến máy tính bạn thành 1 máy chủ Web. Nghĩa là có ai đó truy cập vào thì nó sẽ trả lại 1 Webpage.

Trước hết mình xin giới thiệu giao thức HTTP.

1. Giao thức HTTP

Bước 1: Giả sử người sử dụng gõ địa chỉ URL là:http://webserver.com/Page?ID=1″

Lúc này thì Client tại người sử dụng sẽ Connect tới Webserver và gởi một nội dung Request như sau:

GET /Page?ID=1 HTTP/1.1
Host: webserver.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

Nội dung trên bao gồm:
– GET (lấy trang web). Ngoài ra còn có POST
– Host: Tên SERVER muốn kết nối!
– User-Agent: Tên chương trình Web Browser
– Ngoài ra còn một số thông tin về ngôn ngữ.

Nếu khi đã nhận được trang WEB mà trong Web còn có hình ảnh hay âm thanh thì tiếp tục gởi Request tới Server
Như:
GET /samples/images/IMAGE.jpg HTTP/1.1

Bước 2: Xử lý tại Server

Lúc này tại Web Server sẽ làm gì?
Nó sẽ gửi lại 1 Header như sau:
HTTP/1.0 200 OK
Server: <TÊN SERVER>
Date:
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified:
Content-Length: <kich thuoc trang WEB>

Và sau đó nó sẽ căn cứ vào yêu cầu của Client: GET /Page?ID=1 HTTP/1.1 để gửi trả lại 1 trang HTML hoặc file hình ảnh, âm thanh… tương ứng.
Ví dụ như:
<html>
<head><title>DEMO WEBSERVER – WINSOCKC++</title></head>
<body>
<h1>Hello eXecutve!</h1>
<br>
CopyRight 2007 by eXecutive;
stairwaytoheaven187@yahoo.com
</body>
</html>

2. Chương trình DEMO

Như vậy đã đủ. Chúng ta sẽ viết chương trình.

Hàm Thiết lập lắng nghe trên PORT 80

 #define MY_PORT 80
..
..
int SetupIPWS(SOCKET& ListenSock,sockaddr_in& MyListenIP)
{
MyListenIP.sin_family = AF_INET;
MyListenIP.sin_port = htons( MY_PORT );       // PORT 80
MyListenIP.sin_addr.s_addr = INADDR_ANY; // IP của mình
int nResult = bind(ListenSock, (sockaddr*) &MyListenIP,sizeof(MyListenIP));  // Gán IP PORT vào SOCKET
if (nResult == -1){
cout << “Loi khi thiet lap IP va PORT\n”;
WSAGetLastError();
ShutdownWS(ListenSock);
return 1;
}
return 0;
}

Hàm lắng nghe phục vụ kết nối.

void LoopListen(SOCKET& ListenSock,sockaddr_in& MyListenIP){
SOCKET NewConnection;
sockaddr_in ConnectClient;
int nSizeAddr;
int nResult = listen(ListenSock,10);           // Lắng nghe kết nối
if (nResult == -1){
cout << “Khong the lang nghe ket noi\n”;
WSAGetLastError();
return;
}
cout << “Dang lang nghe ket noi… tai IP: ” << inet_ntoa(MyListenIP.sin_addr)
<< ” PORT: ” << MyListenIP.sin_port << “\nCTRL + C de STOP SERVER\n\n”;
char lpBuffRevc[1024]={0}; // Nội dung nhận từ  CLIENT (REQUEST)
// Nội dung gửi trả lại  CLIENT
char headers[] =        ”HTTP/1.0 200 OK\r\n”
“Server: eXecutive Winsock Server\r\n”
“Date: Sat, 3 November 2007\r\n”
“Content-Type: text/html\r\n”
“Accept-Ranges: bytes\r\n”
“Content-Length: 187\r\n”
“\r\n”;           // Kết thức Header \n
char html[] =              ”<html>”
“<head><title>DEMO WEBSERVER – WINSOCK C++ – CopyRight eXecutive</title></head> \r\n”
“<body>\r\n”
“<h1>Hello eXecutve!</h1>\r\n”
“<br>\r\n”
“CopyRight 2007 by eXecutive<br> \r\n”
“stairwaytoheaven187@yahoo.com\r\n”
“</body>\r\n”
“</html>\r\n\n”; // Kết thúc trang web bằng \n\n
while (1){
nSizeAddr = sizeof(sockaddr);        // Quan trọng! Xác định kích thước sockaddr
NewConnection = accept(ListenSock, (sockaddr*) &ConnectClient, &nSizeAddr);    // Chấp nhận 1 kết nối
if (NewConnection == -1){
cout << “Loi ket noi tu Client\n”;
continue;
}
cout << “Nhan ket noi tu: “<< inet_ntoa(ConnectClient.sin_addr) << “\n”;
cout << “\nNoi dung nhan:\n\n”;
// Xóa nội dung REQUEST trước và nhận  REQUEST mới.
memset(lpBuffRevc,0,sizeof(lpBuffRevc));
recv(NewConnection,lpBuffRevc, sizeof(lpBuffRevc) ,0);
/*
XỬ  LÝ REQUEST => Ở ĐÂY MÌNH 1 KHÔNG 1 WEBSERVER CHUYỆN NGHIỆP NÊN BỎ QUA.
*/
// Gửi HEADER của SERVER và trang HTML
send(NewConnection,headers,sizeof(headers),0);
send(NewConnection,html,sizeof(html),0);
cout << lpBuffRevc << “\n”;
// Đóng kết nối!
closesocket(NewConnection);
}
}

Có lẽ 1 chương trình kỳ cục. While(1) không có điều kiện kết thúc. Nhưng đó là cách hoat động của SERVER, 1 chương trình có thể chạy quanh năm suốt tháng. Do đó nên không có vấn đề gì?

Địa chỉ mà mình gõ là “http://localhost” tức là địa chỉ cục bộ = “http://127.0.0.1″= “http://192.168.1.254″ (nếu máy tính của mình đặt IP này)

VI. DEMO MINI WEB BROWSER

Lần này mình sẽ DEMO 1 Web Browser.

Chương trình như sau:
– Người dùng nhập vào địa chỉ Website
– Kết nối và lấy nội dung HTML về lưu lại thành file RESPONSE.htm

1. Các thao tác tại WEB BROWSER

a. Nhận địa chỉ Website và phân giải tên miền thành địa chỉ IP.

char lpDomain[100];
cout << “\n\nNhap ten mien can truy cap: http://”;
cin.getline(lpDomain,100);
hostent *ConnectPC=NULL;
ConnectPC = gethostbyname(lpDomain);  // Lay PC theo Tên Domain
if (!ConnectPC){
cout << “DNS khong the phan giai duoc ten mien nay …\n”;
return 1;
}
ConnectIP.sin_family = AF_INET;
ConnectIP.sin_port = htons(  MY_PORT );
ConnectIP.sin_addr.s_addr = (*(DWORD*)ConnectPC->h_addr_list[0]); // Lay IP
cout << “\nMay chu:”;
cout << ConnectPC->h_name << “\n”;
cout << “IP: ” << inet_ntoa(ConnectIP.sin_addr) << “\n”;

b. Gửi REQUEST tới máy chủ WEB

Ví dụ người sử dụng nhập là “http://www.google.com” thì phải có Request tối thiểu để máy chủ xử lý là:

GET / HTTP/1.1
Host: google.com

User-Agent: eXecutive DEMO Client

-> “Host: google.com” rất quan trọng vì có nhiều WEB thuê HOSTING và lúc này có nhiều WEBSITE cùng trên 1 WEBSERVER. Do đó phải xác định miền truy cập

// NOI DUNG REQUEST
char *GetHTML = “GET / HTTP/1.1\r\n”;
char HostRequest[100];
sprintf(HostRequest,”Host: %s\r\n”,lpDomain);
char *UserAgent = “User-Agent: eXecutive DEMO Client \r\n\n”;
// GUI REQUEST
send(Sock,GetHTML,strlen(GetHTML),0);
send(Sock,HostRequest,strlen(HostRequest),0);
send(Sock,UserAgent,strlen(UserAgent),0);

c. Nhận RESPONSE từ máy chủ WEB

Nhận HEADER từ SERVER:
Nguyên tắc là chúng ta sẽ nhận từng BYTE . Khi thấy ký tự “\n\n” có nghĩa là kết thúc Header.

char HeaderResponse;
while(!done){
nByteRevc = recv(Sock,&HeaderResponse,1,0);
if(nByteRevc<0)    // Loi
done=TRUE;
switch(HeaderResponse)    {
case ‘\r’:
break;
case ‘\n’: // Neu gap “\n\n” hay “\n\r\n” thi ket thuc Header
if (EndHeader == true){
done = TRUE;
}
EndHeader=true;
break;
default:
EndHeader=false;
break;
}
cout << HeaderResponse;
}

Nhận BODY từ SERVER:
Chúng ta sẽ nhận từng lúc 512 Bytes từ SERVER cho đến khi không được SERVER hồi âm nữa (nByteRevc <= 0)
Đoạn CODE như sau:

char HTMLResponse[513]={0};
do{
nByteRevc = recv(Sock,HTMLResponse,sizeof(HTMLResponse)-1,0);        // Đọc 512 Bytes
// Loai bo 1 ky tu bi thua tu HEADER
if (HTMLResponse[0] == 0)
HTMLResponse[0] = ‘ ‘;
if(nByteRevc < 0)    // Ket thuc HTML
break;
HTMLResponse[nByteRevc] = 0; // Ngắt chuỗi nếu RESPONSE nhận < 512 BYTES hoặc cuối cùng để in chuỗi.
cout << HTMLResponse;
}while (nByteRevc > 0);

Rõ ràng giữa send và recv không phải tuân theo quy tắc là gửi ba nhiêu bytes thì phải nhận bấy nhiêu bytes. Và chúng ta có thể gửi hay nhận nhiều lần, vấn đề này không quan trọng trong WINSOCK!

VII. DEMO CLIENT – SERVER VỚI GIAO THỨC UDP

1. UDP là gì?

– Thật ra mình đã giới thiệu về UDP ở phần tạo SOCKET rồi nhưng mình nói lại 1 xíu cho nhanh. UDP là một trong 2 giao thức chính của mô hình TCP/IP để truyền tải dữ liệu. CLIENT – SERVER sử dụng UDP sẽ không bao giờ quan tâm đến về đề dữ liệu có chính xác hay không? Bên gửi cứ gửi và bên nhận cứ nhận, nhận không được thì mặc kệ…

– Người ta sử dụng UDP trong những ứng dụng cần truyền dữ liệu nhanh như CHAT, GAME ONLINE, ….

– Và lần này mình sẽ DEMO 1 bộ ứng dụng Client – Server liên lạc với nhau màClient không cần biết IP Server sử dụng cơ chế Multicast hay Broadcast

2. BROADCAST hay MULTICAST là gì?

a. Broadcast

Chúng ta biết IP chia thành 3 lớp là A,B,C.
Trong cấu trúc địa chỉ IP bao gồm NET_ID và HOST_ID

-> Các host chỉ thấy nhau (liên lạc được) khi chúng có cùng NET_ID.
-> Muốn host liên lạc khi khác NET_ID thì cần phải có cơ chế định tuyến (sử dụng thiết bị router, Server có cấu hình Routing …)

=> Như vậy trong 1 NET_ID phải có 1 địa chỉ nào đó để đại diện cho tất cả các HOST đường mạng đó và đó chính là địa chỉ Broadcast

Ví dụ:
Lớp A: 
Có các IP sau : 10.0.0.100, 10.12.3.1 hay 10.252.252.252
-> Tất cả đều có chung 1 đường mạng 10.0.0.0
-> Và IP đại điện là IP Broadcast: 10.255.255.255.
Nghĩa là khi có 1 gói tin UDP gởi tới địa chỉ này thì tất cả các host trên đều nhận được

b. Multicast

– Địa chỉ Broadcast đại diện cho tất cả các Host nhưng trên cùng 1 đường mạng.
– Địa chỉ Multicast đại diện cho tất cả các HOST trên tất cả các đường mạng khác nhau

Hay chính xác hơn thì địa chỉ “255.255.255.255″ chính là địa chỉ Multicast.
Khi gởi 1 gói tin UDP Packet tới địa chỉ này thì tất cả các host có thể liên lạc được với host gửi (khác đường mạng phải có cơ chế định tuyến) đèu nhận được gói tin này.

Tuy nhiên lưu ý 1 điều là Multicast hay Broadcast chỉ có giá trị trong IPLAN

3. Chương trình DEMO

Ý tưởng: Bạn đã chơi Game Counter Strike chưa???

– Rõ ràng máy Server trong mạng LAN có thể là bất cứ máy tính nào mà Gamer chọn Create Game?
=> Vậy làm sao Client có thể biết được IP máy chủ để Connect.
Và nó phải sử dụng cơ chế tìm IP SERVER sử dụng UDP Broadcast.

Và chương trình DEMO của mình sẽ như sau:

a. SERVER

Cũng như TCP. Nhiệm vụ chính của nó là lắng nghe, tuy nhiên chỉ khác chức năng là không cần phải chấp nhận kết nối từ Client.

// Thiết lập IP PORT
pAddr->sin_family = AF_INET;
pAddr->sin_port = htons(MY_PORT);
pAddr->sin_addr.S_un.S_addr = ADDR_ANY;
// Tìm địa chỉ IP của SERVER <sau này trả lại cho CLIENT>
char lpName[100];
char lpMyIP[100]={0};
gethostname(lpName,sizeof(lpName));
hostent* pMyServer = gethostbyname(lpName);
u_long myIP = *(u_long*)pMyServer->h_addr_list[0];
strcpy(lpMyIP,inet_ntoa(*(in_addr*)&myIP));
// —————————————————————————-
// Đưa thông tin IP và PORT cho SOCKET
nResult = bind(*sock,(sockaddr*)pAddr,sizeof(sockaddr));
if (nResult == -1){
cout << “Loi thiet lap IP va PORT\n”;
HuyWinsock(sock);
return 1;
}
cout << “Dang lang nghe ket noi tren IP: ” << lpMyIP << ” port: ” << MY_PORT << “\n\n”;
int nAddrLen;
int nRevc;
int nSend;
sockaddr_in IPClient;
char buff[512]={0};
while (1){
// Nhận kết nối từ CLIENT
nAddrLen = sizeof(sockaddr_in);
nRevc = recvfrom(*sock,buff,sizeof(buff),0, (sockaddr*)&IPClient,&nAddrLen);
buff[nRevc-1] = 0;
cout << “Nhan ket toi tu CLIENT IP: ” << inet_ntoa(IPClient.sin_addr) << “\n”;
cout << “Noi dung: \”" << buff << “\”\n”;
// Kiểm tra yêu cầu của CLIENT
if (strcmpi(“IP may chu dau???”,buff)==0){
cout << “Hieu yeu cau tu Client!\n”;
// Gửi trả lại cho CLIENT địa chỉ IP của mình
nSend = sendto(*sock,lpMyIP,strlen(lpMyIP),0,(sockaddr*)&IPClient,sizeof(IPClient));
cout << “Gui lai client: ” << nSend << ” Bytes\n”;
}
else{
cout << “Khong hieu yeu cau tu Client!\n”;
}
cout << “\n”;
}

b. CLIENT

-> Nhiệm vụ chính của CLIENT là gửi 1 gói tin BROADCAST với PORT quy định sẵn

Trong chương trình của CLIENT có 1 hàm rất quan trọng để cho phép SOCKET gởi tới địa chỉ BROADCAST

setsockopt(*sock,SOL_SOCKET,SO_BROADCAST,(char*)&b SockBroadcast,sizeof(BOOL));

– Op tùy chọn ở đây là : SO_BROADCAST (gởi gói tin Broadcast);
– bSockBroadcast: TRUE (cho phép), FALSE (không cho phép).

Hàm này có rất nhiều Option. Các bạn có thể tham khảo thêm MSDN.

int FindServer(SOCKET* sock,sockaddr_in* pServerAddr){
BOOL bSockBroadcast=true;
setsockopt(*sock,SOL_SOCKET,SO_BROADCAST,(char*)&bSockBroadcast,sizeof(BOOL));
pServerAddr->sin_family = AF_INET;
pServerAddr->sin_port = htons(MY_PORT);
pServerAddr->sin_addr.S_un.S_addr = inet_addr(“255.255.255.255″); // Địa chỉ đích là MULTICAST
int nSend;
int nRevc;
int nAddrLen;
char buff[512]=”IP may chu dau???”; // Thông tin gửi tới máy chủ
// Gửi tới máy chủ
nSend = sendto(*sock,buff,sizeof(buff),0,(sockaddr*)pServerAddr,sizeof(sockaddr_in));
cout << “Da gui ” << nSend << ” bytes\n”;
// Nhận hồi âm của máy chủ
sockaddr_in ServerIP;
nAddrLen = sizeof(sockaddr_in);
nRevc = recvfrom(*sock,buff,sizeof(buff),0, (sockaddr*)&ServerIP, &nAddrLen);
buff[nRevc] = 0;
cout << “Da nhan ” << nRevc << ” bytes\n”;
cout << “Noi dung nhan <IP SERVER>: ” << buff << “\n”;
return 0;
}
<<To be continued>> …
Advertisements

About Argron Nguyen's Blog

Vietnamese. Photographer. Writer. Illustrator. IT-er. All to some extent.

Posted on 15.03.2012, in Network Programming. Bookmark the permalink. 5 phản hồi.

  1. Nguyễn Trọng Linh

    bác cho em xin cái demo về web server với web browser được không…em làm theo cái này mà ko chạy được…cảm ơn bác rất nhiều

    mail của em là : nguyentronglinh11@gmail.com
    yahoo em là : trong_linh34

    • Rất tiếc là cái bài này mình đã làm lâu rồi nên demo thì chắc không còn. Bạn có thể tham khảo một số article trên codeproject.com mình viết bài này cũng từ demo trên đấy mà ra cả. :)

  2. Bài viết hay dù đôi chỗ hiểu hết. Cảm ơn ad

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Đăng xuất / Thay đổi )

Connecting to %s

%d bloggers like this: