网络编程socket(下)(二)

简介: 网络编程socket(下)(二)

二、多进程版TCP网络程序

2.1 存在问题

当服务端调用accept函数获取到新连接后不是由当前执行流为该连接对应的客户端提供服务,而是当前执行流调用fork函数创建子进程,子进程为父进程获取到的连接提供服务

由于父子进程是两个不同的执行流,当父进程调用fork创建出子进程后,父进程就可以继续从监听套接字中获取新连接,而不用关心获取上来的连接对应的客户端是否服务完毕

子进程继承父进程的文件描述符表

文件描述符表是隶属于一个进程的,子进程创建后会继承父进程的文件描述符表。如父进程打开了一个文件,该文件对应的文件描述符是3,子进程的3号文件描述符也会指向这个打开的文件,若子进程再创建一个子进程,那么孙子进程的3号文件描述符也同样会指向这个打开的文件

44d2dd9e86bc4d1da798deabf68f2bb9.png

当父进程创建子进程后,父子进程之间保持独立性,此时父进程文件描述符表的变化不会影响子进程。譬如,父子进程在使用匿名管道进行通信时,父进程先调用pipe函数得到两个文件描述符,一个是管道读端的文件描述符,一个是管道写端的文件描述符,此时父进程创建出来的子进程就会继承这两个文件描述符,之后父子进程一个关闭管道的读端,另一个关闭管道的写端,这时父子进程文件描述符表的变化是不会相互影响的,此后父子进程就可以通过这个管道进行单向通信了

对于套接字文件也是一样的,父进程创建的子进程也会继承父进程的套接字文件,此时子进程就能够对特定的套接字文件进行读写操作,进而完成对对应客户端的服务

等待子进程问题

当父进程创建出子进程后,父进程是需要等待子进程退出的,否则子进程会变成僵尸进程,进而造成内存泄漏。因此服务端创建子进程后需要调用wait或waitpid函数对子进程进行等待

阻塞式等待与非阻塞式等待:

  • 若服务端采用阻塞的方式等待子进程,那么服务端还是需要等待服务完当前客户端,才能继续获取下一个连接请求,此时服务端仍然是以一种串行的方式为客户端提供服务
  • 若服务端采用非阻塞的方式等待子进程,虽然在子进程为客户端提供服务期间服务端可以继续获取新连接,但此时服务端就需要将所有子进程的PID保存下来,并且需要不断花费时间检测子进程是否退出,且编码较为复杂

服务端要等待子进程退出,无论采用阻塞式等待还是非阻塞式等待,都不尽人意。此时可以考虑让服务端不等待子进程退出的方案

2.2 捕捉SIGCHLD信号

当子进程退出时会给父进程发送SIGCHLD信号,若父进程将SIGCHLD信号进行捕捉,并将该信号的处理动作设置为忽略,此时父进程可以继续从监听套接字中获取新连接

该方案实现较为简单,较为推荐

void TcpServer::StartUp()
{
    //设置忽略SIGCHLD信号
    signal(SIGCHLD, SIG_IGN);
    while(true) 
    {
        //获取连接
        struct sockaddr_in foreign;
        memset(&foreign, '\0', sizeof foreign);
        socklen_t length = sizeof foreign;
        int server_socket_fd = accept(_socket_listen_fd, (struct sockaddr*)&foreign, &length);
        if(server_socket_fd < 0) {
            cerr << "accept fail" << endl;
            continue;
        }
        string client_ip = inet_ntoa(foreign.sin_addr);
        uint16_t client_port = ntohs(foreign.sin_port);
        cout << "New Link: [" << server_socket_fd << "] [" <<  client_ip << "] [" << client_port << "]" << endl;
        //处理客户端请求
        pid_t id = fork();
        if(id == 0) { //child
            Service(server_socket_fd, client_ip,client_port);
            exit(4);
        }
    }
}

网络测试

重新编译程序运行服务端后,可以通过以下监控脚本对服务进程进行监控

while :; do ps axj | head -1 && ps axj | grep tcp_server | grep -v grep;echo "######################";sleep 1;done

一开始没有客户端连接该服务器,此时服务进程只有一个,该服务进程就是不断获取新连接的进程,而获取到新连接后也是由该进程创建子进程为对应客户端提供服务的

81c272f7784d4d7ebd31a322e5523f7e.png

此时启动一个客户端,让该客户端连接服务器,此时服务进程就会调用fork函数创建出一个子进程,由该子进程为这个客户端提供服务

68799242c2024329844b3fbc22212afc.png

