c++ 網絡編程(三)TCP/IP LINUX/windows 進程間的通信原理與實現代碼 基於多進程的服務端實現

原文作者:aircraft

原文鏈接:https://www.cnblogs.com/DOMLX/p/9613027.html

一.進程間通信的基本概念

進程間通信意味着兩個不同進程間可以交換數據,操作系統中應提供兩個進程可以同時訪問的內存空間。

通過管道實現進程間通信

基於管道(PIPE)的進程間通信結構模型:

通過管道完成進程間通信。管道不是進程的資源,屬於操作系統的。兩個進程通過操作系統提供的內存空間進行通信。

創建管道的函數:

父進程調用該函數時創建管道,同時獲取對應於出入口的文件描述符。父進程的目的是與子進程進行數據交換,因此需要將入口或出口中的1個文件描述符傳遞給子進程。調用fork函數傳遞。

二.進程間通信的單向傳遞

簡單的看一個基礎單向通信實例代碼來理解進程間的通信是怎麼實現的:

#include<stdio.h>
#include<unistd.h>
#define BUF_SIZE 30
 
int main(int argc, char *argv[])
{
    int fds[2];
    char str[] = "Who are you?";
    char buf[BUF_SIZE];
    pid_t pid;
 
    pipe(fds);                //創建管道,fds數組中保存用於I/O的文件描述符
    pid = fork();            //子進程將同時擁有管道的I/O文件描述符。
    if (pid == 0)
    {
        write(fds[1],str,sizeof(str));        //fds[1]爲管道入口
    }
    else 
    {
        read(fds[0],buf,BUF_SIZE);            //fds[0]爲管道出口    
        puts(buf);
    }
    return 0;
}

運行結果:who are you ?

上例中,父子進程都可以訪問管道的I/O路徑,但子進程僅用輸入路徑,父進程僅用輸出路徑。

三.進程間通信的雙向傳遞

管道進行雙向數據交換的通信方式:

接下來看一個簡單的實例代碼:

/* 雙向通信的管道 */
#include<stdio.h>
#include<unistd.h>
#define BUF_SIZE 30
 
int main(int argc,char *argv[])
{
    int fds[2];
    char str1[] = "Who are you?";
    char str2[] = "Thank you for your message!";
    char buf[BUF_SIZE];
    pid_t pid;
 
    pipe(fds);
    pid = fork();
    if (pid == 0)
    {
        write(fds[1],str1,sizeof(str1));        //傳輸數據
        sleep(2);                               //睡眠兩秒,避免被下一行的read函數讀取了數據。
        read(fds[0],buf,BUF_SIZE);
        printf("Child proc output: %s \n",buf);    //接收數據
    }
    else 
    {
        read(fds[0],buf,BUF_SIZE);                //接收數據
        printf("Parent proc output: %s \n",buf);
        write(fds[1],str2,sizeof(str2));        //傳輸數據
        sleep(3);        //睡眠,防止父進程在子進程輸出之前結束,可刪除
            //不理解的話你註釋掉這個sleep體會一下就知道了
    }
    return 0;
}

這裏爲什麼有這麼多個sleep 呢,搖一搖你們的小腦袋----有沒有聽見水聲???hhh

書上有句話“向管道傳遞數據的時候,先讀的程序會把數據先取走”

看到這裏明白了嗎???  簡而言之就是數據進入管道就變成了無主數據,這時候如果我子進程先寫入數據,在父進程沒有取出數據前又read把自己的數據給讀出來了!!!!大問題!大問題!對吧,這是要搞事情的節奏啊,被誰打死都不知道!!!

那麼如何避免這個問題呢?---一個管道不夠,我建兩個唄---唉,真是的。。。。。

只用1個管道進行雙向通信並非易事,需要預測並控制運行流程。因此創建2個管道完成雙向通信,各自負責不同的數據流動即可:

由上圖可知,是用2個管道可以避免程序流程的預測或控制。

接下來看雙通道實現通信代碼:

/* 雙管道實現進程間通信 */
#include<stdio.h>
#include<unistd.h>
#define BUF_SIZE 30
 
int main(int argc,char *argv[])
{
    int fds1[2],fds2[2];
    char str1[] = "Who are you?";
    char str2[] = "Thank you for your message!";
    char buf[BUF_SIZE];
    pid_t pid;
 
    pipe(fds1), pipe(fds2);                        //創建兩個管道
    pid = fork();
    if (pid == 0)
    {
        write(fds1[1],str1,sizeof(str1));    //子進程通過數組fds1傳輸數據
        read(fds2[0],buf,BUF_SIZE);
        printf("Child proc output: %s \n",buf);
    }
    else 
    {
        read(fds1[0],buf,BUF_SIZE);
        printf("Parent proc output: %s \n",buf);    
        write(fds2[1],str2,sizeof(str2));    //父進程通過數組fds2傳輸數據
        sleep(3);
    }
    return 0;
}

