我已经在我的资源里上传了这个聊天室的代码了
基于MFC的C++的select模型的TCP聊天室
采用select网络模型,支持多人同时登陆,功能有上线、下线、群聊、私聊
使用CjsonObject进行数据传递,使用了心跳包进行判断下线的情况。
服务端并未使用MFC框架,解决方案包含文件如下:
主函数所在
ChatingServer.cpp
#include <iostream>
#include"Server.h"
using namespace std;
int main()
{
Server tcpServer;
tcpServer.CreateServer("127.0.0.1", 9527);
tcpServer.RunServer();
}
创建一个Server类对象,调用其方法开启服务端。
Server类:
Serve.h
#pragma once
#include "common.h"
#include "CJsonObject.hpp"
#include "TcpSocket.h"
#include "CLock.h"
#include <time.h>
#include <string>
#include <list>
#include <utility>
#include <algorithm>
#include<iostream>
using namespace std;
class Server
{
public:
Server();
~Server();
BOOL CreateServer(const char* szIp, u_short nPort);
BOOL RunServer();
CLock g_lock;
private:
static DWORD WINAPI HandleClientsThread(LPVOID pParam);
private:
class ClientInfo
{
public:
ClientInfo(clock_t cc,list<ClientInfo*>*pLstClients):
m_clockHeartTime(cc),m_pLstClients(pLstClients)
{}
clock_t m_clockHeartTime;
list<ClientInfo*>* m_pLstClients;
CTcpSocket m_tcpsocketClient;
};
private:
CTcpSocket m_tcpSocket;
list<ClientInfo*> m_lstClients;
public:
bool HandleData(ClientInfo* pCL);
};
用户接口为创建服务的CreateServer和开启服务的RunServer
内部结构中,定义一个ClientInfo结构体存储客户端信息,包括该客户端上次发送心跳包的时间,但没有实例化,待会来看如何实例化。
先看一下m_tcpSocket,它是一个CTcpSocket类的对象,来看一下它的结构:
class CTcpSocket
{
public:
CTcpSocket();
~CTcpSocket();
BOOL CreateSocket();
BOOL BindListen(char* szIp, u_short nPort);
BOOL Accept(CTcpSocket* pTcpSocket );
BOOL Connect(char* szIp, u_short nPort);
BOOL Recv(char* pBuff, int* pLen);
BOOL Send(char* pBuff, int* pLen);
BOOL RecvPackage(DATAPACKAGE* pPackage);
BOOL SendPackage(DATAPACKAGE* pPackage);
void CloseSocket();
const sockaddr_in& GetSockaddrIn()const;
SOCKET m_socket;
private:
sockaddr_in m_si;
};
看来是将套接字的功能封装在一起了
定义一个用户处理线程HandleClientsThread,用于处理TCP连接请求。注意,MFC中的线程必须被声明成DWORD WINAPI
查看WINAPI的定义,它是这样定义的
#define WINAPI _stdcall
可以发现CALLBACK也是这样定义的
_stdcall规定了编译时的一些选项
WINAPI是一个宏,所代表的符号是__stdcall, 函数名前加上这个符号表示这个函数的调用约定是标准调用约定,windows API函数采用这种调用约定。
具体来说,他们是关于堆栈的一些说明,首先是函数参数压栈顺序,其次是压入堆栈的内容由谁来清除,调用者还是函数自己?
stdcall的调用约定意味着:
1)参数从右向左压入堆栈;
2)函数自身修改堆栈;
3)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。
好了,Server的结构我们了解了,看一下调用Server成员函数的情况
CreateServer的实现
BOOL Server::CreateServer(const char* szIp, u_short nPort)
{
BOOL bRet = m_tcpSocket.CreateSocket();
if (!bRet)
{
cout << "tcp客户端创建失败" << endl;
return FALSE;
}
bRet = m_tcpSocket.BindListen((char*)"127.0.0.1", 9527);
if (!bRet)
{
cout << "绑定端口监听失败" << endl;
return FALSE;
}
HANDLE hThread = CreateThread(NULL, 0, HandleClientsThread, (LPVOID)this, 0, NULL);
CloseHandle(hThread);
return TRUE;
}
分开来看,其中CreateSocket封装了socket的初始化
BOOL CTcpSocket::CreateSocket()
{
m_socket = socket(
AF_INET,
SOCK_STREAM,
IPPROTO_TCP);
if (m_socket == SOCKET_ERROR)
{
return FALSE;
}
else
{
return TRUE;
}
}
接着调用BindListen
BOOL CTcpSocket::BindListen(char* szIp, u_short nPort)
{
m_si.sin_family = AF_INET;
m_si.sin_port = htons(nPort);
m_si.sin_addr.S_un.S_addr = inet_addr(szIp);
int nRet = bind(m_socket, (sockaddr*)&m_si, sizeof(m_si));
if (nRet == SOCKET_ERROR)
{
return FALSE;
}
nRet = listen(m_socket, SOMAXCONN);
if (nRet == SOCKET_ERROR)
{
return FALSE;
}
return TRUE;
}
可以看出,是绑定端口并监听
的封装。
最后建立处理线程,用来接收客户端数据。
接下来调用RunServer,接受来自客户端的数据
BOOL Server::RunServer()
{
while (TRUE)
{
ClientInfo* pCI = new ClientInfo(clock(), &m_lstClients);
BOOL bRet = m_tcpSocket.Accept(&pCI->m_tcpsocketClient);
if (!bRet)
{
break;
}
printf("IP:%s port:%d 连接到服务器. \r\n",
inet_ntoa(pCI->m_tcpsocketClient.GetSockaddrIn().sin_addr),
ntohs(pCI->m_tcpsocketClient.GetSockaddrIn().sin_port));
m_lstClients.push_back(pCI);
}
return 0;
}
其中的Accept是对accept的封装,用于接受连接
BOOL CTcpSocket::Accept(CTcpSocket* pTcpSocket)
{
sockaddr_in siClient;
int nSize = sizeof(siClient);
SOCKET sockClient = accept(m_socket, (sockaddr*)&siClient, &nSize);
if (sockClient == SOCKET_ERROR)
{
return FALSE;
}
pTcpSocket->m_socket = sockClient;
pTcpSocket->m_si = siClient;
return TRUE;
}
回到RunServer中继续分析:
ClientInfo* pCI = new ClientInfo(clock(), &m_lstClients);
他会调用ClientInfo的构造函数:
class ClientInfo
{
public:
ClientInfo(clock_t cc,list<ClientInfo*>*pLstClients):
m_clockHeartTime(cc),m_pLstClients(pLstClients)
{}
clock_t m_clockHeartTime;
list<ClientInfo*>* m_pLstClients;
CTcpSocket m_tcpsocketClient;
};
&m_lstClients是传引用,即之前在main中创建Server类对象tcpServer时实例化的那个,他是一个双向链表,既然是传引用,容易看出,所有的客户端都共享同一个双向链表。
一旦接受连接,存储客户端信息,由于传递的是引用,pTcpSocket亦即RunServer中的pCI,并在RunServer中判断接受成功后,将客户端信息打印到控制台上。
printf("IP:%s port:%d 连接到服务器. \r\n",
inet_ntoa(pCI->m_tcpsocketClient.GetSockaddrIn().sin_addr),
ntohs(pCI->m_tcpsocketClient.GetSockaddrIn().sin_port));
若没有连接,则会跳出本次循环,继续调用Accept查询链接。
if (!bRet)
{
break;
}
连接成功后,还会向m_lstClients这个list中添加客户端信息
m_lstClients.push_back(pCI);
另一边HandleClientsThread这个线程不断从m_lstClients这个list中遍历存储连接信息,调用select来选择处理哪个连接,再调用HandleData()处理数据,包括判断数据类型,群发私聊等。
自己画的思维导图,有点乱。。。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)