udp/tcp错误总结

发布于:2024-04-28 ⋅ 阅读:(20) ⋅ 点赞:(0)

udp
tcp——多进程
tcp——多线程
tcp——线程池
tcp——守护进程

🎆udp
 ✨pthread_create 错误总结
 ✨LockGuard错误总结
 ✨服务端需要写成多线程
 ✨客户端也需要写成多线程
 ✨多线程调试工具
🎆tcp
 ✨tcp独有调试工具——telnet
 ✨ThreadPool —— 短连接
 ✨pthread_create —— 长连接
 ✨fork——长连接
 ✨进程池版的守护进程

在这里插入图片描述

🎆udp

✨1. pthread_create 错误总结

    static void* Routine(void* argv)
    {
        // 线程本身就是运行起来一次就要结束,结束的动作由外部执行
        Thread* th = static_cast<Thread*>(argv);
        th->_fun(th->_info);// 线程自动调用这个函数,并将参数传递过去
        return nullptr;
    }
    bool Start()
    {
        cout << _name ;
        puts(" 线程将要运行");
        int n = pthread_create(&_tid,nullptr,Routine,this);
        cout << _name;
        puts(" 线程已经运行");
        if(n)
            return false;
        return true;
    }

pthread_create 不能使用 bind 进行绑定,只能使用static 函数进行创建

✨2. LockGuard错误总结

class LockGuard
{
public:
    // 锁必须传指针,不能传值/引用
    // 传值/引用会造成死锁现象
    LockGuard(const std::string& func_name, pthread_mutex_t* mtx):_func_name(func_name),_mutex(mtx)
    {
        // puts("上锁");
        std::cout << "LockGuard:" <<  _func_name << "上锁" << std::endl;
        pthread_mutex_lock(_mutex);// 死锁位置
        // puts("上锁完成");
        std::cout << "LockGuard:" <<  _func_name << "上锁完成" << std::endl;

    }
    ~LockGuard()
    {
        // puts("开始解锁");
        std::cout << "LockGuard:" << _func_name << "解锁" << std::endl;
        pthread_mutex_unlock(_mutex);
        // puts("解锁完成");

    }
private:
    // pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;// 这个锁不能初始化,这个锁用的是外面的锁
    pthread_mutex_t* _mutex;
    std::string _func_name;
};

​ 必须使用传指针的方式,如果使用传引用/传值的方式,就会出现死锁

✨3. 服务端需要写成多线程

如果写成单线程,可能会出现服务端只为一个服务,另一个发送消息显示不出来的情况;当此连接释放才能将内容显示出来

✨4. 客户端也需要写成多线程

需要一个send线程,一个recv线程
如果只有一个线程,线程如果阻塞在send线程,也就是需要从标准输入中读取才能进行发送,那么就无法进行recv;由于udp有自己的缓冲区,所以即使无法运行recv也是可以将少量消息进行记录

✨5. 多线程调试工具

pstack —— 查看多进程函数栈
gdb attach 线程id
info threads——查看线程信息
t 线程编号——切换线程
info b——查看线程信息

详细请参考 多进程调试工具

🎆tcp

✨tcp独有调试工具——telnet

没有写客户端的情况下还想进行调试,可以使用telnet进行调试,telnet只能对tcp进行使用

ThreadPool —— 短连接

  1. 什么是短连接
    长连接:将一个接收和发送作为一个任务,一个线程一直做这一个工作,知道客户端关闭,线程结束
    短连接:将一个接收和发送作为一个任务,一个套接字执行一个任务之后立即释放

  2. 理解为什么TheadPool 模型必须使用短连接
    线程池中线程个数有限如果使用长连接,服务端只能接收有限的用户,很显然是不合理的

  3. 使用function包装函数类型不同头文件中可能出现函数类型重名的问题
    在这里插入图片描述

  4. 灵活规定ThreadPool的任务类型
    在这里插入图片描述
    问题一为什么ThreadPool的类型是Task,为什么不能是一个套接字或者别的
    首先我们要明确线程要执行的任务是:使用套接字进行接收+发送消息
    问题二accept 函数是在主线程or子线程
    肯定是在主线程中,如果在子线程中还需要考虑访问套接字冲突的问题
    所以传递套接字应该是传递的接收消息的套接字
    问题三:为什么要将执行函数封装进去,可不可以在Task类中进行执行
    在Task类中实现执行函数完全是OK的,最关键的一点是要在Task类中重载(),这样在使用的时候就可以像执行函数一样执行了
    在这里插入图片描述

注意:在子线程中,执行完一个任务需要将这个任务的接收消息的套接字关闭,防止资源消耗完
在这里插入图片描述

pthread_create —— 长连接

  1. 一定要明确变量是要被外部使用还是只在本作用域中使用
    在这里插入图片描述
    如果局部变量需要传给外部进行使用,并且接收的参数是指针,当外部进行使用的时候,当前空间已经被回收,就无法拿到原来的数据
    解决方法:
    方法一:将接收参数改为传值接收
    方法二传递堆空间
  2. 创建子线程需要进行分离每个子线程使用的是不同的描述套接字,所以不存在访问冲突的问题,不需要加锁
    在这里插入图片描述

fork——长连接

  1. 因为进程有很强的独立性,所以可以将接收数据的套接字写成成员变量;父子进程间会发生写实拷贝,所有每个进程都会独立的维护自己的接收数据的套接字,完全不用担心在接收到一个全新的的连接之后会将上一个接收数据的套接字进行覆盖的情况
    在这里插入图片描述
  2. 满满的细节
    细节一子进程需要关闭父进程中的接收连接的套接字文件描述符——防止文件描述符的消耗
    细节二:为了不让父进程等待子进程退出,使用孙子进程的方式解决;子进程会立即结束,父进程也会立即回收子进程并等待下次连接
    细节三孙子进程连接结束一定要关闭文件描述符;首先关闭文件描述符是一个好习惯,其次文件描述符是使用引用计数的方式实现的,子进程退出,系统会自动回收子进程的资源,包括文件描述符,虽然在代码中没有什么危险,但是还是要关注这个细节
    细节四:虽然孙子进程会自动被系统回收,但是孙子进程也是需要手动退出,如果孙子进程没有主动退出,那么它将和父进程一样等待下次的连接
    在这里插入图片描述

✨进程池版的守护进程

  1. 手写守护进程
    SIGCLD, SIGPIPE, SIGSTOP 忽略——这三个信号在发出后,系统需要进行处理,需要从用户态转变为内核态,这个过程是很消耗资源,为了防止这种消耗,就将这三个信号进行忽略,并且这中操作在守护进程中是很常见的
    使用setsid来创建守护进程
    并将标准输入,标准输出,标准错误重定向到/dev/null文件中,/dev/null这个文件的内容会被自动丢掉,就像垃圾桶一样
    在这里插入图片描述
  2. 如果线程池先创建出来,再创建守护进程,那么守护进程不会将父进程中的线程继承下来,就导致了守护进程中虽然能将任务push进线程池,但是线程池中根本没有线程能够完成任务
    在这里插入图片描述
    所以根据他使用的时候创建的特点我们想到可以使用懒汉模式——完美解决守护进程无法继承父进程的线程的问题
    在这里插入图片描述还要注意懒汉模式是有线程安全问题,需要双层保障

网站公告

今日签到

点亮在社区的每一天
去签到