背景:利用校园局域网可以互ping,切校内局域网网速普遍大于校外局域网。
服务器:搭建在linux系统
客户端:windows MFC
语言:c++
知识涉及:c++11,sock TCP,FTP,json,AOP,thread,mysql
命令流程图
数据流程图
数据字典
步骤:
server:
按命令流程图走,先把多路复用、AOP、线程池从项目排除掉,剩下单线程的FTP模式服务器。
监听类:
#include <sys/socket.h> #include <netdb.h> //getaddrinfo freeaddrinfo gai_strerror #include <netinet/in.h> //in_addr sockaddr_in sockaddr_storage htons ntohs #include <arpa/inet.h> //inet_pton inet_ntop #include <unistd.h> //close #include <iostream> #include <cstring> using std::cout; class server_welcome_sock { private: int sock; //welcome_sock const char* port; //端口号 const unsigned int limit; //限制监听数 public: server_welcome_sock(const char *temp_port,const unsigned int &temp_limit); ~server_welcome_sock(); int sock_init(); //套接字初始化,返回监听套接字 int return_sock(); //返回welcome_sock int sock_accept(); //接受客户端连接,返回连接成功后的新套接字 void close_sock(); //关闭监听套接字 };
先建立监听class,具体是一个保存监听套接字的int变量、限制连接数、初始化监听方法(采用通用模式ipv4或ipv6)、进行监听方法(返回已成功连接的套接字)以及关闭监听套接字方法。
控制类:
#include <iostream> //int8_t #include <string> #include <fstream> #include <mysql/mysql.h> class controlSock { public: using guard_type= uint32_t; //记录对象所获取文件信息位置数值类型 controlSock(int &&T_sock,const char * const &T_data_port,const char * const &T_data_ip); // 初始化 ~controlSock(); bool C_now_end_thread; //结束此线程 void C_RecvMsg(); // 转态机问题 char C_peer_ip[46]; //客户端ip 46字节 unsigned short C_peer_port; //客户端port std::ofstream C_log_file_handle; //日记文件句柄 private: static void C_LogIn(controlSock *T_this); // 处理客户登录问题 void C_logOut(); // 处理客户退出登录问题 static void C_Search(controlSock *T_this); // 文件检索 static void C_Download(controlSock *T_this); //文件下载 static void C_Switch(controlSock *T_this,const uint8_t &T_answer,const char *T_file_name,guard_type &T_guard); //电影、软件、系统、电视剧检索 static void C_Update(controlSock *T_this); // 更新客户端 static void C_Hot(controlSock *T_this); //热门检索 bool C_RecvMsgLen(char *T_buf,uint16_t T_len); //接收指定大小msg bool C_RecvNum(char *T_buf,uint16_t T_len); //接收整数 std::string C_ReadJson(const char *T_file_name,guard_type &T_guard); //读取所要发送的movie,software,os,tv inline bool C_StartDb(); //启动数据库 inline void C_EndDb(); //关闭数据库 bool C_GetPeerAddr(); //获取客户端ip和端口号 inline void C_ErrorMess(const char *); //错误信息输出 private: MYSQL *C_sql_handle; //数据库句柄 guard_type C_guard_mv; //电影文件检索位置标记 guard_type C_guard_sw; //软件文件检索位置标记 guard_type C_guard_st; //系统文件检索位置标记 guard_type C_guard_tv; //电视剧文件检索位置标记 int C_sock; // 传入的服务socket char C_order[3]; //命令消息 const char *C_data_port; //存放数据端口,取消了全局外部变量 const char *C_data_ip; //保存传输数据ip地址 };
代码风格有点不一样...在做这个项目时,脑抽筋突然想统一风格
首先需要一个方法来反复循环接收客户端发送过来的命令,这里是写在C_RecvMsg()方法中。在这个方法中先接受客户端发送过来的两个字节的命令,进行哈希转换后利用c++11新特性常量表达式
和自定义字面值
在switch中清晰表现出用意。
用了C_RecvMsgLen()和C_RecvNum()接收指定大小消息,区别是后一个没加‘\0’,数值不需要加。
guard_type用来保存当前对象已读取文件位置,客户端请求下一页时则可从这个位置继续往下读取。
void controlSock::C_RecvMsg() { memset(C_order,0,sizeof(C_order)); //如果后期不改大小,手动清零可能更快 if (!C_RecvMsgLen(C_order,2)) { return ; //退出时C-now-end-thread已被置true } switch(BKDRHash(C_order)) { case "DL"_hash: //下载 { Invoke1<C_LogMessage>(C_Download,this,C_log_file_handle,C_peer_ip,C_peer_port,"download"); break; } case "SR"_hash: // 搜索 { Invoke1<C_LogMessage>(C_Search,this,C_log_file_handle,C_peer_ip,C_peer_port,"search"); break; } case "MV"_hash: // 电影 { Invoke2<C_LogMessage>(C_Switch,this,answer_210,movie,C_guard_mv,C_log_file_handle,C_peer_ip,C_peer_port,"movie"); break; } case "SW"_hash: // 软件 { Invoke2<C_LogMessage>(C_Switch,this,answer_211,softwase,C_guard_sw,C_log_file_handle,C_peer_ip,C_peer_port,"software"); break; } case "ST"_hash: // 操作系统 { Invoke2<C_LogMessage>(C_Switch,this,answer_212,os,C_guard_st,C_log_file_handle,C_peer_ip,C_peer_port,"system"); break; } case "TV"_hash: // 电视剧 { Invoke2<C_LogMessage>(C_Switch,this,answer_213,tv,C_guard_tv,C_log_file_handle,C_peer_ip,C_peer_port,"tv"); break; } case "UR"_hash: // 用户登录处理 { Invoke1<C_LogMessage>(C_LogIn,this,C_log_file_handle,C_peer_ip,C_peer_port,"user"); break; } case "OT"_hash: // 用户退出登录处理 { C_logOut(); break; } case "UD"_hash: // 更新 { Invoke1<C_LogMessage>(C_Update,this,C_log_file_handle,C_peer_ip,C_peer_port,"update"); break; } case "HT"_hash: //请求热门信息 { Invoke1<C_LogMessage>(C_Hot,this,C_log_file_handle,C_peer_ip,C_peer_port,"hot"); break; } default: { if(-1== send(C_sock,&answer_201,sizeof(answer_201),0 )) //发送命令失败响应码 { C_ErrorMess("链接已断开,error 003"); shutdown(C_sock,2); close(C_sock); C_now_end_thread= true; return ; } C_ErrorMess("接收命令有误,error 004"); return ; } } }
现在已作出单线程的资源助手,每次只能连接一个客户且在通信期间,监听事件处于完全废弃状态。这时插入了线程池,因为考虑到后期的数据传输也需要线程池,所以在做线程池时用来模板来构建。
在线程池class中用两个list容器,一个存放任务需要的参数,另一个存放申请后的线程。采用生产者与消费者模式,多个线程同时竞争同一个任务list,充分利用线程资源。
C_ThreadPool<std::shared_ptr<controlSock>> control_sock_pool(run,30); //控制线程池,30个线程,第一个参数为所要跑的函数
C_ThreadPool<int> data_sock_pool(func_download,30); //数据下载线程池
参数用模板T来代替,这样就可以一个线程池适用多种不同类型参数,当然也可以做成多参数,更为实用。在线程池中有一个值得注意的是,任务请求速度大于线程处理速度,造成任务拥塞,这时候
如果线程池停止了则需要进行一些释放资源操作,因为参数类型不同所以不能采取同一个方法,这时就可以利用c++11中的 type_traits 库中的std::enable_if实现条件选择重载函数,原型为
template<bool b,class T= void>
struct enable_if;
当b为true时才有效,比如
template <typename F> typename std::enable_if<std::is_integral<F>::value>::type C_Take(F &T_val) { do something } template <typename F> typename std::enable_if<std::is_class<F>::value>::type C_Take(F &T_val) { do something }
当参数F 为整数时,以及F为类时(std::shareptr<>),返回类型为void时,可省略第二个模板参数。
目前已经是多线程资源助手,而接下来需要的是解放主线程。监听套接字在监听时处于阻塞状态,造成了你需要一个线程去等待,变相浪费资源。我们可以用多路复用来解放主线程,在多路复用描述符中加入了监听控制套接字、监听数据连接套接字以及监听键盘输入流。
在键盘输入流处理程序块中可以执行一些脚本来维护整个服务器的运行。
int welcom_sock[2]; //多路复用,用于存储监听套接字 int MaxDesc= -1; //最大描述符数值 bool running= true; //多路复用运行标志 fd_set sock_set; //设置套接字描述符 server_welcome_sock server_control(control_port,listen_limit); //控制监听套接字对象的创建 server_welcome_sock server_data(data_port,listen_limit); //数据监听套接字对象的创建 welcom_sock[0]= std::move(server_control.sock_init()); //添加描述符 welcom_sock[1]= std::move(server_data.sock_init()); //添加描述符 if (welcom_sock[1]>= welcom_sock[0]) { MaxDesc= welcom_sock[1]; } else { MaxDesc= welcom_sock[0]; } while (running) { FD_ZERO(&sock_set); //清零 FD_SET(STDIN_FILENO,&sock_set); //设置数据输入流 FD_SET(welcom_sock[0],&sock_set); //设置监听控制套接字 FD_SET(welcom_sock[1],&sock_set); //设置监听数据套接字 if (-1== select(MaxDesc+ 1,&sock_set,nullptr,nullptr,nullptr)) { std::cout<<"error: select return -1\n"; end_thread= true; //结束线程循环 running= false; //退出循环 } if (FD_ISSET(STDIN_FILENO,&sock_set)) //检查键盘输入流 { do something } if (FD_ISSET(welcom_sock[0],&sock_set)) //检查监听控制套接字 { control_sock_pool.C_AddTask(std::shared_ptr<controlSock>(new controlSock(server_control.sock_accept(),data_port,data_ip))); std::cout<<"create thread control sock\n"; } if (FD_ISSET(welcom_sock[1],&sock_set)) //检查监听数据套接字 { data_sock_pool.C_AddTask(server_data.sock_accept()); std::cout<<"create thread data sock\n"; } }
到现在资源助手已经可以完成大部分工作,而后台怎么知道运行状态呢?这时我们需要插入AOP使得整个服务器看起来更像是服务器。
AOP设置在C_RecvMsg()方法中,对所有请求命令进行记录。由于命令响应方法参数并不是全一样,所以在AOP class中进行了二次修改。
#define HAS_MEMBER(member)\ template<typename T,typename...Args>struct has_member_##member\ {\ private:\ template<typename U> static auto Check(int)->decltype(std::declval<U>().member(std::declval<Args>()...),std::true_type());\ template<typename U> static std::false_type Check(...);\ public:\ static const bool value= std::is_same<decltype(Check<T>(0)),std::true_type>::value;\ };\ HAS_MEMBER(Before) HAS_MEMBER(After) template<typename Func,typename Args1,typename...Args2> //第一个参数为class this struct C_Aspect1 { private: Func C_func; public: C_Aspect1(Func &&f) : C_func(std::forward<Func>(f)) { } template<typename T> typename std::enable_if<has_member_Before<T,Args2...>::value && has_member_After<T,Args2...>::value>::type Invoke1(Args1 &&args1,Args2&&...args2,T &&aspect) { aspect.Before(std::forward<Args2>(args2)...); C_func(std::forward<Args1>(args1)); aspect.After(std::forward<Args2>(args2)...); } template<typename T> typename std::enable_if<has_member_Before<T,Args2...>::value && !has_member_After<T,Args2...>::value>::type Invoke1(Args1 &&args1,Args2&&...args2,T &&aspect) { aspect.Before(std::forward<Args2>(args2)...); C_func(std::forward<Args1>(args1)); //aspect.After(std::forward<Args>(args)...); } template<typename T> typename std::enable_if<!has_member_Before<T,Args2...>::value && has_member_After<T,Args2...>::value>::type Invoke1(Args1 &&args1,Args2&&...args2,T &&aspect) { //aspect.Before(std::forward<Args>(args)...); C_func(std::forward<Args1>(args1)); aspect.After(std::forward<Args2>(args2)...); } template<typename Head,typename... Tail> void Invoke1(Args1 &&args1,Args2&&... args2,Head &&HeadAspect,Tail&&... TailAspect) { HeadAspect.Before(std::forward<Args2>(args2)...); Invoke1(std::forward<Args1>(args1),std::forward<Args2>(args2)...,std::forward<Tail>(TailAspect)...); HeadAspect.After(std::forward<Args2>(args2)...); } }; template<typename T> using identity_t= T; template<typename... AP,typename Args1,typename... Args2,typename Func> void Invoke1(Func &&f,Args1 &&args1,Args2&&...args2) { C_Aspect1<Func,Args1,Args2...> asp(std::forward<Func>(f)); asp.Invoke1(std::forward<Args1>(args1),std::forward<Args2>(args2)...,identity_t<AP>()...); } template<typename Func,typename Args1,typename Args2,typename Args3,typename Args4,typename...Args5> //第一个参数为class this struct C_Aspect2 { private: Func C_func; public: C_Aspect2(Func &&f) : C_func(std::forward<Func>(f)) { } template<typename T> typename std::enable_if<has_member_Before<T,Args5...>::value && has_member_After<T,Args5...>::value>::type Invoke2(Args1 &&args1,Args2 &&args2,Args3 &&args3,Args4 &&args4,Args5&&...args5,T &&aspect) { aspect.Before(std::forward<Args5>(args5)...); C_func(std::forward<Args1>(args1),std::forward<Args2>(args2),std::forward<Args3>(args3),std::forward<Args4>(args4)); aspect.After(std::forward<Args5>(args5)...); } template<typename T> typename std::enable_if<!has_member_Before<T,Args5...>::value && has_member_After<T,Args5...>::value>::type Invoke2(Args1 &&args1,Args2 &&args2,Args3 &&args3,Args4 &&args4,Args5&&...args5,T &&aspect) { //aspect.Before(std::forward<Args5>(args5)...); C_func(std::forward<Args1>(args1),std::forward<Args2>(args2),std::forward<Args3>(args3),std::forward<Args4>(args4)); aspect.After(std::forward<Args5>(args5)...); } template<typename T> typename std::enable_if<has_member_Before<T,Args5...>::value && !has_member_After<T,Args5...>::value>::type Invoke2(Args1 &&args1,Args2 &&args2,Args3 &&args3,Args4 &&args4,Args5&&...args5,T &&aspect) { aspect.Before(std::forward<Args5>(args5)...); C_func(std::forward<Args1>(args1),std::forward<Args2>(args2),std::forward<Args3>(args3),std::forward<Args4>(args4)); // aspect.After(std::forward<Args5>(args5)...); } template<typename Head,typename... Tail> void Invoke2(Args1 &&args1,Args2 &&args2,Args3 &&args3,Args4 &&args4,Args5&&... args5,Head &&HeadAspect,Tail&&... TailAspect) { HeadAspect.Before(std::forward<Args5>(args5)...); Invoke2(std::forward<Args1>(args1),std::forward<Args2>(args2),std::forward<Args3>(args3),std::forward<Args4>(args4),std::forward<Args5>(args5)...,std::forward<Tail>(TailAspect)...); HeadAspect.After(std::forward<Args5>(args5)...); } }; template<typename T> using identity_t= T; template<typename... AP,typename Args1,typename Args2,typename Args3,typename Args4,typename... Args5,typename Func> void Invoke2(Func &&f,Args1 &&args1,Args2 &&args2,Args3 &&args3,Args4 &&args4,Args5&&...args5) { C_Aspect2<Func,Args1,Args2,Args3,Args4,Args5...> asp(std::forward<Func>(f)); asp.Invoke2(std::forward<Args1>(args1),std::forward<Args2>(args2),std::forward<Args3>(args3),std::forward<Args4>(args4),std::forward<Args5>(args5)...,identity_t<AP>()...); } class C_LogMessage { private: char C_time[20]; public: void Before(std::ofstream &T_file_handle,const char *T_ip,const uint16_t &T_port,const char *T_name) { auto t= std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); strftime(C_time,sizeof(C_time),"%Y-%m-%d %X",std::localtime(&t)); T_file_handle<<C_time<<"\n"<<"ip: "<<T_ip<<" port: "<<T_port<<" ASK: "<<T_name<<"\n"<<std::flush; std::cerr<<C_time<<" : "<<"ip: "<<T_ip<<" port: "<<T_port<<" ASK: "<<T_name<<"\n"; } void After(std::ofstream &T_file_handle,const char *T_ip,const uint16_t &T_port,const char *T_name) { auto t= std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); strftime(C_time,sizeof(C_time),"%Y-%m-%d %X",std::localtime(&t)); T_file_handle<<C_time<<"\n"<<"ip: "<<T_ip<<" port: "<<T_port<<" Success: "<<T_name<<"\n"<<std::flush; std::cerr<<C_time<<" : "<<"ip: "<<T_ip<<" port: "<<T_port<<" Success: "<<T_name<<"\n"; } };
主要是记录了请求命令时间、请求命令完成时间以及谁发起的请求,最后显示到后台以及写入到 log文件中。
服务器完成到这里,接着简略一下介绍客户机运行。
客户机:
用MFC对话框构架,由tab control、list control、spin control、ip control以及一些基本组件。
用std::map接收服务器传输过来的json包括路径、文件名和 hot。
对请求的消息设置了缓存,当缓存内显示条数少于page_limit时,才向服务器请求数据。(也就是说保证下一页的数据已经在客户端,而非每次等待数据)
需要注意的是linux下用的utf8,而windows下用unicode所以在客户端需要进行编码转换,在map中是以文件名作为主键,但在界面显示时需要以hot进去排序,1)写个排序函数。2)临时创建一个
multimap设置已hot为主键接着输出界面就行。改了搜索界面的排序,主界面hot忘了也跟着修改了。
项目从2017-2-21开始策划,2017-3-26总体上基本安全运行。从后端到前端,以及同学的出谋划策才有了现在这个版本,还有中途同伴的离去,,去面试了。
从mysql中读取数据加工成json时,因为mysql_fetch_row读取出row所有数据都是字符串,所以数字必须从字符串转为相应类型,否则客户端判断json类型时数字也被当成字符串。
而在linux用sendfile发送大型文件,虽然避免了拷贝到用户缓冲区,但基本上所有数据在经过第一个路由器时都必须进行分片,而且sendfile发送大小也受sock发送缓冲区限制。加上sock缓冲区并不是一有数据就进行发送(粘包),这就说明实际一次发送多大数据并不受sendfile或send函数决定。
可能提速办法
1.服务器进行0拷贝到sock缓冲区,sendfile/select
2.设置服务器发送缓冲区大小,setsockopt
3.设置客户端接收缓冲区大小
4.避免数据在路由器端被分片
1)2)将服务器的网卡速度尽量提到极限,这里也和客户端有关,客户端接收缓冲区满则阻塞服务器发送缓冲区
后期计划实现文件的断点续传和若服务器没有客户需要的资源时,请求外网然后把下载地址发送给客户,接着服务器自行下载为资源库添加新资源。
服务器端还需设置信号处理函数,比如客户端socket非正常关闭。
对阻塞接收客户端消息需设置一个超时,对资源进一步严谨。
该去刷刷数据结构算法为秋招做准备,共勉。