• 服务器搭建(TCP套接字)-fork版(服务端)


        基础版的服务端虽然基本实现了服务器的基本功能,但是如果客户端的并发量比较大的话,服务端的压力和性能就会大打折扣,为了提升服务端的并发性能,可以通过fork子进程的方式,为每一个连接成功的客户端fork一个子进程,这样既达到了并发的要求,还能达到客户端隔离的效果。

    一、fork

    1.1、头文件

    #include 
    #include 
    
    
    • 1
    • 2
    • 3

    1.2、原型

    pid_t fork(void);
    
    • 1

    fork() 是一个系统调用函数,用于在 Unix-like 操作系统中创建一个新的进程。它会复制当前进程(称为父进程),并在新的进程(称为子进程)中继续执行。

    fork() 函数返回的是一个 pid_t 类型的值,其含义如下:

    • 在父进程中,fork() 返回新创建的子进程的进程 ID(PID)。
    • 在子进程中,fork() 返回 0。
    • 如果创建子进程失败,fork() 返回 -1。

        fork() 函数在创建子进程时会返回两次,这是因为它是一个复制当前进程的系统调用。具体来说,fork() 函数会创建一个新的进程(子进程),并将父进程的所有内容(包括代码、数据、堆栈等)复制到子进程中。

    第一次返回:

    • 父进程中,fork() 返回新创建的子进程的进程 ID(PID)。
    • 如果创建子进程失败,fork() 返回 -1。

    第二次返回:

    • 子进程中,fork() 返回 0。

    通过这两次返回,父进程和子进程可以根据不同的返回值采取不同的逻辑分支。

    在父进程中,可以根据返回的子进程 PID 做一些与子进程相关的操作,如记录子进程的 PID、等待子进程的终止等。

    在子进程中,由于 fork() 返回的是 0,可以根据此特性来区分自己是子进程,从而执行特定的子进程代码逻辑。

    需要注意的是,父进程和子进程会继续执行 fork() 调用之后的代码,并且它们是在不同的进程上下文中运行的,拥有各自独立的内存空间和资源。因此,在使用 fork() 创建子进程时,通常需要在父子进程中进行不同的处理,以避免竞态条件和不必要的资源共享问题。

    1.3、代码实现

    #include 
    //socket
    #include 
    #include 
    //close
    #include 
    //exit
    #include 
    //perror
    #include 
    //memset
    #include 
    //htons
    #include 
    
    
    #define PORT 8596
    #define MESSAGE_SIZE 1024
    
    int main(){
    
      int ret=-1;
      int socket_fd=-1;
      int accept_fd=-1;
      int backlog=10;
      int flag=1;
      int pid;
    
      struct sockaddr_in local_addr,remote_addr;
    
      //create socket
      socket_fd=socket(AF_INET,SOCK_STREAM,0);
      if(socket_fd == -1){
        perror("create socket error");
        exit(1);
      }
     //set option of socket
      ret = setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
      if ( ret == -1 ){
        perror("setsockopt error");
      }
     //set socket address
      local_addr.sin_family=AF_INET;
      local_addr.sin_port=htons(PORT);
      local_addr.sin_addr.s_addr=INADDR_ANY;
      bzero(&(local_addr.sin_zero),8);
     //bind socket
     ret=bind(socket_fd, (struct sockaddr *)&local_addr,sizeof(struct sockaddr_in));
     if(ret == -1){
        perror("bind socket error");
        exit(1);
     }
    
      ret=listen(socket_fd, backlog);
      if(ret ==-1){
       perror("listen error");
       exit(1);
      }
      //loop to accept client
      for(;;){
       socklen_t addrlen = sizeof(remote_addr);
       accept_fd=accept(socket_fd,( struct sockaddr *)&remote_addr, &addrlen);
       pid=fork();
       //子进程
       if(pid==0){
        char in_buf[MESSAGE_SIZE]={0,};
        for(;;){
         memset(in_buf,0,MESSAGE_SIZE);
         //read data
         ret =recv(accept_fd, (void*)in_buf, MESSAGE_SIZE, 0);
         pid_t currentID = getpid();
         std::cout << "Current Process ID: " << currentID << std::endl;
         if(ret ==0){
            break;
         }
         printf("receive data:%s\n",in_buf);
         send(accept_fd, (void *)in_buf, MESSAGE_SIZE, 0);
       }
       printf("close client connection......");
       close(accept_fd);
      }
     }
     if(pid !=0){
      printf("quit server....");
      close(socket_fd);
     }
      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
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    1.4、实现效果

    1.4.1、客户端1

    连接成功,并成功收发数据

    1.4.2、服务端接收客户端1

    在这里插入图片描述

    1.4.3、客户端2

    在这里插入图片描述

    1.4.4、服务端接收客户端2

    在这里插入图片描述
    可以看到,服务端对于两个客户端的处理是在不同的子进程中进行的。

    使用 fork() 函数创建子进程的服务器有以下优点和缺点:

    优点:

    • 简单易用:使用 fork() 函数创建子进程的服务器相对简单,不需要使用复杂的多线程或多进程编程模型。通过复制父进程的内存空间,子进程可以独立运行,处理客户端请求。
    • 高并发处理:每个客户端连接都可以创建一个独立的子进程,这样服务器能够同时处理多个客户端请求,实现高并发性能。
    • 数据共享:父进程和子进程共享文件描述符,可以轻松共享一些资源和状态信息,例如打开的文件、缓冲区等。
    • 可靠性:由于每个子进程是独立运行的,一个子进程的崩溃或异常不会影响其他子进程或主服务器进程。

    缺点:

    • 内存开销:每个子进程都需要复制父进程的内存空间,因此在大规模并发的情况下,服务器的内存开销会比较大。
    • 进程切换开销:由于每个客户端连接都需要创建子进程,因此涉及到进程之间的切换开销,包括上下文切换和进程间通信开销,这可能对服务器性能产生一定的影响。
    • 可伸缩性:由于每个客户端连接都需要创建子进程,服务器的可伸缩性可能受到限制。在大规模并发情况下,为每个连接创建子进程可能会导致系统资源耗尽。
      进程间通信复杂性:如果子进程之间需要进行通信或共享数据,就需要使用进程间通信(IPC)机制,如管道、共享内存等。这增加了编程的复杂性。

        综上所述,使用 fork() 函数创建子进程的服务器适用于简单的并发场景和较小规模的应用,但在大规模高并发、资源消耗较大或需要更高可伸缩性的情况下,可能需要考虑其他并发模型,如多线程或事件驱动模型。

  • 相关阅读:
    Vue2基础
    前端反爬思考,好友从百度搜到了我的文章,链接却是别人的
    Expected linebreaks to be ‘LF‘ but found ‘CRLF‘. 使用 ESlint 插件自动格式化配置 解决报错
    秋天过后马上进入冬天
    基于ssm+Vue音乐播放器管理系统java源码
    MySQL大小版本升级步骤
    爬虫逆向实战--猿人学第一题分析
    netty系列之:netty中的自动解码器ReplayingDecoder
    牛视源码开发,抖音矩阵系统。。。。
    3、设计原则
  • 原文地址:https://blog.csdn.net/u011557841/article/details/132981657