网络编程——多线程技术实现网络聊天程序
最近学习了网络编程,在学习多线程技术后,就尝试着用多线程技术写了一个网络聊天程序。下面介绍一下具体的实现过程。
既然是聊天程序,肯定要发送数据和接收数据,这里我新创建了一个线程来接收数据,用主线程来发送数据。同一个进程中的两个线程可以独立运行并相互交通信。因此可以实现数据的接收和发送。
先建立一个基于对话框的应用程序,并在对话框上放置接收数据的编辑框、发送数据的编辑框、目的主机的IP地址控件、发送按钮等相应控件,并修改其ID值。然后开始添加代码,实现网络聊天程序。
网络聊天程序都是有一定的步骤可循的,因此我按一定的步骤来实现该程序:
一、加载套接字库
在用Winsock编程之前,肯定要加载套接字库并进行版本协商。加载套接字库可以用系统提供的AfxSocketInit()函数来加载套接字库并进行版本协商。根据MSDN的提示,我们可以在CWinApp::InitInstance() 函数中实现。具体实现代码如下:
BOOL CChatbeta1App::InitInstance()
{
if (!AfxSocketInit())
{
AfxMessageBox("加载套接字库失败!");
return FALSE;
}
……
}
二、初始化套接字信息
加载了套接字库后,我们就可以开始正式的Winsock编程了。按照步骤,我们应该先创建套接字,接着讲套接字绑定到指定的IP地址和端口上。那么在基于对话框的程序设计中,我们可以将这一系列操作放到一个对话框类的成员函数中来初始化这些套接字信息。因此我们为对话框类添加成员函数。实现代码如下:
头文件中:
public:
BOOL Initsocket(void);
SOCKET m_socket;
源文件中:
/*****************************************************************************
初始化套接字
********************************************************************************/
BOOL CChatbeta1Dlg::Initsocket(void)
{
m_socket=socket(AF_INET,SOCK_DGRAM,0);
if (INVALID_SOCKET==m_socket)
{
MessageBox("创建套接字失败!");
return FALSE;
}
SOCKADDR_IN addrSock;
addrSock.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
addrSock.sin_family=AF_INET;
addrSock.sin_port=htons(6000);
int retval;
retval=bind(m_socket,(const sockaddr*)&addrSock,sizeof(SOCKADDR));
if (SOCKET_ERROR==retval)
{
closesocket(m_socket);
MessageBox("绑定套接字失败!");
return FALSE;
}
return 0;
}
在这个初始化函数中,我们将创建的套接字绑定到指定IP地址和端口上。下面就可以实现接收数据和发送数据的功能了。
三、接收数据
Windows套接字在两种模式下执行I/O操作,一种是阻塞模式,一种是非阻塞模式。在阻塞模式下,在I/O操作完成之前,执行I/O操作的Winsock函数会一直等待下去,不会立即返回程序,将控制权交回程序。在非阻塞模式下,Winsock函数都会立即返回。这里我们使用在阻塞模式下编程,既然在阻塞模式下,假如接收数据的函数一直等待,那么程序就暂停执行,将不能发送数据。为了避免这种情况的发生,我们可以创建一个新的线程,让线程函数来接收数据,它不会阻碍主线程的执行,让主线程发送数据。
因此我们要用CreateThread()创建新线程。我们想让线程接收数据,需要向新线程传递参数,向其传递执行网络通信的套接字和接收数据的窗口句柄。但是该函数只有第四个参数可以接收数据,而我们需要传递两个参数。但是我们发现,第四个参数是个指针类型,我们可以构造一个结构体指针,向其传递两个参数。实现代码如下:
头文件中:
struct RECVPARAM
{
SOCKET socket;
HWND hwnd;
};
源文件中OnInitDialog()中:
/****************************************************************
创建接收线程
*****************************************************************/
RECVPARAM *pRecvParam=new RECVPARAM;
pRecvParam->socket=m_socket;
pRecvParam->hwnd=m_hWnd;
HANDLE hThread=CreateThread(NULL,0,RecvProc,(LPVOID*)pRecvParam,0,NULL);
CloseHandle(hThread);
但是这时又有一个问题,线程函数在何处实现呢。既然是基于对话框的应用程序,我们可以把该函数作为对话框的成员函数,但是对成员函数的调用,需要由类对象来调用,这时创建对话框对象还没有完成,自然不能创建成员函数。但是我们可以把该函数创建为静态成员函数来实现。静态成员函数只属于类本身,不属于任何对象。实现代码如下:
头文件中:
static DWORD WINAPI RecvProc(LPVOID lpParameter);
源文件中:
/**************************************************************************
接收数据的线程函数的实现
************************************************************************/
DWORD WINAPI CChatbeta1Dlg::RecvProc(LPVOID lpParameter)
{
SOCKET socket=((RECVPARAM*)lpParameter)->socket;
HWND hwnd=((RECVPARAM*)lpParameter)->hwnd;
delete lpParameter;
SOCKADDR_IN addrFrom;
int len=sizeof(SOCKADDR);
char recvBuf[200];
char tempBuf[300];
int retval;
while(TRUE)
{
retval=recvfrom(socket,recvBuf,200,0,(sockaddr*)&addrFrom,&len);
if (SOCKET_ERROR==retval)
break;
sprintf(tempBuf,"%s 说:%s",inet_ntoa(addrFrom.sin_addr),recvBuf);
::PostMessage(hwnd,WM_RECVDATA,0,(LPARAM)tempBuf);
}
return 0;
}
我们想要把聊天记录写入聊天记录对话框中,我们可以直接在线程函数中实现,这里我用了发送自定义的消息实现。实现过程如下:
头文件中:
#define WM_RECVDATA WM_USER+1//自定义消息
源文件中:
/***************************************************************************
想编辑框中写入聊天记录
**************************************************************************/
LRESULT CChatbeta1Dlg::OnRecvData(WPARAM wParam,LPARAM lParam)
{
CString str=(char*)lParam;
CString strTemp;
GetDlgItemText(IDC_EDIT_RECV,strTemp);
str+="\r\n";
str+=strTemp;
SetDlgItemText(IDC_EDIT_RECV,str);
return TRUE;
}
四、发送数据
当在发送编辑框中输入信息后,单击发送按钮就可以将数据发送给指定的主机。在按钮的单击函数中可以实现这样的功能:
void CChatbeta1Dlg::OnClickedBtnSend()
{
// TODO: Add your control notification handler code here
DWORD dwIP;
((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);
SOCKADDR_IN addrTo;
addrTo.sin_addr.S_un.S_addr=htonl(dwIP);
addrTo.sin_family=AF_INET;
addrTo.sin_port=htons(6000);
CString strSend;
GetDlgItemText(IDC_EDIT_SEND,strSend);
sendto(m_socket,strSend,strSend.GetLength()+1,0,
(const sockaddr*)&addrTo,sizeof(SOCKADDR));
SetDlgItemText(IDC_EDIT_SEND,"");
}
运行结果如下:
这样,整个程序就完成了。在这个小程序中,我还是按照网络编程的步骤一步一步实现的。用到的技术关键点有:创建新线程来接收数据;向线程函数传递参数。而且我用了在阻塞模式下的编程模式。