网络-----多进程及多线程版本的TCP网络程序

之前我们已经利用socket编程实现了一个单进程的TCP网络程序(https://blog.csdn.net/qq_34021920/article/details/80153071
我们再开启一个终端去连接服务器端,可以发现的是第二个客户端不能和服务器正常通信了,除非我们第一个客户端退出之后,第二个客户端才能和服务器正常通信。可以注意到大部分的socket接口都是阻塞型的。实际上除非特别指定,几乎所有的IO接口(包括socket接口)都是阻塞型的
就像我们之前实现的代码,在accept接受了一个请求之后就会一直在while循环里面尝试去read,而没有继续去调用accept,导致不能接受到新的请求。
可以肯定的是这样的设计是不合理的,所以我们将之前的代码进行改进。


多进程版本

我们可以在一个进程接到来自客户端新的请求时就fork出一个子进程让子进程来处理,父进程只需负责监控请求的到来。这样就能做到并发处理。

直接上代码:(在这里我们只需要修改服务器端的代码即可)

tcp_server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

//多进程版本
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        printf("Usage %s ip port\n",argv[0]);
        return 1;
    }
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        return 2;
    }

    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(atoi(argv[2]));
    local.sin_addr.s_addr=inet_addr(argv[1]);

    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
    {
        perror("bind");
        return 3;
    }

    if(listen(sock,5)<0)
    {
        perror("listen");
        return 4;
    }
    while(1)
    {
        struct sockaddr_in peer;
        char buf[1024];
        buf[0]=0;
        socklen_t len=sizeof(peer);
        int newsock=accept(sock,(struct sockaddr *)&peer,&len);
        if(newsock<0)
        {
            perror("accept");
            return 5;
        }
        inet_ntop(AF_INET,&peer.sin_addr,buf,sizeof(buf));
        printf("get a connect,ip:%s,port:%d\n",buf,ntohs(peer.sin_port));

        pid_t id=fork();//创建子进程
        if(id<0)
        {
            perror("fork");
            return -1;
        }
        else if(id==0)
        {//child
            close(sock);//子进程不需要sock所以关闭
            if(fork()==0)//在子进程中再进行fork创建孙子进程
            { //grand_child
                while(1)
                {
                    ssize_t s=read(newsock,buf,sizeof(buf));
                    if(s>0)
                    {
                        buf[s]=0;
                        printf("[%s:%d] %s",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port),buf);
                    }
                    else
                    {
                        printf("client quit\n");
                        close(newsock);
                        break;
                    }
                    write(newsock,buf,strlen(buf));
                }
            }
            exit(0);//子进程直接退出
        }
        else
        {//father
            close(newsock);
            waitpid(id,NULL,0);
        }
    }
    close(sock);
    return 0;
}

解释一下为什么要fork两次。
我们都知道,fork之后,父进程是需要等待回收子进程的,不然就会造成僵尸进程的现象。父进程一直在接受连接请求,而子进程就应该为连接上的客户提供服务。而我们在子进程中再fork一次创建出孙子进程,让孙子进程去提供服务,子进程就直接退出,父进程就可以不用等待回收子进程而继续接受连接请求。
此时我们的孙子进程就变成了孤儿进程,它的回收管理就交给1号进程了。

tcp_client.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        printf("Usage %s ip port\n",argv[0]);
        return 1;
    }
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        return 2;
    }

    struct sockaddr_in server;
    server.sin_family=AF_INET;
    server.sin_port=htons(atoi(argv[2]));
    server.sin_addr.s_addr=inet_addr(argv[1]);

    if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0)
    {
        perror("connect");
        return 3;
    }
    while(1)
    {
        char buf[1024];
        buf[0]=0;
        printf("Please Enter:");
        fflush(stdout);
        ssize_t s=read(0,buf,sizeof(buf));
        if(s>0)
        {
            buf[s]=0;
        }
        write(sock,buf,sizeof(buf));
        if(strncmp(buf,"quit",4)==0)
        {
            break;
        }
        read(sock,buf,sizeof(buf));
        printf("server echo:%s",buf);
    }
    close(sock);
    return 0;
}

