• 聊天室(C++客户端+Pyhton服务器)_1.框架搭设


    聊天室

    一.客户端发送

    用MFC可视化做个客户端登录界面。

     

     

     

    先点击注册账号按钮,注册账号的时候就需要连接到服务器,

    服务器需要查数据库,并做出相应的回应。

    所以开始写C++客户端套接口类用来连接到服务器。

    demosocket.cpp文件

    #include "pch.h"
    #include <WS2tcpip.h>
    #include "DemoSocket.h"


    // 构造函数,用于执行初始化和套接字的创建
    DemoSocket::DemoSocket()
    {
    // 0. 使用必须的结构体
    WSAData WsaData = { 0 };

    // 1. 初始化网络环境并请求指定版本
    if (WSAStartup(MAKEWORD(2, 2), &WsaData))
    {
    MessageBox(NULL, L"网络环境初始化失败!", L"错误", MB_OK | MB_ICONERROR);
    ExitProcess(0);
    }

    // 2. 判断版本信息是否匹配
    if (MAKEWORD(2,2) != WsaData.wVersion)
    {
    MessageBox(NULL, L"套接字版本不匹配!", L"错误", MB_OK | MB_ICONERROR);
    ExitProcess(0);
    }

    // 3. 创建一个套接字
    ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (ClientSocket == INVALID_SOCKET)
    {
    MessageBox(NULL, L"创建套接字失败!", L"错误", MB_OK | MB_ICONERROR);
    ExitProcess(0);
    }
    }


    // 析构函数,主要用于清理网络环境和释放套接字
    DemoSocket::~DemoSocket()
    {
    // 清理网络环境和释放套接字
    closesocket(ClientSocket);
    WSACleanup();

    // 结束接收线程并关闭内核对象句柄
    }


    // 用于设置对应的主窗口
    void DemoSocket::SetMainWnd(CDialogEx* MainDialog)
    {
    // 主窗口对象主要用于获取主窗口的句柄,并发送数据
    this->MainWnd = MainDialog;
    }


    // 用于连接到对应的服务器
    void DemoSocket::Connect(LPCSTR Ip, u_short Port)
    {
    // 1. 使用传入的 Ip 和 Port 填充结构体
    sockaddr_in ServerAddr = { AF_INET };
    ServerAddr.sin_port = htons(Port);
    inet_pton(AF_INET, Ip, &ServerAddr.sin_addr);

    // 2. 调用 connect 连接到服务器
    if (SOCKET_ERROR == connect(ClientSocket, (sockaddr*)& ServerAddr, sizeof(ServerAddr)))
    {
    MessageBox(NULL, L"连接服务器失败!", L"错误", MB_OK | MB_ICONERROR);
    ExitProcess(0);
    }

     

     

     

     

    客户端点击注册的时候具体怎么实现连接到服务器呢。

    把登录窗口作为主窗口,主窗口初始化的时候实例化套接口类连接服务器,并把套接字绑定在主窗口上。

    在主窗口类构造函数直接初始化连接到服务器

    CMainDialog::CMainDialog(CWnd* pParent /*=nullptr*/)
    : CDialogEx(IDD_DIALOG1, pParent)
    , m_UserName(_T("xiaoming"))
    , m_PassWord(_T("xiaoming"))
    {
    // 让套接字指向当前主窗口
    MySocket.SetMainWnd(this);

    // 进行网络连接,传入地址和端口
    MySocket.Connect("127.0.0.1", 6666);
    }

     

    客户端点击注册按钮后,需要发送信息给服务器,那么套接字类需要添加发送的函数,同时定义发送的信息的结构体长什么样。

     

    信息的结构体

    结构体枚举

    结构体的联合应用

    通过自定义消息发送到服务器

    自定义消息宏定义未识别,所以要自己定义

    发送函数

    发送

    //放基础信息  pch.h文件里面
    // 拟定的消息类型
    enum TYPE {
    REGISTER = 0, LOGIN, ADDFRD, UPDATEFRD, FRDMSG,
    CRTGRP = 5, ADDGRP, UPDATEGRP, GRPMSG, UPDATEGRPUSER
    };

    // 0. 注册的结构体
    typedef struct _REGISTER_INFO
    {
    WCHAR UserName[32];
    WCHAR PassWord[32];
    } REGISTER_INFO, *PREGISTER_INFO;

    // 用于保存发送的数据
    typedef struct _SEND_INFO
    {
    // 1. 类型:标识发送的消息是什么
    TYPE Type;

    // 2. 句柄:表示消息由谁发送
    HWND hWnd;

    // 3. 联合体: 不同类型对应不同的结构体
    union {
    REGISTER_INFO RegisterInfo;
    LOGIN_INFO LoginInfo;
    ADDFRD_INFO AddFrdInfo;
    UPDATEFRD_INFO UpdateFrdInfo;
    FRDMSG_INFO FrdMsg;
    CRTGRP_INFO CrtGrpInfo;
    ADDGRP_INFO AddGrpInfo;
    UPDATEGRP_INFO UpdateGrpInfo;
    GRPMSG_INFO GrpMsg;
    UPDATEGRPUSER_INFO UpdateGrpUserInfo;
    };
    }SEND_INFO, * PSEND_INFO;

    //demosocket.cpp文件里的函数
    // 用于发送数据给服务器的函数
    void DemoSocket::Send(LPVOID Buffer, INT Size)
    {
    // 发送消息并判断是否成功
    if (SOCKET_ERROR == send(ClientSocket, (PCHAR)Buffer, Size, 0))
    {
    MessageBox(NULL, L"发送消息失败!", L"错误", MB_OK | MB_ICONERROR);
    ExitProcess(0);
    }
    }


    //点击注册按钮CMainDlg.cpp文件里面
    // 注册按钮的响应
    void CMainDialog::OnBnClickedRegister()
    {
    // 1. 获取用户输入的数据
    UpdateData(TRUE);

    // 2. 判断输入的数据是否为空
    if (m_UserName.IsEmpty() || m_PassWord.IsEmpty())
    {
    MessageBox(L"请输入用户名和密码");
    return;
    }

    // 3. 填充需要发送的数据内容
    SEND_INFO SendInfo = { TYPE::REGISTER, GetSafeHwnd() };
    memcpy(SendInfo.RegisterInfo.UserName, m_UserName.GetBuffer(), m_UserName.GetLength() * 2);
    memcpy(SendInfo.RegisterInfo.PassWord, m_PassWord.GetBuffer(), m_PassWord.GetLength() * 2);

    // 4. 将数据通过自定义消息发送到服务器
    SendMessage(UM_SEND_MSG, (WPARAM)& SendInfo, sizeof(SEND_INFO));
    }

    //自定义消息,打开类向导-选择自定义消息-编辑UM_SEND_MSG
    // 任何一个窗口都需要通过这个消息发送数据到服务器
    #define UM_SEND_MSG WM_USER + 1
    // 发送消息, wParam 是需要发送的数据, lPram 是大小
    afx_msg LRESULT CMainDialog::OnUmSendMsg(WPARAM wParam, LPARAM lParam)
    {
    // 直接的发送消息
    MySocket.Send((LPVOID)wParam, lParam);
    return 0;
    }

     

     

    二.python服务器

    接受到客户端发来的消息处理完再发回给客户端

    Mysql

    数据库 user_table(user,pswd)

    账号密码需要保存在数据库。所以开始写数据库相关的东西。

    两个方法,一个有返回值,一个没有。

    import pymysql


    # 用于执行数据库操作
    class Mysql(object):

       # 构造函数,用于连接数据库
       def __init__(self):
           try:
               # 获取连接对象,用于操作连接数据库
               self.connect = pymysql.connect(host="127.0.0.1",
                   port=3306, user="root", password="123456",db="chatroom")
               # 获取游标对象
               self.cursor = self.connect.cursor()
           except Exception as info:
               print(info)

       # 执行带返回值的指令
       def select(self, sql):
           try:
               # 执行sql指令
               self.cursor.execute(sql)
               # 获取执行的结果
               return self.cursor.fetchall()
           except Exception as info:
               # 回滚操作
               self.connect.rollback()
               # 输出错误信息
               print(info)


       # 执行不带返回值的指令
       def exec(self, sql):
           try:
               # 执行sql指令
               self.cursor.execute(sql)
               # 提交完成的操作
               self.connect.commit()
           except Exception as info:
               # 回滚操作
               self.connect.rollback()
               # 输出错误信息
               print(info)

     

    首先是账号密码。

    客户端连接上服务器,判断账号密码是否正确是在服务器上,所以数据库跟python对接,

    # 构造函数,用于初始化网络以及创建套接字
       def __init__(self):
           # 1.创建套接字,是服务器的套接字
           self.server = socket.socket()
           # 2.绑定到指定的地址和端口,与客户端请求的一致
           self.server.bind(("127.0.0.1", 6666))
           # 3.设置当前套接字的监听状态
           self.server.listen(socket.SOMAXCONN)
           # 4. 初始化mysql对象
           self.mysql = mysql.Mysql()
               # 实例方法,用于接收客户端的连接
       def accept(self):
           while True:
               # 1. 直接调用函数接收客户端并保存套接字和地址
               client, address = self.server.accept()
               # 2. 输出显示客户端的连接消息
               print(address, "连接到了服务器")
               # 3. 为每一个客户端创建一个接收线程
               threading.Thread(target=self.recevice, args=(client,)).start()

     

     

    C++点击登录之后,信息打包发给服务器,服务器要不断接受到信息,并根据不同消息分配不同函数。

     # 实例方法,用于分别接收每一个客户端的消息
       def recevice(self, client):
           while True:
               try:
                   # 1. 获取了客户端发送的消息
                   message = client.recv(2048)
                   # 2. 解包获取其中的类型(返回的是元组)
                   type, = struct.unpack("i", message[:4])
                   # 3. 根据不同的类型调用不同的函数,第一个参数是self
                   DemoChat.dict_func[type](self, client, message[4:])
               except Exception as info:
                   # 如果产生了一场,说明客户端已经退出了
                   for username, client_socket in self.dict_client.items():
                       # 如果套接字相同,就从字典中移除,说明退出登录
                       if client_socket == client:
                           self.dict_client.pop(username)
                           print(username, "退出了聊天室")
                           #################################
                           print(info)
                           #################################
                           break
                   break

       

     

    服务器做判断。根据数据建判断

    # 定义一个枚举类型,标注消息类型
    class Type(enum.Enum):
       REGISTER = 0
       LOGIN = 1
       ADDFRD = 2
       UPDATEFRD = 3
       FRDMSG = 4
       CRTGRP = 5
       ADDGRP = 6
       UPDATEGRP = 7
       GRPMSG = 8
    # 创建一个字典,用于保存类型和与之匹配的处理函数
       dict_func = {
           Type.REGISTER.value: on_register,
           Type.LOGIN.value: on_login,
           Type.ADDFRD.value: on_addfrd,
           Type.UPDATEFRD.value: on_updatefrd,
           Type.FRDMSG.value: on_frdmsg,
           Type.CRTGRP.value: on_crtgrp,
           Type.ADDGRP.value: on_addgrp,
           Type.UPDATEGRP.value: on_updategrp,
           Type.GRPMSG.value: on_grpmsg
      }

     

     

    分配到了注册的方法

    根据数据库检查是否存在账号,不存在就注册

    # 实例方式:用于处理注册消息
       def on_register(self, client, message):
           # 解包获取发送来的数据
           hwnd, username, password = struct.unpack("i64s64s", message[:132])
           # 将用户名和密码进行解码
           username = username.decode("UTF16").strip("x00")
           password = password.decode("UTF16").strip("x00")
           # 判断用户是否已存在
           if self.mysql.select("SELECT * FROM user_table WHERE user='%s'" % username):
               # 发送的内容 => 类型 + 句柄 + 是否成功 + 服务器返回的信息
               client.send(struct.pack("iii40s", Type.REGISTER.value, hwnd, 0, "用户名已存在".encode("UTF16")))
           else:
               # 如果不存在,向数据库中添加一组数据
               self.mysql.exec("INSERT INTO user_table(user,pswd) VALUE('%s',MD5('%s'))" % (username, password))
               # 将注册结果进行返回
               client.send(struct.pack("iii40s", Type.REGISTER.value, hwnd, 1, "注册成功".encode("UTF16")))

     

    三.客户端接收到服务器发来的消息根据消息处理。

    客户端发送消息结构体给服务器后,要循环接收服务器的反馈,所以在客户端要写个线程不断接受来自服务器的信息,
    // 3. 创建线程用于接收服务器发过来的消息,线程函数必须是静态或全局的
    Thread = CreateThread(NULL, NULL, WorkerThread, this, NULL, NULL);
    if (NULL == Thread)
    {
    MessageBox(NULL, L"接收线程创建失败!", L"错误", MB_OK | MB_ICONERROR);
    ExitProcess(0);
    }
    }
    // 线程函数,用于接收消息并将消息发送给主窗口
    DWORD WINAPI DemoSocket::WorkerThread(LPVOID lpThreadParameter)
    {
    // 1. 获取需要用到的所有数据
    PCHAR Buffer = new CHAR[sizeof(RECV_INFO)]{ 0 };
    DemoSocket* pMySocket = (DemoSocket*)lpThreadParameter;

    // 2. 循环的接收服务器发送的数据
    while (recv(pMySocket->ClientSocket, Buffer, sizeof(RECV_INFO), 0) > 0)
    {
    // 3. 将接收到的消息转交给主窗口进行分发
    SendMessage(pMySocket->MainWnd->GetSafeHwnd(), UM_RECV_MSG, NULL,(LPARAM)Buffer);
    }

    // 4. 网络断开连接(自己的网服务器)
    MessageBox(NULL, L"网络连接已断开", L"错误", MB_OK | MB_ICONERROR);
    delete[] Buffer; ExitProcess(0);
    }

     

    线程接受到的信息后,转发给主窗口响应UM_RECV_MSG消息

    所以要自定义消息,打开类向导-选择自定义消息-编辑UM_RECV_MSG,

    主窗口把注册的消息转发给了对应的消息,这里是UM_RECV_REGISTER

    // 套接字使用这个消息将接收到的内容发送给主窗口
    #define UM_RECV_MSG WM_USER + 2
    // 接收到服务器发送的消息并进行处理(关键函数)
    afx_msg LRESULT CMainDialog::OnUmRecvMsg(WPARAM wParam, LPARAM lParam)
    {
    // 1. 将 lParam 转换成对应的结构体
    PRECV_INFO RecvInfo = (PRECV_INFO)lParam;

    // 2. 根据不同的类型,转发消息给不同的窗口
    switch (RecvInfo->Type)
    {
    // 响应注册
    case TYPE::REGISTER:
    {
    // 对于注册是否成功的消息,wParam 没有用,lParam 指向 RECV_STATE
    ::SendMessage(RecvInfo->hWnd, UM_RECV_REGISTER, NULL, (LPARAM)&RecvInfo->RecvState);
    break;
    }

    return 0;
    }

    通过自定义消息响应接受信息

    自定义消息宏定义未识别,所以要自己定义

    // 是否注册成功
    #define UM_RECV_REGISTER WM_USER + 3
    // 是否注册成功
    afx_msg LRESULT CMainDialog::OnUmRecvRegister(WPARAM wParam, LPARAM lParam)
    {
    // 将 lParam 转换成对应的的结构
    PRECV_STATE RecvState = (PRECV_STATE)lParam;

    // 输出服务器返回的消息
    MessageBox(RecvState->MsgInfo);

    return 0;
    }

    接受信息的结构体

    // 返回服务器处理的结果
    typedef struct _RECV_STATE
    {
    BOOL IsSuccess; // 是否成功
    WCHAR MsgInfo[20]; // 如果失败了,原因是什么
    }RECV_STATE, *PRECV_STATE;


    // 用于保存接收的数据
    typedef struct _RECV_INFO
    {
    // 1. 类型:标识接收的消息是什么
    TYPE Type;

    // 2. 句柄:表示消息由谁发送
    HWND hWnd;

    // 3. 联合体: 不同类型对应不同的结构体
    union {
    RECV_STATE RecvState;
    UPDATEFRD_INFO UpdateFrdInfo;
    FRDMSG_INFO FrdMsg;
    UPDATEGRP_INFO UpdateGrpInfo;
    };
    } RECV_INFO, * PRECV_INFO;

    聊天室(C++客户端+Pyhton服务器)框架搭设完毕。

  • 相关阅读:
    JAVA中获取当前系统时间
    关于JAVA中URL传递中文参数,取值是乱码的解决办法
    javaweb学习总结——Filter高级开发
    java项目(java project)如何导入jar包的解决方案列表
    使用过滤器(Filter)解决请求参数中文乱码问题(复杂方式)
    关于配置Tomcat的URIEncoding
    web.xml中load-on-startup的作用
    最详细的Log4j使用教程
    使用MyEclipse开发第一个Web程序
    java.net.BindException: Address already in use: JVM_Bind
  • 原文地址:https://www.cnblogs.com/ltyandy/p/11031474.html
Copyright © 2020-2023  润新知