errorbeep@home:~$

客户端(多线程,面向对象)


接上篇:服务端(多线程, 面向对象)。实现服务端收到消息后自动向客户端发送收到提示,且客户端并行接收来自服务端的消息和向服务端发送消息。 运用面向对象编程方法和多线程编程,将客户端抽象为类,构造两个线程分别用于从服务端接收消息和向服务端发送消息。

主程序中创建客户端对象,并用目标服务端IP和端口号初始化,客户端开始运行,连接服务端,创建两个线程实现发送和接收消息,且发送线程结束后接收线程立即结束。

客户端类的属性应包含:服务端IP和端口号,客户端套接字描述符,用于向服务端发送消息的函数,从服务度接收消息的函数,启动客户端的函数。

为确保发送消息的线程先于接收消息的线程结束,可以利用关闭服务端和客户端之间的连接后接收服务端消息的线程接收到的字符串的长度小于等于0,当服务端接收到客户端发送的退出提示时关闭客户端与服务端的连接,在接收服务端消息的线程中判断接收到的消息长度是否小于等于0,若成立则终止接收服务端消息的进程,且应让主线程阻塞等待发送消息的线程结束再让接收消息的线程结束;

IP用string存储,端口号和套接字描述符用int;

#ifndef CLIENT_H
#define CLIENT_H

#include "global.h"

class client{
    private:
        int server_port;//服务器端口
        string server_ip;//服务器ip
        int sock;//与服务器建立连接的套接字描述符
    public:
        client(int port,string ip);
        ~client();
        void run();//启动客户端服务
        static void SendMsg(int conn);//发送线程
        static void RecvMsg(int conn);//接收线程
};
#endif
#include "client.h"

client::client(int port,string ip):server_port(port),server_ip(ip){}
client::~client(){
    close(sock);
}

启动客户端的函数定义为普通的非静态成员函数即可:

1. 使用对象属性中的服务端IP和端口号创建网络地址;
2. 创建一个套接字描述符;
3. 申请建立到前述网络地址的连接;
4. 使用发送消息的函数和接收消息的函数创建线程;
5. 让主线程等待发送消息的线程结束在让其等待接收消息的线程结束;  
void client::run(){

    //定义sockfd
    sock = socket(AF_INET,SOCK_STREAM, 0);

    //定义sockaddr_in
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(server_port);  //服务器端口
    servaddr.sin_addr.s_addr = inet_addr(server_ip.c_str());  //服务器ip

    //连接服务器,成功返回0,错误返回-1
    if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        perror("connect");
        exit(1);
    }
    cout<<"连接服务器成功\n";

    //创建发送线程和接收线程
    thread send_t(SendMsg,sock),recv_t(RecvMsg,sock);
    send_t.join();
    cout<<"发送线程已结束\n";
    recv_t.join();
    cout<<"接收线程已结束\n";
    return;
}

向服务端发送消息的函数和从服务端接收消息的函数可以都定义为静态函数,若定义为非静态成员函数则创建线程时还要传入该客户端的实例对象,定义为静态函数则只要要传入客户端的套接字描述符; 向服务端发送消息的函数:

1. 创建缓冲区;
2. 创建循环:
    a. 重置缓冲区;
    b. 读取用户输入数据到缓冲区;
    c. 发送缓冲区中数据;
    d. 若发送失败或发送结束提示则结束循环;  
//注意,前面不用加static!
void client::SendMsg(int conn){
    char sendbuf[100];
    while (1)
    {
        memset(sendbuf, 0, sizeof(sendbuf));
        cin>>sendbuf;
        int ret=send(conn, sendbuf, strlen(sendbuf),0); //发送
        //输入exit或者对端关闭时结束
        if(strcmp(sendbuf,"exit")==0||ret<=0)
            break;
    }
}

从服务端接收消息的函数:

1. 创建缓冲区;
2. 创建循环:
    a. 重置缓冲区;
    b. 接收服务端数据写入缓冲区;
    c. 若接收字符串长度小于等于0则终止循环;  
