聊聊(群聊)

1、概述

项目名称:聊聊(群聊)
开发环境:Linux
使用技术:生产者消费者模型、C++STL、多线程、网络编程、自定义协议(登陆和注册)、jsoncpp、ncurses
项目简介:用户可以申请注册账号和登陆账号,然后进行群聊功能
项目原理
服务端:主线程负责TCP监听,每有一个注册或者登陆的请求则创建线程去解析并处理,并将注册的用户信息存储在数据库里;两个生产者线程负责把udp缓冲区的消息校验合法后放入消息池,两个消费者线程负责把消息池的消息群发给各个在线用户。
客户端:注册、登陆、发送消息都通过joson串组织好格式去和服务端交互;用ncurses库结合客户端收发数据逻辑创建了4个线程绘制了4个窗口(标识、用户列表、聊天、输入窗口)实现群聊功能。

2、结构图

整体逻辑

在这里插入图片描述

服务端结构图

在这里插入图片描述

客户端结构图

在这里插入图片描述

3、实现原理

服务端

注册:客户首先通过TCP发送注册请求给服务端(注册请求是规定好的枚举类型),服务端主线程监听到后会创建一个线程为其处理注册请求,服务端接收到注册请求后,客户端发送注册需要的信息(已经规定成结构体)给服务端,服务端接收到后,校验其合法性后给用户注册,将信息存入数据库,并且组织一个应答结构体返还给客户,里面有:注册得到的id,注册是否成功的枚举(注册成功:REGISTER_SUCCESS,注册失败:REGISTER_FAILED)。

登陆:然后客户进行登录,发送登陆请求(登陆请求也是规定好的枚举类型),服务端会创建一个线程为其处理登陆请求,服务端接收到登录请求后,客户端发送登陆的id和密码(已经规定成结构体),服务端接收到后,校验密码,校验后会返回一个应答的结构体:你注册时候的用户信息,注册是否成功的枚举(如果密码正确,里面的一个标志位则置为LOGIN_SUCCESS,否则是LOGIN_FAILED),如果登陆成功则把该用户的在线状态置为WILL_BE_ONLINE(即将登陆状态,为什么不是直接就登陆了:因为这个项目是用UDP来聊天的,所以必须要知道用户UDP的IP和端口才能聊天),客户端收到了LOGIN_SUCCESS后便立刻发送一个UDP数据(已经规定成结构体),当服务端收到这个UDP数据后,而且服务端的用户状态是WILL_BE_ONLINE,则把用户的状态置为ONLINE,并将这个用户放到在线用户列表。

聊天:服务端刚启动的时候就会创建2个生产者线程和2个消费者线程,生产者线程负责从UDP缓冲区取走数据(已规定成结构体),并且校验其消息的合法性:1、该用户是否存在(不存在则不合法) 2、该用户是否在线(OFFLINE则不合法) 3、该用户是否是WILL_BE_ONLINE状态(如果是,则把该用户放入在线用户列表里) ,如果不是以上三个状态,则是ONLINE状态,则会把这个消息放入消息池。消费者线程负责从消息池中取出消息,然后遍历自己存的在线用户列表(里面存了用户的UDP地址),将消息一个个发送给客户。

客户端

注册:客户端先和服务端建立TCP连接,然后发送一个枚举大小的注册请求(REGISTER),然后接着就发送自己的注册信息(保存在一个结构体里,通过TCP发送),之后会受到服务端的应答结构体,如果里面的状态是REGISTER_FAILED则需要重新发送注册请求,如果是REGISTER_SUCCESS,则服务器返回的应答结构体里有注册成功的有效user_id。

登陆:客户端先和服务端建立TCP连接,然后发送一个枚举大小的登陆请求(LOGIN),然后接着就发送自己的登陆信息(保存在一个结构体里,通过TCP发送),之后会收到服务端的应答结构体,如果里面的状态是LOGIN_FAILED则需要重新发送登陆请求,如果是LOGIN_SUCCESS,则服务器返回的应答结构体里有自己的基本信息(因为你这个账号的信息(你的昵称,生日等)肯定是存在服务端的),然后客户端会保存自己的基本信息,并且进入聊天页面

聊天:登陆成功进入聊天后,客户端会创建4个窗口线程:1、显示这个聊天软件的标志 2、展示所有人的聊天消息 3、展示所有在线用户的列表 4、展示自己的输入信息。第一个线程,只是将“Chat Chat ~ ~”展示在顶部;第二个线程,通过接收UDP缓冲区的数据,数据里面有:发送的消息内容,该消息的用户id、昵称、生日等信息,然后将这个用户的信息存到在线用户列表里,然后将消息内容展示到聊天窗口;第三个线程,将第二个线程更新的在线用户列表展示出来;第四个线程,将自己在输入框输入的消息整理后(需要整理成一个结构体:发送的消息内容、自己的id、昵称、生日)发送给服务端,再由服务端转发。

4、模块划分

服务端

MsgPool(消息池):实质是生产者消费者模型。用了两个条件变量、一把锁和一个消息队列实现。将服务端从UDP缓冲区收到的数据在检验合法后便丢进消息池;服务端也会通过消息池取出数据发送给在线用户。

UserManager(用户管理模块):存放了用户的所有信息,包括当前的用户状态(ONLINE/OFFLINE)和用户的UDP地址信息,里面用了多线程的mutex来保证各个线程处理用户管理模块的线程安全,用MYSQL来永久存放用户信息,用STL的map来处理用户信息。每次用户来注册账号都会将其基本信息(不包括UDP地址信息,因为注册是TCP)保存在map里,然后还会存在数据库里;在用户登陆的时候,会把用户放到在线用户列表里,并且保存他的UDP地址信息。

FormatMessage(格式化消息):使用了jsoncpp库,封装了一个规范消息的类。服务端使用这个模块是为了把接受到的UDP消息反序列化,拿到其中的用户信息。客户端使用这个模块是为了把要发送的消息和当前用户的信息序列化后,方便发出。

Log(日志模块):封装了一个展示信息的函数。其他模块可以通过调用这个函数打印出某条信息,如:严重程度(FATAL/ERROR/INFO等)+ 时间 + 文件 + 行数。

ConnectInfo(登陆注册时候的数据规范 ):这个模块是自定制协议的数据规范,避免了粘包问题,客户端用来序列化发送,服务端用来反序列化解析。里面有三个结构体:注册信息(昵称、生日、密码),登陆信息(账号、密码),应答信息(本次处理状态、账号、昵称、生日(后三个信息是为登陆成功后返还的信息));然后还有两个枚举:注册和登陆类型(发送的登陆注册请求),应答类型(REGISTER_SUCCESS、REGISTER_FAILED、LOGIN_SUCCESS、LOGIN_FAILED、REQUEST_ERROR,是为了规范应答结构体里的处理状态)。

客户端

ChatWindow(聊天窗口):使用了ncurses库创建了四个线程来绘制了四个窗口,并用多线程的mutex来保证窗口刷新的线程安全。头部标识窗口:打印“Chat Chat ~ ~”在聊天框的顶部,并且左右移动;聊天消息窗口:将收到的消息反序列化后,得到:1、消息内容,并将其打印在窗口 2、这条消息的用户信息,并将其存在在线用户列表里;消息输入窗口:将在窗口输入的信息+当前用户的信息(id,生日等)序列化后发给服务端;用户列表窗口:将所有用户信息打印在窗口上。

