• 项目名称:群聊工具的开发

  • 系统原理:该项目是源于《Linux高性能服务器编程》一书中所提到的多进程或者多线程编程的部分,利用多线程编程来实现一个简单的多人聊天室。在多进程或多线程编程中,最重要的问题便是实现进程的通信机制(IPC),我们常用的IPC方法有:管道、消息队列、和信号量机制。该项目中在编写客户端和服务器端程序时,所采用的 便是基于共享内存的IPC,使用到了posix信号量机制来控制多线程之间的同步和互斥。
  • 系统功能:该系统工具可以实现多人聊天的功能,类似于我们常用的交流工具--qq,微信等。我们实现的多人聊天系统不管是在界面还是在功能上都是比较简单的,主要是为了了解类似该系统的开发原理。当客户端有一个用户发送消息时,其他在线用户都可以收到。同样当自己发送一条消息时,其他在线用户也是可以收到自己发送的消息内容的。
  • 系统简介

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字段,来表示客户端的状态

一、client模块

//udp_client.h文件,
#ifndef _udp_client_H_ //ifdef条件编译,确保头文件的编译只进行一次,避免重复编译
#define _udp_client_H_#include <iostream>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <map>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <fcntl.h>
#include "log.h"class udp_client
{
public://客户端构造函数声明(传入参数:ip地址和端口号,使用const修饰代表构造函数中,不可以改变所传入的参数_ip的值udp_client(const std::string& _ip,int port);//初始化的成员函数声明int init_client();//接收消息的成员函数声明int recv_msg(std::string& out);//发送消息的成员函数声明int send_msg(const std::string& in);//将用户加入在线列表声明int add_online_user(struct sockaddr_in *client);//析构函数声明~udp_client();
private:udp_client(const udp_client&);
private://私有数据成员,存放建立套接字的ip地址、端口号,创建的套接字文件的文件描述符std::string ip;int port;int sock;
};
#endif
//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<std::string> fl;
udp_client *qclient = NULL;
int flag = 1;//退出标志
//提示信息:输入ip和端口号
void usage(const char* arg)
{std::cout<<"Usage: "<<arg<<"[client_ip] [client_port]" <<std::endl;
}void quit(int n)
{std::string out;data q;定义data类的对象q,将对象qq.nick_name = name;q.school = school;q.cmd = "QUIT";q.msg = "";q.data_to_string(out);//调用成员函数对数据进行序列化qclient->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<std::string>::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<std::string>::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<std::string>::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标题的功能是滚动的播放welcome2. 输出框又分为了两部分,分别是输出用户信息和在线成员,并且实现框满清屏的效果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;
}

二、comm模块:

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 <iostream>
#include <string>
#include "base_json.h"//data类实现了将序列化和反序列化函数进行简单的封装
class data
{
public://构造函数,析构函数data();~data();//数据的序列化value->stringvoid data_to_string(std::string& out);//数据的反序列化string->valuevoid 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:"<<out<<std::endl;
//
//  data r;
//  r.string_to_data(out);
//  std::cout <<r.nick_name<<"-"<<r.school<<"-"<<r.msg<<std::endl;
//  return 0;
//}
//base_json.h文件
#ifndef _BASE_JSON_H__
#define _BASE_JSON_H__#include <iostream>
#include <string>
#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;

三、server模块:

#ifndef _UDP_SERVER_H_
#define _UDP_SERVER_H_#include <iostream>
#include <string.h>
#include <stdlib.h>
#include <map>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <fcntl.h>
#include "log.h"
#include "pool.h"
#include "data.h"
//定义udp_server类
class udp_server
{
public://构造函数,使用const修饰,表示不可以改变所传来的参数_ip的值udp_server(const std::string& _ip,int port);//初始化,成员函数声明,int init_server();//接收消息,成员函数声明int recv_msg(std::string& out);//发送消息,成员函数声明int send_msg(const std::string& in,struct sockaddr_in& peer,\const socklen_t& len);//广播消息,声明int brocast_msg();//添加用户,删除用户int add_online_user(struct sockaddr_in *client);int del_online_user(struct sockaddr_in *client);//析构函数~udp_server();
private://私有成员函数udp_server(const udp_server&);private://私有数据成员存放套接字的相关信息和套接字文件描述符std::string ip;int port;int sock;//用户数据表//map内部自建一颗红黑树(一种非严格意义上的平衡二叉树),这颗树具有对数据自动排序的功能,所以在map内部所有的数据都是有序的, map是STL的一个关联容器,它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值)的数据处理能力,
//在线用户序号与套接字类型结构体之间是一一映射的std::map<int,struct sockaddr_in> online_user;pool data_pool;//数据池是一个vector<string>
};
#endif
//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是一种模板类型,包含两个数据值,两个数据的类型可以不同,也可以相同// pair<int ,double>p2(1,2.4)//用给定值初始化online_user.insert(std::pair<int,struct sockaddr_in>(client->sin_addr.s_addr,*client));//map型容器,使用insert方法进行插入(pair型数据的插入l)
}
//删除用户
int udp_server::del_online_user(struct sockaddr_in *client)
{   //map函数实现一一映射,迭代器遍历在线用户,找到对应的用户,进行删除即可std::map<int,struct sockaddr_in>::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 <int, struct sockaddr_in >::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: "<<arg<<"[server_ip] [server_port]" <<std::endl;
}
void* brocast(void *arg)
{//服务器端不停地广播消息udp_server* serverp = (udp_server*)arg;while(1){serverp->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();//创建线程,新线程给客户端发数据,主线程将从客户端读取的数据写入poolpthread_t id;pthread_create(&id,NULL,brocast,(void*)&server);std::string msg;while(1){    //服务器端不停地接收来自客户端发来的消息server.recv_msg(msg);}return 0;
}

