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. 子线程
while(1){
	//close(lfd)  不可以关闭lfd,主线程还要用
	read()   //读客户端数据
	//处理数据
	write() //写回数据
	pthread_exit() //线程退出,可以指定返回值
}

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. 在子线程执行体的最开始处添加一行

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

pthread_detach(pthread_self())
  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); //非阻塞,可立即返回

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. 传动态指针,每次传的指针都是新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. 注意不可以直接传变化值的指针,多个线程之间存在竞争,线程函数中对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;
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
C++网络编程多线程是两个在软件开发中常见的主题。网络编程是指使用C++语言来创建网络应用程序,实现网络通信和数据传输。多线程是指在一个程序中同时执行多个线程,以提高程序的并发性和性能。 在C++中进行网络编程可以使用一些库,如Boost.Asio和Poco等。这些库提供了一些类和函数,用于创建网络连接、发送和接收数据等操作。通过这些库,可以实现各种网络应用,如客户端和服务器程序、网络通信协议的实现等。 多线程程是指在一个程序中同时执行多个线程,每个线程可以独立执行不同的任务。C++提供了一些多线程程的支持,如标准库中的std::thread和std::mutex等。使用这些库,可以创建和管理多个线程,并实现线程间的同步和通信。 在网络编程中,多线程可以用于处理多个客户端的请求。当有多个客户端同时连接到服务器时,可以为每个客户端创建一个线程来处理其请求,以提高服务器的并发性能。通过多线程,可以同时处理多个客户端的请求,而不会阻塞其他客户端的连接。 需要注意的是,在多线程程中需要考虑线程安全性和同步机制。多个线程同时访问共享资源时,可能会引发竞态条件和数据不一致的问题。因此,需要使用互斥锁、条件变量等同步机制来保证线程的安全性和正确性。 总结起来,C++网络编程多线程是两个在软件开发中常见的主题。网络编程用于创建网络应用程序,实现网络通信和数据传输。多线程用于提高程序的并发性和性能,特别适用于处理多个客户端的请求。在使用这两个主题时,需要注意线程安全性和同步机制的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值