Log(日志模块):封装了一个展示信息的函数。其他模块可以通过调用这个函数打印出某条信息,如:严重程度(FATAL/ERROR/INFO等)+ 时间 + 文件 + 行数。

ConnectInfo(登陆注册时候的数据规范 ):这个模块是自定制协议的数据规范,避免了粘包问题,客户端用来序列化发送,服务端用来反序列化解析。里面有三个结构体:注册信息(昵称、生日、密码),登陆信息(账号、密码),应答信息(本次处理状态、账号、昵称、生日(后三个信息是为登陆成功后返还的信息));然后还有两个枚举:注册和登陆类型(发送的登陆注册请求),应答类型(REGISTER_SUCCESS、REGISTER_FAILED、LOGIN_SUCCESS、LOGIN_FAILED、REQUEST_ERROR,是为了规范应答结构体里的处理状态)。

FormatMessage(格式化消息):使用了jsoncpp库,封装了一个规范消息的类。服务端使用这个模块是为了把接受到的UDP消息反序列化,拿到其中的用户信息。客户端使用这个模块是为了把要发送的消息和当前用户的信息序列化后,方便发出。

5、源码

Log.hpp

#pragma once
#include<iostream>
#include<stdio.h>
#include<cstring>

//日志模块是给所有其他模块打印当前代码的详细信息用的

const char* LevelStr[] = {
    "INFO","WARNING","ERROR","FATAL"
}; 

enum Level
{
    INFO = 0,
    WARNING,
    ERROR,
    FATAL
};

void GetTime(std::string* time_stamp)
{
    //拿到现在时间的时间戳
    time_t now_time;
    time(&now_time);
    //格式化时间戳
    struct tm* st = localtime(&now_time);
    char format_time[23] = {'\0'};
    snprintf(format_time,sizeof(format_time)-1,"%04d-%02d-%02d %02d:%02d:%02d",st->tm_year+1900,st->tm_mon+1,st->tm_mday,st->tm_hour,st->tm_min,st->tm_sec);
    time_stamp->assign(format_time,strlen(format_time));
}

std::ostream& Log(Level level,const std::string& msg,const char* file,int line)
{
    std::string level_info = LevelStr[level];
    std::string time_now;
    GetTime(&time_now);
    std::cout << "[" << time_now << " " << level_info << " " << file << ":" << line << "]" << msg;
    return std::cout;
}

#define LOG(level,msg) Log(level,msg,__FILE__,__LINE__)

UserManager.hpp

#pragma once
#include<mysql/mysql.h>
#include<string>
#include<cstring>
#include<vector>
#include<unordered_map>
#include<set>
#include<utility>
#include<unistd.h>
#include<arpa/inet.h>

#include"Log.hpp"
#include"ConnectInfo.hpp"

#define MYSQL_IP "127.0.0.1"
#define MYSQL_USER "root"
#define MYSQL_PASSWORD "1"
#define MYSQL_DB "user_info_db"
#define MYSQL_TB "user_info_tb"

//该用户信息管理模块是用来将注册了的、登陆了的用户信息进行管理(登陆、注册)

//表示当前用户的状态
enum UserStatus
{
    OFFLINE = 0,//下线
    ONLINE,//上线
    WILL_BE_ONLINE//即将上线,这个状态是TCP验证密码通过后的状态,客户端即将发送udp数据,然后获取到udp地址才算正式登陆
};

class Mysql
{
    public:
        static MYSQL* MysqlInit()
        {
            //1、初始化mysql句柄
            MYSQL* mysql = NULL;
            mysql = mysql_init(NULL);
            if(mysql == NULL)
            {
                LOG(FATAL,"mysql_init failed!") << std::endl;
                return NULL;
            }

            //2、连接服务器
            if(mysql_real_connect(mysql,MYSQL_IP,MYSQL_USER,MYSQL_PASSWORD,MYSQL_DB,0,NULL,0) == NULL)
            {
                LOG(FATAL,"mysql_real_connect failed!") << std::endl;
                MysqlDestory(mysql); 
                return NULL;
            }
            //3、设置客户端字符编码集
            if(mysql_set_character_set(mysql,"utf8") != 0)
            {
                LOG(FATAL,"mysql_set_character_set failed!") << std::endl;
                MysqlDestory(mysql); 
                return NULL;
            }
            return mysql;
        }

        static bool MysqlQuery(MYSQL* mysql,const std::string& sql_query)
        {
            int ret = mysql_query(mysql,sql_query.c_str());
            if(ret != 0)
            {
                LOG(FATAL,"MysqlQuery failed!") << std::endl;
                return false;
            }
            return true;
        }

        static void MysqlDestory(MYSQL* mysql)
        {
            if(mysql != NULL)
            {
                mysql_close(mysql);
            }
        }
};

class UserInfo
{
    public:
        //基本信息
        std::string _nick_name;  
        std::string _birthday;
        std::string _password;
        uint32_t _user_id;
        //聊天时候用的udp信息
        struct sockaddr_in _addr;
        socklen_t _addr_len;
        UserStatus _status;
    public:
        UserInfo(const std::string& nick_name,const std::string& birthday,const std::string& password,uint32_t user_id)
            :_nick_name(nick_name),_birthday(birthday),_password(password),_user_id(user_id)
        {
            //默认是下线状态
            _status = OFFLINE;
            memset(&_addr,0,sizeof(_addr));
            _addr_len = -1;
        }
};

