早期网络刚刚普及的时候,给人们印象最深的就是上网聊天,虽然作为一名上世纪的尾巴刚刚出生的我没有经历过,但仍从有所耳闻,那个时期是网络聊天室风靡的年代,全国知名聊天室大家都争破头的想要进去,基于如上和一点点小的机遇我摸索完成了一个能基本实现聊天功能的聊天室项目
先贴一下源码,Github代码:https://github.com/GreenDaySky/ChatSystem_
我们先来看一下的聊天室成品
有点小丑........................
登陆界面
好的基于以上我来简述下我的聊天室是如何构造的
首先我们看一下这个聊天室项目的整体框架结构,这里由于视窗问题我没有将库文件信息贴上去
此次项目我使用了两个附加库来协助项目的完成
soncpp库:对传输的数据进行序列化和反序列化操作,方便进行传输
ncurses库:进行界面构造 简单构造聊天室的页面
接下来我们进行一步一步拆解
我们忽略掉两个ssh文件,bulid.sh/run.sh是为了方便我们编译和启动服务器的工具
下来具体谈一谈我的聊天系统的思路,我将会省略功能性函数的贴图,所有的代码上述源码链接里都有,感兴趣的朋友请自行查看
整个聊天系统的主功能在ChatClient.cc和ChatServer.cc两个文件当中,而其所依赖的功能被封装在ChatClient.hpp和ChatServer.hpp中
一、ChatClient
我们首先从ChatClient来分析,我设计的ChatClient是需要手动输入IP地址进行启动的
启动后根据提示输入你想要进行的操作 1.注册 2.登陆 3.退出
while(1){
32 Menu(select);
33 switch(select){
34 case 1:
35 cp->Register();
36 break;
37 case 2:
38 if(cp->Login()){
39 cp->Chat();
40 cp->Logout();
41 }else{
42 std::cout << "Login Failed!" << std::endl;
43 }
44 break;
45 case 3:
46 exit(0);
47 break;
48 default:
49 exit(1);
50 break;
51 }
52 }
显而易见 我们的为了代码良好的可视性,将所有的功能通通封装在了头文件当中
Register()
bool Register()
62 {
63 if(Util::RegisterEnter(nick_name, school, passwd) && ConnectServer()){
64 Request rq;
65 rq.method = "REGISTER\n";
66
67 Json::Value root;
68 root["name"] = nick_name;
69 root["school"] = school;
70 root["passwd"] = passwd;
71
72
73 Util::Seralizer(root, rq.text);
74
75 rq.content_length = "Content_Length: ";
76 rq.content_length += Util::IntToString((rq.text).size());
77 rq.content_length += "\n";
78
79 Util::SendRequest(tcp_sock, rq);
80 recv(tcp_sock, &id, sizeof(id), 0);
81
82 bool res = false;
83 if(id >= 10000){
84 std::cout << "Register Success! Your Login ID Is :" << id << std ::endl;
85 res = true;
86 }else{
87 std::cout << "Register Failed! Code is :" << id << std::endl;
88 }
89
90 close(tcp_sock);
91 return res;
92 }
这里首先使用ProtocolUtil当中封装的的RegisterEnter进行注册输入并使用ConnectServer()判断是否成功会话连接服务器
这里我要提一句为了确保安全性,我在注册和登陆的时候选用的是TCP协议进行通信,而在聊天阶段,为了数据的方便接收退而求其次使用了UDP协议进行通信
接下来就是将我们的注册信息传输到服务器端,这里我们自定义了一个简单的协议以便于我们的数据传输
我们的协议仿照的是http协议的格式,感兴趣的朋友查看这位朋友的博文https://blog.csdn.net/yamaxifeng_132/article/details/61466365
33 class Util{
34 public:
21 public:
22 std::string method;//Login Register Logout
23 std::string content_length;//"Content_Legth:89"
24 std::string blank;
25 std::string text;
26 public:
27 Request():blank("\n")
28 {}
29 ~Request()
30 {}
31 };
32
这里创建了一个的对象rq,更改他的method为注册,填写将注册信息用jsoncpp进行序列化并发送至服务器
此时服务器接收到我们的信息后会答应一个注册成功ID,我们的ID设计为从10000开始,所以做一个简单判断如果ID的值符合我们的规划就将ID展现给用户否则输出错误id以用来判断错误
Login()
bool Login()
96 {
97 if(Util::LoginEnter(id, passwd) && ConnectServer()){
98 Request rq;
99 rq.method = "LOGIN\n";
100
101 Json::Value root;
102 root["id"] = id;
103 root["passwd"] = passwd;
104
105 Util::Seralizer(root, rq.text);
106
107
108 rq.content_length = "Content_Lengeh: ";
109 rq.content_length += Util::IntToString((rq.text).size());
110 rq.content_length += "\n";
111
112
113 Util::SendRequest(tcp_sock, rq);
114
115
116 unsigned int result = 0;
117 recv(tcp_sock, &result, sizeof(result), 0);
118 bool res = false;
119 if(result >= 10000){
120 res = true;
121 std::string name_ = "None";
122 std::string school_ = "None";
123 std::string text_ = "Hello I am login!!!";
124 unsigned int id_ = result;
125 unsigned int type_ = LOGIN_TYPE;
126
127 Message m(name_, school_, text_, id_, type_);
128 std::string sendString;
129 m.ToSendString(sendString);
130 UdpSend(sendString);
131
132
133 std::cout << "Login Success! Your ID Is:" << id << std::endl;
134 }else{
135 std::cout << "Login Failed! Code is :" << result << std::endl;
136 }
137
138 close(tcp_sock);
139 return res;
140 }
141 }
登陆同注册十分相似,首先通过LoginEnter()和ConnectServer()输入登陆信息和链接服务器
然后写入协议里的方法和正文当中的登录信息,将登陆信息序列化后发送至服务器
服务器对登陆信息进行回应一个result,正确就返回ID,错误返回错误代码
如果正确则将你你的身份信息和一条上线的消息作为消息主体发送给服务器
这里的Message就是一个消息类,这里的type_使用来区分消息类型的,这条是登陆消息,服务器根据消息类型进行不同的反应,比如这条上线通知就要对所有的在线用户进行通知
Chat()
void Chat()
182 {
183 Window w;
184 pthread_t h, l;
185
186 struct ParamPair pp = {&w, this};
187
188 pthread_create(&h, NULL, Welcome, &w);
189 pthread_create(&l, NULL, Input, &pp);
190
191 std::string recvString;
192 std::string showString;
193 std::vector<std::string> online;
194
195 w.DrawOutput();
196 w.DrawOnline();
197 for(;;){
198 UdpRecv(recvString);
199 Message msg;
200 msg.ToRecvValue(recvString);
201
202 if(msg.Id() == id && msg.Type() == LOGIN_TYPE){
203 nick_name = msg.NickName();
204 school = msg.School();
205 }
206
207 showString = msg.NickName();
208 showString += " - ";
209 showString += msg.School();
210
211
212 std::string f = showString;//zhangsan-qinghua
213 Util::addUser(online, f);
showString += " # ";
217 showString += msg.Text();//zhangsan-sust# nihao
218 w.PutMessageToOutput(showString);
219
220 w.PutUserToOnline(online);
221 }
222 }
接下来是畅聊系统最主体的框架了
实现肯定是需要构建一个视图窗口界面来进行聊天
我们可以看到这里有四个窗口,分别为欢迎窗口、消息输出窗口、在线用户窗口、消息输入窗口
我们这里至少需要用三个线程维护窗口的正常运行(消息输出窗口和在线用户窗口可以使用一个线程来维护,这里我的选择是使用主线程维护),所以我们建立两个线程分别来维护输入窗口和welcome窗口
input()函数的作用就是将用户所输入的信息发送至服务器并不断刷新自己的输入窗口
welocme()函数的作用是打赢出最上方的welcome窗口
接下来是主线程接收到服务器发送过来的信息后,首先将用户信息读取出来作为消息信息的来源放在输出的string的最前面
然后将消息主体跟在之后 并判断信息消息的类型如果是初次登陆则将用户信息提取出来加入在线用户列表
二、ChatServse
/./ChatServer tcp_port udp_port
29 int main(int argc, char *argv[])
30 {
31 if(argc != 3){
32 Usage(argv[0]);
33 exit(1);
34 }
35
36 int tcp_port = atoi(argv[1]);
37 int udp_port = atoi(argv[2]);
38
39 ChatServer* sp = new ChatServer(tcp_port, udp_port);
40 sp->InitServer();
41
42
43 pthread_t c, p;
44 pthread_create(&p, NULL, RunProduct, (void*)sp);
45 pthread_create(&c, NULL, RunConsume, (void*)sp);
46 sp->Start();
47
48 return 0;
49 }
这个是ChatClient.cc文件,也是服务器端运行的主线路
首先是做一个启动输入参数即两个端口号,然后进行初始化InitServer()
void InitServer()
48 {
49 tcp_listen_sock = SocketApi::Socket(SOCK_STREAM);
50 udp_work_sock = SocketApi::Socket(SOCK_DGRAM);
51 SocketApi::Bind(tcp_listen_sock, tcp_port);
52 SocketApi::Bind(udp_work_sock, udp_port);
53
54 SocketApi::Listen(tcp_listen_sock);
55 }
初始化服务器的主要目的是搞定通信的基础设置,包括套接字的一系列操作
然后创建生产者线程和消费者线程分别调用生产和消费活动
8 //UDP
69 void Product()
70 {
71 std::string message;
72 struct sockaddr_in peer;
73 Util::RecvMessage(udp_work_sock, message, peer);
74 std::cout << "debug: recv message: " << message << std::endl;
75 if(!message.empty()){
76 Message m;
77 m.ToRecvValue(message);
78 if(m.Type() == LOGIN_TYPE){
79 um.AddOnlineUser(m.Id(), peer);
80 std::string name_;
81 std::string school_;
82 um.GetUserInfo(m.Id(), name_, school_);
83 Message new_msg(name_, school_, m.Text(), m.Id(), m.Type());
84 new_msg.ToSendString(message);
85 }
86
87
88 pool.PutMessage(message);
89 }
90
91 }
92 void Consume()
93 {
94 std::string message;
95 pool.GetMessage(message);
96 std::cout << "debug: send message: " << message << std::endl;
97 auto online = um.OnlineUser();
98 for(auto it = online.begin(); it != online.end(); it++){
99 Util::SendMessage(udp_work_sock, message, it->second);
100 }
101 }
这是生产和消费的活动方法,我们这里使用了生产者消费者模型构建环形结构来达成消息的处理任务,注意前文已经说过这里的聊天主体是通过UDP来实现的
生产活动的主体是,先socket接收客户端发送来的消息,将消息信息反序列化
判断消息类型,如果是第一次登陆则将用户信息添加到在线用户并返回在线用户信息给当前在线用户列表当中并将消息内容放入消息数据池中,如果常规消息则直接将消息内容放入消息池中
消费活动是从消息数据池中拿到消息,然后发送给所有的在线用户
随后启动
160 void Start()
161 {
162 int port;
163 std::string ip;
164 for(;;){
165 int sock = SocketApi::Accept(tcp_listen_sock, ip, port);
166 if(sock > 0){
167 std::cout << "get a new client:" << ip << ":" << port << std::en dl;
168
169 Param* p = new Param(this, sock, ip, port);
170 pthread_t tid;
171 pthread_create(&tid, NULL, HandlerRequest, p);
172
173 }
174 }
175 }
启动除了通信socket之外最主要的操作就是HandlerRequest
static void *HandlerRequest(void *arg)
108 {
109 Param* p = (Param*)arg;
110 int sock = p->sock;
111 ChatServer* sp = p->sp;
112 int port = p->port;
113 std::string ip = p->ip;
114
115 delete p;
116 pthread_detach(pthread_self());
117
118 Request rq;
119 Util::RecvRequest(sock, rq);
120
121 //std::cout << rq.text << std::endl;
122 //rq.text为NULL
123
124 Json::Value root;
125 LOG(rq.text, NORMAL);
126 Util::UnSeralizer(rq.text, root);
127
128
129 if(rq.method == "REGISTER"){
130 std::string name = root["name"].asString();//asInt
131 std::string school = root["school"].asString();
132 std::string passwd = root["passwd"].asString();
133
134
135 //std::cout << root["name"] << std::endl;
136 //std::cout << school << std::endl;
137 //std::cout << passwd << std::endl;
138
139 unsigned int id = sp->RegisterUser(name, school, passwd);
140 send(sock, &id, sizeof(id), 0);
141 }else if(rq.method == "LOGIN"){
142 unsigned int id = root["id"].asInt();
143 std::string passwd = root["passwd"].asString();
144
145 //std::cout << passwd << std::endl;
146 //std::cout << "hello" << std::endl;
147
148 //check, get to online
149 unsigned int result = sp->LoginUser(id, passwd, ip, port);
150
151 send(sock, &result, sizeof(result), 0);
152 }else{
153 //Logout
154 }
155 //recv:sock
156 //分析&&处理
157 //response
158 close(sock);
159 }
这个函数的主体功能就是处理客户端提交的各种消息类型,那么首先需要做的就是将客户端发来的序列化信息进行反序列化
然后判断自己定义的协议的类型,根据不同的方法进行不同的响应
如果是注册类方法信息则调用注册方法并返回用户的注册Id,并将注册好的用户信息添加至自己的用户存储当中
如果是登陆类方法信息则调用登陆方法即在自己的用户储存当中验证密码信息,如果正确则返回ID否则返回错误码
接下来就剩一个退出类方法了,这个我还没有完善 0-0!!!
主体的框架就是这样,剩下的服务类我在这里简单的提一下,感兴趣的朋友可查看源码参考
1.log.hpp
这个文件是记录了一些日志功能,即客户的消息内容和方法,是为了方便验证我们的功能而存在的,当然在功能更强大的项目中我们是可以通过log来将信息存输在服务器端以供客户随时查询的
2.Window
这个文件是用来构建窗口的,这里不是我构建系统的主要学习内容,大家应该也能看到我的窗口也十分简陋,这属于前端操作,这里就不阐述了
4.ProtocolUtil
整个通信的服务类操作,包括通信协议的定制,socket通信的实现,各种支持Message类收发的基础功能,还有各种服务客户端的登陆,注册操作,上面我们有所提及
5.Datapool
环形生产者消费者模型的构建,这里使用信号量,通过计数器的方式加以PV操作维护消息处理
6.UserManager
用户管理类,构建服务器用户存储的各类方法,维护用户信息和在线列表的主要类,提一句我们使用map构建了两个用户列表,所有用户和在线用户以方便处理消息和维护,如果要更一步扩展我们的项目类容就应当由此着手
7.Message
这个类里封装了各种消息的构建和处理消息的方法,例如序列化反序列化等