一、簡單的TCP服務器
介紹
WinSock API是一套供Microsoft Windows操作系統使用的套接字程序庫,它最初基於Berkeley套接字,但是其中加入了一些Microsoft的特殊改動。在這篇文章中,我要試着給你介紹如何使用WinSock來進行套接字程序設計,並假設你沒有在任何操作系統上進行過網絡編程的經驗。
如果你只有一臺單獨的機器,那麼不用着急,你仍然可以進行WinSock程序設計。你可以使用名爲localhost的本地迴環地址,它的IP地址是127.0.0.1。這樣一來,如果你在機器上運行了一個TCP服務器,那麼同一機器上的客戶端程序就可以使用這個迴環地址連接到服務器了。
簡單的TCP服務器
在本文中,我將通過一個簡單的TCP服務器來向你介紹WinSock,我們會一步一步地創建這個程序。但是,在我們開始之前,你還必須做一些事情,這樣我們才能爲開始我們的WinSock程序做好準備。
·首先,使用VC++ 6.0應用程序嚮導來創建一個Win32 console application。
·選擇add support for MFC選項。
·打開stdafx.h文件,並添加這一行:#include <winsock2.h>。
·選擇Project-Settings-Link,並在庫模塊列表中加入ws2_32.lib。
main函數
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
int nRetCode = 0;
cout << "Press ESCAPE to terminate program/r/n";
AfxBeginThread(ServerThread,0);
while(_getch()!=27);
return nRetCode;
}
我們在main()中所做的是開啓一個線程,然後對一個_getch()調用進行循環。_getch()僅僅是等待一個鍵的按下,並返回這個讀入字符的ASCII值。我們一直循環,直到返回27這個值爲止——既然27是ESCAPE鍵的ASCII碼。你可能想知道的是,即使我們按下了ESCAPE,我們開啓的線程也還會是活動的狀態。不用爲這些事情擔心,因爲當main()返回的時候,進程就會被終止,主線程開啓的線程也會被突然終止。
ServerThread函數
現在我所要做的事情就是把我們的ServerThread函數列出來,並使用代碼的註釋來解釋相關的代碼行做了些什麼。我們的TCP服務器主要做的事情是監聽端口20248,這個數字也就是我在Code Project的成員ID。這個過程中的事件是:當客戶端連接的時候,服務器將會向客戶端發回一條消息告知它的IP地址,然後關閉連接並繼續接收20248端口的連接。它還會在運行的控制檯上打印出連接來自的IP地址。總而言之,你可能會認爲這是一個絕對沒用的程序。事實上,你們中的有些人甚至可能會認爲它和Windows中的SNDREC32.EXE一樣沒用。我說,你們也忒苛刻了吧。
UINT ServerThread(LPVOID pParam)
{
cout << "Starting up TCP server/r/n";
// SOCKET其實是unsigned int的一個typedef。
// 在Unix中,套接字句柄就像文件句柄一樣,都是unsigned int。
// 既然在Windows下這些不是真的,那麼我們就定義了一種新的數據類型,名爲SOCKET。
SOCKET server;
// WSADATA是一個struct,WSAStartup的調用將會填充之。
WSADATA wsaData;
// sockaddr_in爲TCP/IP套接字指定了套接字的地址。
// 其它的協議都使用相似的結構。
sockaddr_in local;
// WSAStartup爲程序調用WinSock進行了初始化。
// 第一個參數指定了程序允許使用的WinSock規範的最高版本。
int wsaret=WSAStartup(0x101,&wsaData);
// 如果成功,WSAStartup返回零。
// 如果失敗,我們就退出。
if(wsaret!=0)
{
return 0;
}
// 現在我們來爲sockaddr_in結構賦值。
local.sin_family=AF_INET; // 地址族
local.sin_addr.s_addr=INADDR_ANY; // 網際IP地址
local.sin_port=htons((u_short)20248); // 使用的端口
// 由socket函數創建我們的SOCKET。
server=socket(AF_INET,SOCK_STREAM,0);
// 如果socket()函數失敗,我們就退出。
if(server==INVALID_SOCKET)
{
return 0;
}
// bind將我們剛創建的套接字和sockaddr_in結構聯繫起來。
// 它主要使用本地地址及一個特定的端口來連接套接字。
// 如果它返回非零值,就表示出現錯誤。
if(bind(server,(sockaddr*)&local,sizeof(local))!=0)
{
return 0;
}
// listen命令套接字監聽來自客戶端的連接。
// 第二個參數是最大連接數。
if(listen(server,10)!=0)
{
return 0;
}
// 我們需要一些變量來保存客戶端的套接字,因此我們在此聲明之。
SOCKET client;
sockaddr_in from;
int fromlen=sizeof(from);
while(true) // 無限循環
{
char temp[512];
// accept()將會接收即將到來的客戶端連接。
client=accept(server,
(struct sockaddr*)&from,&fromlen);
sprintf(temp,"Your IP is %s/r/n",inet_ntoa(from.sin_addr));
// 我們簡單地向客戶端發送這個字符串。
send(client,temp,strlen(temp),0);
cout << "Connection from " << inet_ntoa(from.sin_addr) <<"/r/n";
// 關閉客戶端套接字
closesocket(client);
}
// closesocket()關閉套接字,並釋放套接字描述符。
closesocket(server);
// 最初這個函數也許有些用處,現在保留它只是爲了向後兼容。
// 但是調用它可能會更安全,因爲我相信某些實現會使用它來結束WS2_32.DLL的使用。
WSACleanup();
return 0;
}
測試
運行這個服務器,並在它運行的時候使用telnet來連接機器的20248端口。如果你是在同一臺機器上使用,那麼就連接到localhost。
示例輸出
我們將會在服務器上看到這樣的輸出:
E:/work/Server/Debug>server
Press ESCAPE to terminate program
Starting up TCP server
Connection from 203.200.100.122
Connection from 127.0.0.1
E:/work/Server/Debug>
這是客戶端得到的:
nish@sumida:~$ telnet 202.89.211.88 20248
Trying 202.89.211.88...
Connected to 202.89.211.88.
Escape character is '^]'.
Your IP is 203.200.100.122
Connection closed by foreign host.
nish@sumida:~$
總結
呃,在本文中你瞭解瞭如何創建一個簡單的TCP服務器。在以後的文章中,我會給你一些更多的材料,你可以通過這些材料創建一個合適的TCP客戶端。如果有誰對於編譯這些代碼有問題的話可以mail我,我會發給你一個壓縮了的工程。謝謝您的閱讀。
二、簡單的TCP客戶端
介紹
本文是《Winsock程序設計入門(1) - 簡單的TCP服務器》一文的結局,如果你還沒有讀過第1部分的話,我還是建議你首先讀一下。在本文中,我將示範給你如何編寫一個簡單的TCP客戶端程序。我們要編寫一個程序,這個程序將連接到一個HTTP服務器,並獲得一個文件。
一個簡單的TCP客戶端程序流程
1、使用WSAStartup()初始化WinSock庫。
2、使用socket()創建一個IPPROTO_TCP SOCKET。
3、使用gethostbyname()/gethostbyaddr()獲取主機信息。
4、使用connect()和我們創建的套接字連接服務器。
5、使用send()/recv()發送和接收數據,直到我們的TCP會話結束。
6、使用closesocket()關閉套接字連接。
7、使用WSACleanup()釋放WinSock。
初始化WinSock
正如其它每個WinSock程序一樣,我們需要初始化WinSock庫。這也基本上是一種檢查WinSock是否在當前系統可用的方法,對於以前的版本,我們當然希望是這樣。
int wsaret=WSAStartup(0x101,&wsaData);
if(wsaret)
return;
創建SOCKET
套接字是一種實體,它擔當了客戶端和服務器之間的端點。當客戶端連接到服務器之後,就會存在兩個套接字——客戶端一邊的套接字和相應的服務器一邊的套接字。讓我們來稱它們爲CLIENTSOCK和SERVERSOCK。當客戶端在CLIENTSOCK使用send()時,服務器可以在SERVERSOCK使用recv()來接收客戶端所發送的數據,反之亦然。對於我們的目的,我們使用一個名爲socket()的函數來創建套接字。
SOCKET conn;
conn=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(conn==INVALID_SOCKET)
return;
獲取主機信息
顯然,我們在連接到主機(服務器)之前,要獲取它的信息。我們可以使用兩個函數——gethostbyname()和gethostbyaddr()。當我們擁有服務器的DNS名稱時,我們可以使用gethostbyname()函數,例如codeproject.com或ftp.myserver.org之類的名稱。當我們擁有要連接的服務器的IP地址時,可以使用gethostbyaddr()函數,例如192.168.1.1或202.54.1.100。
顯然,我們希望能使我們的最終用戶既能使用DNS名稱,也能使用IP地址。那麼,爲了這些工作對他來說透明,我們需要像下面這樣玩一個小把戲。我們對入口字符串使用inet_addr(),這個函數會把一個IP地址轉換成一個標準的網絡地址格式。這樣一來,如果它返回失敗,我們就可以知道這個字符串不是一個IP地址,如果它成功的話,我們就可以假設它是一個有效的IP地址了。
if(inet_addr(servername)==INADDR_NONE)
{
hp=gethostbyname(servername);
}
else
{
addr=inet_addr(servername);
hp=gethostbyaddr((char*)&addr,sizeof(addr),AF_INET);
}
if(hp==NULL)
{
closesocket(conn);
return;
}
連接到服務器
connect()函數用於向目標服務器建立連接。我們向它傳遞我們先前創建的套接字和一個sockaddr結構。我們使用由gethostbyname()/gethostbyaddr()返回的主機地址爲sockaddr成員賦值,並輸入一個要連接的有效端口。
server.sin_addr.s_addr=*((unsigned long*)hp->h_addr);
server.sin_family=AF_INET;
server.sin_port=htons(80);
if(connect(conn,(struct sockaddr*)&server,sizeof(server)))
{
closesocket(conn);
return;
}
會話
當套接字連接建立後,客戶端和服務器就可以通過send()和recv()來發送/接收數據了。這通常稱爲TCP會話。對於我們的特定情況,我們需要進行HTTP會話。和那些複雜的SMTP或POP3協議相比,它還是比較簡單的。HTTP的GET命令用於從HTTP服務器上獲取文件。這個文件可以是HTML文件、圖像文件、壓縮文件、MP3文件等等。這樣,這個文件就會被髮送了(這是它最簡單的形式)。當然,還有一些更復雜的方法來使用這個命令。
GET http-path-to-file/r/n/r/n
在我們的程序中,我們像這樣來發送GET命令:
sprintf(buff,"GET %s/r/n/r/n",filepath);
send(conn,buff,strlen(buff),0);
當我們發送了這個命令的時候,我們就應該知道服務器就要開始把我們所請求的文件發送給我們了。就像我們使用send()來發送我們的命令一樣,我們可以使用recv()來接收服務器發送給我們的數據。我們循環調用recv(),直到它返回零,這時候我們就會知道服務器已經將數據發送完畢了。並且,對於我們的特定情況,我們可以將這些數據寫入文件,就像我們要下載並保存這個文件一樣。
while(y=recv(conn,buff,512,0))
{
f.Write(buff,y);
}
關閉連接
現在我們的會話結束了,我們必須關閉連接。在我們的情況下,HTTP連接在文件發送完畢之後就會被服務器關閉了,但是這不要緊,我們仍然需要關閉我們的套接字並釋放資源。在更加複雜的會話中,我們通常在調用closesocket()之前調用shutdown()來確定緩衝區已經被刷新,否則可能會有部分數據丟失。
closesocket(conn);
釋放WinSock
我們調用WSACleanup()來結束WinSock的使用。
WSACleanup();
感謝您的閱讀。