溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

IPC之共享內(nèi)存·即時通訊小程序(二)

發(fā)布時間:2020-07-19 16:29:59 來源:網(wǎng)絡(luò) 閱讀:5438 作者:SherryX 欄目:系統(tǒng)運(yùn)維

上次說到,使用消息隊列可以做到簡易的登錄、退出功能。那么,該思考一下,聊天的用戶列表和聊天記錄應(yīng)該存在哪兒呢?當(dāng)然是服務(wù)器上,那么,就需要用到共享內(nèi)存了。

共享內(nèi)存

  • 共享內(nèi)存允許兩個不相關(guān)的進(jìn)程去訪問同一部分邏輯內(nèi)存
  • 如果需要在兩個運(yùn)行中的進(jìn)程之間傳輸數(shù)據(jù),共享內(nèi)存將是一種效率極高的解決方案
  • 共享內(nèi)存是由IPC為一個進(jìn)程創(chuàng)建的一個特殊的地址范圍,它將出現(xiàn)在進(jìn)程的地址空間中。
  • 其他進(jìn)程可以把同一段共享內(nèi)存段“連接到”它們自己的地址空間里去。
  • 所有進(jìn)程都可以訪問共享內(nèi)存地址,就好像它們是有malloc分配的一樣
  • 如果一個進(jìn)程向這段共享內(nèi)存寫了數(shù)據(jù),所做的改動會立刻被有權(quán)訪問同一段共享內(nèi)存的其他進(jìn)程看到
    IPC之共享內(nèi)存·即時通訊小程序(二)

    共享內(nèi)存函數(shù)

    共享內(nèi)存涉及到的函數(shù),與消息隊列相似,如下

    shmget

       #include < sys/ipc.h>
       #include < sys/shm.h>
       int shmget(key_t key, size_t size, int shmflg);

    作用:用來創(chuàng)建共享內(nèi)存

  • key:這個共享內(nèi)存段的名字
  • size:需要共享的內(nèi)存大小
  • shflg:由9個權(quán)限標(biāo)志構(gòu)成,它們的用法和創(chuàng)建文件時使用的mode模式標(biāo)志是一樣的
  • 返回值:成功-返回一個非負(fù)整數(shù),即該段共享內(nèi)存的標(biāo)識碼,失敗-返回‘-1’

    shmat

    void *shmat(int shmid, const void *shmaddr, int shmflg);

    作用:共享內(nèi)存剛被創(chuàng)建的時候,任何進(jìn)程還都不能訪問它,為了建立對這個共享內(nèi)存段的訪問渠道,必須由我們來把它連接到某個進(jìn)程的地址空間,shmat函數(shù)就是用來完成這項工作的。

  • shmid:shmget返回的共享內(nèi)存標(biāo)識
  • shmaddr:把共享內(nèi)存連接到當(dāng)前進(jìn)程去的時候準(zhǔn)備放置它的那個地址
  • shmflg:shmflg是一組按位OR(或)在一起的標(biāo)志。它的兩個可能取值是SHM_RND和SHM_RDONLY
  • 返回值:成功-返回一個指針,指針指向共享內(nèi)存的第一個字節(jié);失敗-返回‘-1’
    shmaddr為0,核心自動選擇一個地址
    shmaddr不為0且shmflg無SHM_RND標(biāo)記,則以shmaddr為連接地址。
    shmaddr不為0且shmflg設(shè)置了SHM_RND標(biāo)記,則連接的地址會自動向下調(diào)整為SHMLBA的整數(shù)倍。公式:shmaddr - (shmaddr % SHMLBA)
    shmflg=SHM_RDONLY,表示連接操作用來只讀共享內(nèi)存
    在fork() 后,子進(jìn)程繼承已連接的共享內(nèi)存

    shmdt

    int shmdt(const void *shmaddr);

    作用:把共享內(nèi)存與當(dāng)前進(jìn)程脫離開

  • shmaddr:由shmat返回的地址指針
  • 成功-返回‘0’;失敗-返回“-1”
  • 脫離共享內(nèi)存并不等于刪除它,只是當(dāng)前進(jìn)程不能再繼續(xù)訪問它

    shmctl

    int shmctl(int shmid, int cmd, struct shmid_ds *buf);

    作用:共享內(nèi)存的控制函數(shù)

  • shmid:由shmget返回的共享內(nèi)存標(biāo)識碼
  • cmd:將要采取的動作
  • buf:指向一個保存著共享內(nèi)存的模式狀態(tài)和訪問權(quán)限的數(shù)據(jù)結(jié)構(gòu)
  • 返回值:成功-‘0’;失敗-‘-1’
    cmd將要采取的動作有以下三個值:
    IPC之共享內(nèi)存·即時通訊小程序(二)
    IPC之共享內(nèi)存·即時通訊小程序(二)

    練習(xí)

    寫一段測試代碼練練:
    shmwrite.c

    #include < stdio.h>
    #include < sys/msg.h>
    #include < sys/ipc.h>
    #include < sys/types.h>
    int main()
    {
    int shmid;
    char * addr;
    
    //先檢測該key的共享內(nèi)存是否存在,如果存在,將原來的刪除,重建
    shmid = shmget(1000,0,0);
    if(shmid != -1)
    {
        //已經(jīng)存在,刪除
        shmctl(shmid,IPC_RMID,NULL);
    }
    shmid = shmget(1000,1024,IPC_CREAT);
    if(shmid == -1)
    {
        perror("error");
    }
    //  printf("shmid: %d\n",shmid);
    //地址映射
    addr = (char*)shmat(shmid,NULL,0);
    strcpy(addr,"hello world!");
    
    return 0;
    }

    shmread.c

    #include < stdio.h>
    #include < sys/msg.h>
    #include < sys/ipc.h>
    #include < sys/types.h>
    int main()
    {
    int shmid;
    char * addr = NULL;
    
    //先檢測該key的共享內(nèi)存是否存在,如果存在,將原來的刪除,重建
    shmid = shmget(1000,0,0);
    if(shmid == -1)
    {
        perror("error");
    }
    printf("shmit: %d\n",shmid);
    
    addr = (char*)shmat(shmid,NULL,0);
    printf("%s\n",addr);
    //解除映射
    shmdt(&addr);
    
    return 0;
    }

    運(yùn)行:
    IPC之共享內(nèi)存·即時通訊小程序(二)
    IPC之共享內(nèi)存·即時通訊小程序(二)
    查看ipc狀態(tài):
    IPC之共享內(nèi)存·即時通訊小程序(二)

    即時通訊小程序

    接下來,可以繼續(xù)我們的小程序了。當(dāng)有用戶上線的時候,每個客戶端都需要更新在線用戶列表,也就是說服務(wù)器收到用戶登錄的消息之后,需要發(fā)消息(signal,kill)給每一個在線用戶,客戶端在重寫消息的處理函數(shù)中,進(jìn)行用戶列表的打印工作。
    那么,登錄用戶的狀態(tài)信息就應(yīng)該放在共享內(nèi)存中,這樣,服務(wù)器和客戶端都可以訪問。設(shè)計共享內(nèi)存如下:
    IPC之共享內(nèi)存·即時通訊小程序(二)
    假設(shè)最大連接用戶數(shù)位100,可開辟一塊空間,前100個單位存放每個用戶的狀態(tài),后100個單位存放相應(yīng)的用戶信息。每當(dāng)一位新用戶登錄時,服務(wù)器在共享內(nèi)存里尋找一個空閑塊(標(biāo)志為0),比如找到userAddr+3的位置,將標(biāo)志置為1,同時移動pUser指針,指向user3,將用戶信息插入。同理,用戶退出的是時候,需要將標(biāo)志置為0。同時,發(fā)送信號,通知客戶端讀取共享內(nèi)存的內(nèi)容,這樣客戶端就能及時更新用戶列表了。(由于發(fā)送信號的時候,需要遍歷所有用戶的id,所以可以用容器來存儲用戶信息)
    基于上次,具體代碼如下:
    public.h:
    #ifndef _PUBLICH
    #define _PUBLICH

    #include < stdio.h>
    #include < string.h>
    #include < sys/types.h>
    #include < sys/ipc.h>
    #include < sys/msg.h>
    #include < sys/shm.h>
    #include < signal.h>
    #include < string>
    #include < map>
    #include < iostream>
    using namespace std;
    
    //用戶信息結(jié)構(gòu)體
    typedef struct user_t
    {
        pid_t pid;
        char uname[10]; //后面加上用戶名不重名、密碼驗證
    }USER_T;
    
    //登錄消息結(jié)構(gòu)體
    typedef struct login_msg_t
    {
        long type;
        USER_T user;
    }LMSG_T;
    
    //消息隊列:用戶登錄
    #define LOGIN_TYPE          1
    #define EXIT_TYPE           2
    #define MSG_KEY             1000
    #define MSG_SIZE            sizeof(LMSG_T)-sizeof(long)
    
    //共享內(nèi)存:用戶列表(空閑塊:0-空閑,1-占用)
    #define SHM_USER_KEY        1001
    #define MAX_USER            100
    #define SHM_USER_SIZE       MAX_USER + MAX_USER * sizeof(USER_T)
    
    #define SIGNAL_USERS        34
    #endif

    server.cpp:
    #include "public.h"
    int main()
    {
    int msg_id;
    int shm_id;
    LMSG_T loginMsg = {0};
    char userAddr;
    USER_T
    pUser; //用戶真正寫入的地址
    map<int,string> userMap; //用戶列表
    map<int,string>::iterator it;
    int i;

    /*1、創(chuàng)建消息隊列:用戶登錄*/
    msg_id = msgget(MSG_KEY,0);
    if(msg_id == -1)
    {
        msg_id = msgget(MSG_KEY,IPC_CREAT);
        if (msg_id == -1)
        {
            perror("server msgget");
            return -1;
        }
    }
    
    /*2、創(chuàng)建共享內(nèi)存:用戶列表*/
    shm_id = shmget(SHM_USER_KEY,0,0);
    if (shm_id != -1)
    {//已經(jīng)存在,刪除
        shmctl(shm_id,IPC_RMID,NULL);
    }
    shm_id = shmget(SHM_USER_KEY,SHM_USER_SIZE,IPC_CREAT);
    userAddr = (char *)shmat(shm_id,NULL,0);//映射
    pUser = (USER_T *)(userAddr + MAX_USER);
    memset(userAddr,0,SHM_USER_SIZE);//初始化
    
    //一直監(jiān)聽,是否有用戶上線
    while (1)
    {
        memset(&loginMsg,0,sizeof(LMSG_T));
        msgrcv(msg_id,&loginMsg,MSG_SIZE,0,0);  //任何消息都接收
        switch(loginMsg.type)
        {
        case LOGIN_TYPE:
            //登錄
            cout<<"client "<<loginMsg.user.uname<<":"<<loginMsg.user.pid<<" is logining..."<<endl;
            //2.1 將登錄信息寫入共享內(nèi)存(先找到空閑塊)
            for (i = 0 ; i < MAX_USER ; i++)
            {
                if (*(userAddr + i) == 0)
                {
                    //空閑
                    break;
                }
            }
            if (i < MAX_USER)
            {
                *(userAddr + i) = 1;
                *(pUser + i) = loginMsg.user;
                userMap.insert( pair<int,string>(loginMsg.user.pid,loginMsg.user.uname) );
            }
            else
            {
                cout<<"online users are full.\n"<<endl;
                return 1;
            }
            //2.2 發(fā)消息通知所有在線用戶
            for (it = userMap.begin();it != userMap.end();it++)
            {
                kill((*it).first,SIGNAL_USERS);
            }
    
            break;
        case EXIT_TYPE:
            //退出
            cout<<"client "<<loginMsg.user.uname<<":"<<loginMsg.user.pid<<" is exiting..."<<endl;
            for (i = 0 ; i < MAX_USER ; i++)
            {
                if ((pUser+i)->pid == loginMsg.user.pid)
                {
                    *(userAddr+i) = 0;
                    break;
                }
            }
            for (it = userMap.begin();it != userMap.end();it++)
            {
                if ((*it).first == loginMsg.user.pid)
                {
                    continue;   //自己退出,不用再通知自己
                }
                kill((*it).first,SIGNAL_USERS);
            }
            break;
        }
    
    }
    
    return 0;

    }

