1、客户端
从标准输入读出数据之后,将数据进行序列化,发送至网络,其中客户端也采用了多线程实现,创建了3个线程用于实现三部分不同的功能:头部,输出框,输入框,三个线程之间具有同步的关系
那么为什么要实现序列化与反序列化呢?请见文章最后的解释
2、服务器端
(1)服务器端使用多线程+(生产者、消费者模型)(该部分主要是通过posix信号量机制实现的多线程下的生产者和消费者模型)
(2) 其中生产者(也就是主线程)不停地读取网络中传来的数据,将数据信息放到数据池中,再将用户信息加入好友列表中
消费者(新创建的线程)从数据池中读取数据,不停地并将信息广播给所有在线的好友用户
(3)服务器端还采用了map容器,map容器内部自建议一棵红黑树,具有自动排序的功能,可以提供一一映射的功能,序列号和在线用户之间为一一对应的映射关系。数据池主要是通过vector来实现的,可以动态的增加数据。
3、JSON是JavaScript Object Notation的缩写,它是一种数据交换格式。它是比XML(数据交换格式,适合在网络上传输数据)更加轻量级的数据交换格式。实现数据交换的原理在于,首先我们需要将字符串对象进行序列化,序列化成json字符串,这样才能通过网络传递给其它计算机。当我们接收到一个json字符串时,则需要将其反序列化为字符串对象,
开发环境:centos7(64位操作系统)
编程语言:C语言、C++
序列化和反序列化工具:jsoncpp
进行窗口设计的框架:ncurse,ncurses
操作系统知识:生产者和消费者模型、posix信号量机制、多线程、socket套接字网络编程
client模块:从标准输入读取用户数据信息,将字符串序列化,发送给服务器;将接收到的数据进行反序列化,输出到标准输出
comm模块:使用到了json库,可以实现数据的序列化和反序列化
server模块:收到用户发送的字符串后,将用户信息存储到用户列表中,将数据存储到数据池中,再将数据广播给所有在线用户。服务器端要转发数据给客户端,所以需要维护一张用户列表,该系统使用map实现,使用用户的ip作为key值,使用sockaddr_in作为value值
data_pool模块:服务器端维持数据池,从数据池中存取数据,数据池实际上是基于生产者和消费者模型的环形队列
window模块:实现客户端的界面,使用到了ncurse库
lib模块:提供第三方库模块
conf模块:提供server的配置文件
plugin模块:启停服务器的脚本文件
因为不能将客户端输入的内容直接发送给服务器端,是因为用户比较多时,服务器端无法区分消息是由哪个用户所发的, 因此我们需要给客户端发送的每条消息都附加上当前用户的信息。所以服务器端收到的来自客户端发送的消息(是由用户信息和输入框输入的消息拼接而成的)
其次用户退出时,服务器要将该用户从用户列表中删除,因此在拼接信息时增加一个cmd字段,来表示客户端的状态
//udp_client.h文件,
#ifndef _udp_client_H_ //ifdef条件编译,确保头文件的编译只进行一次,避免重复编译
#define _udp_client_H_
#include
#include
#include
#include
#include
//udp_client.cpp文件
#include "udp_client.h"
//定义构造函数,利用传入的参数(_ip,_port)进行赋值,不做其他操作
udp_client::udp_client(const std::string& _ip,int _port):ip(_ip),port(_port),sock(-1)
{}
//初始化成员函数定义:创建套接字文件,返回该文件的文件描述符
int udp_client::init_client()
{
sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0)
{
write_log("socket error",FATAL);
return -1;
}
}
//从server端接收数据,开辟缓冲区buf,存放读取到的内容,使用recvfrom函数读取消息,带返回作用的参数out指向buf的首地址
int udp_client::recv_msg( std::string& out)
{
char buf[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int ret = recvfrom(sock,buf,sizeof(buf)-1,0,\
(struct sockaddr*)&peer,&len);
if(ret > 0)
{
buf[ret] = 0;
out = buf;
return 0;
}
return -1;
}
//发送数据,定义存放ipv4套接字类型的结构体server,存放服务器端套接字相关信息之前,要先使用htons()将端口号由整型转化为网络字节序列,使用inet_addr()将ip字符串转化为整型形式
int udp_client::send_msg(const std::string & in)
{
struct sockaddr_in server;
server.sin_family =AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
//使用sendto函数(参数:1.是建立起连接的套接字文件描述符,2.传送的消息3.希望传送到的网络地址)
int ret = sendto(sock,in.c_str(),in.size(),0,\
(struct sockaddr*)&server,sizeof(server));
if(ret < 0 )
{
write_log("client send_msg errror",WARNING);
return -1;
}
return 0;
}
//析构函数,sock大于0时,代表该套接字文件存在时
udp_client::~udp_client()
{
if(sock >0)
close(sock);
}
//chat_client.cpp文件
#include "udp_client.h"
#include "data.h"
#include "window.h"
typedef struct net_window
{//定义结构体存放用户信息和当前页面信息
udp_client *cp;
window *wp;
}net_window_t,*net_window_p;
//定义昵称,学校,定义vector容器存放好友列表,可以动态增加好友
std::string name;
std::string school;
std::vector fl;
udp_client *qclient = NULL;
int flag = 1;//退出标志
//提示信息:输入ip和端口号
void usage(const char* arg)
{
std::cout<<"Usage: "<send_msg(out);//发送消息
endwin();
exit(1);
}
void* show_header(void *arg)//顶部部分展示界面
{
net_window_p obj = (net_window_p)arg;
window *winp = obj->wp;//当前的页面信息
udp_client *clientp = obj->cp;//当前的用户信息
winp->create_header();//创建窗口的头部
wrefresh(winp->header);//用于刷新窗口的头部
int x=0,y=0;
getmaxyx(winp->header,y,x);
std::string msg = "welcome to chat sysytem";
int i=1;
//跑马灯实现方法,先放置消息,然后清屏重画,产生滚动播放的效果
while(1)
{
winp->put_str_to_win(winp->header,y/2,i++,msg);
wrefresh(winp->header);
usleep(200000);
winp->clear_win_line(winp->header,y/2,1);
if(i == x - msg.length())
i=1;
winp->create_header();
wrefresh(winp->header);
}
}
//添加好友,采用迭代器遍历容器,直至容器的最后位置,再使用push_back加入新用户
void add_user(std::string& user)
{
std::vector::iterator iter = fl.begin();
for(;iter!= fl.end();++iter)
{
if(user == *iter)
return ;
}
fl.push_back(user);
}
//删除用户,利用迭代器遍历,当找到与想要删除的user匹配时,调用erase函数进行删除
void del_user(std::string& user)
{
std::vector::iterator iter = fl.begin();
for(;iter!= fl.end();)
{
if(user == *iter)
{
iter = fl.erase(iter);
break;
}
else
++iter;
}
}
void* show_output_fl(void *arg)
{
net_window_p obj = (net_window_p)arg;
window *winp = obj->wp;
udp_client *clientp = obj->cp;
//显示输出窗口:读取数据,反序列化
data r;
std::string r_str;
std::string show_str;
std::string friends;
int i = 1,j =1;
int x =0,y=0;
int fx=0,fy=0;
winp->create_output();
winp->create_friends_list();
wrefresh(winp->output);
wrefresh(winp->friends_list);
while(1)
{
//读取数据,反序列化
clientp->recv_msg(r_str);
r.string_to_data(r_str);
//判断是否为退出的客户端
//构建输出语句和朋友列表信息
show_str = r.nick_name;
show_str += "- ";
show_str += r.school;
friends = show_str;
show_str += "# ";
show_str += r.msg;
if(r.cmd == "QUIT")
{
del_user(friends);
}
else
{
add_user(friends);
//输出到output窗口
winp->put_str_to_win(winp->output,i++,1,show_str);
wrefresh(winp->output);
//判断是否输满
getmaxyx(winp->output,y,x);
if(i == y-1)
{
i=1;
usleep(200000);
winp->clear_win_line(winp->output,1,y-1);
winp->create_output();
wrefresh(winp->output);
}
}
//显示好友列表
std::vector::iterator iter = fl.begin();
for(;iter!= fl.end();++iter)
{
winp->put_str_to_win(winp->friends_list,j++,1,*iter);
wrefresh(winp->friends_list);
getmaxyx(winp->output,fy,fx);
if(j == fy-1)
{
j=1;
winp->clear_win_line(winp->friends_list,1,fy-1);
winp->create_friends_list();
wrefresh(winp->friends_list);
}
}
j=1;
usleep(200000);
}
}
void* show_input(void *arg)
{
net_window_p obj = (net_window_p)arg;
window *winp = obj->wp;
udp_client *clientp = obj->cp;
std::string str = "Please Enter# ";
std::string out;
data w;
w.nick_name = name;
w.school = school;
w.cmd = "";
while(1)
{
winp->create_input();
winp->put_str_to_win(winp->input,1,2,str);
wrefresh(winp->input);
winp->get_str(winp->input,w.msg);
//序列化,发送
w.data_to_string(out);
clientp->send_msg(out);
//清屏
winp->clear_win_line(winp->input,1,1);
winp->create_input();
wrefresh(winp->input);
}
}
int main(int argc,char*argv[])
{
if(argc != 3)
{
usage(argv[0]);
return -1;
}
std::cout<<"please enter nick_name:";
std::cin>>name;
std::cout<<"please enter school:";
std::cin>>school;
udp_client client(argv[1],atoi(argv[2]));//定义client对象,调用构造函数进行初始化ip地址和端口号
client.init_client();//进行初始化
window win;
net_window_t nw={&client,&win};//nw对象用于存放当前的用户界面信息和用户信息
qclient = &client;
// 客户端需要创建三个线程,完成每一模块的工作
1. 头部header标题的功能是滚动的播放welcome
2. 输出框又分为了两部分,分别是输出用户信息和在线成员,并且实现框满清屏的效果
3. 输入框使用户用来输入消息,按回车键就发送出去
pthread_t theader,toutput_fl,tinput;
pthread_create(&theader,NULL,show_header,&nw);
pthread_create(&toutput_fl,NULL,show_output_fl,&nw);
pthread_create(&tinput,NULL,show_input,&nw);
//用于实现线程的同步
pthread_join(theader,NULL);
pthread_join(toutput_fl,NULL);
pthread_join(tinput,NULL);
return 0;
}
1、先来介绍jsoncpp相关概念及使用
(1)jsoncpp主要包含了三种类型的类:Value,Reader,Writer。使用jsoncpp中对象或类名,只需要包含json.h即可
(2)对象是以健值对的形式进行存放的
Json::Value root; // 表示整个 json 对象
root["key_string"] = Json::Value("value_string"); //表示新建一个 Key(名为:key_string),赋予字符串值:"value_string"。
root["key_number"] = Json::Value(12345); // 表示新建一个 Key(名为:key_number),赋予数值:12345。
(3)jsoncpp的Json::Writer类是一个纯虚类,不可直接使用,因此我们使用其子类:Json::FastWriter,该类对象可以用来输出json对象所包含的内容
(4)Value类是用来读取的,也就是将字符串转化为Json::Value对象的
Json::Reader reader;
Json::Value json_object;
const char* json_document = "{/"age/" : 26,/"name/" : /"huchao/"}";
if (!reader.parse(json_document, json_object))
return 0;
std::cout << json_object["name"] << std::endl;
std::cout << json_object["age"] << std::endl;
//输出结果为:
//"huchao"
//26
2、具体实现
//data.h文件
#ifndef _DATA_H_//使用条件编译,避免重复编译
#define _DATA_H_
#include
#include
#include "base_json.h"
//data类实现了将序列化和反序列化函数进行简单的封装
class data
{
public:
//构造函数,析构函数
data();
~data();
//数据的序列化value->string
void data_to_string(std::string& out);
//数据的反序列化string->value
void string_to_data(std::string& in);
public:
//私有数据成员:昵称,学校,消息内容,状态信息
std::string nick_name;
std::string school;
std::string msg;
std::string cmd;
};
#endif
//客户端将这些信息发送出去以后,在网络中会序列化为一个字符串,服务器接收到数据以后,再将字符串反序列化为用户信息,进行存储和处理
//data.cpp文件
#include "data.h"
//默认构造函数和析构函数
data::data(){}
data::~data(){}
//将data 序列化,value->string,
//void serialize(Json::Value& val,std::string& outString)
void data::data_to_string(std::string& out)
{
Json::Value val;
val["nick_name"] = nick_name;
val["school"] = school;
val["msg"] = msg;
val["cmd"] = cmd;
serialize(val,out);//进行序列化,将用户信息与消息的内容进行捆绑
}
//反序列化 将序列化value转化为string
//void un_serialize(Json::Value& val,std::string& in)
void data::string_to_data(std::string& in)
{
Json::Value val;
un_serialize(val,in);//进行反序列化,将用户信息与消息内容进行解绑
nick_name = val["nick_name"].asString();
school = val["school"].asString();
msg = val["msg"].asString();
cmd = val["cmd"].asString();
}
//测试代码
//int main()
//{
// data d;
// d.nick_name = "boy";
// d.school = "bit";
// d.msg = "hello";
// d.cmd = "";
// std::string out;
// d.data_to_string(out);
// std::cout <<"out:"<
//base_json.h文件
#ifndef _BASE_JSON_H__
#define _BASE_JSON_H__
#include
#include
#include "json/json.h"
//序列化
void serialize(Json::Value& val,std::string &out);
//反序列化
void un_serialize(Json::Value& val,std::string &in);
#endif
//base_json.cpp
#include "base_json.h"
//序列化
void serialize(Json::Value& val,std::string &out)
{
//以JSON格式输出值,使用到了FastWriter类,来输出对象所包含的信息
Json::FastWriter w;
out = w.write(val);
}
//反序列化
void un_serialize(Json::Value& val,std::string &in)
{
Json::Reader read;
read.parse(in,val,false);
}
//Json::Writer 与Json::Reader相反,将Json::Value转化成字符串流,Jsoncpp 的 Json::Writer 类是一个纯虚类,并不能直接使用。在此我们使用 Json::Writer 的子类:Json::FastWriter。
//例如: Json::FastWriter fast_writer;
// std::cout << fast_writer.write(root) << std::endl;
#ifndef _UDP_SERVER_H_
#define _UDP_SERVER_H_
#include
#include
#include
#include
//udp_server.cpp文件
#include "udp_server.h"
//默认构造函数的定义
udp_server::udp_server(const std::string& _ip,int _port):ip(_ip),port(_port),sock(-1),data_pool(256)
{}
//初始化成员函数定义,创建套接字文件。定义结构体,存放相关的套接字信息
int udp_server::init_server()
{
sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0)
{
write_log("socket error",FATAL);
return -1;
}
struct sockaddr_in local;
local.sin_family =AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip.c_str());
//服务器端需要绑定套接字ip地址和端口,才能进行正常的通信
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
write_log("bind sock error",FATAL);
return -2;
}
return 0;
}
//添加用户,ip标记用户(有问题NAT技术)
int udp_server::add_online_user(struct sockaddr_in *client)
{
//pair是一种模板类型,包含两个数据值,两个数据的类型可以不同,也可以相同
// pairp2(1,2.4)//用给定值初始化
online_user.insert(std::pair(client->sin_addr.s_addr,*client));//map型容器,使用insert方法进行插入(pair型数据的插入l)
}
//删除用户
int udp_server::del_online_user(struct sockaddr_in *client)
{ //map函数实现一一映射,迭代器遍历在线用户,找到对应的用户,进行删除即可
std::map::iterator iter = online_user.find(client->sin_addr.s_addr);
if(iter != online_user.end())
online_user.erase(iter);
}
//从客户端读取数据,然后写到data_pool里面
//out指从客户端输出的数据
int udp_server::recv_msg(std::string& out)
{
//开辟缓冲区buf,用于存放读取到的消息
char buf[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//调用recv函数将读到的信息写到buf中,返回实际读取的字节数
int ret = recvfrom(sock,buf,sizeof(buf)-1,0,\
(struct sockaddr*)&peer,&len);
if(ret > 0)
{
buf[ret] = 0;
out = buf;//将buf的首地址赋给out
//将收到的信息放入数据池中
data_pool.put_data(out);
data d;
//进行反序列化
d.string_to_data(out);
if(d.cmd == "QUIT")
{
del_online_user(&peer);
}
else
{
add_online_user(&peer);
}
return 0;
}
return -1;
}
//将消息发送出去,in指从pool得到的数据
int udp_server::send_msg(const std::string& in,\
struct sockaddr_in& peer,const socklen_t& len)
{ //调用sento函数发送消息
int ret = sendto(sock,in.c_str(),in.size(),0,\
(struct sockaddr*)&peer,len);
if(ret < 0 )
{
write_log("server send_msg errror",WARNING);
return -1;
}
return 0;
}
//从数据池里面取出消息,广播发送给每个用户。
int udp_server::brocast_msg()
{
std::string msg ;
data_pool.get_data(msg);
//使用迭代器遍历在线用户,为每一位用户发送消息
std::map ::iterator iter = online_user.begin();
for(;iter != online_user.end();++iter)
{
send_msg(msg,iter->second,sizeof(iter->second));
}
return 0;
}
udp_server::~udp_server()
{
if(sock >0)
close(sock);
}
//chat_system.cpp
#include "udp_server.h"
void usage(const char* arg)
{
std::cout<<"Usage: "<brocast_msg();
}
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
//定义构造函数
udp_server server(argv[1],atoi(argv[2]));
//初始化
server.init_server();
//创建线程,新线程给客户端发数据,主线程将从客户端读取的数据写入pool
pthread_t id;
pthread_create(&id,NULL,brocast,(void*)&server);
std::string msg;
while(1)
{ //服务器端不停地接收来自客户端发来的消息
server.recv_msg(msg);
}
return 0;
}
//pool.h
#ifndef _POOL_H_
#define _POOL_H_
#include
#include
#include
#include
//数据池的实现,是一个环形队列
class pool
{
public:
pool(int);//构造函数
//从数据池取出数据
int get_data(std::string& out);
//向数据池中放数据
int put_data(const std::string& in);
~pool();
private:
pool(const pool&);
private:
//私有数据成员(posix信号量机制)
sem_t c_sem;//消费者可用资源
sem_t p_sem;//生产者可用资源
std::vector data_pool;//用vector容器维护一个环形队列,存放数据(数据池)
int c_step;//消费者的步数
int p_step;// 生产者的步数
int cap;//环形队列的容量,可以存放数据的总量
};
#endif
#include "pool.h"
//构造函数,信号量的数据类型为结构sem_t,函数sem_init()用来初始化一个信号量。
//extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
//sem为指向信号量结构的一个指针
//pshared不为0时此信号量在进程间共享,为0时表示为当前进程的所有线程共享;
//value给出了信号量的初始值。
pool::pool(int size)
:data_pool(size)
,c_step(0)
,p_step(0)
,cap(size)
{
sem_init(&c_sem,0,0);//消费者可用资源数,系统中初始资源为0
sem_init(&p_sem,0,size);//生产者可用资源数,开始时资源为0,因此生产者可用资源为size
}
int pool::get_data(std::string& out)
{ //从数据池中取数据,sem_wait函数是P操作,它的作用是从信号量的值减去一个“1”.
sem_wait(&c_sem);//消费者进行P(c_sem操作)
out = data_pool[c_step++];//入队列,并将取到的信息用参数out返回
c_step %= cap;//环形队列的实现
sem_post(&p_sem);//生产者的可用资源数加1
}
int pool::put_data(const std::string& in)
{ //向数据池中放数据
sem_wait(&p_sem);//P(p_sem)等到生产者可用资源数大于等于1时,开始放数据
data_pool[p_step++] = in;//将数据放入数据池
p_step %= cap;
sem_post(&c_sem);//V(c_sem)消费者可用资源数加1
}
pool::~pool()
{
sem_destroy(&c_sem);
sem_destroy(&p_sem);
}
//window.h
#ifndef _WINDOW_H_
#define _WINDOW_H_
//ncurses库,它提供了API,可以允许程序员编写独立于终端的基于文本的用户界面
#include
#include
#include
#include
#include
class window
{
public:
window();
~window();
//清除窗口内消息
void clear_win_line(WINDOW* w,int begin,int lines);
//从窗口获取消息
void get_str(WINDOW* win,std::string& out);
//向窗口放置消息
void put_str_to_win(WINDOW* w,int y,int x,std::string& msg);
WINDOW* create_newwin(int _h,int _w,int _y,int _x);
void create_header();//绘制头部标题栏
void create_output();//绘制输出栏
void create_friends_list();//绘制好友列表栏
void create_input();//绘制输入栏
public:
WINDOW* header;//标题窗口句柄
WINDOW* output;//输出窗口句柄
WINDOW* friends_list;//好友列表窗口句柄
WINDOW* input;//输入窗口句柄
};
#endif
windows.cpp文件
#include "window.h"
window::window()//构造函数
{ //WINDOW * initscr(void),initscr函数在一个程序中只能调用一次。如果成功,返回一个指向stdscr结构的指针;
initscr();
//这个函数用来设制光标是否可见。它的参数可以是:0(不可见),1(可见),2(完全可见)
curs_set(0);
}
window::~window()
{ //析构函数,删除各个窗体
delwin(header);
delwin(input);
delwin(friends_list);
delwin(output);
endwin();
}
//获取窗口内的消息
void window::get_str(WINDOW* win,std::string& out)
{
char msg[1024];
wgetnstr(win,msg,sizeof(msg));
out = msg;
}
void window::put_str_to_win(WINDOW* w,int y,int x,std::string& msg)
{ //添加序列到窗口指定的位置
mvwaddstr(w,y,x,msg.c_str());
}
void window::clear_win_line(WINDOW* w,int begin,int lines)
{
while(lines-- >0)
{
//移动光标
wmove(w,begin++,0);
wclrtoeol(w);
}
}
WINDOW* window::create_newwin(int _h,int _w,int _y,int _x)
{ //创建一个新窗体
WINDOW *win = newwin(_h,_w,_y,_x);
//box(WINDOW *win,char1,char2);该函数用在linux程序的curses编程里,用来设计窗口的边框,win为窗口的指针,
box(win,0,0);
return win;
}
void window::create_header()
{
int _y = 0;
int _x = 0;
int _h = LINES/5;
int _w = COLS;
header = create_newwin(_h,_w,_y,_x);
}
void window::create_output()
{
int _y = LINES/5;
int _x = 0;
int _h = (LINES*3)/5;
int _w = (COLS*3)/4;
output = create_newwin(_h,_w,_y,_x);
}
void window::create_friends_list()
{
int _y = LINES/5;
int _x = (COLS*3)/4;
int _h = (LINES*3)/5;
int _w = COLS/4;
friends_list = create_newwin(_h,_w,_y,_x);
}
void window::create_input()
{
int _y =(LINES*4)/5;
int _x = 0;
int _h = LINES/5;
int _w = COLS;
input = create_newwin(_h,_w,_y,_x);
}
//测试代码
//int main()
//{
// window win;
// win.create_header();
// sleep(1);
// wrefresh(win.header);
// win.create_output();
// sleep(1);
// wrefresh(win.output);
// win.create_friends_list();
// sleep(1);
// wrefresh(win.friends_list);
// win.create_input();
// sleep(1);
// wrefresh(win.input);
//
// //放置消息
// std::string msg = "please Enter#";
// mvwaddstr(win.input,1,2,msg.c_str());
// wrefresh(win.input);
// sleep(2);
// int x=0,y=0;
// getmaxyx(win.output,y,x);
//
// int hx = 0,hy=0;
// getmaxyx(win.header,hy,hx);
// int i=1;
// int j=1;
// std::string running = "welcome to chat system";
// while(1)
// {
// //header跑马灯实现,
//
// mvwaddstr(win.header,hy/2,j++,running.c_str());
// wrefresh(win.header);
// usleep(200000);
// win.clear_win_line(win.header,hy/2,1);
// win.create_header();
// wrefresh(win.header);
// if(j == hx)
// {
// j=1;
// }
//
//
// //output 循环放出消息
// //mvwaddstr(win.output,i,2,msg.c_str());
// //wrefresh(win.output);
// //usleep(200000);
// //i++;
// //i %=(y-1);
// //if(i==0)
// //{
// // i=1;
// // win.clear_win_line(win.output,1,y-1);
// // win.create_output();
// // wrefresh(win.output);
// //}
// }
// return 0;
//ctrl_server.sh,shell脚本
ROOT_PATH=/home/yinyunhong/chatroom/Chat_Master
BIN=$ROOT_PATH/chat_system
CONF=$ROOT_PATH/conf/server.conf
pid=''
porc=$(basename $0)
function usage()
{
printf "%s %s\n" "$porc" "[start | -s] [stop | -t] [restart | -rt] [status | -a]"
}
check_status()
{
name=$(basename $BIN)
pid=$(pidof $name)
//如果正在运行的进程的pid不为0,
if [ ! -z "$pid" ];then
return 0
else
return 1
fi
}
server_start()
{ //正在运行的进程号不为0,代表服务器正在running
if check_status ;then
echo "server is ready running,pid : $pid"
else
//当进程号为0时,在配置文件server.conf中寻找IP地址和端口号,打印最后一列的内容即打印出ip地
址和端口
ip=$(awk -F: '/^IP/{print $NF}' $CONF)
port=$(awk -F: '/^PORT/{print $NF}' $CONF)
$BIN $ip $port
echo "server is start......done, "
fi
}
server_stop()
{
if check_status ;then
kill -9 $pid
echo "server stop......done"
else
echo "server is not running"
fi
}
server_restart()
{
server_stop
server_start
}
server_status()
{
if check_status ;then
echo "server is running...,pid is $pid"
else
echo "server is not running ....."
fi
}
if [ $# -ne 1 ] ;then
usage
exit 1
fi
//输入的参数不同,进行的选项不同
case $1 in
start | -s )
server_start
;;
stop | -t )
server_stop
;;
restart | -rt )
server_restart
;;
status | -a )
server_status
;;
*)
usage
exit 2
;;
esac
check_status
conf模块中包含了server.conf,主要是指定了服务器的ip地址:0和端口号:8080
ROOT=$(shell pwd)
SERVER=$(ROOT)/server
CLIENT=$(ROOT)/client
LOG=$(ROOT)/log
POOL=$(ROOT)/data_pool
COMM=$(ROOT)/comm
LIB=$(ROOT)/lib
WINDOW=$(ROOT)/window
CONF=$(ROOT)/conf
PLUGIN=$(ROOT)/plugin
SERVER_BIN=chat_system
CLIENT_BIN=chat_client
//指定头文件以及库文件的搜索路径,链接动态库文件
INCLUDE=-I$(POOL) -I$(LOG) -I$(COMM) -I$(LIB)/include -I$(WINDOW)
LDFLAGS= -L$(LIB)/lib -lpthread -ljson -lncurses
//使用正则表达式,将各文件夹下的.cpp文件替换成为.o文件
SERVER_OBJ=$(shell ls $(SERVER) | grep -E '\.cpp$$' | sed 's/\.cpp/\.o/')
SERVER_OBJ+=$(shell ls $(LOG) | grep -E '\.cpp$$' | sed 's/\.cpp/\.o/')
SERVER_OBJ+=$(shell ls $(POOL) | grep -E '\.cpp$$' | sed 's/\.cpp/\.o/')
SERVER_OBJ+=$(shell ls $(COMM) | grep -E '\.cpp$$' | sed 's/\.cpp/\.o/')
CLIENT_OBJ=$(shell ls $(CLIENT) | grep -E '\.cpp$$' | sed 's/\.cpp/\.o/g')
CLIENT_OBJ+=$(shell ls $(LOG) | grep -E '\.cpp$$' | sed 's/\.cpp/\.o/')
CLIENT_OBJ+=$(shell ls $(COMM) | grep -E '\.cpp$$' | sed 's/\.cpp/\.o/')
CLIENT_OBJ+=$(shell ls $(WINDOW) | grep -E '\.cpp$$' | sed 's/\.cpp/\.o/')
CC=g++
//makefile文件中python定义的伪目标,它不代表一个真正的文件名,在执行make时可以指定这个目标来执行所在规则定义的命令,伪目标通过PHONY来指明。
//PHONY伪目标可以解决源文件不是最终目标直接依赖(实际上可以认为是间接依赖)带来的不能自动检查更新规则
.PHONY:all
all:$(SERVER_BIN) $(CLIENT_BIN)
$(SERVER_BIN):$(SERVER_OBJ)
@$(CC) -o $@ $^ $(LDFLAGS)
@echo "linking [$^] to [$@] .....done"
$(CLIENT_BIN):$(CLIENT_OBJ)
@$(CC) -o $@ $^ $(LDFLAGS)
@echo "linking [$^] to [$@] .....done"
%.o:$(CLIENT)/%.cpp
@$(CC) -c $< $(INCLUDE)
@echo "comping [$^] to [$@]......done"
%.o:$(SERVER)/%.cpp
@$(CC) -c $< $(INCLUDE)
@echo "comping [$^] to [$@]......done"
%.o:$(POOL)/%.cpp
@$(CC) -c $<
@echo "comping [$^] to [$@]......done"
%.o:$(LOG)/%.cpp
@$(CC) -c $<
@echo "comping [$^] to [$@]......done"
%.o:$(COMM)/%.cpp
@$(CC) -c $< $(INCLUDE)
@echo "comping [$^] to [$@]......done"
%.o:$(WINDOW)/%.cpp
@$(CC) -c $< $(INCLUDE)
@echo "comping [$^] to [$@]......done"
.PHONY:debug
debug:
echo $(SERVER_OBJ)
echo $(CLIENT_OBJ)
.PHONY:output
output:
mkdir -p output/server
mkdir -p output/client
mkdir -p output/server/log
cp $(SERVER_BIN) output/server
cp $(CLIENT_BIN) output/client
cp $(PLUGIN)/ctl_server.sh output/server
cp -rf $(CONF) output/server
.PHONY:clean
clean:
rm -rf *.o $(SERVER_BIN) $(CLIENT_BIN) output
该项目包含了许多的第三方库文件,所以在写makefile文件时,指定库文件的搜索路径以及头文件的搜索路径比较的关键,将头文件和库文件封装成一个lib模块,链接时便于链接。
(1)面对的首要问题:是如何识别客户端中哪个客户发来的消息。其实是通过序列化与反序列化解决该问题,通过序列化将用户发送的消息与用户信息进行绑定,之前没有了解过json的序列化和反序列化,后来多次尝试将其封装到data类中,实现了该操作。
(2)序列化和反序列化引入了json库,进行窗口化设计引入了ncurse库。但是引入这两个库后,编写makefile文件时,一直报错,提示有“未定义的引用”,修改了好久,才编写好正确的makefile文件,发现原来自己的文件路径上写的有问题。
其次还有在编译链接库时,一些编译选项也有点儿忘了,-L指定的是库优先搜索的路径,-l指定的是搜索动态库文件libjson.so,还是应该多写makefile文件,对整个项目进行组织,有清晰的架构
(3)实现客户端的窗口界面对于我来说,也有些陌生,如:设置光标的可见性,创建删除新窗体等操作,在查阅了相关的资料以及相关函数的原型或源码后,对该部分内容有了更加深入的了解
当然最重要的是,对于库文件的路径问题一定要处理好,因为单单makefile的编写就占据了我很长的时间