上次说到,使用消息队列可以做到简易的登录、退出功能。那么,该思考一下,聊天的用户列表和聊天记录应该存在哪儿呢?当然是服务器上,那么,就需要用到共享内存了。

共享内存

  • 共享内存允许两个不相关的进程去访问同一部分逻辑内存
  • 如果需要在两个运行中的进程之间传输数据,共享内存将是一种效率极高的解决方案
  • 共享内存是由IPC为一个进程创建的一个特殊的地址范围,它将出现在进程的地址空间中。
  • 其他进程可以把同一段共享内存段“连接到”它们自己的地址空间里去。
  • 所有进程都可以访问共享内存地址,就好像它们是有malloc分配的一样
  • 如果一个进程向这段共享内存写了数据,所做的改动会立刻被有权访问同一段共享内存的其他进程看到

共享内存函数

共享内存涉及到的函数,与消息队列相似,如下

shmget

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

作用:用来创建共享内存

  • key:这个共享内存段的名字
  • size:需要共享的内存大小
  • shflg:由9个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
  • 返回值:成功-返回一个非负整数,即该段共享内存的标识码,失败-返回‘-1’

shmat

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

作用:共享内存刚被创建的时候,任何进程还都不能访问它,为了建立对这个共享内存段的访问渠道,必须由我们来把它连接到某个进程的地址空间,shmat函数就是用来完成这项工作的。

  • shmid:shmget返回的共享内存标识
  • shmaddr:把共享内存连接到当前进程去的时候准备放置它的那个地址
  • shmflg:shmflg是一组按位OR(或)在一起的标志。它的两个可能取值是SHM_RND和SHM_RDONLY
  • 返回值:成功-返回一个指针,指针指向共享内存的第一个字节;失败-返回‘-1’ shmaddr为0,核心自动选择一个地址 shmaddr不为0且shmflg无SHM_RND标记,则以shmaddr为连接地址。 shmaddr不为0且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA) shmflg=SHM_RDONLY,表示连接操作用来只读共享内存 在fork() 后,子进程继承已连接的共享内存

shmdt

int shmdt(const void *shmaddr);

作用:把共享内存与当前进程脱离开

  • shmaddr:由shmat返回的地址指针
  • 成功-返回‘0’;失败-返回“-1”
  • 脱离共享内存并不等于删除它,只是当前进程不能再继续访问它

shmctl

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

作用:共享内存的控制函数

  • shmid:由shmget返回的共享内存标识码
  • cmd:将要采取的动作
  • 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 PUBLIC_H #define PUBLIC_H

	#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 PUBLIC_H #define PUBLIC_H

	#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一下,连续发送两条消息,结果第一条消息的内容会被覆盖。这是我们不想看到的。 所以,又需要用到信号量的知识了,下次复习(先写着聊天试试)~