多进程和多线程并发服务器编程模型

文章目录

  • 一、多进程并发服务器
    • 1、多进程并发服务器的概念
    • 2、多进程并发服务器的流程
      • (1)面试题--->为什么要多次关闭文件描述符?
      • (2)面试题--->shutdown和close的区别?
    • 3、TCP多进程并发服务器的demo
  • 二、多线程并发服务器
    • 1、多线程并发服务器的概念
    • 2、多线程并发服务器的流程
    • 3、TCP多线程并发服务器的demo

一、多进程并发服务器

  在网络程序里面,一般来说都是许多客户对应一个服务器(多对一),为了处理客户的请求,对服务端的程序就提出了特殊的要求。目前最常用的服务器模型有:

  • 迭代服务器:服务器在同一时刻只能响应一个客户端的请求;

  • 并发服务器:服务器在同一时刻可以响应多个客户端的请求。

1、多进程并发服务器的概念

  在 Linux 环境下多进程的应用很多,其中最主要的就是网络/客户服务器多进程服务器是当客户有请求时,服务器用一个子进程来处理客户请求。父进程继续等待其它客户的请求。这种方法的优点是当客户有请求时,服务器能及时处理客户,特别是在客户服务器交互系统中。对于一个 TCP 服务器,客户与服务器的连接可能并不马上关闭,可能会等到客户提交某些数据后再关闭,这段时间服务器端的进程会阻塞,所以这时操作系统可能调度其它客户服务进程,这比起迭代服务器大大提高了服务性能。

2、多进程并发服务器的流程

#include <头文件>

// 信号处理函数,回收子进程
void sig_chld(int signo) { }

int main(int argc, char const *argv[])
{
	lfd = socket();
	bind();
	listen();
	
	signal(); // 发送SIGCHLD信号

	while(1)
	{
		int cfd = accept();
		if (fork() == 0)
		{
			close(lfd); // 子进程关闭父进程的套接字
			/*事务处理*/
			close(cfd); // 子进程关闭自己的套接字
			exit(0); // 子进程退出
		}
		// 父进程关闭子进程的套接字(子进程文件描述符引用计数变为0,就关闭客户端)
		close(cfd); 
	}	
	close(lfd);
	return 0;
}

(1)面试题—>为什么要多次关闭文件描述符?

  每一个socket描述符都有对应的引用计数,该计数存在文件表中。上面程序中打开了lfd和cfd,引用计数分别为1和1,在fork()以后,子进程复制了父进程的socket描述符,所以引用计数也会增加,即lfd和cfd的引用计数都变成了2。此时在子进程中关闭lfd(close(lfd)),在父进程中关闭close(cfd)。这个就保证了子进程处理与客户的连接,父进程负责在监听套接字lfd再次调用accept来接收客户的下一个连接。
多进程和多线程并发服务器编程模型_第1张图片
多进程和多线程并发服务器编程模型_第2张图片
多进程和多线程并发服务器编程模型_第3张图片
  从上面分析和图示可以看出socket描述符是有引用计数的,只有当引用计数为0的时候,close才会发送FIN报文,这就解释了子进程仍然可以用cfd进行数据处理了,子进程处理完后再次调用close,此时引用计数从1变为0,最后发送FIN报文结束cfd连接。而对于lfd,只有在子进程中进行了close,父进程中一直保留着引用计数为1,所以父进程通过for循环可以持续accept新连接

(2)面试题—>shutdown和close的区别?

  通常调用close函数对socket进行关闭,为啥还要选用shutdown来关闭socket,原因有如下两个:

  • 在介绍close的时候,已经说明了,close只有在对应socket的引用计数为0时,才会真正发送FIN报文来关闭这个连接,shutdown没有这个限制,直接发送FIN报文

  • close同时终止了读和写两个方向的数据传输。但是TCP的双工的,我们有时候需要只接受数据,而不发送数据,shutdown可以指定关闭读端或者写端

int shutdown(int fd, int how);
/*
how为SHUT_RD(关闭读端),则无法从套接字读取数据;-------不发送FIN
how为SHUT_WR(关闭写端),则无法从套接字写数据;---------发送FIN
how为SHUT_RDWR(关闭读写),则无法从套接字读和写数据;-------发送FIN
*/

3、TCP多进程并发服务器的demo

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

// 信号处理函数
void sig_chld(int signo)
{
    pid_t pid;
    int stat; // 回收状态
    // 以非阻塞形式回收子进程
    while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
        printf("child %d terminated\n", pid);
    return;
}

int main(int argc, char **argv)
{
    int lfd, cfd;
    pid_t pid;
    socklen_t clt_addr_len;
    struct sockaddr_in srv_addr, clt_addr;
    // 将地址结构清零(按字节),容易出错(后面两个参数容易颠倒)
    // memset(&srv_addr, 0, sizeof(srv_addr));
    // bzero也可以用来清零操作 
    bzero(&srv_addr, 0);
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(8000);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    // 创建套接字
    lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 端口复用
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));

    // 绑定套接字
    bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));

    // 监听客户端的连接
    listen(lfd, 128);
    
    clt_addr_len = sizeof(clt_addr);
    
    // 子进程无论是正常退出还是异常退出都会给父进程发送SIGCHLD信号
    signal(SIGCHLD, sig_chld);

    char buf[512];
    while (1)
    {
        // 阻塞接收客户端的连接
        cfd = accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
                
        // 使用fork创建子进程
        if ( (pid = fork()) < 0)
            sys_err("fork");
        
        // 子进程
        else if (pid == 0)
        {
        // 关闭不需要的套接字可节省系统资源,
        // 同时可避免父子进程共享这些套接字,
        // 可能带来的不可预计的后果。
            close(lfd); // 关闭服务器套接字描述符,这个套接字是从父进程继承过来的
            while (1)
            {
                // 清空数组
                memset(buf, 0, 512); 
                // 接收数据
                recv(cfd, buf, sizeof(buf), 0);
                int ret = strlen(buf);
                if (ret == 0)
                {
                    // 没有接收到数据说明客户端关闭了连接
                    close(cfd);
                    exit(1);
                }
                for (int i = 0; i < ret; ++i)
                    buf[i] = toupper(buf[i]);
                // 回射给客户端
                send(cfd, buf, ret, 0);
                // 服务器把数据写到标准输出
                write(STDOUT_FILENO, buf, ret);
                memset(buf, 0, 512);
                close(cfd); // 子进程关闭自己的套接字
                exit(0); // 子进程退出
            }
        }
        // 父进程
        else
        {
            // 关闭客户端套接字描述符
            close(cfd);
            continue;
        }
    }
    close(lfd); // 最后关闭服务器套接字描述符
    return 0;
}

二、多线程并发服务器

1、多线程并发服务器的概念

  多线程服务器是对多进程的服务器的改进,由于多进程服务器在创建进程时要消耗较大的系统资源,所以用线程来取代进程,这样服务处理程序可以较快的创建。据统计,创建线程与创建进程要快 10100 倍,所以又把线程称为“轻量级”进程。线程与进程不同的是:一个进程内的所有线程共享相同的全局内存、全局变量等信息,这种机制又带来了同步问题

2、多线程并发服务器的流程

#include <头文件>

void *client_process(void *arg)
{
	int cfd = *(int *)arg; // 传过来的已连接套接字
	/*事务处理*/
	close(cfd);
}
int main(int argc, char const *argv[])
{
	lfd = socket();
	bind();
	listen();

	while(1)
	{
        cfd = accept();
        pthread_t thread_id;
        if (cfd > 0)
        {
            //给回调函数传的参数,&connfd,地址传递
            pthread_create(&thread_id, NULL, client_process, (void *)&cfd);  //创建线程
            pthread_detach(thread_id); // 线程分离,结束时自动回收资源
        }
	}	
	close(lfd);
	return 0;
}
pthread_create(&thread_id, NULL, client_process, (void *)&cfd)

  在这里我们使用的是按地址传递,所以会有这么一个问题,假如有多个客户端要连接这个服务器,正常的情况下,一个客户端连接对应一个 cfd,相互之间独立不受影响,但是,假如多个客户端同时连接这个服务器,A 客户端的连接套接字为 cfd,服务器正在用这个 cfd 处理数据,还没有处理完,突然来了一个 B 客户端,accept()之后又生成一个 cfd, 因为是地址传递, A 客户端的连接套接字也变成 B 这个了,这样的话,服务器肯定不能再为 A 客户端服务器了,这时候,我们就需要考虑多任务的互斥或同步问题了,这里通过互斥锁来解决这个问题,确保这个cfd值被一个临时变量保存过后,才允许修改

3、TCP多线程并发服务器的demo

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

pthread_mutex_t mutex;

// 信号处理函数
void *client_process(void *arg)
{
    char buf[1024] = "";	// 接收缓冲区
	int cfd = *(int *)arg; // 传过来的已连接套接字

	// 解锁,pthread_mutex_lock()唤醒,不阻塞
	pthread_mutex_unlock(&mutex);
    
    // 接收数据
    recv(cfd, buf, sizeof(buf), 0);
    int ret = strlen(buf);
    if (ret == 0)
    {
        // 没有接收到数据说明客户端关闭了连接
        close(cfd);
        exit(1);
    }
    for (int i = 0; i < ret; ++i)
        buf[i] = toupper(buf[i]);
    // 回射给客户端
    send(cfd, buf, ret, 0);
    // 服务器把数据写到标准输出
    write(STDOUT_FILENO, buf, ret);
	close(cfd); // 子进程关闭自己的套接字
    return NULL;
}

int main(int argc, char **argv)
{
    int lfd, cfd;
    socklen_t clt_addr_len;
    struct sockaddr_in srv_addr, clt_addr;

    pthread_mutex_init(&mutex, NULL); // 初始化互斥锁,互斥锁默认是打开的

    // 将地址结构清零(按字节),容易出错(后面两个参数容易颠倒)
    // memset(&srv_addr, 0, sizeof(srv_addr));
    // bzero也可以用来清零操作 
    bzero(&srv_addr, 0);
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(8000);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    // 创建套接字
    lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 端口复用
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));

    // 绑定套接字
    bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));

    // 监听客户端的连接
    listen(lfd, 128);
    
    clt_addr_len = sizeof(clt_addr);
    
    pthread_t thread_id;
    
    while (1)
    {
        // 阻塞接收客户端的连接
        cfd = accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
        if (cfd > 0)
        {
            //给回调函数传的参数,&connfd,地址传递
            pthread_create(&thread_id, NULL, client_process, (void *)&cfd);  //创建线程
            pthread_detach(thread_id); // 线程分离,结束时自动回收资源
        }
    }
    close(lfd);
    return 0;
}

参考:https://blog.csdn.net/tennysonsky/article/details/45671215

你可能感兴趣的:(linux网络编程)