輸入結果:Parent proc output: ”Who are you?"; 

Child proc output: "Thank you for your message!";

好的基本概念都介紹完了,那我們用一下玩玩唄???

四.基於多進程的回聲服務端實現

注意啦這裏是對我上一章博客代碼的擴充,沒有看我的上一張網絡編程(二)......可以去看看了

這裏對網絡編程(二)加了一個功能,“可以將回聲客戶端傳輸的字符串按序保存到文件中去”

 LINUX 下:

/* 實現併發服務器端 */
/* echo_storeserv.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<sys/socket.h>
 
#define BUF_SIZE 100
void error_handling(char *message)
{
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(1);
}
 
/* Handler */
void read_childproc(int sig)
{
    pid_t pid;
    int status;
    pid = waitpid(-1,&status,WNOHANG);
    printf("removed proc id: %d \n",pid);
}
 
int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    int fds[2];
 
    pid_t pid;
    struct sigaction act;
    socklen_t adr_sz;
    int str_len, state;
    char buf[BUF_SIZE];
    if (argc != 2) {
        printf("Usage: %s <port> \n",argv[0]);
        exit(1);
    }
 
    act.sa_handler = read_childproc;            //設置信號處理函數
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD,&act,0);            //子進程終止時調用Handler
    
    serv_sock = socket(PF_INET,SOCK_STREAM,0);
    memset(&serv_adr,0,sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
 
    if (bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if (listen(serv_sock,5) == -1)
        error_handling("listen() error");
 
    pipe(fds);
  //這裏創建一個子進程來服務寫入文件數據
    pid = fork();
    if (pid == 0)
    {
        FILE* fp = fopen("echomsg.txt","wt");
        char msgbuf[BUF_SIZE];
        int i, len;
 
        for (i = 0; i < 10; i++ )
        {
            len = read(fds[0],msgbuf,BUF_SIZE);    //從管道出口fds[0]讀取數據並保存到文件中
            fwrite((void*)msgbuf,1,len,fp);        
        }
        fclose(fp);
        return 0;
    }
 
 
    while (1) 
    {
        adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz);
        if (clnt_sock == -1)
            continue;
        else 
            puts("new client connected...");
        //這裏創建一個子進程來將數據寫入管道
        pid = fork();
        if (pid == 0)        //子進程運行區域
        {
            close(serv_sock);
            while((str_len = read(clnt_sock,buf,BUF_SIZE)) != 0)
            {    
                write(clnt_sock,buf,str_len);
                write(fds[1],buf,str_len);        //將從客戶端接收到的數據寫入到管道入口fds[1]中
            }
 
            close(clnt_sock);
            puts("client disconnected...");
            return 0;        //調用Handler
        }
        else                //父進程運行區域
            close(clnt_sock);
    }
    close(serv_sock);
    return 0;
} 

上面處理文件的進程代碼裏,可能有的人會對那個for循環怎麼實現恰好讀十次數據結束,,,有點疑惑------關鍵在於read函數,這個函數如果沒有從管道里面讀取到數據就會繼續等待!!!  這也是大工程需要注意出現BUG的地方

這裏需要大家多開幾個客戶端來驗證服務端的效果,當10次fwrite函數調用完後,大家就可以打開文件查看結果了,如果沒有客戶端代碼可以參考我上一篇博客。

windows下基於多進程的回聲服務端實現代碼:

/*
 *  @file  : TestEchoServerMultiProcess.cpp
 *  @author: Shilyx
 *  @date  : 2014-04-23 08:43:27.206
 *  @note  : Generated by SlxTemplates, 多進程echo服務器演示
 */
 
#include <WinSock2.h>
#include <Windows.h>
#include <Shlwapi.h>
#pragma warning(disable: 4786)
#include <iostream>
 
#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "Shlwapi.lib")
 
using namespace std;
 
// 初始化WinSock,未檢查返回值
void InitWinSock()
{
    WSADATA wd;
 
    WSAStartup(MAKEWORD(2, 2), &wd);
}
 