四、data_pool模块:(数据池模块)

//pool.h
#ifndef _POOL_H_
#define _POOL_H_#include <iostream>
#include <string>
#include <vector>
#include <semaphore.h>//数据池的实现,是一个环形队列
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<std::string> 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);//消费者可用资源数,系统中初始资源为0sem_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 模块 :

//window.h
#ifndef _WINDOW_H_
#define _WINDOW_H_
//ncurses库,它提供了API,可以允许程序员编写独立于终端的基于文本的用户界面
#include <iostream>
#include <ncurses.h>
#include <string.h>
#include <string>
#include <unistd.h>
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;

六、plugin模块(控制服务器):

//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" ];thenreturn 0 elsereturn 1fi
}
server_start()
{   //正在运行的进程号不为0,代表服务器正在runningif check_status ;thenecho "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 $portecho "server is start......done, "fi
}server_stop()
{if check_status ;thenkill -9 $pidecho "server stop......done"elseecho "server is not running"fi
}server_restart()
{server_stopserver_start
}
server_status()
{if check_status ;thenecho "server is running...,pid is $pid"elseecho "server is not running ....."fi
}if [ $# -ne 1 ] ;thenusageexit 1
fi//输入的参数不同,进行的选项不同
case $1 instart | -s )server_start;;stop | -t )server_stop;;restart | -rt )server_restart;;status | -a )server_status;;*)usageexit 2;;
esac
check_status

七、conf模块

conf模块中包含了server.conf,主要是指定了服务器的ip地址:0和端口号:8080

八、makefile文件的编写

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)/pluginSERVER_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/servermkdir -p output/clientmkdir -p output/server/logcp $(SERVER_BIN) output/servercp $(CLIENT_BIN) output/clientcp $(PLUGIN)/ctl_server.sh output/servercp -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的编写就占据了我很长的时间