若再有一个客户端连接服务器,此时服务进程会再创建出一个子进程,为这个客户端提供服务

37fc52eaf75145b88d3fe39290ad6a67.png

这两个客户端分别由两个不同的执行流提供服务,因此这两个客户端可以同时享受到服务,发送给服务端的数据都能够在服务端输出,并且服务端对两个客户端的数据都会进行响应

f6dfdd8eefc64e2f8bcce85425e8df17.png

当客户端陆续退出后,在服务端对应为之提供服务的子进程也会退出,但无论如何服务端都至少会有一个服务进程,这个服务进程的任务就是不断获取新连接

98bcc827eedb4c979311f57189d8e932.png

2.3 孙子进程提供服务

  • 爷爷进程:在服务端调用accept函数获取客户端连接请求的进程
  • 爸爸进程:爷爷进程调用fork函数创建出来的进程
  • 孙子进程:爸爸进程调用fork函数创建出来的进程,该进程调用Service函数为客户端提供服务

爸爸进程创建完孙子进程后立刻退出,此时服务进程(爷爷进程)调用wait/waitpid函数等待爸爸进程就能立刻等待成功,此时孙子进程变成孤儿进程被1号进程领养,此后服务进程就能继续调用accept函数获取其他客户端的连接请求。不需要处理孙子进程,其资源由系统释放

关闭对应的文件描述符

服务进程(爷爷进程)调用accept函数获取到新连接后,会让孙子进程为该连接对应的服务端提供服务,此时服务进程已经将文件描述符表继承给了爸爸进程,而爸爸进程又会调用fork函数创建出孙子进程,然后再将文件描述符表继承给孙子进程

而父子进程创建后,其各自的文件描述符表是独立的,不会相互影响。因此服务进程在调用fork函数后,服务进程就不需要再关心刚才从accept函数获取到的文件描述符了,此时服务进程就可以调用close函数将该文件描述符进行关闭

对于爸爸进程和孙子进程来说,是不需要关心从服务进程(爷爷进程)继承下来的监听套接字的,因此服务进程可以将监听套接字关掉

  • 对于服务进程来说,调用fork函数后就必须将从accept函数获取的文件描述符关掉。因为服务进程会不断调用accept函数获取新的文件描述符(服务套接字),若服务进程不及时关掉不用的文件描述符,最终服务进程中可用的文件描述符就会越来越少
  • 对于孙子进程而言,还是建议关闭从服务进程继承下来的监听套接字。实际就算不关闭监听套接字,最终也只会导致这一个文件描述符泄漏,但还是建议关上。因为孙子进程在提供服务时可能会对监听套接字进行某些误操作,此时就会对监听套接字当中的数据造成影响
  • 实际编码时,在爸爸进程fork之前将其监听套接字关闭,孙子进程继承的文件描述符表中自然没有监听套接字了
void TcpServer::StartUp()
{
    while(true) 
    {
        //获取连接
        struct sockaddr_in foreign;
        memset(&foreign, '\0', sizeof foreign);
        socklen_t length = sizeof foreign;
        int server_socket_fd = accept(_socket_listen_fd, (struct sockaddr*)&foreign, &length);
        if(server_socket_fd < 0) {
            cerr << "accept fail" << endl;
            continue;
        }
        string client_ip = inet_ntoa(foreign.sin_addr);
        uint16_t client_port = ntohs(foreign.sin_port);
        cout << "New Link: [" << server_socket_fd << "] [" <<  client_ip << "] [" << client_port << "]" << endl;
        //处理客户端请求
        pid_t id = fork();
        if(id == 0) { //爸爸进程
            close(_socket_listen_fd);//关闭监听套接字
            if(fork() > 0) exit(4);//服务进程子进程直接退出
            //孙子进程处理
            Service(server_socket_fd, client_ip,client_port);
            exit(5);
        }
        close(server_socket_fd);//服务进程关闭连接客户端时获取的文件描述符
        waitpid(id, nullptr, WNOHANG);//等待爸爸进程,立即成功
    }
}

网络测试

while :; do ps axj | head -1 && ps axj | grep tcp_server | grep -v grep;echo "######################";sleep 1;done

此时没有客户端连接服务器,只监控到一个服务进程,该服务进程正在等待客户端的连接请求

9ba4e2261f7c481ab362de040417571b.png