client.cpp:
#include "public.h"

char *userAddr;
USER_T *pUser;

void PrtUserList(int sig_no)
{
    //讀取共享內(nèi)存里的用戶列表數(shù)據(jù)
    cout<<"==== online users ===="<<endl;
    for (int i = 0 ;i < MAX_USER ; i++)
    {
        if(*(userAddr + i) == 1)
        {
            cout<<(pUser + i)->uname<<endl;
        }
    }
}

int main()
{
    char acBuf[20] = "";
    int msg_id;
    LMSG_T loginMsg = {0};
    char uname[10] = "";
    int shm_id;

    cout<<"------------onlineChat-------------"<<endl;
    cout<<"username:";
    cin>>uname;

    //2.3 注冊消息(放在最前面)
    signal(SIGNAL_USERS,PrtUserList);

    /*2、打開用戶列表共享內(nèi)存(要比寫消息隊列早)*/
    shm_id = shmget(SHM_USER_KEY,0,0);
    if (shm_id == -1)
    {
        perror("client shmget");
        return -1;
    }
    userAddr = (char*)shmat(shm_id,NULL,0);
    pUser = (USER_T*)(userAddr + MAX_USER);

    /*1、打開消息隊列*/
    msg_id = msgget(MSG_KEY,0);
    if(msg_id == -1)
    {
        perror("client msgget");
        return -1;
    }
    //登錄,寫消息隊列
    loginMsg.type = LOGIN_TYPE;     //設(shè)置登錄的消息類型為1
    loginMsg.user.pid = getpid();
    memcpy(loginMsg.user.uname,uname,strlen(uname));
    cout<<loginMsg.user.uname<<" is logining..."<<endl;
    msgsnd(msg_id,&loginMsg,MSG_SIZE,0);

    //等待寫
    while(1)
    {
        putchar('#');
        fflush(stdout);
        scanf("%s",acBuf);
        if (strcmp(acBuf,"exit") == 0)
        {
            cout<<loginMsg.user.uname<<" is exiting..."<<endl;
            break;
        }
    }

    loginMsg.type = EXIT_TYPE;      //設(shè)置退出的消息類型為2
    msgsnd(msg_id,&loginMsg,MSG_SIZE,0);

    return 0;
}

