客户端网络编程
客户控制端的网络编程和服务被控端的区别在于,客户端没有listen和accept,取而代之的是connect。还有就是服务端通常可以不指定固定的IP地址连接就是可以来者不拒。而客户端必须指定要连接的客户端的IP地址端口。
然后就是要明确,建立的连接是要短连接还是长连接。
因为在我实现过程中发现内存泄漏情况。即我在堆区开辟了服务端的接收缓冲区和客户端的接收缓冲区。但是我又想这个一直是全局有效的。我采用了两种方法的尝试解决,一个是智能指针管理,还有一个是交给STL的vector管理,这两个的安全性都封装的非常好。
服务客户双端联调的技巧:把每个基础阶段步骤都打出日志,先从日志宏观入手,判断哪个端出问题,以及哪个部分。然后断点深入函数寻找异常,查看对应返回值的正确性。
ClientSocket.h
#pragma once
#include <mutex>
#include<vector>
#include"Packet.h"
#define BUFFER_SIZE 4096
class CClientSocket
{
private:
std::vector<char> m_buffer;
SOCKET m_skt;//当前端的socket
CPacket packet;
bool sign = false;
//static CServerSocket* m_instance;
static std::shared_ptr<CClientSocket> m_instance; // 唯一实例化的对象且加上了智能指针的管理
static std::mutex mtx;// 静态的互斥锁,用于确保多线程环境下的安全性 C++11后可以不需要类外初始化mutex
public:
//初始化套接字环境(Windows编程特有)
BOOL InitSktEnv();
//初始化套接字
BOOL InitSkt(const std::string& strIPAddress, short port);
//TODO:发送数据的操作Send
bool Send(const char* pData, int nSize);
bool Send(CPacket& packet);
//关闭客户端
//void CloseClient();
static CClientSocket* GetInstance();
//处理命令
int DealCommand();
//
CPacket GetPacket() { return packet; }
//
bool GetFilePath(std::string& strPath);
~CClientSocket() {
closesocket(m_skt);
WSACleanup();
}
private:
CClientSocket() {
//m_client = INVALID_SOCKET;// -1
if (InitSktEnv() == FALSE) {
//Windows API 函数用于创建一个消息对话框
MessageBox(NULL, //无父窗口,即该消息对话框为顶层窗口
_T("无法初始化套接字环境,请检查网路设置!"), //要显示的文本
_T("初始化错误!"), //消息对话框的标题
MB_OK | MB_ICONERROR //消息框的风格,表示一个包含 OK 按钮的消息框,并显示错误图标。
); //_T 是一个宏,用于支持在编译时实现 Unicode 和 ANSI 字符串切换。使代码更具可移植性,可以在不同的编译设置下工作。
exit(0);//程序正常退出
}
m_buffer.resize(BUFFER_SIZE);
//创建套接字
m_skt = socket(PF_INET, SOCK_STREAM, 0);
//TODO:m_skt创建失败的日志
}
CClientSocket(const CClientSocket& ss) {
m_skt = ss.m_skt;
//m_client = ss.m_client;
}
CClientSocket& operator=(const CClientSocket& ss) {}
};
ClientSocket.cpp
#include "pch.h"
#include "ClientSocket.h"
#pragma warning (disable:4996)
//全局区
std::string GetErrInfo(int wsaErrCode) {
std::string ret;
LPVOID lpMsgBuf = NULL;
FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER,
NULL,
wsaErrCode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf,
0,
NULL
);
ret = (char*)lpMsgBuf;
LocalFree(lpMsgBuf);
return ret;
}
//
//记得类外初始化静态成员变量
std::shared_ptr<CClientSocket> CClientSocket::m_instance = nullptr;
std::mutex CClientSocket::mtx;
CClientSocket* pclient = CClientSocket::GetInstance();//这样就可以外部获取一个全局唯一对象的指针了,且总是在全局区实例化好了对象
BOOL CClientSocket::InitSktEnv()
{
//Windows下Socket网络编程的API简称WSA
WSADATA data;//存储Winsock的版本信息
//WSAStartup初始化Winsock库
if (WSAStartup(MAKEWORD(1, 1), &data) != 0) {
TRACE(_T("Init Winsock Library Failed\r\n"));
return FALSE;
}
TRACE(_T("Init Winsock Library Succeeded\r\n"));
return TRUE;
}
BOOL CClientSocket::InitSkt(const std::string& strIPAddress,short port)
{
if (m_skt == -1) {
TRACE(_T("m_skt never existed\r\n"));
return false;
}
sockaddr_in serv_adr;// IPv4 地址的结构体
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET; //地址族设置(AF_INET 表示 IPv4)
serv_adr.sin_addr.s_addr = inet_addr(strIPAddress.c_str());//服务端可以通过任何网络接口连接,而不用担心特定的网络接口或 IP 地址
serv_adr.sin_port = htons(port);//设置连接的端口号
if (serv_adr.sin_addr.s_addr == INADDR_NONE) {
AfxMessageBox("指定的IP地址不存在");
return false;
}
int ret = 0;
if (sign == false) {
ret = connect(m_skt, (sockaddr*)&serv_adr, sizeof(serv_adr));
sign = true;
}
if (ret == -1) {
AfxMessageBox("connect failed");
TRACE("connect failed: %d, %s\r\n",WSAGetLastError(), GetErrInfo(WSAGetLastError()).c_str());
sign = false;
return false;
}
//一切正常
return true;
}
bool CClientSocket::Send(const char* pData, int nSize)
{
if (m_skt == -1) return false;
int ret = send(m_skt, pData, nSize, 0);
if (ret > 0) return true;
TRACE(_T("Client Send Failed"));
return false;
}
bool CClientSocket::Send(CPacket& packet)
{
if (m_skt == -1) return false;
//发送一整个包过去,然后大小为 2B包头 + 4BPacketLength + PacketLength值
return send(m_skt, packet.EntirePacket(), packet.PacketLength + 6, 0) > 0;
}
CClientSocket* CClientSocket::GetInstance()
{
if (m_instance == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
if (m_instance == nullptr) {
m_instance.reset(new CClientSocket());
}
}
return m_instance.get();
}
#define BUFFER_SIZE 4096
int CClientSocket::DealCommand()
{
TRACE(_T("Processing commands\r\n"));
if (m_skt == -1) {
TRACE(_T("Client socket = -1 is disconnected\r\n"));
return -1;
}
char* buffer = m_buffer.data();//让容器去管理内存泄漏
if (buffer == NULL) {
TRACE("no heap memory\r\n");
return -2;
}
memset(buffer, 0, BUFFER_SIZE);
size_t nowIndex = 0;//实际接收缓冲区数据的右边界指针表示接收缓冲区的数据大小
while (1) {
//***
size_t recvLength = recv(m_skt, buffer + nowIndex, BUFFER_SIZE - nowIndex, 0);//获得接收缓冲区的增量值
if (recvLength <= 0) {
return -1;
}
nowIndex += recvLength; //更新当前接收缓冲区大小
recvLength = nowIndex;//此时的该变量已然没用,就充当当前接收缓冲区大小,保护nowIndex值
packet = CPacket((BYTE*)buffer, recvLength);//解析包后更新recvLength为接收一个数据包具体接收了接收缓冲区的多少
if (recvLength > 0) {
memmove(buffer, buffer + recvLength, BUFFER_SIZE - recvLength);//把已经被解析成包的数据覆盖掉,后面往前推。
nowIndex -= recvLength;//更新右边界
return packet.PacketCommand;
}
}
return -1;
}
bool CClientSocket::GetFilePath(std::string& strPath)
{
if (packet.PacketCommand >= 7 && packet.PacketCommand <= 9)
{
strPath = packet.PacketData; return true;
}
return false;
}
RemoteControl_ClientDlg.cpp
void CRemoteControlClientDlg::OnBnClickedBtnTest()//基于MFC实现的测试连接按钮
{
// TODO: 在此添加控件通知处理程序代码
CClientSocket* pClient = CClientSocket::GetInstance();
bool ret = pClient->InitSkt("127.0.0.1", 9527);
if (!ret) {
AfxMessageBox("Network Init Failed!");
}
CPacket pack(520, NULL, 0);
ret = pClient->Send(pack);
TRACE("Send ret = %d\r\n", ret);
int cmd = pClient->DealCommand();
TRACE("ack:%d\r\n", cmd);
}
int CTest::DealCommand()//在服务端接收到520命令则会回发1314命令的数据报来测试连接
{
CPacket pack(1314, NULL, 0);
CServerSocket::GetInstance()->Send(pack);
return 520;
}