此时启动一个客户端,让该客户端连接服务端,此时服务进程会创建出爸爸进程,爸爸进程再创建出孙子进程,之后爸爸进程就会立刻退出,而由孙子进程为客户端提供服务。所以只看到了两个服务进程,其中一个是一开始用于获取连接的服务进程,还有一个就是孙子进程,该进程为当前客户端提供服务,其PPID为1,表明这是一个孤儿进程

0ea8e2e48a584ef7aac445cd3e542489.png

启动第二个客户端连接服务器后,就又会创建出一个孤儿进程为该客户端提供服务

10700047d4ca474c9d8e23275edad0d1.png

两个客户端是由两个不同的孤儿进程提供服务的,因此是能够同时享受服务的,可以看到这两个客户端发送给服务端的数据都能够在服务端输出,并且服务端也会对这两个客户端的数据进行响应

9bf31b88eaac46a5993072b4e10ed76c.png

当客户端全部退出后,对应为客户端提供服务的孤儿进程也会跟着退出,这时这些孤儿进程会被系统回收,而最终剩下那个获取连接的服务进程

adb9dd8578dc423385583420b13d0a93.png

三、多线程版TCP网络程序

创建进程的成本是很高的,创建进程时需要创建该进程对应的进程控制块(task_struct)、进程地址空间(mm_struct)、页表等结构。而创建线程的成本比创建进程的成本会小得多,因为线程本质是在进程地址空间内运行,创建出来的线程会共享该进程的大部分资源,因此在实现多执行流的服务器时最好采用多线程进行实现

服务进程调用accept函数获取到一个新连接后,可创建一个线程,让该线程为对应客户端提供服务

主线程创建出新线程后,也是需要等待新线程退出回收资源的,否则也会造成资源浪费的问题。但对于线程来说,若不想让主线程等待新线程退出,可以让创建出来的新线程调用pthread_detach函数进行线程分离,当这个线程退出时系统会自动回收该线程所对应的资源。此时主线程就可以继续调用accept函数获取新连接,而让新线程去服务对应的客户端

各个线程共享同一张文件描述符表

文件描述符表维护的是进程与文件之间的对应关系,因此一个进程对应一张文件描述符表。而主线程创建出来的新线程依旧属于这个进程,因此创建线程时并不会为该线程创建独立的文件描述符表,所有的线程看到的都是同一张文件描述符表

948f9afe0a0945a09cc56b2c364e4a53.png

当主线程调用accept函数获取到一个文件描述符后,新线程是能够直接访问这个文件描述符的

虽然新线程能够直接访问主线程accept上来的文件描述符,但此时新线程并不知道其所服务的客户端对应的是哪一个文件描述符,因此主线程创建新线程后需要告诉新线程对应应该访问的文件描述符,即告诉每个新线程在服务客户端时,应该对哪一个套接字进行操作

参数结构体

实际新线程在为客户端提供服务时调用Service函数,而调用Service函数时是需要传入三个参数的,分别是客户端对应的套接字、IP地址和端口号。因此主线程创建新线程时需要给新线程传入三个参数,而实际在调用pthread_create函数创建新线程时,只能传入一个类型为void*的参数

这时可以设计一个参数结构体ThreadDate,这三个参数可以存放到ThreadDate结构体中,当主线程创建新线程时就可以定义一个ThreadDate对象,将客户端对应的套接字、IP地址和端口号设置进这个ThreadDate对象中,然后将Param对象的地址作为新线程执行例程的参数进行传入

此时新线程在执行例程当中再将这个void*类型的参数强转为Param*类型,然后就能够拿到客户端对应的套接字,IP地址和端口号,进而调用Service函数为对应客户端提供服务

class ThreadDate
{
public:
    ThreadDate(int fd, string ip,uint16_t port):_server_socket_fd(fd),_client_ip(ip),_client_port(port) {}
    ~ThreadDate() {}
public:
    int _server_socket_fd;//accept获取连接得到文件描述符,用于服务
    string _client_ip;
    uint16_t _client_port;
};

文件描述符关闭的问题

所有线程看到的都是同一张文件描述符表,因此当某个线程要对文件描述符表做某种操作时,不仅要考虑当前线程,还要考虑其他线程。

  • 对于主线程accept来的文件描述符,主线程不能对其进行关闭操作,该文件描述符的关闭操作应该又新线程来执行。因为是新线程为客户端提供服务的,只有当新线程为客户端提供的服务结束后才能将该文件描述符关闭
  • 对于监听套接字,虽然创建出来的新线程不必关心监听套接字,但新线程不能将监听套接字对应的文件描述符关闭,否则主线程就无法从监听套接字当中获取新连接了

Service函数定义为静态成员函数

