例子代碼就在個人博客中,包括六個UDP和TCP發送接受的cpp文件,一個基於MFC的局域網聊天小工具工程,和此小工具的全部運行時庫、資源和執行程序。代碼的壓縮包位置是http://www.blogjava.net/Files/wxb_nudt/socket_src.rar。java
在一些經常使用的編程技術中,Socket網絡編程能夠說是最簡單的一種。並且Socket編程須要的基礎知識不多,適合初學者學習網絡編程。目前支持網絡傳輸的技術、語言和工具繁多,可是大部分都是基於Socket開發的,雖然說這些「高級」的網絡技術屏蔽了大部分底層實現,號稱能極大程度的簡化開發,而事實上若是你沒有一點Socket基礎,要理解和應用這些技術仍是很困難的,並且會讓你成爲「半瓢水」。ios
深有感觸的是當年我學習CORBA的時候,因爲當時各方面的基礎薄弱,整整啃了半年書,最終仍是一頭霧水。若是如今讓我帶一我的學CORBA,我必定會安排好順序:首先弄清C++語法;而後是VC編譯環境或者nmake的用法;接下來學習一些網絡基礎知識;而後是Socket編程;這些大概要花費三、4個月。有了這些基礎學習CORBA一週便可弄懂,兩個月就能夠基於CORBA進行開發了。c++
好了,說了半天其實中心思想就一個,Socket很簡單,很好學!若是你會C++或者JAVA,又懂一點點網絡基礎如TCP和UDP的機制,那麼你看完本文就能夠熟練進行Socket開發了。程序員
(本節內容所有抄自網絡,不保證正確性,有興趣的能夠看看!)編程
80年代初,美國政府的高級研究工程機構(ARPA)給加利福尼亞大學Berkeley分校提供了資金,讓他們在UNIX操做系統下實現TCP/IP協議。在這個項目中,研究人員爲TCP/IP網絡通訊開發了一個API(應用程序接口)。這個API稱爲Socket接口(套接字)。今天,SOCKET接口是TCP/IP網絡最爲通用的API,也是在INTERNET上進行應用開發最爲通用的API。windows
90年代初,由Microsoft聯合了其餘幾家公司共同制定了一套WINDOWS下的網絡編程接口,即WindowsSockets規範。它是BerkeleySockets的重要擴充,主要是增長了一些異步函數,並增長了符合Windows消息驅動特性的網絡事件異步選擇機制。WINDOWSSOCKETS規範是一套開放的、支持多種協議的Windows下的網絡編程接口。從1991年的1.0版到1995年的2.0.8版,通過不斷完善並在Intel、Microsoft、Sun、SGI、Informix、Novell等公司的全力支持下,已成爲Windows網絡編程的事實上的標準。目前,在實際應用中的WINDOWSSOKCETS規範主要有1.1版和2.0版。二者的最重要區別是1.1版只支持TCP/IP協議,而2.0版能夠支持多協議。2.0版有良好的向後兼容性,任何使用1.1版的源代碼,二進制文件,應用程序均可以不加修改地在2.0規範下使用。網絡
SOCKET實際在計算機中提供了一個通訊端口,能夠經過這個端口與任何一個具備SOCKET接口的計算機通訊。應用程序在網絡上傳輸,接收的信息都經過這個SOCKET接口來實現。在應用開發中就像使用文件句柄同樣,能夠對SOCKET句柄進行讀,寫操做。多線程
網上不少文章對於Socket的前因後果有如教科書通常的精準。可是涉及具體編程技術就每每被VC等集成開發環境所毒害了,把Windows SDK、MFC、Socket、多線程、DLL以及編譯連接等等技術攪合在一塊兒煮成一鍋夾生飯。框架
既然要學習Socket,就應該用最簡單直白的方式把Socket的幾個使用要點講出來。我認爲程序員最關心的有如下幾點,按照優先級排列以下:
1. Socket的機制是什麼?
2. 用C/C++寫Socket須要什麼頭文件、庫文件、DLL,它們能夠由誰提供,安裝後通常處於系統的哪一個文件夾內?
3. 編寫Socket程序須要的編程基礎是什麼?
4. Socket庫內最重要的幾個函數和數據類型是什麼?
5. 兩個最簡單的例子程序;
6. 一個貼近應用的稍微複雜的Socket應用程序。
我將一一講述這些要點,並給出從簡到繁,從樸素到花哨的全部源代碼以及編譯連接的命令。
咱們能夠簡單的把Socket理解爲一個能夠連通網絡上不一樣計算機程序之間的管道,把一堆數據從管道的A端扔進去,則會從管道的B端(也許同時還能夠從C、D、E、F……端冒出來)。管道的端口由兩個因素來惟一確認,即機器的IP地址和程序所使用的端口號。IP地址的含義全部人都知道,所謂端口號就是程序員指定的一個數字,許多著名的木馬程序整天在網絡上掃描不一樣的端口號就是爲了獲取一個能夠連通的端口從而進行破壞。比較著名的端口號有http的80端口和ftp的21端口(我記錯了麼?)。固然,建議你們本身寫程序不要使用過小的端口號,它們通常被系統佔用了,也不要使用一些著名的端口,通常來講使用1000~5000以內的端口比較好。
Socket能夠支持數據的發送和接收,它會定義一種稱爲套接字的變量,發送數據時首先建立套接字,而後使用該套接字的sendto等方法對準某個IP/端口進行數據發送;接收端也首先建立套接字,而後將該套接字綁定到一個IP/端口上,全部發向此端口的數據會被該套接字的recv等函數讀出。如同讀出文件中的數據同樣。
對於目前使用最普遍的Windows Socket2.0版本,所需的一些文件以下(以安裝了VC6爲例說明其物理位置):
l 頭文件winsock2.h,一般處於C:"Program Files"Microsoft Visual Studio"VC98"INCLUDE;查看該頭文件可知其中又包含了windows.h和pshpack4.h頭文件,所以在windows中的一些經常使用API均可以使用;
l 庫文件Ws2_32.lib,一般處於C:"Program Files"Microsoft Visual Studio"VC98"Lib;
l DLL文件Ws2_32.dll,一般處於C:"WINDOWS"system32,這個是能夠猜到的。
在開始編寫Socket程序以前,須要如下編程基礎:
l C++語法;
l 一點點windows SDK的基礎,瞭解一些SDK的數據類型與API的調用方式;
l 一點點編譯、連接和執行的技術;知道cl和link的最經常使用用法便可。
用最通俗的話講,所謂UDP,就是發送出去就無論的一種網絡協議。所以UDP編程的發送端只管發送就能夠了,不用檢查網絡鏈接狀態。下面用例子來講明怎樣編寫UDP,並會詳細解釋每一個API和數據類型。
下面是一個用UDP發送廣播報文的例子。
#include <winsock2.h>
#include <iostream.h>
void main()
{
SOCKET sock; //socket套接字
char szMsg[] = "this is a UDP test package";//被髮送的字段
//1.啓動SOCKET庫,版本爲2.0
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 2, 0 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( 0 != err ) //檢查Socket初始化是否成功
{
cout<<"Socket2.0初始化失敗,Exit!";
return;
}
//檢查Socket庫的版本是否爲2.0
if (LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 0 )
{
WSACleanup( );
return;
}
//2.建立socket,
sock = socket(
AF_INET, //internetwork: UDP, TCP, etc
SOCK_DGRAM, //SOCK_DGRAM說明是UDP類型
0 //protocol
);
if (INVALID_SOCKET == sock ) {
cout<<"Socket 建立失敗,Exit!";
return;
}
//3.設置該套接字爲廣播類型,
bool opt = true;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, reinterpret_cast<char FAR *>(&opt), sizeof(opt));
//4.設置發往的地址
sockaddr_in addrto; //發往的地址
memset(&addrto,0,sizeof(addrto));
addrto.sin_family = AF_INET; //地址類型爲internetwork
addrto.sin_addr.s_addr = INADDR_BROADCAST; //設置ip爲廣播地址
addrto.sin_port = htons(7861); //端口號爲7861
int nlen=sizeof(addrto);
unsigned int uIndex = 1;
while(true)
{
Sleep(1000); //程序休眠一秒
//向廣播地址發送消息
if( sendto(sock, szMsg, strlen(szMsg), 0, (sockaddr*)&addrto,nlen)
== SOCKET_ERROR )
cout<<WSAGetLastError()<<endl;
else
cout<<uIndex++<<":an UDP package is sended."<<endl;
}
if (!closesocket(sock)) //關閉套接字
{
WSAGetLastError();
return;
}
if (!WSACleanup()) //關閉Socket庫
{
WSAGetLastError();
return;
}
}
編譯命令:
CL /c UDP_Send_Broadcast.cpp
連接命令(注意若是找不到該庫,則要在後面的/LIBPATH參數後加上庫的路徑):
link UDP_Send_Broadcast.obj ws2_32.lib
執行命令:
D:"Code"成品代碼"Socket"socket_src>UDP_Send_Broadcast.exe
1:an UDP package is sended.
2:an UDP package is sended.
3:an UDP package is sended.
4:an UDP package is sended.
^C
下面一一解釋代碼中出現的數據類型與API函數。有耐心的能夠仔細看看,沒耐心的依葫蘆畫瓢也能夠寫程序了。
SOCKET是socket套接字類型,在WINSOCK2.H中有以下定義:
typedef unsigned int u_int;
typedef u_int SOCKET;
可知套接字實際上就是一個無符號整型,它將被Socket環境管理和使用。套接字將被建立、設置、用來發送和接收數據,最後會被關閉。
WORD類型是一個16位的無符號整型,在WTYPES.H中被定義爲:
typedef unsigned short WORD;
其目的是提供兩個字節的存儲,在Socket中這兩個字節能夠表示主版本號和副版本號。使用MAKEWORD宏能夠給一個WORD類型賦值。例如要表示主版本號2,副版本號0,可使用如下代碼:
WORD wVersionRequested;
wVersionRequested = MAKEWORD( 2, 0 );
注意低位內存存儲主版本號2,高位內存存儲副版本號0,其值爲0x0002。使用宏LOBYTE能夠讀取WORD的低位字節,HIBYTE能夠讀取高位字節。
WSADATA類型是一個結構,描述了Socket庫的一些相關信息,其結構定義以下:
typedef struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
} WSADATA;
typedef WSADATA FAR *LPWSADATA;
值得注意的就是wVersion字段,存儲了Socket的版本類型。LPWSADATA是WSADATA的指針類型。它們不用程序員手動填寫,而是經過Socket的初始化函數WSAStartup讀取出來。
WSAStartup函數被用來初始化Socket環境,它的定義以下:
int PASCAL FAR WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData);
其返回值爲整型,調用方式爲PASCAL(即標準類型,PASCAL等於__stdcall),參數有兩個,第一個參數爲WORD類型,指明瞭Socket的版本號,第二個參數爲WSADATA類型的指針。
若返回值爲0,則初始化成功,若不爲0則失敗。
這是Socket環境的退出函數。返回值爲0表示成功,SOCKET_ERROR表示失敗。
socket的建立函數,其定義爲:
SOCKET PASCAL FAR socket (int af, int type, int protocol);
第一個參數爲int af,表明網絡地址族,目前只有一種取值是有效的,即AF_INET,表明internet地址族;
第二個參數爲int type,表明網絡協議類型,SOCK_DGRAM表明UDP協議,SOCK_STREAM表明TCP協議;
第三個參數爲int protocol,指定網絡地址族的特殊協議,目前無用,賦值0便可。
返回值爲SOCKET,若返回INVALID_SOCKET則失敗。
這個函數用來設置Socket的屬性,若不能正確設置socket屬性,則數據的發送和接收會失敗。定義以下:
int PASCAL FAR setsockopt (SOCKET s, int level, int optname,
const char FAR * optval, int optlen);
其返回值爲int類型,0表明成功,SOCKET_ERROR表明有錯誤發生。
第一個參數SOCKET s,表明要設置的套接字;
第二個參數int level,表明要設置的屬性所處的層次,層次包含如下取值:SOL_SOCKET表明套接字層次;IPPROTO_TCP表明TCP協議層次,IPPROTO_IP表明IP協議層次(後面兩個我都沒有用過);
第三個參數int optname,表明設置參數的名稱,SO_BROADCAST表明容許發送廣播數據的屬性,其它屬性可參考MSDN;
第四個參數const char FAR * optval,表明指向存儲參數數值的指針,注意這裏可能要使用reinterpret_cast類型轉換;
第五個參數int optlen,表明存儲參數數值變量的長度。
sockaddr_in定義了socket發送和接收數據包的地址,定義:
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其中in_addr的定義以下:
struct in_addr {
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
首先闡述in_addr的含義,很顯然它是一個存儲ip地址的聯合體(忘記union含義的請看c++書),有三種表達方式:
第一種用四個字節來表示IP地址的四個數字;
第二種用兩個雙字節來表示IP地址;
第三種用一個長整型來表示IP地址。
給in_addr賦值的一種最簡單方法是使用inet_addr函數,它能夠把一個表明IP地址的字符串賦值轉換爲in_addr類型,如
addrto.sin_addr.s_addr=inet_addr("192.168.0.2");
本例子中因爲是廣播地址,因此沒有使用這個函數。其反函數是inet_ntoa,能夠把一個in_addr類型轉換爲一個字符串。
sockaddr_in的含義比in_addr的含義要普遍,其各個字段的含義和取值以下:
第一個字段short sin_family,表明網絡地址族,如前所述,只能取值AF_INET;
第二個字段u_short sin_port,表明IP地址端口,由程序員指定;
第三個字段struct in_addr sin_addr,表明IP地址;
第四個字段char sin_zero[8],很搞笑,是爲了保證sockaddr_in與SOCKADDR類型的長度相等而填充進來的字段。
如下表明指明瞭廣播地址,端口號爲7861的一個地址:
sockaddr_in addrto; //發往的地址
memset(&addrto,0,sizeof(addrto));
addrto.sin_family = AF_INET; //地址類型爲internetwork
addrto.sin_addr.s_addr = INADDR_BROADCAST; //設置ip爲廣播地址
addrto.sin_port = htons(7861); //端口號爲7861
sockaddr類型是用來表示Socket地址的類型,同上面的sockaddr_in類型相比,sockaddr的適用範圍更廣,由於sockaddr_in只適用於TCP/IP地址。Sockaddr的定義以下:
struct sockaddr {
u_short sa_family;
char sa_data[14];
};
可知sockaddr有16個字節,而sockaddr_in也有16個字節,因此sockaddr_in是能夠強制類型轉換爲sockaddr的。事實上也每每使用這種方法。
線程掛起函數,表示線程掛起一段時間。Sleep(1000)表示掛起一秒。定義於WINBASE.H頭文件中。WINBASE.H又被包含於WINDOWS.H中,而後WINDOWS.H被WINSOCK2.H包含。因此在本例中使用Sleep函數不須要包含其它頭文件。
在Socket中有兩套發送和接收函數,一是sendto和recvfrom;二是send和recv。前一套在函數參數中要指明地址;然後一套須要先將套接字和一個地址綁定,而後直接發送和接收,不需綁定地址。sendto的定義以下:
int PASCAL FAR sendto (SOCKET s, const char FAR * buf, int len, int flags, const struct sockaddr FAR *to, int tolen);
第一個參數就是套接字;
第二個參數是要傳送的數據指針;
第三個參數是要傳送的數據長度(字節數);
第四個參數是傳送方式的標識,若是不須要特殊要求則能夠設置爲0,其它值請參考MSDN;
第五個參數是目標地址,注意這裏使用的是sockaddr的指針;
第六個參數是地址的長度;
返回值爲整型,若是成功,則返回發送的字節數,失敗則返回SOCKET_ERROR。
該函數用來在Socket相關API失敗後讀取錯誤碼,根據這些錯誤碼能夠對照查出錯誤緣由。
關閉套接字,其參數爲SOCKET類型。成功返回0,失敗返回SOCKET_ERROR。
總結以上內容,寫一個UDP發送程序的步驟以下:
1. 用WSAStartup函數初始化Socket環境;
2. 用socket函數建立一個套接字;
3. 用setsockopt函數設置套接字的屬性,例如設置爲廣播類型;不少時候該步驟能夠省略;
4. 建立一個sockaddr_in,並指定其IP地址和端口號;
5. 用sendto函數向指定地址發送數據,這裏的目標地址就是廣播地址;注意這裏不須要綁定,即便綁定了,其地址也會被sendto中的參數覆蓋;若使用send函數則會出錯,由於send是面向鏈接的,而UDP是非鏈接的,只能使用sendto發送數據;
6. 用closesocket函數關閉套接字;
7. 用WSACleanup函數關閉Socket環境。
那麼,與之相似,一個UDP接收程序的步驟以下,注意接收方必定要bind套接字:
1. 用WSAStartup函數初始化Socket環境;
2. 用socket函數建立一個套接字;
3. 用setsockopt函數設置套接字的屬性,例如設置爲廣播類型;
4. 建立一個sockaddr_in,並指定其IP地址和端口號;
5. 用bind函數將套接字與接收的地址綁定起來,而後調用recvfrom函數或者recv接收數據; 注意這裏必定要綁定,由於接收報文的套接字必須在網絡上有一個綁定的名稱才能保證正確接收數據;
6. 用closesocket函數關閉套接字;
7. 用WSACleanup函數關閉Socket環境。
廣播接收程序見源程序代碼UDP_Recv_Broadcast.cpp。編譯、連接、執行與UDP_Send_Broadcast相似。
廣播發送和接收使用並不普遍,通常來講指定發送和接收的IP比較經常使用。點對點方式的UDP發送和接收與上面的例子很是相似,不一樣的就是須要指定一個具體的IP地址。而且不須要調用setsockopt設置socket的廣播屬性。
其具體源代碼見UDP_Send_P2P.cpp和UDP_Recv_P2P.cpp。
注意在使用這兩個程序時要設爲本身所需的IP。
TCP與UDP最大的不一樣之處在於TCP是一個面向鏈接的協議,在進行數據收發以前TCP必須進行鏈接,而且在收發的時候必須保持該鏈接。
發送方的步驟以下(省略了Socket環境的初始化、關閉等內容):
1. 用socket函數建立一個套接字sock;
2. 用bind將sock綁定到本地地址;
3. 用listen偵聽sock套接字;
4. 用accept函數接收客戶方的鏈接,返回客戶方套接字clientSocket;
5. 在客戶方套接字clientSocket上使用send發送數據;
6. 用closesocket函數關閉套接字sock和clientSocket;
而接收方的步驟以下:
1. 用socket函數建立一個套接字sock;
2. 建立一個指向服務方的遠程地址;
3. 用connect將sock鏈接到服務方,使用遠程地址;
4. 在套接字上使用recv接收數據;
5. 用closesocket函數關閉套接字sock;
值得注意的是,在服務方有兩個地址,一個是本地地址myaddr,另外一個是目標地址addrto。本地地址myaddr用來和本地套接字sock綁定,目標地址被sock用來accept客戶方套接字clientSocket。這樣sock和clientSocket鏈接成功,這兩個地址也鏈接上了。在服務方使用clientSocket發送數據,則會從本地地址傳送到目標地址。
在客戶方只有一個地址,即來源地址addrfrom。這個地址被用來connect遠程的服務方套接字,connect成功則本地套接字與遠程的來源地址鏈接了,所以可使用該套接字接收遠程數據。其實這時客戶方套接字已經被隱性的綁定了本地地址,因此不須要顯式調用bind函數,即便調用也不會影像結果。
具體源代碼見TCP_Send.cpp和TCP_Recv.cpp。注意將源代碼中的IP地址修改成符合本身須要的IP。爲了減小代碼複雜性,沒有使用讀取本機IP的代碼,後續例子程序中含有此功能代碼。
bind函數用來將一個套接字綁定到一個IP地址。通常只在服務方(即數據發送方)調用,不少函數會隱式的調用bind函數。
從服務方監聽客戶方的鏈接。同一個套接字能夠屢次監聽。
connect是客戶方鏈接服務方的函數,而accept是服務方贊成客戶方鏈接的函數。這兩個配套函數分別在各自的程序中被成功調用後就能夠收發數據了。
send和recv是用來發送和接收數據的兩個重要函數。send只能在已經鏈接的狀態下使用,而recv能夠面向鏈接和非鏈接的狀態下使用。
send的定義以下:
int WSAAPI send(
SOCKET s,
const char FAR * buf,
int len,
int flags
);
其參數的含義和sendto中的前四個參數同樣。而recv的定義以下:
int WSAAPI recv(
SOCKET s,
char FAR * buf,
int len,
int flags
);
其參數含義與send中的參數含義同樣。
掌握了以上關於socket的基本用法,編寫一個局域網聊天程序也就變得很是簡單,如同設計一個普通的對話框程序同樣。
功能設計以下:
1. 要可以指定聊天對象的IP和端口(端口能夠內部肯定);
2. 要可以發送消息給指定聊天對象;
3. 要可以接收聊天對象的消息;
4. 接收消息時要播放聲音;
5. 接收消息時若是當前對話框不是最前端,要閃動圖標;
6. 要有托盤圖標,能夠將對話框收入托盤;
將內部端口設爲3456,提供一個IP地址控件來設置聊天對象的IP。該控件必須可以讀取IP地址並賦值給內部變量。將地址轉換爲in_addr類型。
發送消息須要使用一個套接字。
接收消息也須要使用一個套接字,因爲發送消息也使用了一個套接字,爲了在同一個進程中同時發送和接收消息,須要使用多線程技術,將發送消息的線程設爲主線程;而接收消息的線程設爲子線程,子線程只負責接收UDP消息,在收到消息後顯示到主界面中。
接收消息時播放聲音這個功能在子線程中完成,使用sndPlaySound函數,並提供一個wav文件便可。
閃動圖標這個最白癡的功能須要使用一個Timer,在主對話框類中添加一個OnTimer函數,定時檢查當前窗口狀態變量是否爲假,若爲假就每次設置另外一個圖標。若當前窗口顯示到最頂端,則設置爲默認圖標。
托盤圖標功能用網上下載的CtrayIcon類輕鬆搞定。須要提供一個自定義消息,一個彈出菜單資源。
頭文件:winsock2.h,Mmsystem.h
庫文件:ws2_32.lib,winmm.lib
dll:Ws2_32.dll,winmm.dll
wav文件:recv.wav
圖標:一個主程序圖標IDI_MAIN、四個變化圖標IDI_ICON1~4;
菜單:一個給托盤用的彈出菜單IDR_TRAYICON;
說明,Mmsystem.h和winmm.lib、winmm.dll是爲了那個播放聲音的功能。
托盤屬於界面功能,是變動不多的需求,所以首先完成。
1. 引入TRAYICON.H和TRAYICON.cpp兩個類;
2. 在CLANTalkDlg類中加入一個CTrayIconm_trayIcon;屬性;
3. 在CLANTalkDlg的構造函數中初始化m_trayIcon,m_trayIcon(IDR_TRAYICON);
4. 添加一個自定義消息WM_MY_TRAY_NOTIFICATION,即在三個地方添加消息定義、消息響應函數、消息映射;
5. 在InitDialog方法中調用托盤初始化的兩個函數 m_trayIcon.SetNotificationWnd(this, WM_MY_TRAY_NOTIFICATION); m_trayIcon.SetIcon(IDI_MAIN);
6. 重寫OnClose方法,添加彈出菜單的OnAppSuspend和OnAppOpen以及OnAppAbout方法;
7. 重寫對話框的OnCancel方法。
動態圖標也是界面相關功能,首先完成。
1. 添加四個HICON變量m_hIcon1,m_hIcon2,m_hIcon3,m_hIcon4;
2. 在構造函數中初始化這四個變量m_hIcon1 = AfxGetApp()->LoadIcon(IDI_ICON1);
3. 在InitDialog中設置調用SetTimer(1,300,NULL);設置一個timer,id爲1,間隔爲300微秒;
4. 添加一個布爾屬性m_bDynamicIcon,指示目前是否須要動態圖標,並給出一個設置函數SetDynamicIcon;
5. 添加一個OnTimer函數,讓每次timer調用時根據m_bDynamicIcon的值修改圖標;
兩個地方是用來設置動態圖標的,一個是當程序收到消息而且程序不在桌面頂端時,這時設置爲動態圖標,在後面的消息接收線程中處理;二是當程序顯示到桌面頂端時,設置爲非動態;
重載OnActivate方法能夠完成第二個時刻的要求。當窗口狀態爲WA_ACTIVE或者WA_CLICKACTIVE時SetDynamicIcon(false),不然設置SetDynamicIcon(true);
發送UDP報文只需在主線程中完成,須要如下步驟:
1. 初始化Socket環境,這能夠在CLANTalkApp的InitInstance中完成,同理關閉Socket環境在ExitInstance中完成;咱們可使用前面的方法,也能夠直接調用MFC中的AfxSocketInit函數,這個函數能夠確保在程序結束時自動關閉Socket環境;
2. 建立socket,考慮到報錯信息須要彈出對話框,所以不在CLANTalkDlg的構造函數中建立,而是在InitDialog中構建;發送報文的socket爲m_sendSock;
3. 設置目的地址功能,須要一個地址賦值函數setAddress(char* szAddr);能夠將一個字符串地址賦值給sockaddr_in形式的地址;在CLANTalkDlg中增長一個sockaddr_in m_addrto;屬性;
4. 讀取文本框中的文字,用sendto發送到對象地址;
5. 清空文本框,在記錄框中添加聊天記錄。
這時可使用前面的UDP簡單接收程序來輔助測試,由於此時還未完成報文接收功能。
接收UDP報文要考慮幾個問題,第一個是要建立一個子線程,在子線程中接收報文;第二是接收報文和發送報文要有互斥機制,以避免衝突;第三是接收到報文要播放聲音;第四是接收報文且當前窗口不在桌面頂端要調用動態圖標功能。
按照以上需求設計步驟以下:
1. 建立接收套接字m_recvSock,
2. 利用gethostname和gethostbyname等函數獲取本機IP,並將套接字bind到該地址;
3. 添加一個CwinThread* m_pRecvThread屬性,並在InitDialog中調用AfxBeginThread建立子線程;
4. 編寫子線程運行函數void RecvProcess(LPVOID pParam),這時一個全局函數,爲了方便調用CLANTalkDlg類中的各類變量與方法,將CLANTalkDlg類的指針做爲參數傳入子線程函數,並將RecvProcess設置爲CLANTalkDlg類的友元。
5. 子線程函數中完成如下功能:利用recv接收報文;保存聊天記錄;判斷當前窗口是否在前臺,並修改動態圖標屬性;播放聲音。
6. 用來記錄聊天信息的ClistBox的Sort屬性要去掉,不然記錄會按內容排序,很很差看。在RC編輯器中去掉這個屬性便可。
7. 最後要注意,在主線程退出時要保證子線程退出,但此時子線程還阻塞在recv方法上,所以主線程向本身發送一條消息消除阻塞,同時改變子線程退出標誌保證子線程能夠退出。
點擊「確認對象」按鈕時,檢測IP地址控件,若是IP地址有效,則將IP地址讀入內部屬性。這個IP地址做爲發送信息的目標地址。
這個設置只能設置發送消息的對象,全部人均可以向本機發送信息,只要他的端口是正確的。
下載壓縮包後能夠打開VC工程編譯連接,若直接運行則能夠點擊LANTalkExeFile目錄中的可執行文件,這個目標包含了運行所須要的全部dll和資源文件。
固然,若是須要能夠用InstallShield作一個安裝程序,不過看來是沒有必要的。
這個聊天程序很簡單,可是基本上具備了一個框架,能夠有最簡單的聊天功能。要在此基礎上進行擴展幾乎已經沒有什麼技術問題了。
本文中全部的技術儘可能採用最原始的方式來使用。例如多線程使用的是AfxBeginThread,套接字使用了最原始的套接字,並在不少地方直接使用了SDK函數,而儘可能避免了MFC等代碼框架,這是爲了方便他人掌握技術的最基本內涵。
其實在具體的編程中,固然是怎麼方便怎麼來,Socket和多線程以及界面等功能都有大量方即可用的代碼庫,複用這些代碼庫會比本身動手寫方便不少。可是,掌握了基本原理再使用這些庫,事半功倍!