TCP并发模型

对于之前的TCP服务器而言,一个服务器只能处理一个客户端你的操作,效率低,当客户端推出后,服务器也就结束了。

为了实现一个服务器可以应对多个客户端,我们先引入循环模型

一、循环TCP服务器

#include<myhead.h>

#define SER_PORT 8888           //服务器端口号
#define CLI_IP "192.168.250.100"  //服务器IP


int main(int argc, const char *argv[])
{
    //1、创建一个套接字
    int sfd = -1;
    sfd = socket(AF_INET, SOCK_STREAM, 0);    
    //参数1:表示创建的是网络通信的套接字
    //参数2:表示使用的是TCP通信协议
    //参数3:参数2指定了协议,参数3填0即可
    if(sfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("%d success sfd = %d\n", __LINE__, sfd);   //3

    //2、绑定IP地址和端口号
    //2.1填充地址信息结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;      //地址族
    sin.sin_port = htons(SER_PORT);       //端口号
    sin.sin_addr.s_addr = inet_addr(SER_IP);      //IP地址

    //2.2 绑定
    if(bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) ==-1)
    {
        perror("bind error");
        return -1;
    }
    printf("%d  bind success\n", __LINE__);

    //3、将套接字设置成被动监听状态
    if(listen(sfd, 128) == -1)
    {
        perror("listen error");
        return -1;
    }
    printf("%d  listen success\n", __LINE__);

    //4、阻塞等待客户端的链接请求
    int newfd = -1;
    //定义结构体变量接收对方地址信息结构体
    struct sockaddr_in cin;          //用于接收客户端地址信息结构体
    socklen_t addrlen = sizeof(cin);      //用于接收客户端结构体的大小

    while(1)
    {
        if((newfd = accept(sfd, (struct sockaddr*)&cin, &addrlen)) == -1)
        {
            perror("accept error");
            return -1;
        }
        printf("[%s  %d]:发来连接请求\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));

        //5、收发数据
        char rbuf[128] = "";       //用于接收客户发发来的数据
        while(1)
        {
            //将容器清空
            bzero(rbuf, sizeof(rbuf));     //memset(rbuf, 0, sizeof(rbuf));

            //从套接字中读取数据‘
            int res = recv(newfd, rbuf, sizeof(rbuf)-1, 0);   
            if(res == 0)
            {
                printf("客户端已经下线\n");
                break;
            }
            printf("[%s  %d]: %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);


            //加个笑脸再回回去
            strcat(rbuf,"*_*");

            send(newfd, rbuf, strlen(rbuf), 0);
            printf("发送成功\n");

        }
        //关闭跟客户端通信的套接字
        close(newfd);
    }

    //6、关闭服务器
    close(sfd);


    return 0;
}

1.由于循环服务器每次只能处理一个客户端,下一个客户端的处理,必须建立在上一个客户端结束后

2.原因:接收函数是阻塞的,跟客户端交互时的读写函数也是阻塞的,想要多个阻塞任务并发执行,需要引入相关的机制完成

3.解决办法:多进程或多线程、IO多路复用

二、多进程实现TCP并发服务器

       让父进程专门用于接收客户端的连接请求,每连接一个客户端,就创建一个子进程跟其进行通信,子进程负责通信

#include <head.h>
#define SER_PORT 6666           // 服务器端口号
#define SER_IP "192.168.118.60" // 服务器IP

// 定义信号处理函数
void handler(int signo)
{
    if (signo == SIGCHLD)
    {
        // 以非阻塞的形式回收僵尸进程
        while (waitpid(-1, NULL, WNOHANG) > 0)
            ;
    }
}

int main(int argc, const char *argv[])
{
    // 将子进程退出时发射的SIGCHLD信号绑定到信号处理函数中
    if (signal(SIGCHLD, handler) == SIG_ERR)
    {
        perror("signal");
        return -1;
    }

    // 1、创建一个套接字
    int sfd = -1;
    sfd = socket(AF_INET, SOCK_STREAM, 0);
    // 参数1:表示创建的是网络通信的套接字
    // 参数2:表示使用的是TCP通信协议
    // 参数3:参数2指定了协议,参数3填0即可
    if (sfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("%d success sfd = %d\n", __LINE__, sfd); // 3
    // 设置端口号快速重用
    int reuse = 1;
    if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
    {
        perror("setsockopt error");
        return -1;
    }
    printf("端口号快速重用成功\n");

    // 2、绑定IP地址和端口号
    // 2.1填充地址信息结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;                // 地址族
    sin.sin_port = htons(SER_PORT);          // 端口号
    sin.sin_addr.s_addr = inet_addr(SER_IP); // IP地址
    // 2.2 绑定
    if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("%d  bind success\n", __LINE__);

    // 3、将套接字设置成被动监听状态
    if (listen(sfd, 128) == -1)
    {
        perror("listen error");
        return -1;
    }
    printf("%d  listen success\n", __LINE__);
    // 4、阻塞等待客户端的链接请求
    int newfd = -1;
    // 定义结构体变量接收对方地址信息结构体
    struct sockaddr_in cin;          // 用于接收客户端地址信息结构体
    socklen_t addrlen = sizeof(cin); // 用于接收客户端结构体的大小

    // 定义进程号变量
    pid_t pid = -1;

    while (1)
    {
        // 父进程执行连续操作
        if ((newfd = accept(sfd, (struct sockaddr *)&cin, &addrlen)) == -1)
        {
            perror("accept error");
            return -1;
        }
        printf("[%s  %d]:发来连接请求 newfd=%d\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd);

        // 创建子进程
        pid = fork();
        if (pid > 0)
        {
            // 关闭newfd
            close(newfd);
        }
        else if (pid == 0)
        {
            // 关闭sfd
            close(sfd);

            // 5、收发数据
            char rbuf[128] = ""; // 用于接收客户发发来的数据
            while (1)
            {
                // 将容器清空
                bzero(rbuf, sizeof(rbuf)); // memset(rbuf, 0, sizeof(rbuf));

                // 从套接字中读取数据‘
                int res = recv(newfd, rbuf, sizeof(rbuf) - 1, 0);
                if (res == 0)
                {
                    printf("客户端已经下线\n");
                    break;
                }
                printf("[%s  %d]: %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);
                // 加个笑脸再回回去
                strcat(rbuf, "*_*");

                send(newfd, rbuf, strlen(rbuf), 0);
                printf("发送成功\n");
            }

            // 关闭跟客户端通信的套接字
            close(newfd);

            // 退出子进程
            exit(EXIT_SUCCESS);
        }
    }
    // 关闭服务器
    close(sfd);

    return 0;
}

三、多线程实现并发服务器

1.原理:主线程用于接收客户端的连接请求,分支线程用于跟客户端进行交换

#include <head.h>
#define SER_PORT 6666            // 服务器端口号
#define SER_IP "192.168.250.100" // 服务器IP

// 定义向线程提提供参数的结构体类型
struct BufInfo
{
    int newfd;              // 用于通信的客户端套接字
    struct sockaddr_in cin; // 客户端地址信息结构体
};
// 定义线程体函数
void *deal_cli_msg(void *arg)
{
    // 接收传过来的信息
    int newfd = ((struct BufInfo *)arg)->newfd;
    struct sockaddr_in cin = ((struct BufInfo *)arg)->cin;

    // 5、收发数据
    char rbuf[128] = ""; // 用于接收客户发发来的数据
    while (1)
    {
        // 将容器清空
        bzero(rbuf, sizeof(rbuf)); // memset(rbuf, 0, sizeof(rbuf));

        // 从套接字中读取数据‘
        int res = recv(newfd, rbuf, sizeof(rbuf) - 1, 0);
        if (res == 0)
        {
            printf("客户端已经下线\n");
            break;
        }
        printf("[%s  %d]: %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);
        // 加个笑脸再回回去
        strcat(rbuf, "*_*");

        send(newfd, rbuf, strlen(rbuf), 0);
        printf("发送成功\n");
    }

    // 6、关闭服务器
    close(newfd);

    // 退出线程
    pthread_exit(NULL);
}

int main(int argc, const char *argv[])
{
    // 1、创建一个套接字
    int sfd = -1;
    sfd = socket(AF_INET, SOCK_STREAM, 0);
    // 参数1:表示创建的是网络通信的套接字
    // 参数2:表示使用的是TCP通信协议
    // 参数3:参数2指定了协议,参数3填0即可
    if (sfd == -1)
    {
        perror("socket error");
        return -1;
    }
    printf("%d success sfd = %d\n", __LINE__, sfd); // 3
    // 设置端口号快速重用
    int reuse = 1;
    if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
    {
        perror("setsockopt error");
        return -1;
    }
    printf("端口号快速重用成功\n");

    // 2、绑定IP地址和端口号
    // 2.1填充地址信息结构体
    struct sockaddr_in sin;
    sin.sin_family = AF_INET;                // 地址族
    sin.sin_port = htons(SER_PORT);          // 端口号
    sin.sin_addr.s_addr = inet_addr(SER_IP); // IP地址
    // 2.2 绑定
    if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
    {
        perror("bind error");
        return -1;
    }
    printf("%d  bind success\n", __LINE__);

    // 3、将套接字设置成被动监听状态
    if (listen(sfd, 128) == -1)
    {
        perror("listen error");
        return -1;
    }
    printf("%d  listen success\n", __LINE__);
    // 4、阻塞等待客户端的链接请求
    int newfd = -1;
    // 定义结构体变量接收对方地址信息结构体
    struct sockaddr_in cin;          // 用于接收客户端地址信息结构体
    socklen_t addrlen = sizeof(cin); // 用于接收客户端结构体的大小

    // 定义线程号变量用于接收线程
    pthread_t tid = -1;
    while (1)
    {

        // 当执行到accept函数时,该函数会预选一个当前最小的文件描述符
        // 在预选后,到执行该函数期间,如果有更小的文件描述符被释放,也不会更新
        // 直到下一次再运行到该函数时,会预选新的最小未使用的文件描述符
        if ((newfd = accept(sfd, (struct sockaddr *)&cin, &addrlen)) == -1)
        {
            perror("accept error");
            return -1;
        }
        printf("[%s  %d]:发来连接请求\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));

        // 定义要传递数据的结构体变量
        struct BufInfo buf;
        buf.newfd = newfd;
        buf.cin = cin;

        // 创建分支线程用于处理客户端
        if (pthread_create(&tid, NULL, deal_cli_msg, &buf) != 0)
        {
            fprintf(stderr, "pthread_create error\n");
            return -1;
        }

        // 回收分支分支线程的资源
        pthread_detach(tid);
    }
    close(sfd);

    return 0;
}