void Serve(USHORT port)
{
    InitWinSock();
 
    SOCKET sock_base = INVALID_SOCKET;
 
    do
    {
        sock_base = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 
        if (sock_base == INVALID_SOCKET)
        {
            cerr<<"socket error "<<WSAGetLastError()<<endl;
            break;
        }
 
        sockaddr_in sin;
 
        sin.sin_family = AF_INET;
        sin.sin_addr.s_addr = INADDR_ANY;
        sin.sin_port = htons(port);
 
        if (SOCKET_ERROR == bind(sock_base, (sockaddr *)&sin, sizeof(sin)))
        {
            cerr<<"bind error "<<WSAGetLastError()<<endl;
            break;
        }
 
        if (SOCKET_ERROR == listen(sock_base, 100))
        {
            cerr<<"listen error "<<WSAGetLastError()<<endl;
            break;
        }
 
        HANDLE hProcess = NULL;
        DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &hProcess, 0, TRUE, DUPLICATE_SAME_ACCESS);
 
        if (NULL == hProcess)
        {
            cerr<<"DuplicateHandle error "<<GetLastError()<<endl;
            break;
        }
 
        TCHAR szSelfPath[MAX_PATH];
 
        GetModuleFileName(GetModuleHandle(NULL), szSelfPath, RTL_NUMBER_OF(szSelfPath));
        PathQuoteSpaces(szSelfPath);
 
        while (true)
        {
            int len = sizeof(sin);
            SOCKET sock = accept(sock_base, (sockaddr *)&sin, &len);
 
            if (sock == INVALID_SOCKET)
            {
                cerr<<"accept error "<<WSAGetLastError()<<endl;
                break;
            }
            else
            {
                TCHAR szCommand[MAX_PATH * 2];
                STARTUPINFO si = {sizeof(si)};
                PROCESS_INFORMATION pi;
 
                si.dwFlags = STARTF_USESHOWWINDOW;
                si.wShowWindow = SW_SHOW;
 
                wnsprintf(szCommand, RTL_NUMBER_OF(szCommand), TEXT("%s %u %u"), szSelfPath, sock, hProcess);
 
                if (CreateProcess(NULL, szCommand, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi))
                {
                    CloseHandle(pi.hProcess);
                    CloseHandle(pi.hThread);
                }
                else
                {
                    cerr<<"CreateProcess error "<<GetLastError()<<endl;
                }
 
                closesocket(sock);
            }
        }
 
    } while (false);
 
    if (sock_base != INVALID_SOCKET)
    {
        closesocket(sock_base);
    }
}
 
DWORD CALLBACK WorkProc(LPVOID lpParam)
{
    SOCKET sock = (SOCKET)lpParam;
 
    while (TRUE)
    {
        char szBuffer[4096];
        int len = recv(sock, szBuffer, sizeof(szBuffer), 0);
 
        if (len <= 0)
        {
            break;
        }
 
        if (send(sock, szBuffer, len, 0) <= 0)
        {
            break;
        }
    }
 
    closesocket(sock);
 
    return 0;
}
 
void Work(SOCKET sock, HANDLE hParentProcess)
{
    InitWinSock();
 
    HANDLE hObjects[] = {hParentProcess, CreateThread(NULL, 0, WorkProc, (LPVOID)sock, 0, NULL)};
 
    WaitForMultipleObjects(RTL_NUMBER_OF(hObjects), hObjects, FALSE, INFINITE);
 
    CloseHandle(hObjects[0]);
    CloseHandle(hObjects[1]);
}
 
int main(int argc, char *argv[])
{
    // 加端口參數啓動爲父進程
    // 加套接字句柄參數和進程句柄參數爲子進程
    // 不加參數顯示用法
 
    if (argc == 2)
    {
        int port = StrToIntA(argv[1]);
 
        if (port < 0 || port > 65535)
        {
            cerr<<"端口錯誤:"<<port<<endl;
            return 0;
        }
 
        // 在端口port處啓動echo服務器
        Serve((USHORT)port);
    }
    else if (argc == 3)
    {
        SOCKET sock = StrToIntA(argv[1]);
        HANDLE hParentProcess = (HANDLE)StrToIntA(argv[2]);
 
        // 針對具體tcp連接套接字和父進程句柄開始echo工作
        Work(sock, hParentProcess);
    }
    else
    {
        cout<<"加端口參數啓動爲父進程"<<endl
            <<"加套接字句柄參數和進程句柄參數爲子進程"<<endl
            <<"不加參數顯示用法"<<endl;
    }
 
    return 0;
}

同時多進程服務端也是有缺點的,每創建一個進程就代表大量的運算與內存空間佔用,相互進程數據交換也很麻煩。。。那麼怎麼解決呢,在我後面的博客也許會給出答案-----hhhhhhh

最後說一句啦。本網絡編程入門系列博客是連載學習的,有興趣的可以看我博客其他篇。。。。

好了今天對網絡編程的學習就到這裏結束了,小飛機我要撤了去吃飯了。,,,很多人大學都很迷茫不知道學點什麼好,,,,,管他的,想那麼多幹嘛,先學了再說,對技術如有偏見,那麼你的領域就侷限於此了---《一專多精》

參考博客:https://blog.csdn.net/my3439955/article/details/9749869

參考書籍:《TCP/IP 網絡編程 --尹聖雨》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章