由于调用pthread_create函数创建线程时,新线程的执行例程是一个参数为void*,返回值为void*的函数。若要将这个执行例程定义到类内,就需要将其定义为静态成员函数,否则这个执行例程的第一个参数是隐藏的this指针

在线程的执行例程中会调用Service函数,由于执行例程是静态成员函数,静态成员函数无法调用非静态成员函数,因此需要将Service函数定义为静态成员函数,恰好Service函数内部进行的操作都不涉及类内数据的修改,因此直接在Service函数前面加上一个static即可

class TcpServer
{
public:
    TcpServer(uint16_t port):_socket_listen_fd(-1),_server_port(port) {}
    void InitServer();
    void StartUp();
    static void Service(int, string, uint16_t);
    ~TcpServer();
private:
    int _socket_listen_fd;
    uint16_t _server_port;
};
void TcpServer::StartUp()
{
    while(true) 
    {
        //获取连接
        struct sockaddr_in foreign;
        memset(&foreign, '\0', sizeof foreign);
        socklen_t length = sizeof foreign;
        int server_socket_fd = accept(_socket_listen_fd, (struct sockaddr*)&foreign, &length);
        if(server_socket_fd < 0) {
            cerr << "accept fail" << endl;
            continue;
        }
        string client_ip = inet_ntoa(foreign.sin_addr);
        uint16_t client_port = ntohs(foreign.sin_port);
        cout << "New Link: [" << server_socket_fd << "] [" <<  client_ip << "] [" << client_port << "]" << endl;
        //处理客户端请求
        ThreadDate* ptr = new ThreadDate(server_socket_fd, client_ip, client_port);
        pthread_t thread_id;
        pthread_create(&thread_id, nullptr, HandlerClient, (void*)ptr);
        /*应将ThreadDate数据开辟在堆区,若开辟在主线程栈区,主线程循环accept并处理客户端请求时,会修改TheadDate内数据*/
    }
}
void* TcpServer::HandlerClient(void* args)
{
    pthread_detach(pthread_self());//线程分离,资源由系统回收
    ThreadDate* ptr = (ThreadDate*)args;
    Service(ptr->_server_socket_fd, ptr->_client_ip, ptr->_client_port);
    delete ptr;
    return nullptr;
}

网络测试

监控时使用的不再是 ps -axj 命令,而是 ps -aL 命令

while :; do ps -aL|head -1&&ps -aL|grep tcp_server;echo "####################";sleep 1;done

启动服务端,通过监控发现此时只有一个服务线程(主线程),现在在等待客户端的连接请求

041357c11e0e4019be1d948b34c10287.png

当一个客户端连接到服务端后,此时主线程就会为该客户端构建一个参数结构体,然后创建一个新线程,将该参数结构体的地址作为参数传递给这个新线程,此时该新线程就能够从这个参数结构体中提取出对应的参数,然后调用Service函数为该客户端提供服务,因此在监控中显示了两个线程

11cfc1dc2b85462695339110d794b3f1.png

当第二个客户端发来连接请求时,主线程会进行相同的操作,最终再创建出一个新线程为该客户端提供服务,此时就有了三个线程

f0811186f35c48a78188767d37ae5dae.png

由于为这两个客户端提供服务的是两个不同的执行流,因此这两个客户端可同时享受服务端提供的服务,发送给服务端的消息都能够在服务端打印,并且这两个客户端都能够收到服务端的回显数据

d97a9b99cb9f4a519f61b2ac2d783687.png

此时无论有多少个客户端发来连接请求,在服务端都会创建出相应数量的新线程为对应客户端提供服务,而当客户端一个个退出后,为其提供服务的新线程也就会相继退出,最终就只剩下主线程在等待新连接的到来

df72ce38468445d29e692eecf9ae7859.png