運(yùn)行結(jié)果:
首先運(yùn)行服務(wù)器,然后登陸Julia,再登陸Tomy,再退出Tomy。
IPC之共享內(nèi)存·即時通訊小程序(二)
IPC之共享內(nèi)存·即時通訊小程序(二)
IPC之共享內(nèi)存·即時通訊小程序(二)

至此,可以進(jìn)行聊天的功能設(shè)計了,聊天內(nèi)容同樣存儲在共享內(nèi)存。由于私聊和群聊的功能,我們需要獲取在線用戶列表,所以同樣,客戶端也需要一個容器來暫時存儲用戶信息。(list也可以,直接存放用戶結(jié)構(gòu)體)
代碼如下:
public.h
#ifndef _PUBLICH
#define _PUBLICH

    #include < stdio.h>
    #include < string.h>
    #include < sys/types.h>
    #include < sys/ipc.h>
    #include < sys/msg.h>
    #include < sys/shm.h>
    #include < signal.h>
    #include < string>
    #include < map>
    #include < iostream>
    using namespace std;

    //用戶信息結(jié)構(gòu)體
    typedef struct user_t
    {
        pid_t pid;
        char uname[10]; //后面加上用戶名不重名、密碼驗證
    }USER_T;

    //登錄消息隊列結(jié)構(gòu)體
    typedef struct login_msg_t
    {
        long type;
        USER_T user;
    }LMSG_T;

    //聊天消息結(jié)構(gòu)體
    typedef struct msg_t
    {
        USER_T user;
        char acMsg[100];
    }MSG_T;

    //消息隊列:用戶登錄
    #define LOGIN_TYPE          1
    #define EXIT_TYPE           2
    #define MSG_KEY             1000
    #define MSG_SIZE            sizeof(LMSG_T)-sizeof(long)

    //共享內(nèi)存:用戶列表(空閑塊:0-空閑,1-占用)
    #define SHM_USER_KEY        1001
    #define MAX_USER            100
    #define SHM_USER_SIZE       MAX_USER + MAX_USER * sizeof(USER_T)

    //共享內(nèi)存:聊天內(nèi)容
    #define SHM_MSG_KEY         1002
    #define SHM_MSG_SIZE        sizeof(MSG_T)

    //信號:更新用戶列表,讀消息
    #define SIGNAL_USERS        34
    #define SIGNAL_CHAT         35

    #endif

