1. 程式人生 > >利用fork實現並發服務器

利用fork實現並發服務器

客戶端 簡單 reat 順序 ima efi define read 是把

(1) fork 淺析

linux 中, 一個進程可以通過fork()系統調用來創建一個與自己相同的子進程, 這個子進程是父進程的克隆, 他繼承了父進程的整個地址空間, 包括進程上下文, 堆棧地址, 內存信息, 進程控制塊等。值得註意的是, 調用fork一次, 他卻返回兩次, 一次是在父進程中返回子進程的進程id, 一次是在子進程中返回0, 這看起來有點難理解, 我們先看下面這段程序:

 #include <unistd.h>
 #include <stdio.h>
 #include <sys/types.h>
 #include <string.h>
 int
main() { pid_t pid = fork(); if(pid == -1) { perror("fork error"); return -1; } if(pid > 0) { printf("parent: child pid is %d\n", pid); } else if(pid == 0) { printf("child: parent pid is %d \t, self pid id %d \n", getppid(), getpid()); } printf(
"after fork\n"); return 0; }

運行結果如下:

技術分享圖片

這個結果很容易理解, 當父進程調用fork後, 系統創建了一個與父進程同樣的子進程, 他們擁有一樣的上下文, 在父進程中, fork()返回了子進程的id, 在子進程中返回了0, 然後他們分別往下運行, 父進程走入了if(pid > 0) 程序段中, 而子進程走入了else if(pid == 0) 程序段中, 然後他們又分別繼續往下運行, 都打印了after fork\n。這裏需要註意的是, fork()出來的子進程並不是從頭開始運行, 因為他跟父進程有一樣的上下文, 這也是為什麽他會返回兩次(父子進程中各返回一次), 同時父子進程的運行順序是不確定的, 多核機器上可能交替執行也可能同時運行, 所以打印順序沒有太大意義。

(2) 利用fork實現服務器的並發

簡單了解了一下fork, 我們知道了調用fork以後, 會創建一個與父進程一樣的子進程, 他們擁有一樣的資源, 於是我們就可以利用這個特性來實現一個簡單的並發服務器。

首先來看一下下面這段代碼:

 1 #include <sys/socket.h>
 2 #include <sys/types.h>
 3 #include <arpa/inet.h>
 4 #include <netinet/in.h>
 5 #include <stdio.h>
 6 #include <stdlib.h>
 7 #include <string.h>
 8 #include <unistd.h>
 9 
10 #define SERV_PORT 4333
11 #define MAXLINE 1024
12 
13 /*讀入客戶端的輸入, 然後帶上處理進程id加以返回*/
14 void dosomething(int sockfd)
15 {
16     pid_t pid = getpid();
17     int n;
18     char buff[MAXLINE], sendbuff[MAXLINE];
19     while(true)
20     {
21         n = read(sockfd, buff, MAXLINE);
22         if(n > 0)
23         {
24             snprintf(sendbuff, MAXLINE, "pid: %d\t %s\n", getpid(), buff);
25             write(sockfd, sendbuff, strlen(sendbuff));
26         }
27         else if(n < 0)
28         {
29             perror("read error");
30             exit(-1);
31         }
32         else
33             break;
34     }
35 }
36 
37 int main()
38 {
39     int servfd, connfd;
40     sockaddr_in servaddr;
41     pid_t pid;
42 
43     if((servfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
44     {
45         perror("create socket error");
46         exit(-1);
47     }
48 
49     // 初始化監聽地址
50     bzero(&servaddr, sizeof(servaddr));
51     servaddr.sin_family = AF_INET;
52     servaddr.sin_port = htons(SERV_PORT);
53     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
54 
55     // 綁定監聽地址
56     if(bind(servfd, (sockaddr*)&servaddr, sizeof(servaddr)) < 0)
57     {
58         perror("bind error");
59         exit(-1);
60     }
61 
62     if(listen(servfd, 5) < 0)
63     {
64         perror("listen error");
65         exit(-1);
66     }
67 
68     for(;;)
69     {
70         connfd = accept(servfd, (sockaddr*)nullptr, nullptr);
71         if((pid = fork()) == 0)
72         {
73             close(servfd); //減少一次引用
74             dosomething(connfd);
75             close(connfd); //在子進程中真正關閉套接字
76             exit(0); 
77         }
78         close(connfd); //減少一次引用
79     }
80 
81 
82     return 0;
83 }

這是一個並發服務器的簡單例子,它的功能是把客戶端發來的字符串帶上處理的進程id然後發回給客戶端, 在代碼71行, 我們調用fork創建了一個子進程, 這樣對客戶端的響應就交給了子進程。第73行我們關閉了一次servfd, 他並沒有真正關閉套接字, 而僅僅減少了一次套接字的引用, 只有套接字的引用減為零才會執行四次揮手來真正關閉連接。創建子進程後, servfd的引用加了1, 如果不在這裏對其關閉一次, 那麽當子進程退出後, servfd的引用將無法減至0, 這將導致套接字servfd永遠無法真正關閉。第78行和75行意義與此相同。

接下來給出客戶端的代碼:

#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>

#define MAXLINE 1024
#define SERV_PORT 4333

void dosomething(FILE* fp, int sockfd)
{
    char sendline[MAXLINE], recvline[MAXLINE];
    while(fgets(sendline, MAXLINE, fp) != NULL)
    {
        write(sockfd, sendline, strlen(sendline));
        bzero(recvline, MAXLINE);
        if(read(sockfd, recvline, MAXLINE) <= 0)
        {
            perror("read error");
            exit(-1);
        }
        fputs(recvline, stdout);
    }
}

int main(int argc, char** argv)
{
    int connfd;
    sockaddr_in servaddr;

    if((connfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("create socket error");
        exit(-1);
    }

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr.s_addr);

    if((connect(connfd, (sockaddr*)&servaddr, sizeof(servaddr))) < 0)
    {
        perror("connect error");
        exit(-1);
    }
    dosomething(stdin, connfd);
    return 0;
}

編譯後, 我們首先運行服務端程序, 之後我們運行兩次客戶端程序來看效果

技術分享圖片

技術分享圖片

至此, 我們利用fork系統調用, 實現了一個簡單的並發服務器

利用fork實現並發服務器