class UserManager
{
    private:
        pthread_mutex_t _lock;
        //所有用户信息列表
        std::unordered_map<uint32_t,UserInfo> _user_info_list;                
        //在线用户列表
        std::vector<UserInfo> _online_list;
        //预分配的用户id
        uint32_t _prepare_user_id;
        //管理用户信息的数据库
        MYSQL* _mysql;
    public:
        UserManager()
        {
            _mysql = Mysql::MysqlInit();
            if(InitUserListInfo() == false)   
            {
                exit(20);
            }
            pthread_mutex_init(&_lock,NULL);
        }
        ~UserManager()
        {
            pthread_mutex_destroy(&_lock);
            Mysql::MysqlDestory(_mysql);
        }
        bool Register(const std::string nick_name,const std::string& birthday, const std::string& password,uint32_t* id)
        {
            //如果输入的注册信息为空,则注册失败
            if(nick_name.size() == 0 || birthday.size() == 0 || password.size() == 0)
            {
                LOG(ERROR,"Register failed!") << std::endl;
                return false;
            }
            pthread_mutex_lock(&_lock);
            //1、创建一个用户信息对象,并将注册的信息填入
            UserInfo user_info(nick_name,birthday,password,_prepare_user_id);
            user_info._status = OFFLINE;
            //2、将这个用户信息插入到用户信息列表里
            _user_info_list.insert(std::make_pair(_prepare_user_id,user_info));
            //2.1、将用户信息插入到数据库里
            InsertUserInfo(user_info); 
            //3、返回注册得到的新ID
            *id = _prepare_user_id;
            //4、将预分配的id++
            ++_prepare_user_id;
            LOG(INFO,"Register a user success") << std::endl;
            pthread_mutex_unlock(&_lock);
            return true;
        }
        bool Login(uint32_t user_id,const std::string& password,ReplyInfo* response)
        {
            pthread_mutex_lock(&_lock);
            //查看该id是否存在
            auto it = _user_info_list.find(user_id);
            if(it == _user_info_list.end())
            {
                LOG(ERROR,"UserId is not exists") << std::endl;
                pthread_mutex_unlock(&_lock);
                return false;
            }
            //校验密码
            if(it->second._password != password)
            {
                LOG(ERROR,"Password or UserId is not correct") << std::endl;
                pthread_mutex_unlock(&_lock);
                return false;
            }
            //密码正确,把用户状态更新
            it->second._status = WILL_BE_ONLINE;
            pthread_mutex_unlock(&_lock);
            //登陆成功后,用户从服务端得到自己的信息(因为自己的信息肯定是存在服务端的,而不是客户端
            strcpy(response->_nick_name,it->second._nick_name.c_str());
            strcpy(response->_birthday,it->second._birthday.c_str());
            response->_user_id = it->second._user_id;
            return true;
        }
        bool IsLogin(const uint32_t& user_id,const struct sockaddr_in& cli_addr,const socklen_t& addr_len)
        {
            if(sizeof(cli_addr) <= 0 || addr_len <= 0)
            {
                return false;
            }
            pthread_mutex_lock(&_lock);
            //1、先查找是否存在用户
            auto it = _user_info_list.find(user_id);
            if(it == _user_info_list.end())
            {
                pthread_mutex_unlock(&_lock);
                return false;
            }
            //2、判断用户的状态
            if(it->second._status == OFFLINE)
            {
                pthread_mutex_unlock(&_lock);
                return false;
            }
            //如果是即将上线的,也就是说这是一条登陆的信息,则更新用户状态,并把它放入在线列表里
            if(it->second._status == WILL_BE_ONLINE)
            {
                it->second._status = ONLINE;
                memcpy(&it->second._addr,&cli_addr,sizeof(cli_addr));
                it->second._addr_len = addr_len;
                _online_list.push_back(it->second);
                LOG(INFO,"user Login success!") << std::endl;
                pthread_mutex_unlock(&_lock);
                //这里返回false是表示不需要把这条消息放入消息池里
                return false;
            }
            pthread_mutex_unlock(&_lock);
            return true; 
        }
        void GetOnlinelist(std::vector<UserInfo>* online_list)
        {
             *online_list = _online_list;
        }
    private:
        //数据库处理
        //得到给用户注册时候用到的预分配账号,并且把数据库的用户信息加载到内存
        bool InitUserListInfo()
        {
            //1、输入mysql查询语句:查找所有信息
            char buf[1024] = {0};
            sprintf(buf,"select * from %s;",MYSQL_TB);
            if(Mysql::MysqlQuery(_mysql,buf) == false)
            {
                return false;
            }
            //2、查询结果
            MYSQL_RES* res = mysql_store_result(_mysql);
            if(res == NULL)
            {
                LOG(FATAL,"GetPrepareNum get result failed!") << std::endl;
                return false;
            }
            //2.1、将用户数量赋值给预分配id
            int num_rows = mysql_num_rows(res);
            _prepare_user_id = num_rows; 
            //2.2、将所有用户信息加载到map
            for(int i = 0; i < num_rows; ++i)
            {
                MYSQL_ROW row = mysql_fetch_row(res);
                uint32_t id = std::stoi(row[3]);
                UserInfo ui(row[0],row[1],row[2],id);
                _user_info_list.insert(std::make_pair(id,ui));
            }
            mysql_free_result(res);
            return true;
        }
        bool InsertUserInfo(const UserInfo& user_info)
        {
            //输入mysql插入语句:插入一个用户信息
            char buf[1024] = {0};
            sprintf(buf,"insert into %s values('%s','%s','%s',%d);",MYSQL_TB,user_info._nick_name.c_str(),user_info._birthday.c_str(),user_info._password.c_str(),user_info._user_id);
            if(Mysql::MysqlQuery(_mysql,buf) == false)
            {
                return false;
            }
            return true;
        }
        
};

MsgPool.hpp

#pragma once
#include<pthread.h>
#include<queue>
#include<string>

//数据池模块就是给服务端收发信息做缓冲用的

#define MSG_POOL_SIZE 1024

class MsgPool
{
    private:
        //保证了生产者和消费者的放和取的安全
        pthread_mutex_t _lock;
        //存放数据的队列
        std::queue<std::string> _msg_queue;
        //数据池的大小
        size_t _capacity;
        //消费者条件变量
        pthread_cond_t _consume_cond;
        //生产者条件变量
        pthread_cond_t _product_cond;
    public:
        MsgPool()
        {
            pthread_mutex_init(&_lock,NULL);
            pthread_cond_init(&_consume_cond,NULL);
            pthread_cond_init(&_product_cond,NULL);
            _capacity = MSG_POOL_SIZE;
        }
        ~MsgPool()
        {
            pthread_mutex_destroy(&_lock);
            pthread_cond_destroy(&_consume_cond);
            pthread_cond_destroy(&_product_cond);
        }
        void PushMsgToPool(const std::string& msg)
        {
            pthread_mutex_lock(&_lock);
            while(IsFull())
            {
                pthread_cond_wait(&_product_cond,&_lock);
            }
            _msg_queue.push(msg);
            pthread_mutex_unlock(&_lock);
            pthread_cond_signal(&_consume_cond);
        }
        void PopMsgFromPool(std::string* msg)
        {
            pthread_mutex_lock(&_lock);
            while(IsEmpty())
            {
                pthread_cond_wait(&_consume_cond,&_lock);
            }
            *msg = _msg_queue.front();
            _msg_queue.pop();
            pthread_mutex_unlock(&_lock);
            pthread_cond_signal(&_product_cond);
        }
    private:
        bool IsFull()
        {
            return _msg_queue.size() == _capacity;
        }
        bool IsEmpty()
        {
            return _msg_queue.empty();
        }
};

ConnectInfo.hpp

#pragma once
#include<string>

//该模块用来给TCP收发数据来规定格式的,里面包含三个收发数据的结构体

//客户端给服务端发送的注册/登陆标识
enum RegisterAndLogin : char
{
    REGISTER = 0,
    LOGIN
};

//返回给客户的状态信息
enum ReplyStatus : char
{
    REGISTER_SUCCESS = 0,
    REGISTER_FAILED,
    LOGIN_SUCCESS,
    LOGIN_FAILED,
    REQUEST_ERROR
};
//客户发送来的注册信息
struct RegisterInfo
{
    char _nick_name[15];
    char _birthday[20];
    char _password[15];
};
//客户发送来的登录信息
struct LoginInfo
{
    uint32_t _user_id;
    char _password[15];
};
//服务端返回给客户端的信息
struct ReplyInfo
{
    ReplyStatus _status;
    uint32_t _user_id;
    char _nick_name[15];
    char _birthday[20];
};

