手记

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 100void 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 网络编程 --尹圣雨》

原文出处:https://www.cnblogs.com/DOMLX/p/9613027.html

1人推荐
随时随地看视频
慕课网APP