errorbeep@home:~$

私聊(互斥锁, map, tuple)


续:用户登录(c++, mysql)。实现用户私聊功能,用户登录成功后选择私聊,输入目标用户名即可发起私聊。成功发起私聊后,用户要发送的信息会被先发到服务端,服务端根据目标用户名将信息发给对应的客户端;

思路

  1. 需要处理的客户端请求共两个:建立私聊和转发消息;
    a. 当客户端需要建立与某个用户的私聊时发送建立私聊请求,该请求中应包含的信息有:请求类型即私聊、目标用户名、发起私聊的用户名;服务端接收到该请求并解析后,根据目标用户名查找其套接字描述符并保存以待使用;
    b. 客户端发送私聊消息的请求应包含的信息:请求类型即发送消息、消息内容;当服务端接收到发送私聊消息请求时,根据之前保存的目标用户的套接字描述符向其发送解析出的消息;
  2. 要找到某个用户名对应的套接字描述符,就要事先建立用户名和套接字描述符的映射;每次用户登录时都会与服务端建立连接,因此可以在登录时将用户名和描述符的映射保存起来;
  3. 客户端主要任务就是发送请求和显示回复,而服务端除了负责建立私聊和转发消息外还要做到:接收到用户的登录请求后不仅要核对登录信息返回核对结果,还要在登录成功后保存客户端的用户名和套接字描述符的映射,此映射可以使用unorderer_map存储,将用户名作为键值,描述符作为实值;
  4. 服务端建立和客户端的连接后会建立一个线程用于处理该客户端的所有请求,用于建立线程的函数是接收客户端发来的消息的函数RecvMsg(),该函数循环接收客户端消息,并在每次循环中调用处理客户度端消息的函数HanleRequests(),HandleRequests要根据客户端状态处理每个请求,因此要把客户端状态作为参数传入;目前服务端处理的请求不止一个,对每个请求的处理都可能会影响到用户的状态以致影响后续处理过程,所以在HandleRequests中应可以改变客户端的状态,故客户端的状态应使用引用传递给HandleRequests;
  5. 用户名和客户端套接字描述符的映射可以设置为类的静态变量以实现共享;服务端可能会创建多个线程,这些线程都会访问或修改前述映射,某个线程对登录请求的处理过程会对该映射做出修改,另一个线程对私聊请求的处理会访问该映射,这种情况下可能会出现线程竞争问题;对前述映射引入互斥锁,实现线程对此映射的互斥访问即可解决线程竞争问题;

客户端:

主要对启动客户端后调用的函数HandleClient做如下修改:

  1. 客户端的登录成功后的选项中添加建立私聊选项,选择发起私聊后,提示用户输入目标用户名,将这些信息整合成私聊请求发送至服务端;
  2. 提示用户输入要发送的消息并建立一个发送消息的线程将其发送至服务端,且在该线程内部要把消息封装成发送消息请求;
  3. 还要创建一个线程用于从服务端接收消息即来自私聊的目标用户的经服务端转发的消息;
  4. 确保发送消息线程先结束,接收消息线程后结束。

Code

