基于UDP和环形队列实现的多人聊天室

1.多人聊天系统功能简介

多人聊天系统从名字就可卡出它的功能是支持多个人一起聊天,相当于qq的群聊功能。下边是该系统的主要工作流程:

  • 一个客户端将消息发送到网络中。
  • 服务器中存在两个线程,一个生产者线程用来接收网络中的数据,并且将这个消息放到服务器维护的一个数据池(环形队列)中。该环形队列是模拟生产者-消费者模型实现线程的同步与互斥。
  • 服务器的另一个消费者线程是为了从数据池(环形队列)中拿到消息,然后转发给在线列表中的所有好友。因为采用的是UDP协议,所以服务器必须维护一张在线列表,要来存储各个客户端的信息。
  • 当某个客户端退出时,客户端会向服务器发送一条特殊的退出消息,服务器解析此消息后将该消息转发给其他客户端并且将此客户端从在线列表中删除,其他客户端收到消息后,也从自己维护的在线列表中删除掉该好友,然后在重新绘制窗口即可。

2.项目使用的第三方库

  • jsoncpp:实现序列化和反序列化
  • ncurses:用来绘制客户端的界面

3.多人聊天系统的原理图

在这里插入图片描述

  • 服务器基于环形队列模拟生产者-消费者模型,生产者线程从网络中拿数据存到数据池中,消费者线程从数据池中拿数据发送到网络中,从而达到转发的目的。

基于环形队列的生产者-消费者模型:
使用空格信号量和数据信号量来实现生产者-消费者的同步和互斥。生产者步伐==消费者步伐时,存在两种情况:

  • 数据池为空时,当消费者申请数据信号量时,申请失败,所以此时只能生产者先生产。
  • 数据池为满的时候,当生产者申请空格信号量的时候,申请失败,此时只能消费者先消费。
  • 其他情况,只要保证消费者步伐跟在生产者步伐的后边,并且不会被套一个圈就可以保证同步与互斥。
#pragma once 

#include <iostream>
#include <vector>
#include <semaphore.h>

#define NUM 256

class DataPool
{
public:
  DataPool(int _cap = NUM);

  void GetMess(std::string &out_mess);    // 从数据池中拿数据

  void PutMess(const std::string &in_mess);// 向数据池中放数据

  ~DataPool();

private:
  std::vector<std::string> pool;
  int cap;          //环形队列的长度
  int consume_step; //消费者步伐
  int product_step; //生产者步伐
  sem_t blank_sem;  //空格信号量
  sem_t data_sem;   //数据信号量
};

注:基于环形队列的生产者-消费者模型以前我在学习线程部分实现过了,github链接:https://github.com/hansionz/Linux_Code/tree/master/pthread/ring_cp

4.服务器维护的在线列表

本项目的通信采用的是面向无连接的UDP协议,所以服务器要想其他客户端转发就必须维护一张在线列表来记录客户端的地址信息。本项目中,采用map结构来存储客户端ip地址和struct sockaddr的映射关系:

#pragma once 

#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include  <map>
#include <string>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <data_pool.h>

#define SIZE 1024 //发送信息的最大长度

class UdpServer
{
public:
  UdpServer(uint16_t _port);

  void InitServer();

  //服务器收数据
  void RecvData(std::string &out_string);

  //服务器向目的地址单向发送消息
  void SendData(const std::string &in_string, const struct sockaddr_in &peer);

  //服务器向在线列表中的所有人发送消息
  void BroadCast();

  ~UdpServer();

private:
  int sock;
  int port;
  DataPool pool;
  std::map<uint32_t, struct sockaddr_in> online;
};

5.使用ncurses库绘制客户端

在本项目中,我们采用ncurses库为客户端绘制图形界面,绘制结果如下:
在这里插入图片描述

  • 实现细节:
#pragma once 

#include <iostream>
#include <ncurses.h>

#define SIZE 1024

class Window
{
public:
  Window();
  //绘制4个窗口
  void DrawHeader();
  void DrawOutput();
  void DrawInput();
  void DrawOnline();
  //向该窗口写入字符串(y:高度,纵坐标 x:宽度,横坐标)
  void PutWindow(WINDOW *w, int y, int x,std::string& message);  
  //从该窗口获得字符串
  void GetWindow(WINDOW *w, std::string &out_string);

  //获得4个窗口的句柄
  WINDOW* GetHeader();
  WINDOW* GetOutput();
  WINDOW* GetInput();
  WINDOW* GetOnline();
  ~Window();
private:
  WINDOW *header;
  WINDOW *output;
  WINDOW *input;
  WINDOW *online;
};

6.序列化和反序列化

为什么要对输入的消息进行序列化和反序列化呢?
本群聊系统,当多个客户端给服务器发送消息的时候,我们不能只发送消息的内容,同时也要附带上消息的来源等信息,所以客户端非服务器发送的消息其实是由用户的信息和消息组成的。那既然发送的消息是一个结构体,而在网络传输时传的是字符串,当然我们可以直接将结构体中的成员拼接成一个字符串然后发送出去,但是在服务器是很难解析出来的,这时我们就需要对发送的数据进行序列化和反序列化。我们先将要发送的结果体序列化为一个字符串,然后发送到服务器,服务器对接收到的字符串进行反序列化成一个结构体,然后在把消息拼接好进行序列化转发给在线列表中的所有客户端。

#pragma once 

#include <iostream>
#include <string>
#include <value.h>
#include <write.h>
#include <read.h>

// nick_name("zs") , school("XUST"), message("hello"), type("none") 
// json串:{nick_name:"zs",school:"XUST",message:"hello",type:"None"}" 

class Data
{
public:
  Data();
  //序列化
  void Serialize(std::string &out_string);
  //反序列化
  void Unserialize(std::string &in_string);
  ~Data();
public:
  std::string nick_name;
  std::string school;
  std::string message;
  std::string type;      //保留字段,用来做退出处理
};

7.客户端退出问题

客户端设置了一个is_stop标志位,然后我们将SIGINT信号的处理动作注册为SendQuit函数,当一个客户端退出ctrl+c时,客户端将type字段设置为quit,服务器接收到该消息后,先将该消息放到数据池中转发给客户端,然后将该消息反序列化后判断type是否==“quit”,如果等于则从维护的在线列表中删除该好友,当客户端接收到消息在反序列化后发现该字段为空后,将该好友从在线列表删除,至此,完成了客户端退出的工作。

8.项目存在的问题

  • 采用UDP协议,如果发生丢包该怎么办?
  • 如果一个客户端的退出消息丢失该怎么处理?
  • 可以将TCP协议中的一些保证可靠性的措施引入到项目中

9.项目扩展

  • 可以模拟实现一个登录界面
  • 可以根据ncurses库来实现,点击右边在线列表中的某个好友单聊
  • 引入可靠机制保证可靠性

10.项目源码

https://github.com/hansionz/UdpChatSystem

发布了221 篇原创文章 · 获赞 200 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/hansionz/article/details/86764173