FormatMessage.hpp

#pragma once
#include<iostream>
#include<jsoncpp/json/json.h>
#include<string>

//该模块客户端和服务端都使用,是用来给收到的数据进行反序列化,将要发送的数据进行反序列化

class FormatMessage
{
    public:
        uint32_t _user_id;
        std::string _nick_name;
        std::string _birthday;
        //需要发送的信息内容
        std::string _msg;
    public:
        //反序列化:将发送来的msg进行格式组织,存到成员变量里
        void Deserialize(const std::string& msg)
        {
            Json::Reader reader;
            Json::Value value;
            reader.parse(msg,value,false);
            
            _user_id = value["_user_id"].asInt();
            _nick_name = value["_nick_name"].asString();
            _birthday = value["_birthday"].asString();
            _msg = value["_msg"].asString();
        }
        //序列化:将成员变量序列化成字符串
        void Serialize(std::string* msg)
        {
            Json::Value value;
            value["_user_id"] = _user_id;
            value["_nick_name"] = _nick_name;
            value["_birthday"] = _birthday;
            value["_msg"] = _msg;

            Json::FastWriter writer;
            *msg = writer.write(value);
        }
};

ChatWindow.hpp

#pragma once
#include<pthread.h>
#include<ncurses.h>
#include<pthread.h>
#include<stdlib.h>
#include<stdlib.h>

#include"FormatMessage.hpp"
#include"ChatClient.hpp"
#include"Log.hpp"

class ChatWindow;
//这个类是为了打包数据,因为线程入口函数只能传一个值
class Helper
{
    public:
        ChatWindow* _cw;
        ChatClient* _cc;
        //为了区别跑哪一个窗口函数
        int _thread_num;
    public:
        Helper(ChatWindow* cw,ChatClient* cc,int thread_num)
            :_cw(cw),_cc(cc),_thread_num(thread_num)
        {

        }
};
class ChatWindow
{
    private:
        //输出顶部的标识语
        WINDOW* _head;
        //输出所有的聊天信息
        WINDOW* _output;
        //输入框
        WINDOW* _input;
        //输出所有在线用户
        WINDOW* _user_list;
        //窗口刷新的时候是线程不安全的,需要加锁
        pthread_mutex_t _lock;
        ChatClient* _cc;
    public:
        ChatWindow(ChatClient* cc)
            :_head(nullptr),_output(nullptr),_input(nullptr),_user_list(nullptr),_cc(cc)
        {
            pthread_mutex_init(&_lock,NULL);
            //初始化屏幕
            initscr();
            //0表示不显示光标
            curs_set(0);
        }
        ~ChatWindow()
        {
            if(_head)
                delwin(_head);
            if(_output)
                delwin(_output);
            if(_input)
                delwin(_input);
            if(_user_list)
                delwin(_user_list);
            //销毁屏幕变量
            endwin();
            pthread_mutex_destroy(&_lock);
        }
        void Start()
        {
            std::vector<pthread_t> threads;
            pthread_t tid;
            for(int i = 0; i < 4; ++i)
            {
                //每次传入this,客户端,第几个线程标识
                Helper* hp = new Helper(this,_cc,i);
                //全部先进入一个入口函数,然后在入口函数里再去分支
                int ret = pthread_create(&tid,NULL,DrawWindowStart,(void*)hp);
                if(ret < 0)
                {
                    LOG(FATAL,"DrawWindowStart failed!") << std::endl;
                    exit(1);
                }
                threads.push_back(tid);
            }
            for(int i = 0; i < 4; ++i)
            {
                pthread_join(threads[i],NULL);
            }
        }
    private:
        static void* DrawWindowStart(void* arg)
        {
           Helper* hp = (Helper*)arg;
           int thread_num = hp->_thread_num;
           ChatClient* cc = hp->_cc;
           ChatWindow* cw = hp->_cw;
           //根据传来的数字不同,跑不同的窗口函数
           switch(thread_num)
           {
               case 0:
                   RunHead(cw);
                   break;
               case 1:
                   RunOutPut(cw,cc);
                   break;
               case 2:
                   RunInPut(cw,cc);
                   break;
               case 3:
                   RunUserList(cw,cc);
                   break;
           }
           return NULL;
        }
        static void RunHead(ChatWindow* cw)
        {
            std::string tips = "Chat Chat ~ ~";
            int y,x;
            size_t pos = 1;
            //标志了字体移动的方向
            //目的是让标识字符移动起来
            char direction = 'r';
            while(1)
            {
                getmaxyx(cw->_head,y,x);
                cw->DrawHead();
                cw->PutStringToWindow(cw->_head,y/2,pos,tips);
                //如果碰到了两边的边边,就换方向动
                if(pos > x-tips.size()-2)
                {
                    direction = 'l';
                }
                if(pos < 2)
                {
                    direction = 'r';
                }
                if(direction == 'l')
                {
                    --pos;
                }
                else
                {
                    ++pos;
                }
                sleep(1);
            }
        }
        static void RunOutPut(ChatWindow* cw,ChatClient* cc)
        {
            std::string recv_msg;
            FormatMessage fmsg; 
            cw->DrawOutput();
            int y,x;
            int row = 1;
            while(1)
            {
                getmaxyx(cw->_output,y,x);
                //1、接收消息并解析消息
                cc->RecvMsg(&recv_msg);
                fmsg.Deserialize(recv_msg);
                //2、把消息内容打印在屏幕
                std::string show_msg = fmsg._nick_name + " : " + fmsg._msg;
                cw->PutStringToWindow(cw->_output,row,1,show_msg);
                //3、把这条消息的用户信息存起来
                std::string user_info = fmsg._birthday + " - " + fmsg._nick_name;
                cc->PushOnlineUser(user_info);
                ++row;
                if(row > y - 2)
                {
                    row = 1;
                    //只有消息满了才刷新消息窗口
                    cw->DrawOutput();
                }
            }
        }
        static void RunInPut(ChatWindow* cw,ChatClient* cc)
        {
            std::string tips = "Enter Message : ";
            std::string send_msg; 
            while(1)
            {
                cw->DrawInput();
                cw->PutStringToWindow(cw->_input,1,1,tips);
                //通过取出窗口输入的消息,发送给服务端
                cw->GetStringFromWindow(cw->_input,&send_msg);
                cc->SendMsg(send_msg);
            }
        }
        static void RunUserList(ChatWindow* cw,ChatClient* cc)
        {
            cw->DrawUserList();
            while(1)
            {
                cw->DrawUserList();
                auto user_list = cc->GetOnlineUser();
                int row = 1;
                //把客户端存的在线用户列表一个个打印出来
                for(const auto& user_info:user_list)
                {
                    cw->PutStringToWindow(cw->_user_list,row++,1,user_info);
                }
                sleep(1);
            }
        }
    private:
        void PutStringToWindow(WINDOW* win,int y,int x,const std::string& msg)
        {
            mvwaddstr(win,y,x,msg.c_str());
            pthread_mutex_lock(&_lock);
            wrefresh(win);
            pthread_mutex_unlock(&_lock);
        }
        void GetStringFromWindow(WINDOW* win,std::string* msg)
        {
            char buf[1024] = {'\0'};
            memset(buf,'\0',sizeof(buf));
            wgetnstr(win,buf,sizeof(buf)-1);
            msg->assign(buf,strlen(buf));
        }
        void DrawHead()
        {
            int row = LINES/5;
            int col = COLS;
            int start_y = 0;
            int start_x = 0;
            //初始化:从(x,y)开始绘制:|=row,一=col的框
            _head = newwin(row,col,start_y,start_x);
            //两个0表示:横竖的边框都采用默认的符号
            box(_head,0,0); 
            //一定要加锁,不加锁会导致所有模块的框框会混乱
            pthread_mutex_lock(&_lock);
            wrefresh(_head);
            pthread_mutex_unlock(&_lock);
        }
        void DrawOutput()
        {
            int row = (LINES*3)/5;
            int col = (COLS*3)/4;
            int start_y = LINES/5;
            int start_x = 0;
            //初始化:从(x,y)开始绘制:|=row,一=col的框
            _output = newwin(row,col,start_y,start_x);
            //两个0表示:横竖的边框都采用默认的符号
            box(_output,0,0); 
            //一定要加锁,不加锁会导致所有模块的框框会混乱
            pthread_mutex_lock(&_lock);
            wrefresh(_output);
            pthread_mutex_unlock(&_lock);
        }
        void DrawInput()
        {
            int row = LINES/5;
            int col = (COLS*3)/4;
            int start_y = (LINES*4)/5;
            int start_x = 0;
            //初始化:从(x,y)开始绘制:|=row,一=col的框
            _input = newwin(row,col,start_y,start_x);
            //两个0表示:横竖的边框都采用默认的符号
            box(_input,0,0); 
            //一定要加锁,不加锁会导致所有模块的框框会混乱
            pthread_mutex_lock(&_lock);
            wrefresh(_input);
            pthread_mutex_unlock(&_lock);
        }
        void DrawUserList()
        {
            int row = (LINES*4)/5;
            int col = (COLS*1)/4;
            int start_y = LINES/5;
            int start_x = (COLS*3)/4;
            //初始化:从(x,y)开始绘制:|=row,一=col的框
            _user_list = newwin(row,col,start_y,start_x);
            //两个0表示:横竖的边框都采用默认的符号
            box(_user_list,0,0); 
            //一定要加锁,不加锁会导致所有模块的框框会混乱
            pthread_mutex_lock(&_lock);
            wrefresh(_user_list);
            pthread_mutex_unlock(&_lock);
        }
};