目录
相关文章
|
12月前
|
开发者 Python
Python Socket编程:不只是基础,更有进阶秘籍,让你的网络应用飞起来!
在数字时代,网络应用成为连接世界的桥梁。Python凭借简洁的语法和丰富的库支持,成为开发高效网络应用的首选。本文通过实时聊天室案例,介绍Python Socket编程的基础与进阶技巧。基础篇涵盖服务器和客户端的建立与数据交换;进阶篇则探讨多线程与异步IO优化方案,助力提升应用性能。通过本案例,你将掌握Socket编程的核心技能,推动网络应用飞得更高、更远。
182 1
|
10月前
|
Kubernetes 网络协议 Python
Python网络编程:从Socket到Web应用
在信息时代,网络编程是软件开发的重要组成部分。Python作为多用途编程语言,提供了从Socket编程到Web应用开发的强大支持。本文将从基础的Socket编程入手,逐步深入到复杂的Web应用开发,涵盖Flask、Django等框架的应用,以及异步Web编程和微服务架构。通过本文,读者将全面了解Python在网络编程领域的应用。
151 1
|
11月前
|
消息中间件 监控 网络协议
Python中的Socket魔法:如何利用socket模块构建强大的网络通信
本文介绍了Python的`socket`模块,讲解了其基本概念、语法和使用方法。通过简单的TCP服务器和客户端示例,展示了如何创建、绑定、监听、接受连接及发送/接收数据。进一步探讨了多用户聊天室的实现,并介绍了非阻塞IO和多路复用技术以提高并发处理能力。最后,讨论了`socket`模块在现代网络编程中的应用及其与其他通信方式的关系。
863 3
|
11月前
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【10月更文挑战第10天】网络协议定义了机器间通信的标准格式,确保信息准确无损地传输。主要分为两种模型:OSI七层模型与TCP/IP模型。
|
12月前
|
网络协议 Python
告别网络编程迷雾!Python Socket编程基础与实战,让你秒变网络达人!
在网络编程的世界里,Socket编程是连接数据与服务的关键桥梁。对于初学者,这往往是最棘手的部分。本文将用Python带你轻松入门Socket编程,从创建TCP服务器与客户端的基础搭建,到处理并发连接的实战技巧,逐步揭开网络编程的神秘面纱。通过具体的代码示例,我们将掌握Socket的基本概念与操作,让你成为网络编程的高手。无论是简单的数据传输还是复杂的并发处理,Python都能助你一臂之力。希望这篇文章成为你网络编程旅程的良好开端。
137 3
|
12月前
|
网络协议 Python
网络世界的建筑师:Python Socket编程基础与进阶,构建你的网络帝国!
在数字宇宙中,网络如同复杂脉络连接每个角落,Python Socket编程则是开启这一世界的钥匙。本文将引导你从基础概念入手,逐步掌握Socket编程,并通过实战示例构建TCP/UDP服务器与客户端。你将学会使用Python的socket模块进行网络通信,了解TCP与UDP的区别,并运用多线程与异步IO提升服务器性能。跟随本文指引,成为网络世界的建筑师,构建自己的网络帝国。
108 2
|
12月前
|
网络协议 开发者 Python
网络编程小白秒变大咖!Python Socket基础与进阶教程,轻松上手无压力!
在网络技术飞速发展的今天,掌握网络编程已成为开发者的重要技能。本文以Python为工具,带你从Socket编程基础逐步深入至进阶领域。首先介绍Socket的概念及TCP/UDP协议,接着演示如何用Python创建、绑定、监听Socket,实现数据收发;最后通过构建简单的聊天服务器,巩固所学知识。让初学者也能迅速上手,成为网络编程高手。
191 1
|
网络协议 安全 网络安全
震惊!Python Socket竟能如此玩转网络通信,基础到进阶全攻略!
【9月更文挑战第12天】在网络通信中,Socket编程是连接不同应用与服务的基石。本文通过问答形式,从基础到进阶全面解析Python Socket编程。涵盖Socket的重要性、创建TCP服务器与客户端、处理并发连接及进阶话题如非阻塞Socket、IO多路复用等,帮助读者深入了解并掌握网络通信的核心技术。
363 6
|
消息中间件 网络协议 网络安全
解锁Python Socket新姿势,进阶篇带你玩转高级网络通信技巧!
【9月更文挑战第13天】在掌握了Python Socket编程基础后,你是否想进一步提升技能?本指南将深入探讨Socket编程精髓,包括从阻塞到非阻塞I/O以提高并发性能,使用`select`进行非阻塞操作示例;通过SSL/TLS加密通信保障数据安全,附带创建SSL服务器的代码实例;以及介绍高级网络协议与框架,如HTTP、WebSocket和ZeroMQ,帮助你简化复杂应用开发。通过学习这些高级技巧,你将在网络编程领域更进一步。
122 3
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【9月更文挑战第14天】网络协议是机器间交流的约定格式,确保信息准确传达。主要模型有OSI七层与TCP/IP模型,通过分层简化复杂网络环境。IP地址全局定位设备,MAC地址则在本地网络中定位。网络分层后,数据包层层封装,经由不同层次协议处理,最终通过Socket系统调用在应用层解析和响应。

热门文章

最新文章