void client::HandleClient(int conn){
    int choice;
    string name,pass,pass1;
    bool if_login=false;//记录是否登录成功
    string login_name;//记录成功登录的用户名

    cout<<" ------------------\n";
    cout<<"|                  |\n";
    cout<<"| 请输入你要的选项:|\n";
    cout<<"|    0:退出        |\n";
    cout<<"|    1:登录        |\n";
    cout<<"|    2:注册        |\n";
    cout<<"|                  |\n";
    cout<<" ------------------ \n\n";

    //开始处理注册、登录事件
    while(1){
        if(if_login)
           break;
        cin>>choice;
        if(choice==0)
            break;
        //注册
        else if(choice==2){
            cout<<"注册的用户名:";
            cin>>name;
            while(1){
                cout<<"密码:";
                cin>>pass;
                cout<<"确认密码:";
                cin>>pass1;
                if(pass==pass1)
                    break;
                else
                    cout<<"两次密码不一致!\n\n";
            }
            name="name:"+name;
            pass="pass:"+pass;
            string str=name+pass;
            send(conn,str.c_str(),str.length(),0);
            cout<<"注册成功!\n";
            cout<<"\n继续输入你要的选项:";
        }
        //登录
        else if(choice==1&&!if_login){
            while(1){
                cout<<"用户名:";
                cin>>name;
                cout<<"密码:";
                cin>>pass;
                string str="login"+name;
                str+="pass:";
                str+=pass;
                send(sock,str.c_str(),str.length(),0);//发送登录信息
                char buffer[1000];
                memset(buffer,0,sizeof(buffer));
                recv(sock,buffer,sizeof(buffer),0);//接收响应
                string recv_str(buffer);
                if(recv_str.substr(0,2)=="ok"){
                    if_login=true;
                    login_name=name;
                    cout<<"登录成功\n\n";
                    break;
                }
                else
                    cout<<"密码或用户名错误!\n\n";
            }
        }
    }
    //登录成功
    while(if_login&&1){
        if(if_login){
            system("clear");
            cout<<"        欢迎回来,"<<login_name<<endl;
            cout<<" -------------------------------------------\n";
            cout<<"|                                           |\n";
            cout<<"|          请选择你要的选项:               |\n";
            cout<<"|              0:退出                       |\n";
            cout<<"|              1:发起单独聊天               |\n";
            cout<<"|              2:发起群聊                   |\n";
            cout<<"|                                           |\n";
            cout<<" ------------------------------------------- \n\n";
        }
        cin>>choice;
        if(choice==0)
            break;
        //私聊
        if(choice==1){
            cout<<"请输入对方的用户名:";
            string target_name,content;
            cin>>target_name;
            string sendstr("target:"+target_name+"from:"+login_name);//标识目标用户+源用户
            send(sock,sendstr.c_str(),sendstr.length(),0);//先向服务器发送目标用户、源用户
            cout<<"请输入你想说的话(输入exit退出):\n";
            thread t1(client::SendMsg,conn); //创建发送线程
            thread t2(client::RecvMsg,conn);//创建接收线程
            t1.join();
            t2.join();
        }
    }
    close(sock);
}

服务端

  1. 在接收消息的函数中建立一个tuple用于存储客户端状态,包含用户是否已经登录,用户名,目标用户名,目标用户套接字描述符;
  2. 为服务端类创建一个静态unordered_map类型变量用于存储用户名和套接字描述符的映射;
  3. 在服务端类创建互斥锁,使用posix的pthread_mutex_t类型,且为静态成员,
    ==> 在服务端构造函数中使用函数int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);初始化;
  4. 对HandleRequests函数做如下修改:
    a. 对于HandleRequests函数中处理登录模块,在对用户名和密码核对无误后,
    ==> 使用函数int pthread_mutex_lock(pthread_mutex_t *mutex);申请对前述互斥锁加锁,再将用户名和套接字描述符存储到上述unordered_map,
    ==> 最后使用函数int pthread_mutex_unlock(pthread_mutex_t *mutex);释放上述互斥锁;
    b. 对于HandleRequests中处理建立私聊请求模块,先解析目标用户名,再从unordered_map中以用户名为键值查找对应的套接字描述符,找到保存;
    ==> iterator find ( const key_type& k );如果存在该映射则返回对应迭代器,否则返回unordered_map::end
    利用此函数判断是否存在该映射若不存在则报错,否则直接使用name_sock_map[target]得到套接字描述符;
    c. 对于发送消息的函数,先判断之前对建立私聊的请求处理后是否得到了目标套接字描述符,如果没有则此时再尝试获取一次,若获取失败则报错,否则解析消息内容发送给目标套接字;
    d. 处理完请求后更新客户端状态;

Code

服务端定义:

#include "server.h"

vector<bool> server::sock_arr(10000,false);
unordered_map<string,int> server::name_sock_map;//名字和套接字描述符
pthread_mutex_t server::name_sock_mutx;//互斥锁,锁住需要修改name_sock_map的临界区

server::server(int port,string ip):server_port(port),server_ip(ip){
    pthread_mutex_init(&name_sock_mutx, NULL); //创建互斥锁
}

HandleRequests函数:

void server::HandleRequest(int conn,string str,tuple<bool,string,string,int> &info){
    char buffer[1000];
    string name,pass;
    //把参数提出来,方便操作
    bool if_login=get<0>(info);//记录当前服务对象是否成功登录
    string login_name=get<1>(info);//记录当前服务对象的名字
    string target_name=get<2>(info);//记录目标对象的名字
    int target_conn=get<3>(info);//目标对象的套接字描述符

    //连接MYSQL数据库
    MYSQL *con=mysql_init(NULL);
    mysql_real_connect(con,"127.0.0.1","root","","ChatProject",0,NULL,CLIENT_MULTI_STATEMENTS);

    //注册
    if(str.find("name:")!=str.npos){
        int p1=str.find("name:"),p2=str.find("pass:");
        name=str.substr(p1+5,p2-5);
        pass=str.substr(p2+5,str.length()-p2-4);
        string search="INSERT INTO USER VALUES (\"";
        search+=name;
        search+="\",\"";
        search+=pass;
        search+="\");";
        cout<<"sql语句:"<<search<<endl<<endl;
        mysql_query(con,search.c_str());
    }
    //登录
    else if(str.find("login")!=str.npos){
        int p1=str.find("login"),p2=str.find("pass:");
        name=str.substr(p1+5,p2-5);
        pass=str.substr(p2+5,str.length()-p2-4);
        string search="SELECT * FROM USER WHERE NAME=\"";
        search+=name;
        search+="\";";
        cout<<"sql语句:"<<search<<endl;
        auto search_res=mysql_query(con,search.c_str());
        auto result=mysql_store_result(con);
        int col=mysql_num_fields(result);//获取列数
        int row=mysql_num_rows(result);//获取行数
        //查询到用户名
        if(search_res==0&&row!=0){
            cout<<"查询成功\n";
            auto info=mysql_fetch_row(result);//获取一行的信息
            cout<<"查询到用户名:"<<info[0]<<" 密码:"<<info[1]<<endl;
            //密码正确
            if(info[1]==pass){
                cout<<"登录密码正确\n\n";
                string str1="ok";
                if_login=true;
                login_name=name;
                pthread_mutex_lock(&name_sock_mutx); //上锁
                name_sock_map[login_name]=conn;//记录下名字和文件描述符的对应关系
                pthread_mutex_unlock(&name_sock_mutx); //解锁
                send(conn,str1.c_str(),str1.length()+1,0);
            }
            //密码错误
            else{
                cout<<"登录密码错误\n\n";
                char str1[100]="wrong";
                send(conn,str1,strlen(str1),0);
            }
        }
        //没找到用户名
        else{
            cout<<"查询失败\n\n";
            char str1[100]="wrong";
            send(conn,str1,strlen(str1),0);
        }
    }
    //设定目标的文件描述符
    else if(str.find("target:")!=str.npos){
        int pos1=str.find("from");
        string target=str.substr(7,pos1-7),from=str.substr(pos1+4);
        target_name=target;
        //找不到这个目标
        if(name_sock_map.find(target)==name_sock_map.end())
            cout<<"源用户为"<<login_name<<",目标用户"<<target_name<<"仍未登录,无法发起私聊\n";
        //找到了目标
        else{
            cout<<"源用户"<<login_name<<"向目标用户"<<target_name<<"发起的私聊即将建立";
            cout<<",目标用户的套接字描述符为"<<name_sock_map[target]<<endl;
            target_conn=name_sock_map[target];
        }
    }

    //接收到消息,转发
    else if(str.find("content:")!=str.npos){
        if(target_conn==-1){
            cout<<"找不到目标用户"<<target_name<<"的套接字,将尝试重新寻找目标用户的套接字\n";
            if(name_sock_map.find(target_name)!=name_sock_map.end()){
                target_conn=name_sock_map[target_name];
                cout<<"重新查找目标用户套接字成功\n";
            }
            else{
                cout<<"查找仍然失败,转发失败!\n";
            }
        }
        string recv_str(str);
        string send_str=recv_str.substr(8);
        cout<<"用户"<<login_name<<"向"<<target_name<<"发送:"<<send_str<<endl;
        send_str="["+login_name+"]:"+send_str;
        send(target_conn,send_str.c_str(),send_str.length(),0);
    }

    //更新实参
    get<0>(info)=if_login;//记录当前服务对象是否成功登录
    get<1>(info)=login_name;//记录当前服务对象的名字
    get<2>(info)=target_name;//记录目标对象的名字
    get<3>(info)=target_conn;//目标对象的套接字描述符
}

下一篇:群聊(互斥锁, set)