ChatServer.hpp

#pragma once
#include<iostream>
#include<pthread.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<unistd.h>

#include"Log.hpp"
#include"UserManager.hpp"
#include"FormatMessage.hpp"
#include"MsgPool.hpp"
#include"ConnectInfo.hpp"

#define UDP_PORT 4418
#define TCP_PORT 4419
#define THREAD_NUM 2

class ChatServer;
//封装了new_sock 和 ChatServer ,为了传一个参数
class NewsockAndChatserver
{
    public:
        int _new_sock;
        ChatServer* _cs;
    public:
        NewsockAndChatserver(int new_sock,ChatServer* cs)
            :_new_sock(new_sock),_cs(cs)
        {  }
};

class ChatServer
{
    private:
        //发送消息用的udp
        int _udp_sock;
        uint16_t _udp_port;
        //登陆注册要用的tcp
        int _tcp_sock;
        uint16_t _tcp_port;
        //数据池
        MsgPool* _msg_pool; 
        //用户管理模块
        UserManager* _user_manager;
    public:
        ChatServer()
        {
            _udp_sock = -1;
            _udp_port = UDP_PORT;
            _tcp_sock = -1;
            _tcp_port = TCP_PORT;
            _msg_pool = NULL;
            _user_manager = NULL;
            InitServer();
            Start();
        }
        ~ChatServer()
        {
            if(_msg_pool != NULL)
            {
                delete _msg_pool;
            }
            if(_user_manager != NULL)
            {
                delete _user_manager;
            }
        }
        void InitServer()
        {
            //1、初始化UDP
            //1.1、初始化udp套接字
            _udp_sock = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
            if(_udp_sock < 0)
            {
                LOG(FATAL,"_udp_sock init failed!") << std::endl;
                exit(1);
            }
            //1.2、绑定地址
            struct sockaddr_in svr_udp_addr;
            svr_udp_addr.sin_family = AF_INET;
            svr_udp_addr.sin_port = htons(_udp_port);
            svr_udp_addr.sin_addr.s_addr = inet_addr("0.0.0.0");
            
            int ret = bind(_udp_sock,(struct sockaddr*)&svr_udp_addr,sizeof(svr_udp_addr));
            if(ret < 0)
            {
                LOG(FATAL,"_udp bind failed!") << std::endl;
                exit(2);
            }
            LOG(INFO,"UDP init success!!!") << std::endl;
             
            //2、初始化TCP
            //2.1、初始化tcp套接字
            _tcp_sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
            if(_tcp_sock < 0)
            {
                LOG(FATAL,"_tcp_sock init failed!") << std::endl;
                exit(3);
            }
            //2.2、绑定地址
            struct sockaddr_in svr_tcp_addr;
            svr_tcp_addr.sin_family = AF_INET;
            svr_tcp_addr.sin_port = htons(_tcp_port);
            svr_tcp_addr.sin_addr.s_addr = inet_addr("0.0.0.0");
            //2.2.1、地址复用:服务器突然挂掉的时候可以立即绑定这个地址,否则要等2MSL
            int opt = 1;
            setsockopt(_tcp_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

            ret = bind(_tcp_sock,(struct sockaddr*)&svr_tcp_addr,sizeof(svr_tcp_addr));
            if(ret < 0)
            {
                LOG(FATAL,"_tcp bind failed!") << std::endl;
                exit(4);
            }
            //2.3、监听
            ret = listen(_tcp_sock,5);
            if(ret < 0)
            {
                LOG(FATAL,"_tcp listen failed!") << std::endl;
                exit(5);
            }
            LOG(INFO,"TCP init success!!!") << std::endl;

            //3、初始化数据池
            _msg_pool = new MsgPool();
            if(_msg_pool == NULL)
            {
                LOG(FATAL,"_msg_pool init failed!") << std::endl;
                exit(6);
            }
            LOG(INFO,"MsgPool init success!!!") << std::endl;

            //4、初始化用户管理
            _user_manager = new UserManager();
            if(_user_manager == NULL)
            {
                LOG(FATAL,"_user_manager init failed!") << std::endl;
                exit(7);
            }
            LOG(INFO,"UserManager init success!!!") << std::endl;
        }
        void Start()
        {
            //1、创建UDP生产者消费者线程(将udp缓冲区的信息存放到数据池,将数据池的信息发送给客户)
            for(size_t i = 0; i < THREAD_NUM; ++i)
            {
                pthread_t tid;
                int ret = pthread_create(&tid,NULL,ProductMsgStart,(void*)this);
                if(ret < 0)
                {
                    LOG(FATAL,"ProductMsgStart create failed!") << std::endl;
                    exit(11);
                }
                ret = pthread_create(&tid,NULL,ConsumeMsgStart,(void*)this);
                if(ret < 0)
                {
                    LOG(FATAL,"ConsumeMsgStart create failed!") << std::endl;
                    exit(12);
                }
            }
            LOG(INFO,"Product-Consume threads start!!!") << std::endl;
            //2、创建TCP登陆/注册的线程
            //主线程用来监听,然后accept新的socket,新线程用新的socket去服务一个对象
            while(1)
            {
                //2.1、accept 一个客户
                struct sockaddr_in cli_tcp_addr;
                socklen_t addr_len = sizeof(cli_tcp_addr);
                int new_sock = accept(_tcp_sock,(struct sockaddr*)&cli_tcp_addr,&addr_len);
                if(new_sock < 0)
                {
                    LOG(ERROR,"accept new_sock failed!") << std::endl;
                    continue;
                }
                //2.2、把new_sock和ChatServer的this指针封装了传入一个线程里
                //为什么要封装起来:因为pthread只能传入一个参数
                NewsockAndChatserver* nac = new NewsockAndChatserver(new_sock,this);
                if(nac == NULL)
                {
                    LOG(ERROR,"new NewsockAndChatserver failed!") << std::endl;
                    continue;
                }
                //3、将这个封装的参数传入一个线程,让这个线程来处理登陆或者注册
                pthread_t tid;
                int ret = pthread_create(&tid,NULL,LoginRegisterStart,(void*)nac);
                if(ret < 0)
                {
                    LOG(ERROR,"Create LoginRegisterStart thread failed!") << std::endl;
                    continue;
                }
                LOG(INFO,"Create LoginRegisterStart thread success!!!") << std::endl;
            }
        }
        static void* ProductMsgStart(void* arg)
        {
           //注意要分离线程,否则会资源泄漏,毕竟我们也不需要去关注线程返回状态
           pthread_detach(pthread_self());
           ChatServer* cs = (ChatServer*)arg; 
           while(1)
           {
               cs->RecvMsg();
           }
           return NULL;
        }
        static void* ConsumeMsgStart(void* arg)
        {
           //注意要分离线程,否则会资源泄漏,毕竟我们也不需要去关注线程返回状态
           pthread_detach(pthread_self());
           ChatServer* cs = (ChatServer*)arg; 
           while(1)
           {
               cs->BroadcastMsg();
           }
           return NULL;
        }
        static void* LoginRegisterStart(void* arg)
        {
           //注意要分离线程,否则会资源泄漏,毕竟我们也不需要去关注线程返回状态
            pthread_detach(pthread_self());
            //1、拿到封装的两个数据
            NewsockAndChatserver* nac = (NewsockAndChatserver*)arg;
            ChatServer* cs = nac->_cs;
            int new_sock = nac->_new_sock;
            //2、去new_sock的缓冲区去取一个char大小的请求
            enum RegisterAndLogin request_type;
            int recv_size = recv(new_sock,&request_type,sizeof(request_type),0);
            if(recv_size < 0)
            {
                LOG(ERROR,"recv request_type error!") << std::endl;
                return NULL;
            }
            if(recv_size == 0)
            {
                LOG(ERROR,"client tcp shutdown!") << std::endl;
                return NULL;
            }
            //ReplyIngo里要返回给客户的本次执行状态和用户id
            enum ReplyStatus response_status = REQUEST_ERROR;
            //3、通过收到的请求类型来判断是 注册 还是 登陆
            ReplyInfo response;
            switch(request_type)
            {
                case REGISTER:
                    response_status = cs->DealRegister(new_sock,&response);
                    break;
                case LOGIN:
                    response_status = cs->DealLogin(new_sock,&response);
                    break;
                default:
                    response_status = REQUEST_ERROR; 
                    break;
            }
            //4、填入这次客户请求的执行结果的状态
            response._status = response_status;
            //5、发送给客户端这次的处理情况
            int send_size = send(new_sock,&response,sizeof(response),0);
            if(send_size < 0)
            {
                LOG(ERROR,"send ReplyInfo failed!") << std::endl;
                return NULL;
            }
            LOG(INFO,"Send ReplyInfo success!!!") << std::endl;
            //6、关闭new_sock套接字
            close(new_sock);
            delete nac;
            return NULL;
        }
    private:
        //处理注册
        ReplyStatus DealRegister(int new_sock,ReplyInfo* response)
        {
            //从newsock的接受窗口接受RegisterInfo大小的注册数据
            RegisterInfo ri;
            int recv_size = recv(new_sock,&ri,sizeof(ri),0);
            if(recv_size < 0)
            {
                LOG(ERROR,"recv RegisterInfo failed!") << std::endl;
                return REGISTER_FAILED;
            }
            if(recv_size == 0)
            {
                LOG(ERROR,"client tcp shutdown!") << std::endl;
                return REGISTER_FAILED;
            }
            //将注册信息交给user_manager来处理注册
            bool ret =_user_manager->Register(ri._nick_name,ri._birthday,ri._password,&response->_user_id);
            if(ret == false)
            {
                return REGISTER_FAILED;
            }
            strcpy(response->_nick_name,ri._nick_name);
            strcpy(response->_birthday,ri._birthday);
            return REGISTER_SUCCESS; 
        }
        //处理登陆
        ReplyStatus DealLogin(int new_sock,ReplyInfo* response)
        {
            //在new_sock的接收窗口接受LoginInfo大小的登陆数据
            LoginInfo li;
            int recv_size = recv(new_sock,&li,sizeof(li),0);
            if(recv_size < 0)
            {
                LOG(ERROR,"recv LoginInfo failed!") << std::endl;
                return LOGIN_FAILED;
            }
            if(recv_size == 0)
            {
                LOG(ERROR,"client tcp shutdown!") << std::endl;
                return LOGIN_FAILED;
            }
            //把接受到的LoginInfo里的账号和密码交给user_manager校验
            bool ret = _user_manager->Login(li._user_id,li._password,response);
            if(ret == false)
            {
                LOG(ERROR,"user_id or password is not correct!") << std::endl;
                return LOGIN_FAILED;
            }
            return LOGIN_SUCCESS;
        }

        //从udp接受缓冲区接受一条消息,并解析其合法性和地址信息
        void RecvMsg()
        {
            //1、从udp缓冲区中拿信息数据
            char buf[10240] = {'\0'};
            struct sockaddr_in cli_udp_addr;
            socklen_t addr_len = sizeof(cli_udp_addr);
            int recv_size = recvfrom(_udp_sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&cli_udp_addr,&addr_len);
            if(recv_size < 0)
            {
                LOG(ERROR,"RecvMsg failed!") << std::endl;
                return;
            }
            //2、反序列化该信息 
            std::string recv_msg;
            recv_msg.assign(buf,recv_size);
            FormatMessage fmsg; 
            fmsg.Deserialize(recv_msg);
            //3、从该信息中提取用户信息,看是否是合法用户
            //  3.1:如果在用户信息列表里没有找到,则这条信息不放入数据池
            //  3.2:如果在在线用户列表里没有找到,则这条信息也不放入数据池
            //  3.3:如果是登陆的信息(tcp登录完成后会立即发送udp,因为用户聊天是用的udp),则也不放入数据池,而是把用户登陆
            bool ret = _user_manager->IsLogin(fmsg._user_id,cli_udp_addr,addr_len);
            if(ret == true)
            {
                _msg_pool->PushMsgToPool(recv_msg);
            }
        }
        //发送消息给所有在线用户
        void BroadcastMsg()
        {
            //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!下面一定要先取数据再取列表
            //因为如果反过来。服务端刚开启的时候,就来到了这个函数,然后取列表,列表是空的,然后走取数据的逻辑,因为没有数据,所以阻塞在数据池里,等来了数据后,就去发数据,但是发现列表一个在线的都没有,然后出函数,再while又进来,然后取列表,列表现在是有用户了,然后走到取数据的逻辑,但是这时候之前已经接受了一次数据了,现在数据是空的,然后会一直阻塞在这个接受数据的逻辑里
            //1、从数据池中取数据
            std::string msg;
            _msg_pool->PopMsgFromPool(&msg);
            //2、获取所有在线用户列表
            std::vector<UserInfo> online_list;
            _user_manager->GetOnlinelist(&online_list);
            //3、一个个发送
            for(const auto& user:online_list)
            {
                SendMsg(msg,user._addr,user._addr_len);
            }
        }
        //发送消息给一个客户
        void SendMsg(const std::string& msg,const struct sockaddr_in& cli_udp_addr,const socklen_t& addr_len)
        {
            int send_size = sendto(_udp_sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&cli_udp_addr,addr_len);
            if(send_size < 0)
            {
                LOG(ERROR,"SendMsg failed!") << std::endl;
                return;
            }
        }
};

ChatClient.hpp

#pragma once
#include<iostream>
#include<unistd.h>
#include<arpa/inet.h>
#include<unordered_set>

#include"Log.hpp"
#include"FormatMessage.hpp"
#include"ConnectInfo.hpp"

#define SVR_TCP_PORT 4418
#define SVR_UDP_PORT 4419
#define SVR_IP "192.168.132.128"
#define MAX_MESSAGE_SIZE 1024

class MyOnlineInfo
{
    public:
        std::string _nick_name; 
        std::string _birthday;
        uint32_t _user_id;
};

class ChatClient
{
    private:
        //服务端的用来发消息的udp
        uint16_t _svr_udp_port;
        //服务端的用来登陆/注册的tcp
        uint16_t _svr_tcp_port;
        //服务端的ip地址
        std::string _svr_ip;

        //客户端用来登陆/注册的tcp_sock
        int _cli_tcp_sock;
        //客户端用来发消息的udp_sock
        int _cli_udp_sock;
        //在线用户列表
        std::unordered_set<std::string> _online_list;
        //当前登陆的用户信息
        MyOnlineInfo _me;
    public:
        ChatClient(const std::string& svr_ip = SVR_IP)
        {
            _svr_udp_port = SVR_TCP_PORT;
            _svr_tcp_port = SVR_UDP_PORT;
            _svr_ip = svr_ip;
            _cli_tcp_sock = -1;
            _cli_udp_sock = -1;
            InitUdp();
            //这里没有去初始化TCP,因为虽然登陆和注册都要用到TCP,但是一个客户的请求可能不一样,所以还是放在了登陆和注册函数里去初始化
        }
        ~ChatClient()
        {
            if(_cli_udp_sock != -1)
            {
                close(_cli_udp_sock);
            }
            //这里没有去关闭_cli_tcp_sock,因为在注册和登陆完后都会关闭掉
        }
    public:
        bool Register()
        {
            //如果连接失败会自动退出进程
            InitAndConnectTcp();
            //1、先发送ConnectInfo里的枚举类型的注册请求
            enum RegisterAndLogin request = REGISTER;
            int send_size = send(_cli_tcp_sock,&request,sizeof(request),0);
            if(send_size < 0)
            {
                LOG(ERROR,"Send Register request failed!") << std::endl;
                return false;
            }
            //2、发送注册请求后,填写注册的基本信息RegisterInfo
            RegisterInfo register_info; 
            std::cout << "Please enter your nick_name: " << std::endl;
            std::cin >> register_info._nick_name;
            std::cout << "Please enter your birthday: " << std::endl;
            std::cin >> register_info._birthday;
            while(1)
            {
                char password1[15] = {0}; 
                char password2[15] = {0}; 
                std::cout << "Please enter your password: " << std::endl;
                std::cin >> password1;
                std::cout << "Please enter your password again: " << std::endl;
                std::cin >> password2;
                //如果两次输入的密码不同则重新输入
                if(strcmp(password1,password2) == 0)
                {
                    //密码相同则把输入的密码打包
                    strcpy(register_info._password,password1);
                    break;
                }
                else
                {
                    std::cout << "Twice password were not same! Please enter again!" << std::endl;
                }
            }
            //3、发送注册的信息 
            send_size = send(_cli_tcp_sock,&register_info,sizeof(register_info),0);
            if(send_size < 0)
            {
                LOG(ERROR,"Send RegisterInfo failed!") << std::endl;
                return false;
            }
            //4、查看服务端回过来的注册情况
            ReplyInfo response; 
            int recv_size = recv(_cli_tcp_sock,&response,sizeof(response),0);
            if(recv_size < 0)
            {
                LOG(ERROR,"Recv Register ReplyInfo failed!") << std::endl;
                return false;
            }
            if(recv_size == 0)
            {
                LOG(ERROR,"Server tcp shutdown!") << std::endl;
                return false;
            }
            //  如果请求注册时候填的信息不对或为空则错误
            if(response._status == REGISTER_FAILED || response._status == REQUEST_ERROR)
            {
                LOG(ERROR,"Request error,please check register info!") << std::endl;
                return false;
            }
            std::cout << "Register success! Your new user_id : " << response._user_id << std::endl;
            close(_cli_tcp_sock);
            return true;
        }
        bool Login()
        {
            InitAndConnectTcp();
            //1、给服务端发送登陆的请求
            enum RegisterAndLogin request = LOGIN;
            int send_size = send(_cli_tcp_sock,&request,sizeof(request),0); 
            if(send_size < 0)
            {
                LOG(ERROR,"Send Login request failed!") << std::endl;
                return false;
            }
            //2、组织LoginInfo里的id和密码
            LoginInfo li;
            std::cout << "Please enter your user_id" << std::endl;
            std::cin >> li._user_id;
            std::cout << "Please enter your password" << std::endl;
            std::cin >> li._password;
            //3、发送LoginInfo
            send_size = send(_cli_tcp_sock,&li,sizeof(li),0);
            if(send_size < 0)
            {
                LOG(ERROR,"Send LoginInfo failed!") << std::endl;
                return false;
            }
            //4、查看服务端返回的登陆状态
            ReplyInfo response;
            int recv_size = recv(_cli_tcp_sock,&response,sizeof(response),0);
            if(recv_size < 0)
            {
                LOG(ERROR,"Recv Login ReplyInfo failed!") << std::endl;
                return false;
            }
            if(recv_size == 0)
            {
                LOG(ERROR,"Server tcp shutdown!") << std::endl;
                return false;
            }
            if(response._status != LOGIN_SUCCESS)
            {
                LOG(ERROR,"Login failed! Please check your user_id and password!") << std::endl;
                return false;
            }
            //5、登陆成功后把当前登陆的用户信息存起来
            _me._nick_name = response._nick_name;
            _me._birthday = response._birthday;
            _me._user_id = response._user_id;
            //6、发送udp数据:为了让服务端知道自己的UDP端口(因为聊天用的是UDP)
            //这一步做完才算是真正的登陆了,这一步只需要发送一个序列化消息即可,msg可以是空
            //tcp登陆消息是为了验证密码,udp登陆消息是为了告诉服务端自己的udp信息
            SendMsg("");
            std::cout << "Login success!!!" << std::endl;
            close(_cli_tcp_sock); 
            return true;
        }
        bool SendMsg(const std::string& msg)
        {
            //1、组织好要发送的信息格式
            //如果发送的消息超过了最大的size,则截断
            FormatMessage fmsg;
            if(msg.size() > MAX_MESSAGE_SIZE)
            {
                fmsg._msg = msg.substr(0,MAX_MESSAGE_SIZE);    
            }
            else
            {
                fmsg._msg = msg;
            }
            fmsg._msg = msg;
            fmsg._nick_name = _me._nick_name;
            fmsg._birthday = _me._birthday;
            fmsg._user_id = _me._user_id;
            std::string send_msg;
            fmsg.Serialize(&send_msg);

            //2、组织好服务端的地址信息
            struct sockaddr_in svr_addr;
            svr_addr.sin_family = AF_INET;
            svr_addr.sin_port = htons(_svr_udp_port);
            svr_addr.sin_addr.s_addr = inet_addr(_svr_ip.c_str());

            //3、发送
            int send_size = sendto(_cli_udp_sock,send_msg.c_str(),send_msg.size(),0,(struct sockaddr*)&svr_addr,sizeof(svr_addr));
            if(send_size < 0)
            {
                LOG(ERROR,"SendMsg failed!") << std::endl;
                return false;
            }
            return true;
        }
        bool RecvMsg(std::string* msg)
        {
            //从UDP缓冲区拿数据
            char buf[MAX_MESSAGE_SIZE] = {'\0'};
            memset(buf,'\0',MAX_MESSAGE_SIZE);
            struct sockaddr_in svr_addr;
            socklen_t addr_len = sizeof(svr_addr);
            int recv_size = recvfrom(_cli_udp_sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&svr_addr,&addr_len);
            if(recv_size < 0)
            {
                LOG(ERROR,"RecvMsg failed!") << std::endl;
                return false;
            }
            msg->assign(buf,recv_size);
            return true;
        }
        void PushOnlineUser(const std::string& user_info)
        {
            auto it = _online_list.find(user_info);
            if(it == _online_list.end())
            {
                _online_list.insert(user_info);
            }
        }
        std::unordered_set<std::string>& GetOnlineUser()
        {
            return _online_list;
        }

    private:
        void InitUdp()
        {
            _cli_udp_sock = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
            if(_cli_udp_sock < 0)
            {
                LOG(FATAL,"InitUdp failed!") << std::endl;
                exit(1);
            }
        }
        void InitAndConnectTcp()
        {
            //1、初始化套接字
            _cli_tcp_sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
            if(_cli_tcp_sock < 0)
            {
                LOG(FATAL,"InitTcp failed!") << std::endl;
                exit(2);
            }
            //2、连接服务端
            struct sockaddr_in svr_addr;
            svr_addr.sin_family = AF_INET;
            svr_addr.sin_port = htons(_svr_tcp_port);
            svr_addr.sin_addr.s_addr = inet_addr(_svr_ip.c_str());

            int ret = connect(_cli_tcp_sock,(struct sockaddr*)&svr_addr,sizeof(svr_addr));
            if(ret < 0)
            {
                LOG(FATAL,"Connect SvrTcp failed!") << std::endl;
                exit(3);
            }
        }

};

ChatServer.cpp

#include"ChatServer.hpp"
#include<unistd.h>

int main()
{
    ChatServer cs;
    return 0;
}

ChatClient.cpp

#include"ChatWindow.hpp"

void Menu()
{
    std::cout << "---------------------------------------------" << std::endl;
    std::cout << "|  1.register                      2.login  |" << std::endl;
    std::cout << "|                  3.exit                   |" << std::endl;
    std::cout << "---------------------------------------------" << std::endl;
    std::cout << std::endl;
}

int main()
{
    ChatClient* cc = new ChatClient();
    while(1)
    {
        Menu();
        int select = -1;
        std::cout << "Please select : ";
        fflush(stdout);
        std::cin >> select;
        if(select == 1)
        {
            if(cc->Register())
            {
                std::cout << "Register success! Please login!" << std::endl;
            }
            else
            {
                std::cout << "Register failed! Please check your register info!" << std::endl;
            }
        }
        else if(select == 2)
        {
            if(!cc->Login())
            {
                std::cout << "Login failed! Please check your login info!" << std::endl;
            }
            ChatWindow cw(cc);
            cw.Start();
        }
        else if(select == 3)
        {
            return 0;
        }
        else
        {
            std::cout << "Select error!" << std::endl;
            sleep(2);
        }
    }
    delete cc;
    return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
智慧校园整体解决方案是响应国家教育信息化政策,结合教育改革和技术创新的产物。该方案以物联网、大数据、人工智能和移动互联技术为基础,旨在打造一个安全、高效、互动且环保的教育环境。方案强调从数字化校园向智慧校园的转变,通过自动数据采集、智能分析和按需服务,实现校园业务的智能化管理。 方案的总体设计原则包括应用至上、分层设计和互联互通,确保系统能够满足不同用户角色的需求,并实现数据和资源的整合与共享。框架设计涵盖了校园安全、管理、教学、环境等多个方面,构建了一个全面的校园应用生态系统。这包括智慧安全系统、校园身份识别、智能排课及选课系统、智慧学习系统、精品录播教室方案等,以支持个性化学习和教学评估。 建设内容突出了智慧安全和智慧管理的重要性。智慧安全管理通过分布式录播系统和紧急预案一键启动功能,增强校园安全预警和事件响应能力。智慧管理系统则利用物联网技术,实现人员和设备的智能管理,提高校园运营效率。 智慧教学部分,方案提供了智慧学习系统和精品录播教室方案,支持专业级学习硬件和智能化网络管理,促进个性化学习和教学资源的高效利用。同时,教学质量评估中心和资源应用平台的建设,旨在提升教学评估的科学性和教育资源的共享性。 智慧环境建设则侧重于基于物联网的设备管理,通过智慧教室管理系统实现教室环境的智能控制和能效管理,打造绿色、节能的校园环境。电子班牌和校园信息发布系统的建设,将作为智慧校园的核心和入口,提供教务、一卡通、图书馆等系统的集成信息。 总体而言,智慧校园整体解决方案通过集成先进技术,不仅提升了校园的信息化水平,而且优化了教学和管理流程,为学生、教师和家长提供了更加便捷、个性化的教育体验。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值