server.cpp
服務(wù)器這邊只需要再開一塊共享內(nèi)存就可以了

/*3、創(chuàng)建共享內(nèi)存:聊天信息*/
int shm_msg_id = shmget(SHM_MSG_KEY,0,0);
if (shm_msg_id != -1)
{
    shmctl(shm_msg_id,IPC_RMID,NULL);
}
shm_msg_id = shmget(SHM_MSG_KEY,SHM_MSG_SIZE,IPC_CREAT);
char *msgAddr = (char *)shmat(shm_msg_id,NULL,0);
memset(msgAddr,0,SHM_MSG_SIZE);

client.cpp

#include "public.h"

char *userAddr;
USER_T *pUser;

char *msgAddr;
MSG_T *pMsg;

map<int,string> userMap;    //用戶列表
map<int,string>::iterator it;

void PrtUserList(int sig_no)
{
    //讀取共享內(nèi)存里的用戶列表數(shù)據(jù)
    userMap.clear();
    cout<<"==== online users ===="<<endl;
    for (int i = 0 ;i < MAX_USER ; i++)
    {
        if(*(userAddr + i) == 1)
        {
            cout<<(pUser + i)->uname<<endl;
            userMap.insert(pair<int,string>( (pUser+i)->pid, (pUser+i)->uname ));
        }
    }
}

