您好,登錄后才能下訂單哦!
上次說到,使用消息隊列可以做到簡易的登錄、退出功能。那么,該思考一下,聊天的用戶列表和聊天記錄應該存在哪兒呢?當然是服務器上,那么,就需要用到共享內存了。
共享內存涉及到的函數,與消息隊列相似,如下
#include < sys/ipc.h>
#include < sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
作用:用來創建共享內存
void *shmat(int shmid, const void *shmaddr, int shmflg);
作用:共享內存剛被創建的時候,任何進程還都不能訪問它,為了建立對這個共享內存段的訪問渠道,必須由我們來把它連接到某個進程的地址空間,shmat函數就是用來完成這項工作的。
int shmdt(const void *shmaddr);
作用:把共享內存與當前進程脫離開
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
作用:共享內存的控制函數
返回值:成功-‘0’;失敗-‘-1’
cmd將要采取的動作有以下三個值:
即
寫一段測試代碼練練:
shmwrite.c
#include < stdio.h>
#include < sys/msg.h>
#include < sys/ipc.h>
#include < sys/types.h>
int main()
{
int shmid;
char * addr;
//先檢測該key的共享內存是否存在,如果存在,將原來的刪除,重建
shmid = shmget(1000,0,0);
if(shmid != -1)
{
//已經存在,刪除
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的共享內存是否存在,如果存在,將原來的刪除,重建
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;
}
運行:
查看ipc狀態:
接下來,可以繼續我們的小程序了。當有用戶上線的時候,每個客戶端都需要更新在線用戶列表,也就是說服務器收到用戶登錄的消息之后,需要發消息(signal,kill)給每一個在線用戶,客戶端在重寫消息的處理函數中,進行用戶列表的打印工作。
那么,登錄用戶的狀態信息就應該放在共享內存中,這樣,服務器和客戶端都可以訪問。設計共享內存如下:
假設最大連接用戶數位100,可開辟一塊空間,前100個單位存放每個用戶的狀態,后100個單位存放相應的用戶信息。每當一位新用戶登錄時,服務器在共享內存里尋找一個空閑塊(標志為0),比如找到userAddr+3的位置,將標志置為1,同時移動pUser指針,指向user3,將用戶信息插入。同理,用戶退出的是時候,需要將標志置為0。同時,發送信號,通知客戶端讀取共享內存的內容,這樣客戶端就能及時更新用戶列表了。(由于發送信號的時候,需要遍歷所有用戶的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;
//用戶信息結構體
typedef struct user_t
{
pid_t pid;
char uname[10]; //后面加上用戶名不重名、密碼驗證
}USER_T;
//登錄消息結構體
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)
//共享內存:用戶列表(空閑塊: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、創建消息隊列:用戶登錄*/
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、創建共享內存:用戶列表*/
shm_id = shmget(SHM_USER_KEY,0,0);
if (shm_id != -1)
{//已經存在,刪除
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);//初始化
//一直監聽,是否有用戶上線
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 將登錄信息寫入共享內存(先找到空閑塊)
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 發消息通知所有在線用戶
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)
{
//讀取共享內存里的用戶列表數據
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、打開用戶列表共享內存(要比寫消息隊列早)*/
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; //設置登錄的消息類型為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; //設置退出的消息類型為2
msgsnd(msg_id,&loginMsg,MSG_SIZE,0);
return 0;
}
運行結果:
首先運行服務器,然后登陸Julia,再登陸Tomy,再退出Tomy。
至此,可以進行聊天的功能設計了,聊天內容同樣存儲在共享內存。由于私聊和群聊的功能,我們需要獲取在線用戶列表,所以同樣,客戶端也需要一個容器來暫時存儲用戶信息。(list也可以,直接存放用戶結構體)
代碼如下:
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;
//用戶信息結構體
typedef struct user_t
{
pid_t pid;
char uname[10]; //后面加上用戶名不重名、密碼驗證
}USER_T;
//登錄消息隊列結構體
typedef struct login_msg_t
{
long type;
USER_T user;
}LMSG_T;
//聊天消息結構體
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)
//共享內存:用戶列表(空閑塊:0-空閑,1-占用)
#define SHM_USER_KEY 1001
#define MAX_USER 100
#define SHM_USER_SIZE MAX_USER + MAX_USER * sizeof(USER_T)
//共享內存:聊天內容
#define SHM_MSG_KEY 1002
#define SHM_MSG_SIZE sizeof(MSG_T)
//信號:更新用戶列表,讀消息
#define SIGNAL_USERS 34
#define SIGNAL_CHAT 35
#endif
server.cpp
服務器這邊只需要再開一塊共享內存就可以了
/*3、創建共享內存:聊天信息*/
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)
{
//讀取共享內存里的用戶列表數據
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)
{
//讀取共享內存里的聊天內容
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}; //聊天消息結構體
char acMsg[100] = ""; //聊天內容
cout<<"------------onlineChat-------------"<<endl;
cout<<"username:";
cin>>uname;
//2.3 注冊消息(放在最前面)
signal(SIGNAL_USERS,PrtUserList);
signal(SIGNAL_CHAT,GetChatMsg);
/*2、打開用戶列表共享內存(要比寫消息隊列早)*/
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、打開聊天信息共享內存*/
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; //設置登錄的消息類型為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; //設置退出的消息類型為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) //進入聊天模式
{
cout<<"to who: ";
cin>>toWho;
cout<<"say: ";
memset(acMsg,0,100);
cin>>acMsg;
// 3.1 把聊天內容寫進共享內存
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;
}
運行結果:
那么,共享內存里開辟一塊空間用來存儲一條聊天記錄的話,考慮并發的問題,如果消息發送的太快,還來不及讀取,那么消息就會被覆蓋。如圖:讓讀取消息的函數sleep一下,連續發送兩條消息,結果第一條消息的內容會被覆蓋。這是我們不想看到的。
所以,又需要用到信號量的知識了,下次復習(先寫著聊天試試)~
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。