errorbeep@home:~$

服务端(多线程, 面向对象)


接上篇:客户端和服务端TCP通信(C++),运用面向对象思想和多线程编程重写服务端。将服务端抽象为一个c++类,并且实现服务端的并发性,即服务端可以为多个客户端服务。

1. 定义和初始化一个服务端对象,指定服务端IP和端口号即可;
2. 服务器对象启动,服务器对象运行过程中每收到一个与该服务端对象建立连接请求都创建一个线程专门服务该客户端对象即接收来自该客户端的消息;  

服务端应具有的属性:服务端的ip, 端口号,服务端的套接字描述符,与服务端成功建立连接的客户端的套接字描述符;服务端还应该有一个用于接收客户端消息的函数;另外还应该定义一个用于启动服务端对象,开始监听的成员函数;服务端类的构造函数应该完成服务端属性的初始化;析构函数需要关闭服务端的套接字描述符和所有与该服务端对象连接的客户端对象的套接字描述符。

服务端ip可以用string存储,端口号和套接字描述符用int存储,客户端的套接字描述符存储在vector;启动服务端对象的函数用到了对象相关数据,故应定义为普通的成员函数,

启动服务端对象的步骤为:

1. 创建套接字描述符以初始化服务端对象中的套接字描述符;
2. 创建网络地址并用该对象属性中的IP和端口号初始化;
3. 绑定该对象的套接字描述符和刚创建的网络地址;
4. 将该对象的套接字描述符设置为监听状态;
5. 创建循环:
    a. 获取申请建立连接的客户端套接字描述符;
    b. 若成功获取套接字描述符则将其加入该服务端对象的成功连接客户端列表中,否则退出;
    c. 创建线程用以接收刚刚连接成功的客户端的消息;
    d. 将接收消息的线程与主线程分离,进入下一轮循环。

用于接收消息的函数对与所有服务端对象拥有相同的操作逻辑且没有与具体对象相关的数据,因此可以将其定义为静态函数,接收一个参数即欲接收其消息的客户端的套接字描述符:

1. 先创建一个缓冲区用于暂存接收的数据,
2. 要不停接收消息故创建循环:
    a. 重置缓冲区;
    b. 从客户端套接字读取数据,写入缓冲区;
    c. 根据读取字符串判断是否终止连接;
    d. 打印接收的消息;   

创建线程使用库函数thread时,初始化thread对象后,应调用函数thread.detach()而不应该调用join()。每一个线程服务一个客户端,服务端与该客户端的连接结束即可销毁此线程,主线程还要处理与其它客户端的连接,所以不应该在此阻塞。

Code

类的定义:

#ifndef SERVER_H
#define SERVER_H

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
class server{
    private:
        int server_port;//服务器端口号
        int server_sockfd;//设为listen状态的套接字描述符
        string server_ip;//服务器ip
        vector<int> sock_arr;//保存所有套接字描述符
    public:
        server(int port,string ip);//构造函数
        ~server();//析构函数
        void run();//服务器开始服务
        static void RecvMsg(int conn);//子线程工作的静态函数
};

#endif

具体实现:

#include "server.h"

//构造函数
server::server(int port,string ip):server_port(port),server_ip(ip){}

//析构函数
server::~server(){
    for(auto conn:sock_arr)
        close(conn);
    close(server_sockfd);
}

//服务器开始服务
void server::run(){
    //定义sockfd
    server_sockfd = socket(AF_INET,SOCK_STREAM, 0);

    //定义sockaddr_in
    struct sockaddr_in server_sockaddr;
    server_sockaddr.sin_family = AF_INET;//TCP/IP协议族
    server_sockaddr.sin_port = htons(server_port);//server_port;//端口号
    server_sockaddr.sin_addr.s_addr = inet_addr(server_ip.c_str());//ip地址,127.0.0.1是环回地址,相当于本机ip

    //bind,成功返回0,出错返回-1
    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1)
    {
        perror("bind");//输出错误原因
        exit(1);//结束程序
    }

    //listen,成功返回0,出错返回-1
    if(listen(server_sockfd,20) == -1)
    {
        perror("listen");//输出错误原因
        exit(1);//结束程序
    }

    //客户端套接字
    struct sockaddr_in client_addr;
    socklen_t length = sizeof(client_addr);

    //不断取出新连接并创建子线程为其服务
    while(1){
        int conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length);
        if(conn<0)
        {
            perror("connect");//输出错误原因
            exit(1);//结束程序
        }
        cout<<"文件描述符为"<<conn<<"的客户端成功连接\n";
        sock_arr.push_back(conn);
        //创建线程
        thread t(server::RecvMsg,conn);
        t.detach();//置为分离状态,不能用join,join会导致主线程阻塞
    }
}

//子线程工作的静态函数
//注意,前面不用加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)
            break;
        cout<<"收到套接字描述符为"<<conn<<"发来的信息:"<<buffer<<endl;
    }
}

主函数:

#include"server.h"
int main(){
    server serv(8023,"127.0.0.1");//创建实例,传入端口号和ip作为构造函数参数
    serv.run();//启动服务
}

下一篇:客户端(多线程,面向对象)