当前位置:   article > 正文

C++网络编程踩坑记之多线程服务器,详解代码细节,多问为什么_c++编服务器主程序是四死循环实现吗

c++编服务器主程序是四死循环实现吗

前置知识:

多线程知识
Socket编程


多线程并发服务器思路

  1. lfd=socket(),创建监听套接字 lfd。
  2. setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt, sizeof(opt)),设置端口复用。
  3. bind(),绑定监听套接字lfd与Strcut scokaddr_in srv_addr(服务器IP和端口)。
  4. listen(),把lfd转换成一个被动套接字,进入被动监听状态,用来被动接受来自其他主动套接字的连接请求,并设置监听上限
  5. 主线程
while(1){   //死循环运行服务端
	cfd=accept()  //阻塞监听客户端的连接请求,接收客户端的连接
	pthread_create() //创建线程,子线程去执行与客户端的通信
	pthread_detach() //实现线程分离,回收子线程
	//close(cfd) 不可以关闭cfd,这关了子线程的也没了
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. 子线程
while(1){
	//close(lfd)  不可以关闭lfd,主线程还要用
	read()   //读客户端数据
	//处理数据
	write() //写回数据
	pthread_exit() //线程退出,可以指定返回值
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

10个问题

1 为什么设置线程分离?

主线程与子线程分离,子线程结束后,资源自动回收,防止僵尸线程。

2 为什么是pthread_detach(),而不是pthread_join()?

使用pthread_create创建的线程有两种状态:joinable和unjoinable。默认是joinable 状态。

  • 如果是可结合的joinable状态,则该线程结束后,不会释放资源,等到pthread_join()函数调用后才会释放资源。pthread_join()会阻塞主线程,直到要回收的子线程结束。
  • 如果是分离的unjoinable(detached)状态,则该进程结束后会自动释放占用资源。而这种状态就是通过设置线程分离获得的,主线程与子线程分离,子线程结束后,资源自动回收,防止僵尸线程。pthread_detach(),不会阻塞,调用它后,线程运行结束后会自动释放资源。

在服务器中,当主线程监听并连接到新的客户端,创建子线程通过cfd与客户端通信时,主线程并不希望因为调用pthread_join而阻塞,因为主线程还要继续去执行阻塞监听。你pthread_join把我阻塞在这里算怎么回事。

3 pthread_join和pthread_detach()的应用场景?

pthread_detach()和pthread_join()就是控制子线程回收资源的两种不同的方式。

pthread_join()函数是一个阻塞函数,调用方会阻塞到pthread_join所指定的tid的线程结束后才被回收 ,一般应用在主线程需要等待子线程结束后才继续执行的场景。(子线程合入主线程,主线程会一直阻塞,直到子线程执行结束,然后回收子线程资源,并继续执行。)

pthread_detach()函数不会阻塞,调用它后,使得主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收释放资源,非常方便。

4 设置线程分离的3种方式?

  1. 在创建时指定属性
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, childthread_work, (void *)cfd);
  • 1
  • 2
  • 3
  • 4
  1. 在子线程执行体的最开始处添加一行

也就是说,添加在do_work函数的第一行。

pthread_detach(pthread_self())
  • 1
  1. 直接主线程主动调用,在pthread_create()后添加一行
pthread_t tid;
ret= pthread_create(&tid,NULL,childthread_work, (void *)cfd);
if(ret==-1){
    sys_err("pthread_create error");
}
pthread_detach(tid); //非阻塞,可立即返回
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

5 设置线程分离的3种方式?

由于pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a,所以在使用pthread_create创建线程时,在编译中要加-lpthread。

比如:gcc multhreadserver.c wrap.c -o multhreadserver -lpthread

6 pthread_exit()和return的区别?

return 的含义是返回,它不仅可以用于线程执行的函数,普通函数也可以使用;pthread_exit() 函数的含义是线程退出,它专门用于结束某个线程的执行。
在子线程中,当执行结束, return 和 pthread_exit() 都可以给返回值到主线程,主线程中的 pthread_join() 函数都可以接收到线程的返回值。

  1. pthread_exit()用于线程退出,只退出当前子线程,可以指定返回值,以便其他线程通过pthread_join()函数获取该线程的返回值。注意:如果在主线程中使用 pthread_exit()退出,可以达到主进程退出子线程继续运行的目的。
  2. return返回到调用者那里去。注意,在主线程退出时效果与exit,_exit一样。因为主进程执行完return之后,实际上经过编译器代码优化,会调用exit()函数,该函数除了执行关闭IO等操作之外,还会执行关掉其他子线程的操作。
  3. exit()是进程退出,无论哪个子线程调用,会导致该线程所在进程的其他线程也挂掉,则整个程序都将结束。

7 要想让子线程总能完整执行(不会中途退出)的三种方法?

  1. 在主线程中调用pthread_join对其等待,即pthread_create/pthread_join/pthread_exit或return;
  2. 在主线程退出时使用pthread_exit,这样子线程能继续执行,即pthread_create/pthread_detach/pthread_exit;
  3. 主线程是死循环,那么就要pthread_create/pthread_detach。

8 多线程错误返回值分析

所有线程的错误号返回都只能使用strerror这个函数判断,不能使用perror,因为perror是调用进程的全局错误号,不适合单独线程的错误分析,所以只能使用strerror。

比如:fprintf(stderr, “xxx error: %s\n”, strerror(ret));

9 pthreat_create()参数传递?

  1. 传值。(void *)arg
while(1){
	cfd= Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);
	ret= pthread_create(&tid,NULL, childthread_work,(void *)cfd);
}
  • 1
  • 2
  • 3
  • 4
  1. 传动态指针,每次传的指针都是新malloc的。