//注意,前面不用加static!
void client::RecvMsg(int conn){
    //接收缓冲区
    char buffer[1000];
    //不断接收数据
    while(1)
    {
        memset(buffer,0,sizeof(buffer));
        int len = recv(conn, buffer, sizeof(buffer),0);
        //recv返回值小于等于0,退出
        if(len<=0)
            break;
        cout<<"收到服务器发来的信息:"<<buffer<<endl;
    }
}

服务端在收到客户端退出消息后立即关闭客户端套接字描述符,故当服务端即将销毁调用析构函数时就无需关闭前面已经关闭的套接字描述符了,所以需要确定哪些套接字描述符还处于打开状态;

由于套接字描述符为int类型,故可以建立一个数组,它的每个元素的下标代表套接字描述符,其每个元素值代表该套接字描述符已关闭否,数组长度为系统允许打开的描述符个数(使用命令ulimit -n查看),该数组所有元素全初始化为false,表示尚未打开任何描述符。

由于服务端类的接收消息的函数会在客户端发送消息进程结束后关闭与该客户端的连接,关闭后应置此数组中对应元素为false,而服务器类该接收消息的函数为静态函数,且静态函数不能直接访问非静态成员,故若将此数组定义为非静态成员则可能要将其作为参数传给前述静态函数以进一步操作,但是考虑到机器中的套接字描述符的状态对于任何服务端对象都是一致的,所以可以直接将其定义为静态成员变量以在静态函数中直接访问。

#ifndef SERVER_H
#define SERVER_H

#include "global.h"

class server{
    private:
        int server_port;
        int server_sockfd;
        string server_ip;
        static vector<bool> sock_arr;//改为了静态成员变量,且类型变为vector<bool>
    public:
        server(int port,string ip);
        ~server();
        void run();
        static void RecvMsg(int conn);
};
#endif

服务端接收消息的函数:

1. 创建缓冲区;
2. 创建循环:
    a. 重置缓冲区;
    b. 根据客户端套接字描述符接收客户端发送的消息到缓冲区中;
    c. 判断客户端发送的消息是否为"exit"或长度是否小于等于零,若是则关闭服务器套接字描述符,置相应数组元素为false,终止循环;
    d. 向客户端发送收到提示;
    e. 判断是否发送成功,若发送不成功则关闭客户端套接字且置相应数组元素为false,终止循环。     
//注意,前面不用加static!
void server::RecvMsg(int conn){
    //接收缓冲区
    char buffer[1000];
    //不断接收数据
    while(1)
    {
        memset(buffer,0,sizeof(buffer));
        int len = recv(conn, buffer, sizeof(buffer),0);
        //客户端发送exit或者异常结束时,退出
        if(strcmp(buffer,"exit")==0 || len<=0){
            close(conn);
            sock_arr[conn]=false;
            break;
        }
        cout<<"收到套接字描述符为"<<conn<<"发来的信息:"<<buffer<<endl;
        //回复客户端
        string ans="收到";
        int ret = send(conn,ans.c_str(),ans.length(),0);
        //服务器收到exit或者异常关闭套接字描述符
        if(ret<=0){
            close(conn);
            sock_arr[conn]=false;
            break;
        }
    }
}

服务端的析构函数:

1. 遍历描述系统所有套接字描述符的数组,若某元素值为true则关闭其下标表示的套接字描述符;
2. 关闭服务端套接字描述符。  
server::~server(){
    for(int i=0;i<sock_arr.size();i++){
        if(sock_arr[i])
            close(i);
    }
    close(server_sockfd);
}

客户端向服务端发送消息和从服务端接收消息使用的套接字描述符都是自己的套接字描述符,而服务端向客户端发送消息和从客户端接收消息用的都是客户端的套接字描述符,即无论客户端还是服务端,无论是发送消息还是接收消息使用的都是客户端的套接字。

主函数:

#include"client.h"
int main(){
    client clnt(8023,"127.0.0.1");
    clnt.run();
}

下一篇:注册用户(c++, mysql)