测试结果:

客户端1:
这里写图片描述

客户端2:
这里写图片描述

服务器端:
这里写图片描述

可以看到,我们蓝色框中的英文消息是客户端1发来的,黄色框中的中文消息,是客户端2发来的。实现了多连接。


多线程版本

还是只改变服务器端的代码即可

tcp_server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

//多线程版本   
//为了方便传参将信息封装为结构体
typedef struct
{
    int fd;
    char ip[32];
    int port;
}Arg;

void server(int fd,char* ip,int port)
{
    char buf[1024]={0};
    while(1)
    {
        ssize_t s=read(fd,buf,sizeof(buf));
        if(s>0)
        {
            buf[s]=0;
            printf("[%s:%d] %s",ip,port,buf);
        }
        else
        {
            printf("client quit!\n");
            close(fd);
            break;
        }
        write(fd,buf,strlen(buf));
    }
}
//线程执行函数
void* thread_server(void* arg)
{
    char buf[1024]={0};
    Arg* p=(Arg*)arg;
    server(p->fd,p->ip,p->port);//调用处理请求函数
    free(p);
    return NULL;
}

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        printf("Usage:%s ip port",argv[0]);
        return 1;
    }
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        return 2;
    }

    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(atoi(argv[2]));
    local.sin_addr.s_addr=inet_addr(argv[1]);

    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
    {
        perror("bind");
        return 3;
    }

    if(listen(sock,5)<0)
    {
        perror("listen");
        return 4;
    }
    while(1)
    {
        char buf[1024];
        buf[0]=0;
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        int newsock=accept(sock,(struct sockaddr *)&peer,&len);
        if(newsock<0)
        {
            perror("accept");
            return 5;
        }
        inet_ntop(AF_INET,&peer.sin_addr,buf,sizeof(buf));
        printf("get a connect,ip:%s,port:%d\n",buf,ntohs(peer.sin_port));

        pthread_t tid=0;
        Arg* arg=(Arg*)malloc(sizeof(Arg));
        arg->fd=newsock;
        strcpy(arg->ip,buf);
        arg->port=ntohs(peer.sin_port);
        pthread_create(&tid,NULL,thread_server,(void*)arg);
        pthread_detach(tid);//分离该线程
    }
    close(sock);
    return 0;
}

同样的,创建线程,让线程去服务连接的客户端,而主线程就一直接受连接请求,但是主线程同样应该等待其它线程,所以我们利用pthread_detach函数分离改线程,主线程就可以不用等待别的线程而一直接受连接了。

来看结果:

客户端1
这里写图片描述
客户端2
这里写图片描述
服务器端
这里写图片描述


来比较一下多进程版本和多线程版本

多进程优点

  • 可以处理多个用户
  • 易于编写
  • 稳定,因为进程具有独立性

多进程缺点

  • 连接来了之后才创建进程,性能太低
  • 多进程服务器特别吃资源,而且同时服务的客户有上限,上限也很容易达到
  • 进程越多,CPU在调度时选择一个进程的周期会变长,客户等待的时间就变长。也就是切换成本大,影响性能

多线程
多线程版本的程序同样也有多进程版本的几个缺点,但是相对于进程来说,创建线程的代价要小很多,而且调度线程比调度进程的粒度要小,这样就可以降低成本,提高性能。
但是线程还有一个缺点就是线程不稳定,一个线程的退出会导致主线程直接退出。

总结一下就是,进程的开销要远远大于线程,所以,如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的CPU资源,比如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。

不论是多进程还是多线程都是来了连接请求之后才创建进程或线程,这个问题我们可以用进程池和线程池来解决。

猜你喜欢

转载自blog.csdn.net/qq_34021920/article/details/80213349
今日推荐