最近马同学接到个任务,相关的是用windows的socket技术实现通信,部分要求如下:
(2) 采用TCP/IP Sock协议建立计算机间远程通讯;(5) 采用多线程技术实现实时传输和数据记录;
一、
看要求后,决定参照东东,搞个聊天室,先实现发文件和聊天,实时更新数据下一步搞。参照视频如下:http://www.cctry.com/thread-6-1-1.html,这是syc做的,为图方便,将服务器与客户端为一体了,在他基础上,分离了服务端和客户端。
本程序下载地址:
http://www.cctry.com/forum.php?mod=viewthread&tid=39491&page=1#pid330149
或 http://download.csdn.net/source/3528727
运行效果如下:
采用了skinsharp皮肤,使用方法如下:http://blog.csdn.net/akof1314/article/details/5038769,主要就是添加
#include "SkinH.h"
#pragma comment(lib,"SkinH.lib")
SkinH_Attach();
这3行,一般我喜欢添加在InitInstance中让所有对话框都是蓝色。哈哈
首先的几个框的初始化,比如ip初始化为127.0.0.1什么的,在OnInitDialog()中添加:
SetDlgItemText(IDC_IPADDR,"127.0.0.1");
SetDlgItemText(IDC_PORT_CLIENT,"8001"); //初始化端口和ip
GetDlgItem(IDC_DISCONNECT)->EnableWindow(FALSE);
GetDlgItem(IDC_SENDMSG)->EnableWindow(FALSE);
本程序采用多线程,在启动服务器时,启动监听文件和监听数据2个线程,代码如下:
void CServerDlg::OnStartServer()
{
AfxBeginThread(FileThread,this);//监听文件传输线程
m_hListenThread=CreateThread(NULL,0,ListenThreadFunc,this,0,NULL);
GetDlgItem(IDC_STOP_SERVER)->EnableWindow(TRUE);
GetDlgItem(IDC_SEND)->EnableWindow(TRUE);
}
文件接收线程代码如下:(由于我们是开一个线程去监听,监听到有传送请求后才开线程接收文件,所以这里是2个线程)
struct SAVEFILEPARAM
{
SOCKET sock;
CServerDlg* pCLAN;
};
#define NAMELENGTH 1024
#define BUFFERSIZE 65000
//////////////// 单线程接收文件线程 ////////////////
UINT SaveFileSingle( LPVOID param )
{
SAVEFILEPARAM sp;
sp.pCLAN=((SAVEFILEPARAM*)param)->pCLAN;
sp.sock=((SAVEFILEPARAM*)param)->sock;
CSocket sock;
sock.Attach(sp.sock );
DWORD Length;
char fileName[NAMELENGTH];
CString savePathName;
int modal,err;
memset(fileName,0,NAMELENGTH);
err=sock.Receive(&Length,sizeof(DWORD));//接收文件长度
if(0==err)
{
AfxMessageBox("连接被关闭了。");
sock.Close();
return 0;
}
if(SOCKET_ERROR==err)
{
AfxMessageBox("出错啦,socket的bug");
sock.Close();
return 0;
}
err=sock.Receive(fileName,NAMELENGTH);//接收文件名
if(0==err)
{
AfxMessageBox("连接被关闭啦");
sock.Close();
return 0;
}
if(SOCKET_ERROR==err)
{
AfxMessageBox("出错啦,fuck");
sock.Close();
return 0;
}
int iret = AfxMessageBox("文件到啦,要不要",MB_OKCANCEL);
if( IDCANCEL==iret )
{
sock.Close();
return 0;
}
CFileDialog fdlg(false,NULL,fileName);//保存文件对话框。
modal=fdlg.DoModal( );
if(IDCANCEL==modal)
{
sock.Close( );
return 0; //用户取消保存。
}
savePathName=fdlg.GetPathName();
//////////////////////////////////////////////////////////////
int finish=0;
DWORD step=0;
CFile file;
char buffer[BUFFERSIZE];//每次收,清空buffer?
if( 0==file.Open(savePathName,CFile::modeWrite |CFile::modeCreate| CFile::typeBinary) )
{
AfxMessageBox("打开要写入的文件出错。");
sock.Close();
return 0;
}
//////////////////////////////////////////////////////////
while( true)
{
finish=sock.Receive(buffer,BUFFERSIZE);
if(0==finish)
break;
if(SOCKET_ERROR==finish)
{
sock.Close();
sp.pCLAN->SetFocus();
return 0;
}
file.Write(buffer,finish);
step+=finish;
}
file.Close();
sock.Close();
CString strFinish;
strFinish.Format("已成功接收文件\r\n\r\n%s",fileName);
AfxMessageBox(strFinish);
sp.pCLAN->SetFocus();
return 0;
}
UINT FileThread(LPVOID param)
{//发文件我们固定用7000端口
CServerDlg* pDlg=(CServerDlg*)param;
CSocket fileListen;//定义监听套接字进行监听
if (!AfxSocketInit())
{
AfxMessageBox("AfxSocketInit()失败。");
return 0;
}
if(0==fileListen.Create(7000))//端口是7000啊,别忘啦
{
AfxMessageBox("监听套接字创建失败。");
fileListen.Close();
return 0;
}
if(0==fileListen.Listen())
{
AfxMessageBox("监听套接字监听失败。");
fileListen.Close();
return 0;
}
//监听套接字创建成功。
//监听套接字等待连接请求。
while( true )
{
CSocket sockClient;
if(0==fileListen.Accept(sockClient))
{
AfxMessageBox("监听套接字接受服务失败。");
fileListen.Close();
break ;
}
//用线程函数来接收文件。
/////////////////////////////////////////////////////////////////////////////
SAVEFILEPARAM param;
param.sock=sockClient.Detach();
param.pCLAN=pDlg;
AfxBeginThread(SaveFileSingle,(LPVOID)¶m);
//////////////////////////////////////////////////////////////////////////////
}
fileListen.Close();
return 0 ;
}
监听数据线程代码如下:(SOCKET_Selet为syc提供的线程函数,主要是封装select方法,进行异步I/O)
BOOL SOCKET_Select(SOCKET hSocket, int nTimeOut, BOOL bRead)
{
fd_set fdset; //fd_set为管理多个套接字的结构体
timeval tv; //超时时间结构体
FD_ZERO(&fdset);//将集合初始化为空集合
FD_SET(hSocket, &fdset);//将套接字hSocket加入集合fdset
nTimeOut = nTimeOut > 1000 ? 1000 : nTimeOut;
tv.tv_sec = 0;//超时时间---秒
tv.tv_usec = nTimeOut;//超时时间---毫秒
int iRet = 0;
if ( bRead ) {
iRet = select(0, &fdset, NULL , NULL, &tv);//查看服务器套接字集合是否有可读
}else{
iRet = select(0, NULL , &fdset, NULL, &tv);//查看服务器套接字集合是否有可写
}
if(iRet <= 0) {
return FALSE;
} else if (FD_ISSET(hSocket, &fdset)){ //查看套接字 hSocket是否在集合fdset中
return TRUE;
}
return FALSE;
}
DWORD WINAPI ListenThreadFunc(LPVOID pParam)
{
CServerDlg *pChatRoom = (CServerDlg *)pParam;
ASSERT(pChatRoom != NULL);
pChatRoom->m_ListenSock = socket(AF_INET , SOCK_STREAM , IPPROTO_TCP);
if ( pChatRoom->m_ListenSock == INVALID_SOCKET ) {
AfxMessageBox(_T("新建Socket失败!"));
return FALSE;
}
int iPort = pChatRoom->GetDlgItemInt(IDC_PORT_LISTEN);
sockaddr_in service;
service.sin_family = AF_INET;
service.sin_addr.s_addr = INADDR_ANY;
service.sin_port = htons(iPort);
if ( bind(pChatRoom->m_ListenSock, (sockaddr*)&service, sizeof(sockaddr_in)) == SOCKET_ERROR ) {
AfxMessageBox(_T("绑定端口失败!"));
goto __Error_End;
}
if( listen(pChatRoom->m_ListenSock, 5) == SOCKET_ERROR ) {
AfxMessageBox(_T("监听失败!"));
goto __Error_End;
}
while( TRUE) {
if ( SOCKET_Select(pChatRoom->m_ListenSock, 100, TRUE) ) {
sockaddr_in clientAddr;
int iLen = sizeof(sockaddr_in);
SOCKET accSock = accept(pChatRoom->m_ListenSock, (struct sockaddr *)&clientAddr , &iLen);
if (accSock == INVALID_SOCKET) {
continue;
}
//客户端结点
CClientItem tItem;
tItem.m_Socket = accSock;
tItem.m_pMainWnd = pChatRoom;
tItem.m_strIp = inet_ntoa(clientAddr.sin_addr);
INT_PTR idx = pChatRoom->m_ClientArray.Add(tItem);
tItem.hThread = CreateThread(NULL, 0, ClientThreadProc, &(pChatRoom->m_ClientArray.GetAt(idx)), CREATE_SUSPENDED, NULL);
pChatRoom->m_ClientArray.ElementAt(idx).hThread = tItem.hThread;
ResumeThread(tItem.hThread);//恢复一个线程的执行
Sleep(100);
}
}
__Error_End:
closesocket(pChatRoom->m_ListenSock);
return TRUE;
}
#define MAX_BUF_SIZE 1024
DWORD WINAPI ClientThreadProc(LPVOID lpParameter)//客户端线程的创建
{
CString strMsg;
CClientItem m_ClientItem = *(CClientItem *)lpParameter;
while( TRUE ){
if ( SOCKET_Select(m_ClientItem.m_Socket, 100, TRUE) ) {
TCHAR szBuf[MAX_BUF_SIZE] = {0};
int iRet = recv(m_ClientItem.m_Socket, (char *)szBuf, MAX_BUF_SIZE, 0);
if ( iRet > 0 ) {
//right;
strMsg=szBuf;
strMsg = _T("客户端:") + m_ClientItem.m_strIp + _T(">") + strMsg;
m_ClientItem.m_pMainWnd->ShowMsg(strMsg); //接收线程在这里显示
m_ClientItem.m_pMainWnd->SendClientsMsg(strMsg,&m_ClientItem);//向除原信息发送客户端以外所有客户端发送信息
}else{
//close socket;
strMsg = _T("客户端:") + m_ClientItem.m_strIp + _T(" 离开了聊天室!");
m_ClientItem.m_pMainWnd->ShowMsg(strMsg);
m_ClientItem.m_pMainWnd->RemoveClientFromArray(m_ClientItem);
break;
}
}
Sleep(500);
}
return TRUE;
}
#pragma once
class CServerDlg;
class CClientItem
{
public:
CString m_strIp;
SOCKET m_Socket;
HANDLE hThread;
CServerDlg *m_pMainWnd;
CClientItem()
{
m_pMainWnd = NULL;
m_Socket = INVALID_SOCKET;
hThread = NULL;
}
};
DWORD WINAPI ListenThreadFunc(LPVOID pParam);
DWORD WINAPI ConnectThreadFunc(LPVOID pParam);//客户端连接线程
BOOL SOCKET_Select(SOCKET hSocket, int nTimeOut=100, BOOL bRead=FALSE);
server端增加以下变量和函数:(要使用CArray需要包含头文件afxtempl.h)
SOCKET m_ListenSock;
HANDLE m_hListenThread; //监听线程句柄
CArray m_ClientArray;//服务器接收客户端队列
void ShowMsg(CString strMsg); //保存聊天记录
void RemoveClientFromArray(CClientItem in_Item);//删除客户端连接结点
void SendClientsMsg(CString strMsg, CClientItem *pNotSend=NULL);//客户端发送消息
void StopServer();
两个成员在CServerDlg的构造函数中的初始化如下:
m_ListenSock=INVALID_SOCKET;//初始化
m_hListenThread = NULL;
另外4个函数实现如下:
void CServerDlg::ShowMsg(CString strMsg)
{
m_MsgEdit.SetSel(MAKELONG(-1, -1));//把光标定位到所有文本最后一个位置
m_MsgEdit.ReplaceSel(strMsg+_T("\r\n"));//把光标替换一下,替换存参数括号里面的字符串
}
//删除客户端连接结点
void CServerDlg::RemoveClientFromArray(CClientItem in_Item)
{
for( int idx = 0; idx m_Socket != m_ClientArray.ElementAt(idx).m_Socket ||
pNotSend->hThread != m_ClientArray.ElementAt(idx).hThread ||
pNotSend->m_strIp != m_ClientArray.ElementAt(idx).m_strIp) {
send(m_ClientArray.ElementAt(idx).m_Socket, (char *)szBuf, _tcslen(strMsg)*sizeof(char), 0);
}
}
}
void CServerDlg::OnSend()
{
CString strMsg;
GetDlgItemText(IDC_SEVER_MSG, strMsg);
strMsg = "服务器:>" + strMsg;
ShowMsg(strMsg);
SendClientsMsg(strMsg);//发送给队列中的所有客户端
SetDlgItemText(IDC_SEVER_MSG, _T(""));
}
void CServerDlg::OnStopServer()
{
int iret=MessageBox("您真的想停止吗?","",MB_OKCANCEL);
if (iret=IDOK)
{
StopServer();//客户端线程的结束
ShowMsg("停止客户端成功!");
}
}
//服务端结束函数
void CServerDlg::StopServer()
{
int nCount = m_ClientArray.GetSize();
HANDLE *m_pHandles = new HANDLE[nCount+1];
m_pHandles[0] = m_hListenThread;
for( int idx = 0; idx < nCount; idx++ )
{
m_pHandles[idx+1] = m_ClientArray.ElementAt(idx).hThread;
}
DWORD dwRet = WaitForMultipleObjects(nCount+1, m_pHandles, TRUE, 1000);//等待多个内核对象激发的api
if ( dwRet != WAIT_OBJECT_0 ) {
for( INT_PTR i = 0; i < m_ClientArray.GetSize(); i++ ) {
TerminateThread(m_ClientArray.ElementAt(i).hThread, -1);
closesocket(m_ClientArray.ElementAt(i).m_Socket);
}
TerminateThread(m_hListenThread, -1);
closesocket(m_ListenSock);
}
delete [] m_pHandles;
m_hListenThread = NULL;
m_ListenSock = INVALID_SOCKET;
}
客户端发送文件,固定通过ip控件中的地址,虽然我们初始化为127.0.0.1,发送文件固定用端口7000,于是发送函数如下:
void CClientDlg::OnSendfile()
{
int modal,nCount;
CString fileName;
CString ip ;
GetDlgItemText(IDC_IPADDR,ip);
CFileDialog fdlg(true);
modal=fdlg.DoModal( );
if(IDCANCEL==modal)
{
return ; //用户取消发送。
}
SENDFILEPARAM* p=new SENDFILEPARAM;
fileName=fdlg.GetFileName( );
p->pathName=fdlg.GetPathName( );
nCount=fileName.GetLength( );
memset(p->fileName,0,NAMELENGTH);
for(int i=0;ifileName[i]=fileName.GetAt(i);
}
p->pCLAN=this;
p->ip = ip;
////////////////////////////////////////////////
AfxBeginThread(SendFileSingle,(LPVOID)p);//调用线程函数,fuck
}
调用的发送线程以及其他结构如下:
struct SENDFILEPARAM
{
CString pathName;
CString ip;
char fileName[NAMELENGTH];
CClientDlg* pCLAN;
};
//////////////// 单线程发送文件线程。 ///////////////////////////////////
UINT SendFileSingle( LPVOID param)
{
if (!AfxSocketInit())
{
AfxMessageBox("SendFileSingle AfxSocketInit()失败");
return 0;
}
CClientDlg* pCLAN=((SENDFILEPARAM*)param)->pCLAN;
CString ip;
char fileName[NAMELENGTH];
char buffer[BUFFERSIZE] = {""};
DWORD Length; //文件长度
CFile file;
memset(fileName,0,NAMELENGTH);
strcpy(fileName,((SENDFILEPARAM*)param)->fileName);
ip=((SENDFILEPARAM*)param)->ip;
if( 0==file.Open(((SENDFILEPARAM*)param)->pathName,CFile::modeRead | CFile::typeBinary) )
{
return 0;
}
Length=file.GetLength( );//获取文件长度,字节
//创建发送套接字准备发送
CSocket sockSend;
if( 0==sockSend.Create() )
{
AfxMessageBox("创建发送套接字出错。");
return 0;
}
if( 0==sockSend.Connect(ip, 7000) )
{
AfxMessageBox("猪啊,服务器都没有,你发个毛线,是吧?\r\n");
sockSend.Close( );
return 0;
}
if(SOCKET_ERROR ==sockSend.Send(&Length,sizeof(DWORD)))//发送文件长度
{
AfxMessageBox("发送文件长度出错。\r\n");
sockSend.Close( );
return 0;
}
if(SOCKET_ERROR ==sockSend.Send(fileName,NAMELENGTH))//发送文件名
{
AfxMessageBox("发送文件名出错。\r\n");
sockSend.Close( );
return 0;
}
DWORD step=0;
int over,err;
////////////////////////////////////////////
//发送文件内容。
do
{
file.Seek(step,CFile::begin);
over=file.Read(buffer,BUFFERSIZE);
err=sockSend.Send(buffer,over);
if( BUFFERSIZE>over )
break;
if(SOCKET_ERROR==err)
{
AfxMessageBox("发送文件内容出错。\r\n");
sockSend.Close( );
pCLAN->SetFocus( );
return 0;
}
step+=err; //少了句,发错了,哈哈
//让你丫的不断的发,日啊。。。
///////////////////////////////////////////////////////
}while( true );
file.Close();
sockSend.Close( );
CString strFinish;
strFinish.Format("已成功发送文件\r\n\r\n%s。",fileName);
AfxMessageBox(strFinish);
pCLAN->SetFocus();
delete (SENDFILEPARAM*)param;
return 0;
}
客户端发送,需要两个成员,一个连接进程的句柄,一个连接的socket:
HANDLE m_hConnectThread;
SOCKET m_ConnectSock;
并在构造函数中初始化:
m_hConnectThread = NULL;
m_ConnectSock = INVALID_SOCKET;
点击连接,启动进程
void CClientDlg::OnConnect()
{
m_hConnectThread=CreateThread(NULL,0,ConnectThreadFunc,this,0,NULL);
GetDlgItem(IDC_DISCONNECT)->EnableWindow(TRUE);
GetDlgItem(IDC_SENDMSG)->EnableWindow(TRUE);
}
连接线程:
BOOL SOCKET_Select(SOCKET hSocket, int nTimeOut=100, BOOL bRead=TRUE)
{
fd_set fdset; //fd_set为管理多个套接字的结构体
timeval tv; //超时时间结构体
FD_ZERO(&fdset);//将集合初始化为空集合
FD_SET(hSocket, &fdset);//将套接字hSocket加入集合fdset
nTimeOut = nTimeOut > 1000 ? 1000 : nTimeOut;
tv.tv_sec = 0;//超时时间---秒
tv.tv_usec = nTimeOut;//超时时间---毫秒
int iRet = 0;
if ( bRead ) {
iRet = select(0, &fdset, NULL , NULL, &tv);//查看服务器套接字集合是否有可读
}else{
iRet = select(0, NULL , &fdset, NULL, &tv);//查看服务器套接字集合是否有可写
}
if(iRet <= 0) {
return FALSE;
} else if (FD_ISSET(hSocket, &fdset)){ //查看套接字 hSocket是否在集合fdset中
return TRUE;
}
return FALSE;
}
//客户端连接线程
#define MAX_BUF_SIZE 1024
DWORD WINAPI ConnectThreadFunc(LPVOID pParam)
{
CClientDlg *pChatRoom = (CClientDlg *)pParam;
ASSERT(pChatRoom != NULL);
pChatRoom->m_ConnectSock = socket(AF_INET , SOCK_STREAM , IPPROTO_TCP);
if ( pChatRoom->m_ConnectSock == INVALID_SOCKET ) {
AfxMessageBox(_T("新建Socket失败!"));
return FALSE;
}
CString strServIp;
pChatRoom->GetDlgItemText(IDC_IPADDR, strServIp);
int iPort = pChatRoom->GetDlgItemInt(IDC_PORT_CLIENT);
if ( iPort <= 0 || iPort > 65535 ) {
AfxMessageBox(_T("请输入合适的端口:1 - 65535"));
goto __Error_End;
}
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(iPort);
server.sin_addr.s_addr = inet_addr(strServIp);
if ( connect(pChatRoom->m_ConnectSock, (struct sockaddr *)&server, sizeof(struct sockaddr)) == SOCKET_ERROR ) {
AfxMessageBox(_T("连接失败,请重试!"));
goto __Error_End;
}
pChatRoom->GetDlgItem(IDC_DISCONNECT)->EnableWindow(TRUE);
pChatRoom->ShowMsg(_T("系统信息:连接服务器成功!"));
while( TRUE) {
if ( SOCKET_Select(pChatRoom->m_ConnectSock) ) {
char szBuf[MAX_BUF_SIZE] = {0}; //此处TCHAR改为char
int iRet = recv(pChatRoom->m_ConnectSock, (char *)szBuf, MAX_BUF_SIZE, 0);
if ( iRet > 0 ) {
//right;
pChatRoom->ShowMsg(szBuf); //这里显示出服务器发的信息
}else{
//close socket;
pChatRoom->ShowMsg(_T("服务器已停止,请重新进行连接!"));
break;
}
}
Sleep(500);
}
__Error_End:
closesocket(pChatRoom->m_ConnectSock);
return TRUE;
}
其中ShowMsg函数与Server端定义相同。其余断开连接和发送的函数定义如下:
void CClientDlg::OnDisconnect()
{
int iret=MessageBox("您真的想停止吗?","",MB_OKCANCEL);
if (iret=IDOK)
{
StopClient();//客户端线程的结束
ShowMsg("停止客户端成功!");
}
}
void CClientDlg::OnSendmsg()
{
CString strMsg;
GetDlgItemText(IDC_MSG_SEND, strMsg);
CString strTmp = _T("客户端:>") + strMsg;
int iSend = send(m_ConnectSock, (char *)strMsg.GetBuffer(sizeof(strMsg)), strMsg.GetLength()*sizeof(TCHAR), 0);
strMsg.ReleaseBuffer();
SetDlgItemText(IDC_MSG_SEND, _T(""));
}
void CClientDlg::StopClient()
{
DWORD dwRet = WaitForSingleObject(m_hConnectThread, 1000);//(WaitForSingleObject()等待某一个内核对象被激发,这个函数才返回)
if ( dwRet != WAIT_OBJECT_0 ) { //没正常结束线程
TerminateThread(m_hConnectThread, -1); //强制线程结束方法1
closesocket(m_ConnectSock); //强制线程结束方法2,两个都不建议使用
}
m_hConnectThread = NULL;
m_ConnectSock = INVALID_SOCKET;
}
程序目前运行良好,可以完善的地方, 在于可以加上个进度条显示文件传输啊
二、
鉴于他们任务要求是实时获取数据,这里又没有采集设备什么的,就采取了定时器思想,对前面的程序进行了如下修改:
1、在启动按钮时,启动定时器;
2、添加WM_TIMER响应函数,在这里进行数据发送;
3、监听线程中,不进行其他收发处理,只将客户socket加入CArray中;
即,在启动按钮中添加:(此处为2s刷新一次)
SetTimer(100,2000,0);
在OnTimer中:(这里只是随机生成3个数,需包含time.h并进行发送)
void CServerDlg::OnTimer(UINT nIDEvent)
{
srand(time(NULL));
int name = rand()%1000;
int age = rand()%90;
int freq = rand()%80;
CString str;
str.Format("%d-%d*%d",name,age,freq);
SendClientsMsg(str); //每5s发送一次ai
CDialog::OnTimer(nIDEvent);
}
客户端进行解析并显示就好了
测试界面修改如下:
还是能显示正常的,2s刷新一次,基本可以模拟实时数据更新这一要求。
问题在于收发数据,恐怕得写个协议,以方便解析,不然都是CString,也蛋疼。
基本搞定,就是这样了,菜鸟goes on ~~~
8-22补充:发现一个问题,在启动并结束后,再次启动server,则会提示创建套接字失败,想了想,应该是在结束线程之前关闭套接字,因为用于监听的SOCKET是在线程里创建的。于是StopServer函数的几行改为:
if ( dwRet != WAIT_OBJECT_0 ) {
for( INT_PTR i = 0; i < m_ClientArray.GetSize(); i++ )
{
closesocket(m_ClientArray.ElementAt(i).m_Socket);
TerminateThread(m_ClientArray.ElementAt(i).hThread, -1);
}
closesocket(m_ListenSock);
TerminateThread(m_hListenThread, -1);
}