for (int i=0; i<10; ++i)
    {   
        int *p = malloc(sizeof(*p));
        *p = i;
        if ((ret=pthread_create(&pid[i],NULL,thread,(void*)p)) != 0)
        {   
            fprintf(stderr,"pthread_create:%s\n",strerror(ret));
            exit(1);
        }   
    } 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  1. 注意不可以直接传变化值的指针,多个线程之间存在竞争,线程函数中对arg的使用,其实多个arg指针都指向了同一片内存,如果修改,其他线程里的arg值也会改变。上述两种方法不存在竞争的原因是,一个指针指向一个变量。

总之: 不能在线程创建过程中,改变传递的参数,避免该问题产生的方法是传递值或者使用动态申请内存的方法。

10 为什么主线程不可以关闭cfd,子线程不可以关闭lfd?

同一进程间的线程具有共享和独立的资源,
其中共享的资源有进程代码段、进程的公有数据(利用这些数据,线程很容易实现相互之间的通讯),进程的所拥有资源。详细说:
0、进程代码段
1、进程申请的堆内存
2、进程打开的文件描述符
3、进程的全局数据(可用于线程之间通信)
4、进程ID、进程组ID
5、进程目录
6、信号处理器。

而独占资源有:
1、线程ID
2、寄存器组的值
3、线程堆栈
4、错误返回码
5、信号屏蔽码
6、线程的优先级

因为多线程共享进程打开的文件描述符,与多进程对比,主线程是不可以关闭cfd的,因为子线程并没有把fd表复制过来。如果关闭了cfd,就相当于释放掉了套接字,那么后续子线程就不能进行读写了。同理,lfd也是这样。


代码实现:

提示:多返回值传出的方式,来自ChernoCppTutorial的笔记:1、传引用或者指针,即函数设置多个传出参数。2、直接返回一个数组。当然这不通用,因为必须要同一种类型。当然还能写为vector,不过array会在栈上创建,而vector会把它的底层存储在堆上,所以从技术上来讲返回std::array会更快。3、tuple或pair。4、定义一个结构体,然后返回。

//server.c,需要和wrap.c一起gcc
// Created on 2022/5/22.
//
#include <string.h>
#include <strings.h>
#include<netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include "wrap.h"
#define IP "127.44.44.44"
#define PORT 6266
struct s_info { //定义一个结构体, 将地址结构跟 cfd 捆绑
    struct sockaddr_in cliaddr;
    int connfd;
};

void *do_work(void *s_in) {
    struct s_info *client_info = (struct s_info *) s_in;
    int cfd = (*client_info).connfd;
    int ret;
    char buf[BUFSIZ], clie_ip[BUFSIZ];
    while (1) {
        ret = read(cfd, buf, sizeof(buf));
        if (ret == 0) {
            close(cfd);
            break;
        } else if (ret == -1) {
            sys_err("read error");
        } else {
            printf("received from %s at PORT %d\n",
                   inet_ntop(AF_INET, &(*client_info).cliaddr.sin_addr.s_addr, clie_ip, sizeof(clie_ip)),
                   ntohs((*client_info).cliaddr.sin_port));
            write(STDOUT_FILENO, buf, ret);
            for (int i = 0; i < ret; i++) {
                buf[i] = toupper(buf[i]);
            }
            write(cfd, buf, ret);
        }
    }
    close(cfd);
    return (void*)0;
}

int main(int argc,char *argv[]){
    int lfd,cfd;
    int i=0;
    int ret;
    char buf[BUFSIZ];
    pthread_t tid;
    struct sockaddr_in srv_addr,clt_addr;
    socklen_t clt_addr_len;
    struct s_info s_info_array[256];
    //memset(&saddr,0,sizeof(saddr));
    bzero(&srv_addr,sizeof(srv_addr));
    srv_addr.sin_family=AF_INET;
    srv_addr.sin_port= htons(PORT);
    srv_addr.sin_addr.s_addr= htonl(INADDR_ANY);
    lfd= Socket(AF_INET,SOCK_STREAM,0);

    Bind(lfd,(struct sockaddr *)&srv_addr, sizeof(srv_addr));
    Listen(lfd,128);
    while(1){
        clt_addr_len = sizeof(clt_addr_len);
        cfd= Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);

        s_info_array[i].cliaddr = clt_addr;
        s_info_array[i].connfd = cfd;


        ret= pthread_create(&tid,NULL,do_work,(void *)&s_info_array[i]);
        if(ret==-1){
            sys_err("pthread_create error");
        }
        pthread_detach(tid);
        i++;

    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/197658
推荐阅读
相关标签
  

闽ICP备14008679号