这个项目是我真正意义上的第一个项目,它的名字是基于UDP实现的群聊系统。
目录:
- 实现功能及原理
- 应用知识与技能
- 项目的具体模块的问题
- 注意的问题
- 心得与体会
一。实现功能及其原理:
1.功能:首先这个项目是在Linux下用C++编写的程序,而且是基于UDP的。它能够实现像QQ一样的简单版群聊,就是多个用户给服务器传输数据,服务器会将所有发送信息到每个人的界面上,同时也可以实现界面的显示和序列化与反序列化。
2.原理:
首先先看一看原理图:
解析:左边三个泛指多个客户端,右边是服务端,大致流程为客户端向服务端发送消息,因为是UDP所以不需要连接,此时服务端收到数据之后,会产生两个线程,线程一先将数据放进在线用户中,再将数据放进数据池里(那个环也可以称为临时资源,它的结构相当于一个环形队列),这样方便操作系统操作并节省资源。而线程二会从数据池里取数据在分发给每个客户端,同时要注意此时的两个线程之间可以看成是经典的消费者和生产者模型,他们之间既有同步也有互斥,同时这里必须要用互斥锁来限制,否则会产生混乱,这个过程就是这个项目的大概流程了。
二。应用知识与技能
1.首先最基础的就是要熟练掌握C++语言,熟悉各种模板容器的应用操作,如string、map、vector。
2.掌握Linux操作系统的各种操作,熟悉Makefile的编写。
3.了解网络,了解UDP协议以及socket网络编程操作。
4.熟悉生产者消费者模型,了解同步与互斥以及多线程和互斥锁的应用。
5.了解jspon,ncurse等开源系统库的基本用法。
三。项目的具体模块
这个项目可以分为以下几个模块:
1.服务端与客户端模块:网络通信模块。
2.数据池模块。
3.公共网络通信模块,也就是畅聊系统的底层公共逻辑,相当于编写协议。
4.window分屏终端模块。
5.Makefile模块。
a)服务器与客户端模块:
(1)服务端:因为我是用UDP编写的程序,所以得知道UDP的特点是面向无连接的不可靠的按数据报传输数据的传输层协议。所以在服务端中的大致过程为先创建一个套接字sock,然后等待服务端发送数据,这里就不需要连接了。接收到数据之后再发送数据分发给客户端,最后关闭套接字就完成了。
(2)客户端:同理,先创建套接字sock,然后向服务端发送数据,接收服务端发送的数据,最后关闭套接字即可。
总之这两个模块就是简单的UDP的简单编程,中间再加一些接收和发送的函数即可。
b)数据池模块:
(1)这里的数据池用来作为临时资源,因为操作系统并不是直接将数据发送到网络里,也不是直接从网络里读取数据,而是先将数据放进一个临时资源里,再由操作系统操作。而这里为什么要用数据池呢,因为当有100百万个用户发送数据来时,若没有数据池,那操作系统会一次一次的建立连接,这样又浪费时间又浪费资源,所以数据池可以解决这个问题,它允许应用程序重复使用一个现有的连接,而不是重新建立一个连接,这样可以大大的节省资源。但同时也需要考虑线程安全的问题,所以得加上互斥锁来控制生产者和消费者的使用。
c)公共网络通信模块:
这个模块的本质就是制定网络通信之间的协议,也就是制定规则。在这个畅聊系统中,最后显示的肯定不只有数据一个东西,肯定还有其他信息,比如名字,时间等,可以想象一下QQ的聊天界面,所以这里会产生一个难题,系统把数据当成一堆数据,我们要怎样分辨各是什么类的消息呢,这里就要用到序列化和反序列化了。序列化就是将好多块数据拼成一块数据,反序列化反之。但是这个想实现起来也不容易,所以就有前辈们制作了一个提供我们直接用的一个库,它就是json库。只要调用这个库的函数就能轻易完成序列化合反序列化的任务了。这样这个公共通信模块就完成了。
d)window模块
这里就需要我们来学习另一种库ncurse,来帮助我们进行分屏终端的任务。要把界面想象成一个坐标系,每个像素点都已一个点,这样就能根据计算来设计界面的样式了。剩下只要掌握ncurse库的函数就行了。
e)Makefile模块
这个模块就是Makefile的编写,首先这次的项目我把每个模块都放在同一路径下的各个目录里,然后再各个目录里编写.h文件和.cpp文件,最后通过Makefile将所有.cpp文件生成.o文件到目录所在的路径下,并且将所有生成的.o文件根据关系在分别与服务端和客户端进行链接生成最后可执行文件在当前路径。这就是Makefile实现的功能了。
四。注意的问题
在这个项目中我遇到了很多的问题,被折磨了很久才解决了它们,现在写出来谈一谈。
1.首先第一个难题就是如何控制两个线程发送和接受数据,也就是如何处理线程安全的问题。在这个项目中,当服务端接收到数据后会产生两个线程,一个往数据池里放数据,一个从里面取数据,这里如果是多个数据传输的话,就会产生混乱,所以需要对两个线程做些什么。我前面也说过这两个线程可以看成是消费者生产者模型,需要互斥锁来限制。所以这里我用的是POSIX信号量来作为锁。然后我们还要清楚两者之间的关系,刚开始两个线程指向同一块地方,这里要把数据池看成是一块一块的,就像我先前发的图一样,然后我设置了两个信号量,一个是关于空间的,一个是关于数据的。当数据池为空时,必须让生产者先动,它先将数据放入,这时有了数据,消费者才可以去取数据,这样循环下去就行了。在这个过程中,要遵守(1)生产者一定在消费者的前面或同一位置,在同一位置时,数据池不是空就是慢了。(2)生产者也不能超过消费者一圈。同时为了实现循环队列这个结构,我用了取余算法让生产者或者消费者走的步数与数据池的容量相等时又回到了起点。这就是大概的理论了,剩下照样编写代码就行了。
2.第二个难题就是序列化和反序列化。
当我还不知道有json这个库时,我想的是自己实现一下序列化和反序列化,大概思路就是序列化时,用容器string的相加即可,不过在每次相加时都加上一个特别的符号作为分隔符。这样当反序列化时就可以遍历数据只要遇到分隔符就将数据分割出来,这样也可以完成反序列化了。但是这个做法有很大的缺陷,若是接受和发送的数据里有这个分隔符那就不好办了,所以说还是直接调用存在的库就行了。
3.第三个难题就是Makefilede的编写了。
因为这次牵扯到很多路径的转化和各种应用,所以使得Makefile变得十分复杂,一度让我感到迷茫,经过反复的学习和请教才渐渐能看懂。
4.最后一个大难题就是window终端显示的问题了。
在这里首先虽然界面窗口的设计很简单,但是想把它与客户端联系起来却很费劲,而且其中函数接口也很多,刚开始也让我无处下手,经过反复的操作与练习后才渐渐熟悉了它们。
五。心得与体会
我的第一个项目总的来说不是很难,但是也不能说很简单,里面还是有一些比较困难的知识,尤其对于我这样的新手来说是个很好的锻炼能力的机会,它帮我巩固了以前学的很多知识,也让我有了实际的应用经验,让我意识到以前只是知道理论,按着概念编写,没有在实际应用中运用过这些东西,这样导致我在遇到具体问题时不知如何将知识联系起来运用,而在项目中,我需要把很多知识都要联系在一起来运用,要考虑的东西也很多,虽然很艰辛,但是就是因为这一点我才能变得更好,也能更加的了解自己哪一块的知识比较欠缺,帮助自己复习遗忘的知识,增强自己总和应用的能力。俗话说的好“the good beginning is half of succession”,经历第一个项目后,肯定会有第二个,这样下去我会变得越来越好,加油!Come on!
附代码:
首先是server服务器:
server.h
#include<iostream> //所用的头文件
#include<string>
#include<map>
#include<vector>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>
#include<sys/types.h>
#include"data_pool.h"
#define SIZE 1024
using namespace std;
class server
{
public:
server(int _port); //构造函数
void server_init(); //初始化函数
void recv_data(std::string &out_string); //接收数据
void send_data(std::stirng &in_string); //发送数据
void broadcast(); //分发函数
~server(); //析构函数
private:
int sock; //套接字
int port; //端口号
data_pool pool; //数据池
sd::map<uint32_t,struct sockaddr_in> online; //在线用户
};
server.cpp //.cpp文件
#include"server.h"
using namespace std;
server::server(int _port) //构造,初始化列表
:port(_port)
{}
void server::server_init() //初始化函数。
{
int sock=socket(AF_INET,SOCK_DGRAM,0); //创建socket套接字
if(sock<0)
{
std::cerr<<"socket error"<<std::endl; //错误返回。
return;
}
struct sockaddr_in ser_addr;
socklen_t len=sizeof(struct sockaddr_in);
ser_addr.sin_family=AF_INET; //确定协议、端口号和IP地址。
ser_addr.sin_port=htons(port);
ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(sock,(struct sockaddr*)&ser_addr,len)<0) //绑定地址。
{
std::cerr<<"bind error"<<std::endl;
return;
}
}
void server::recv_data(std::string &out_string) //接收数据
{
char buf[SIZE]={0};
struct sockaddr_in r_addr;
socklen_t len=sizeof(r_addr);
ssize_t i=recvfrom(sock,buf,SIZE,0,(struct sockaddr*)&r_addr,&len); //接收函数调用。
if(i<0)
{
std::cerr<<"reccvfrom error"<<std::endl;
return -1;
}
else if(i>0)
{
buf[i]=0;
out_string=buf;
pool.put_message(out_string); //把数据放进数据池里。
data d; //定义一个对象。
d.unserialize(out_string); //对这个信息进行反序列化。
if(d.type=="quit") //当接收到停止信号后进行以下操作
{
std::map<uint32_t,struct sockaddr_in>::iterator it=online.find(r_addr.sin_addr.s_addr); //定义一个迭代器
if(it!=online.end())
{
online.erase(it->first); //删除在线用户上的已下线的人。
}
}
else{
online.insert(std::pair<uint32_t,sockaddr_in>(r_addr.sin_addr.s_addr,r_addr)); //否则进行插入新用户。
}
}
}
void server::send_data(const std:: string &in_string,const struct sockaddr_in &r_addr)
{
sendto(sock,in_string.c_str(),in_string.size(),0,(struct sockaddr*)&r_addr,sizeof(r_addr)); //发送数据。
}
void server::broadcast()
{
std::string message;
pool.get_message(message); //从数据池得到数据。
std::map<uint32_t,struct sockaddr_in>::iterator it=online.begin();
for(;it!=online.end();it++)
{
send_data(message,it->second); //循环发送即可。
}
}
server::~server() //析构函数
{
close(sock); //关闭套接字。
port=-1;
}
chatserver.cpp
#include"server.h"
using namespace std;
void* recv_message(void* arg) //读取信息
{
server *s1=(server*)arg;
std::string message;
while(1)
{
s1->recv_data(message);
std::cout<<"debug"<<message<<std::endl;
}
}
void* send_message(void * arg) //发送信息
{
server *s2=(server*)arg;
while(1)
{
s2->broadcast();
}
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
std::cout<<"Usage ./server ip port\n"<<std::endl;
return -1;
}
server s(atoi(argv[1]));
s.ser_init();
pthread_t p1,p2;
pthread_create(&p1,NULL,recv_message,(void*)&s); //创建两个线程
pthread_create(&p2,NULL,send_message,(void*)&s);
pthread_join(p1,NULL);
pthread_join(p2,NULL);
return 0;
}
client客户端:
client.h
#pragma once
#include<iostream>
#include<string>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<stdlib.h>
#define SIZE 1024
using namespace std;
class client
{
public:
client(std::string server_ip,int server_port); //构造函数
void client_init(); //初始化
void recv_data(std::string& out_string); //接收信息
void send_data(std::string& in_string); //发送信息
~client();
private:
int sock; //套接字
struct sockaddr_in server; //服务器的对象
};
client.cpp
#include"client.h"
client::client(std::string server_ip,int server_port)
{
server.sin_family=AF_INET; //分配协议、端口号以及IP地址。
server.sin_port=server_port;
serever.sin_addr.s_addr=inet_addr(server_ip.c_str);
}
void client::client_init()
{
sock=socket(AF_INET,SOCK_DGRAM,0); //创建套接字
if(sock<0)
{
std::cerr<<"socket error"<<std::endl;
return -1;
}
}
void client::recv_data(std::string &out_string)
{
char buf[SIZE];
struct sockaddr_in cli_addr;
socklen_t len=sizeof(struct sockaddr_in);
size_t i=recvfrom(sock,buf,SIZE,0,(struct sockaddr*)&cli_addr,&len); //读取数据
if(i>0)
{
buf[i]=0;
out_string=buf;
}
}
void client::send_data(std::string &in_string)
{
sendto(sock,in_string.c_str(),in_string.size(),0,(struct sockaddr*)&cli_addr,sizeof(cli_addr)); //发送数据
}
client::~client()
{
close(sock);
}
chatclient.cpp
#include<pthread.h>
#include"client.h"
#include"data.h"
#include<signal.h>
#include<vector>
#include"window.h"
volatile int is_quit=0;
using namespace std;
typedef struct{ //创建结构体。
client *clip;
window *win;
std::string name;
std::string sex;
std::string school;
}client_t;
client_t wc;
std::vector<string> friends;
static void adduser(std::string &f) //添加用户
{
std::vector<string>::iterator it=friends.begin(); //创建迭代器
for(;it!=friends.end();it++)
{
if(*it==f)
{
return;
}
}
friends.push_back(f); //尾插
}
static void deluser(std::string &f) //删除用户
{
std::vector<string>::iterator it=friends.begin();
for(;it!=friends.end();it++)
{
if(*it==f)
{
friends.erase(it);
}
}
}
void* run_header(void* arg) //首行运行函数
{
client_t *wcp=(client_t)arg;
window *wp=wcp->win;
wp->draw_header(); //画出首行
std::string title="This is the best chatting room"; //标题
int i=1;
int y,x;
int dir=0;
while(1)
{
wp->draw_header();
getmaxyx(wp->get_header(),y,x); //得到任意时刻的横纵坐标
wp->put_string_to_window(wp->get_header(),y/2,i,title); //打印首行
if(i>x-title.size()-2) //限制首行移动范围
{
dir=1;
}
if(i<=2)
{
dir=0;
}
if(dir==0)
{
i++;
}
else{
i--;
}
}
}
void* run_mid(void* arg) //输出和朋友列表的显示
{
client_t *wcp=(client_t)arg;
window *wp=wcp->win;
client *cp=wcp->clip;
wp->draw_output(); //先分别画出来
wp->draw_fri();
int y,x;
int i=1;
std::string out_string;
data d;
std::string show_string;
while(1)
{
cp->recv_data(out_string);
d.unserialize(out_string); //反序列化
show_string=d.name; //拼接起来
show_string+="-";
show_string+=d.sex;
show_string+="-";
show_string+=d.school;
if(d.type=="quit") //若收到停止信号直接删除用户。
{
deluser(show_string);
}
else{
adduser(show_string);
show_string+="#";
show_string+=d.message;
if(i>y-2)
{
i=1;
wp->draw_output();
}
getmaxyx(wp->get_output(),y,x);
wp->put_string_to_window(wp->get_output(),i++,2,out_string); //打印输出
}
//fri
wp->draw_fri();
int j=0;
for(;j<friends.size();j++)
{
wp->put_string_to_window(wp->get_fri(),j+1,2,friends[j]); //打印朋友列表
}
}
}
void* run_input(void* arg) //输入显示
{
client_t *wcp=(client_t)arg;
window *wp=wcp->win;
client *cp=wcp->clip;
std::string tips="please input#"; //提示符
std::string str;
data d;
wp->draw_input();
std::string out_string;
while(1)
{
wp->put_string_to_window(wp->get_input(),1,2,message);
wp->get_string_from_window(wp->get_input(),str);
d.name=wcp->name;
d.sex=wcp->sex;;
d.school=wcp->school;
d.message=str;
d.type="none";
d.serialize(out_string); //序列化
cp->send_data(out_string);
wp->draw_input(); //刷新输入界面
}
}
void send_quit(int s) //发送停止信号
{
data d;
d.name=wc.name;
d.sex=wc.sex;
d.school=wc.school;
d.message="None";
d.type="quit";
std::string out_string;
d.serialize(out_string);
wc.clip->send_data(out_string);
is_quit=1;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
std::cout<<"Usage :./client ip port"<<std::endl;
return -1;
}
std::cout<<"please enter your name";
std::cin>>wc.name;
std::cout<<"please enter your sex";
std::cin>>wc.sex;
std::cout<<"please enter your school";
std::cin>>wc.school;
signal(SIGINT,send_quit);
client cli(argv[1],atoi(argv[2]));
cli.client_init();
window w;
ec.clip=&cli;
wc.win=&w;
pthread_t header,mid,input;
pthread_create(&header,NULL,run_header,(void*)&wc);
pthread_create(&mid,NULL,run_mid,(void*)&wc);
pthread_create(&input,NULL,run_input,(void*)&wc);
while(!is_quit)
{
sleep(1);
}
return 0;
}
data_pool数据池:
data_pool.h
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <semaphore.h>
#define NUM 256
class data_pool{
public:
data_pool(int _cap = NUM); //构造
void get_message(std::string &out_message); //得到数据相当于消费者
void put_message(const std::string &in_message); //传送信息相当于生产者
~data_pool(); //析构
private:
std::vector<std::string> pool;
int cap; //容量
int buy_step; //消费者的步数
int sell_step; //生产者步数
sem_t space_sem; //信号量空间
sem_t message_sem; //信号量数据
};
data_pool.cpp
#include"data_pool.h"
data_pool::data_pool(int _cap=NUM) //构造,初始化列表
:cap(_cap)
,pool(_cap)
{
buy_step=0;
sell_step=0;
sem_init(&space_sem,0,_cap); //信号量的初始化
sem_init(&message_sem,0,0);
}
void data_pool::get_message(std::string &out_message) //消费者取数据
{
sem_wait(&message_sem); //等待有数据了在执行
out_message=pool[buy_step]; //从数据池取数据
sem_post(&space_sem); //发送有空间的信号
buy_step++;
buy_step%=cap; //因为是环形队列
}
void data_pool::put_message(const std::string &in_message) //生产者放数据
{
sem_wait(&space_sem); //等待有空间了
pool[sell_step]=in_message; //往数据池放数据
sem_post(&message_sem); //发送有数据的信号
sell_step++;
sell_step%=cap;
}
data_pool::~data_pool() //析构
{
sem_destroy(&space_sem); //信号量用完必须销毁
sem_destroy(&message_sem);
}
序列化和反序列化:
data.h
#include<iostream>
#include<string>
#include<json/json.h>
using namespace std;
class data
{
public:
data();
void serialize(std::string &out_string); //序列化
void unserialize(std::string &in_string); //反序列化
~data();
private:
std::string name;
std::string sex;
std::string school;
std::string message;
std::string type;
};
data.cpp
#include"data.h"
using namespace std;
data::data()
{
}
void data::serialize(std::string &out_string) //序列化
{
Json::value root; //创造json对象
root["name"]=name; //赋值
root["sex"]=sex;
root["school"]=school;
root["message"]=message;
root["type"]=type;
#ifdef FAST //条件编译
Json::FastWrite w; //一行打印
#else
Json::styledWrite; //有格式打印
#endif
out_string=w.write(root);
}
void data::unserialize(std::string &in_string) //反序列化
{
Json::value root;
Json::Reader r;
r.parse(in_string,root,false);
name=root["name"].asString;
sex=root["ssex"].asString;
school=root["school"].asString;
message=root["message"].asString;
type=root["type"].asString;
}
data::~data()
{}
最后一个窗口模块:
window.h
#pragma once
#include<iostream>
#include<ncurses.h>
#define SIZE 1024
using namespace std;
class window
{
public:
window();
void get_string_from_window(WINDOW *w,std::string &out_string); //打印在屏幕上
void put_string_to_window(WINDOW *w,int y,int x,std::string &message); //得到信息
void draw_header(); //画出首行部分
void draw_output(); //画出输出部分
void draw_fri(); //画出朋友列表部分
void draw_input(); //画出输入部分
WINDOW *get_header(); //得到首行
WINDOW *get_output(); //得到输出
WINDOW *get_fri(); //得到朋友列表
WINDOW *get_input(); //得到输入
void setcolor();
~window();
private:
WINDOW *header;
WINDOW *output;
WINDOW *fri;
WINDOW *input;
};
window.cpp
#include"window.h"
#include<unistd.h>
using namespace std;
window::window()
{
initscr();
start_color();
}
WINDOW *window::get_header()
{
return header;
}
WINDOW *window::get_output()
{
return output;
}
WINDOW *window::get_fri()
{
return fri;
}
WINDOW *window::get_input()
{
return input;
}
void window::get_string_from_window(WINDOW *w,std::string &out_string)
{
char buf[SIZE];
wgetnstr(w,buf,SIZE);
out_string=buf;
}
void window::put_string_to_window(WINDOW *w,int y,int x,std::string &message)
{
mvwaddstr(w, y, x, message.c_str());
wrefresh(w);
}
void window::draw_header()
{
int y=0;
int x=0;
int h=LINES/5;
int w=COLS;
header=newwin(h,w,y,x);
box(header,0,'-');
wrefresh(header);
}
void window::draw_output()
{
int y=LINES/5;
int x=0;
int h=LINES*3/5;
int w=COLS*3/4;
output=newwin(h,w,y,x);
box(output,'|','-');
wrefresh(output);
}
void window::draw_fri()
{
int y=LINES/5;
int x=COLS*3/4;
int h=LINES*3/5;
int w=COLS/4;
fri=newwin(h,w,y,x);
box(fri,'|','-');
wrefresh(fri);
}
void window::draw_input()
{
int y=LINES*4/5;
int x=0;
int h=LINES/5;
int w=COLS;
input=newwin(h,w,y,x);
box(input,'|','-');
wrefresh(input);
}
感谢参观!