【网络聊天室】——基于socket编程的TCP/UDP网络聊天服务器

早期网络刚刚普及的时候,给人们印象最深的就是上网聊天,虽然作为一名上世纪的尾巴刚刚出生的我没有经历过,但仍从有所耳闻,那个时期是网络聊天室风靡的年代,全国知名聊天室大家都争破头的想要进去,基于如上和一点点小的机遇我摸索完成了一个能基本实现聊天功能的聊天室项目

先贴一下源码,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

这个类里封装了各种消息的构建和处理消息的方法,例如序列化反序列化等

猜你喜欢

转载自blog.csdn.net/ladykiller21/article/details/88534879