Linux项目--多人在线聊天系统的开发相关推荐

  1. 基于C语言实现的多人在线聊天系统(客户端和服务端源码)

    资源下载地址:https://download.csdn.net/download/sheziqiong/85768602 资源下载地址:https://download.csdn.net/downl ...

  2. 多人在线游戏服务器端开发心得(转)

    http://edu.21cn.com/youxi/g_141_548352-1.htm 一个多人在线的棋牌类网络游戏的项目临近尾声,我参与了该项目的整个设计流程,并且完成了90%的核心代码.关于这个 ...

  3. 多人在线游戏服务器端开发心得

    一个多人在线的棋牌类网络游戏的项目临近尾声,我参与了该项目的整个设计流程,并且完成了90%的核心代码.关于这个项目,有很多地方值得聊一聊.本系列不打算把这个项目将得多么详细规范,那是设计文档应该描述的 ...

  4. webSocket介绍及项目实战【在线聊天系统】

    文章目录 一:消息推送常用方式介绍 1.1 轮询:浏览器以指定的时间间隔向服务器发出HTTP请求,服务器实时返回数据给浏览器 1.2 长轮询:浏览器发出ajax请求,服务器端接收到请求后,会阻塞请求直 ...

  5. 大型多人在线游戏的开发中,如何做到每个玩家动作的实时同步的?

    游戏开发中,有两种主要的同步模式,一种是状态同步, 一种是帧同步, 这两种同步模式是完全不一样的. 接下来我们来分别的分析一下他们同步的原理. 先看状态同步,服务器上跑所有的游戏逻辑(没有图像,动画, ...

  6. 多人在线斗地主游戏开发——自定义TCP网络通信协议包格式

    什么叫做通信协议?为什么制定通信协议? 怎么制定通信协议? 不知道大家有没有迷茫过这个问题,反正我是有的,,, 想我在刚接触网络编程的时候,是linux下用socket懵懵懂懂地按照pdf书籍上的代码 ...

  7. vs2015移植linux编译,windows平台移植(原linux项目)时,用vs2015开发,碰到的问题及处理方案记录...

    (1)问题记录一: 在#include 头文件后,出现如下报错信息: 解决方案: 在#include 头文件之前,先定义 #define HAVE_STRUCT_TIMESPEC 详细原因,参见以下网 ...

  8. android app利用微信浏览器,万人在线

    智能五笔的安装一般情况下都能安装成功,但有时由于某些情况的影响,可能在安装中会出现报错,并会导致安装不成功. 其一,智能五笔只能运行在中文汉字WINDOWS系统中,因此如你的系统不是中文汉字WINDO ...

  9. KBEngine和Unity3d搭建多人在线游戏(1)

    kbengine环境的搭建 Unity3d及kbengine多人在线游戏的开发 今天和大家一起学习如何使用kbengine及Unity3d搭建一款多人在线游戏,内容设计多个游戏玩家游戏的登录,打怪系统 ...

最新文章

  1. SOA 案例研究:SOA 设计
  2. 一个毕设的建成——记录下我毕设途中的《SOP》
  3. STL源码剖析学习十四:算法之set相关算法
  4. c语言二元一次方程代码,二元一次方程(示例代码)
  5. 攻防世界misc——flag_universe
  6. ABB伺服驱动调试(四)
  7. Maven Setting.xml配置文件下载 阿里云镜像 下载可用
  8. html自动触发双击事件,js主动触发单击事件
  9. php制作国旗头像图片,不要再@微信官方了,自己动手一秒制作国旗头像
  10. NAFSM中值滤波器讲解与实现
  11. 电源纹波和噪声及其测量和改善方法
  12. win7开机显示计算机无法启动,win7无法开机怎么办?解决开机报错代码C0000034的方法...
  13. NGS数据分析实践:06. 数据预处理 - 序列比对+PCR重复标记+Indel区域重比对+碱基质量重校正
  14. 2023首届西安浐灞·保利戏剧节——以“觅”为主题 即将开启
  15. POI - Excel 打印配置
  16. 俞敏洪:忍受孤独,失败,屈辱的能力是成就大业的必备条
  17. leetcode 1074. Number of Submatrices That Sum to Target(和为target的子矩阵个数)
  18. 基于JAVA和Oracle数据库实现的项目信息管理系统
  19. P2P信贷平台业务数据分析
  20. 【展馆设计】互动多媒体投影有哪些实用价值

热门文章

  1. java三元运算符用的多不多_Java多个三元运算符
  2. 有没有测试手机硬件是否损坏的软件,怎么检测手机硬件故障
  3. GC8418 数字光纤音频解码芯片 光纤解码芯片 CS8418替代 MS8413替代
  4. 接入Apple Pay流程
  5. 电脑u盘数据丢失怎么恢复 u盘数据无故丢失怎么恢复
  6. 【C语言】定义一个函数,求长方体的体积
  7. python小数转分数_NumPy:将小数转换为分数
  8. (Python)从零开始,简单快速学机器仿人视觉Opencv---运用一:快速截取图像中指定单个物体
  9. 机器学习笔记之深度信念网络(一)背景介绍与结构表示
  10. CSS3 3D转换和旋转木马案例