void GetChatMsg(int sig_no)
{
    //讀取共享內(nèi)存里的聊天內(nèi)容
    MSG_T msg = {0};
    msg = *pMsg;
    cout<<"receive msg from "<<msg.user.uname<<" : "<<msg.acMsg<<endl;
}

int main()
{
    char acOrder[20] = "";
    int msg_id;
    LMSG_T loginMsg = {0};
    char uname[10] = "";
    int shm_id;
    char toWho[10] = "";    //聊天對象
    MSG_T msg = {0};    //聊天消息結(jié)構(gòu)體
    char acMsg[100] = "";   //聊天內(nèi)容

    cout<<"------------onlineChat-------------"<<endl;
    cout<<"username:";
    cin>>uname;

    //2.3 注冊消息(放在最前面)
    signal(SIGNAL_USERS,PrtUserList);
    signal(SIGNAL_CHAT,GetChatMsg);

    /*2、打開用戶列表共享內(nèi)存(要比寫消息隊列早)*/
    shm_id = shmget(SHM_USER_KEY,0,0);
    if (shm_id == -1)
    {
        perror("client userlist shmget");
        return -1;
    }
    userAddr = (char*)shmat(shm_id,NULL,0);
    pUser = (USER_T*)(userAddr + MAX_USER);

    /*3、打開聊天信息共享內(nèi)存*/
    int shm_msg_id = shmget(SHM_MSG_KEY,0,0);
    if (shm_msg_id == -1)
    {
        perror("client chatmsg shmget");
        return -1;
    }
    msgAddr = (char *)shmat(shm_msg_id,NULL,0);
    pMsg = (MSG_T *)msgAddr;

    /*1、打開消息隊列*/
    msg_id = msgget(MSG_KEY,0);
    if(msg_id == -1)
    {
        perror("client msgget");
        return -1;
    }
    //登錄,寫消息隊列
    loginMsg.type = LOGIN_TYPE;     //設(shè)置登錄的消息類型為1
    loginMsg.user.pid = getpid();
    memcpy(loginMsg.user.uname,uname,strlen(uname));
    cout<<loginMsg.user.uname<<" is logining..."<<endl;
    msgsnd(msg_id,&loginMsg,MSG_SIZE,0);

    //等待寫
    while(1)
    {
        putchar('#');
        fflush(stdout);
        scanf("%s",acOrder);
        if (strcmp(acOrder,"exit") == 0)    //退出
        {
            cout<<loginMsg.user.uname<<" is exiting..."<<endl;
            loginMsg.type = EXIT_TYPE;      //設(shè)置退出的消息類型為2
            msgsnd(msg_id,&loginMsg,MSG_SIZE,0);
            break;
        }
        else if (strcmp(acOrder,"users") == 0)  //查看在線用戶列表
        {
            kill(getpid(),SIGNAL_USERS);
        }
        else if (strcmp(acOrder,"chat") == 0)   //進(jìn)入聊天模式
        {
            cout<<"to who: ";
            cin>>toWho;
            cout<<"say: ";
            memset(acMsg,0,100);
            cin>>acMsg;

            // 3.1 把聊天內(nèi)容寫進(jìn)共享內(nèi)存
            memcpy(msg.acMsg,acMsg,strlen(acMsg));
            msg.user = loginMsg.user;
            memcpy(msgAddr,&msg,SHM_MSG_SIZE);

            if (strcmp(toWho,"all") == 0)   //群聊
            {
                //通知所有人去讀
                for (it = userMap.begin();it != userMap.end();it++)
                {
                    if ((*it).first != getpid())
                    {
                        kill((*it).first,SIGNAL_CHAT);
                    }
                }
            }
            else    //私聊
            {
                for (it = userMap.begin();it != userMap.end();it++)
                {
                    if (strcmp((*it).second.c_str() , toWho) == 0)
                    {
                        kill((*it).first,SIGNAL_CHAT);
                        break;
                    }
                }
            }
        }
        memset(acOrder,0,sizeof(acOrder));
    }

    //解除映射
    shmdt(&userAddr);
    shmdt(&msgAddr);

    return 0;
}

運(yùn)行結(jié)果:
IPC之共享內(nèi)存·即時通訊小程序(二)

那么,共享內(nèi)存里開辟一塊空間用來存儲一條聊天記錄的話,考慮并發(fā)的問題,如果消息發(fā)送的太快,還來不及讀取,那么消息就會被覆蓋。如圖:讓讀取消息的函數(shù)sleep一下,連續(xù)發(fā)送兩條消息,結(jié)果第一條消息的內(nèi)容會被覆蓋。這是我們不想看到的。
IPC之共享內(nèi)存·即時通訊小程序(二)
所以,又需要用到信號量的知識了,下次復(fù)習(xí)(先寫著聊天試試)~

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI