udp终结篇~
根据上一篇文章中对于英汉互译和远程操作的两个小功能大家应该已经学会了,我们的目的是让大家可以深刻的理解udp服务器所需要的接口已经实现的简单步骤,下面我们开始实现网络聊天室。
首先我们的实现思想是,当客户端发消息给服务器,这个时候服务器会将这条消息发给所有在线的用户,这样的话每个用户就能看到别人的消息了,所以我们首先创建一个文件来写用户管理的代码,由于我们的实现只是为了增加对udp服务器的理解,所以我们不会特别详细,就比如应该先注册再用账号和密码登录然后图形化界面什么的我们就不搞了,有兴趣的可以自己下去手动添加,我们只是写出最主要的功能。
#include
using namespace std;
class User
{
public:
User(const string& ip,const uint16_t &port)
:_ip(ip)
,_port(port)
{
}
~User()
{
}
private:
string _ip;
uint16_t _port;
};
首先每个用户都要有自己的端口号和IP,然后我们就暂时写一个构造和析构就好了,在这里大家就可以设计每个用户的昵称等信息了。然后我们还需要有一个管理用户的类:
class UserManager
{
public:
UserManager()
{
}
~UserManager()
{
}
void addUser(const string &ip, const uint16_t port)
{
}
void delUser(const string &ip, const uint16_t &port)
{
}
private:
unordered_map users;
};
我们的目的是直接通过用户的IP和端口号组成一个ID来实现映射,也就是让map去实现用户的增加删除操作。下面我们完善每个接口的代码:
void addUser(const string &ip, const uint16_t port)
{
string id = ip + "-" + to_string(port);
users.insert(make_pair(id,User(ip,port)));
}
void delUser(const string &ip, const uint16_t &port)
{
string id = ip + "-" + to_string(port);
users.erase(id);
}
首先每个用户都有自己的IP和端口号,所以我们用一个字符串去拼接他们的ID和端口号,这样就形成了唯一的id,然后用map的插入,插入的用户对象我们直接用匿名对象构造即可。删除一个用户也是同样的套路,下面我们再写一个判断是否在线的接口:
bool isOnline(const string& ip,const uint16_t& port)
{
string id = ip + "-" + to_string(port);
if (users.find(id)==users.end())
{
return false;
}
else
{
return true;
}
}
我们就通过这个用户是否在map中来判断这个用户是否在线即可。管理用户的部分我们搞定后,下面我们再重新写一个回调方法:
然后我们开始设计消息路由的回调方法:
void routeMessage(int sockfd,string clientip,uint16_t clientport,string message)
{
//首先用户要上线就需要先给服务器发送online
if (message=="online")
{
onlineuser.addUser(clientip,clientport);
}
if (onlineuser.isOnline(clientip,clientport))
{
//进行消息的路由
}
else
{
//不在线就提示用户需要先上线
string response;
struct sockaddr_in client;
socklen_t len = sizeof(client);
bzero(&client,sizeof(client));
client.sin_family = AF_INET;
client.sin_port = htons(clientport);
client.sin_addr.s_addr = inet_addr(clientip.c_str());
response = "你还没有上线,先输入online上线";
sendto(sockfd,response.c_str(),response.size(),0,(struct sockaddr*)&client,len);
}
}
我们首先判断用户是否输入online,如果输入就添加到map中,如果不在线就告诉用户需要先在线,那么我们该如何实现消息的路由呢?因为我们的map中存放的都是已经在线的用户,所以我们直接在用户管理中写一个函数,这个函数的目的是遍历map然后给每个用户发消息,因为我们是知道用户的端口号和ip的,所以发消息的内容就非常简单了:
首先包含网络相关的头文件,然后进行消息广播接口的编写:
void broadcastMessage(int sockfd,const string& message)
{
for (auto& ur:users)
{
struct sockaddr_in client;
bzero(&client,sizeof(client));
client.sin_family = AF_INET;
client.sin_port = htons(ur.second.returnport());
client.sin_addr.s_addr = inet_addr(ur.second.returnip().c_str());
sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&client,sizeof(client));
}
}
首先我们发送消息必须用到sendto接口,这个接口会用到文件描述符,所以我们除了需要一个广播的消息,还需要文件描述符,然后给每个在线的用户发消息,发消息要用到用户的ip和port,这两个参数是用户的私有成员变量,所以我们写两个接口拿到这两个参数:
当然我们考虑到广播消息的时候其他用户还要看到这条消息是谁发的,所以我们应该加入发消息这个人的ip和port:
void broadcastMessage(int sockfd,const string& clientip,const uint16_t& clientport, const string& message)
{
for (auto& ur:users)
{
struct sockaddr_in client;
bzero(&client,sizeof(client));
client.sin_family = AF_INET;
client.sin_port = htons(ur.second.returnport());
client.sin_addr.s_addr = inet_addr(ur.second.returnip().c_str());
string s = clientip + "-" + to_string(clientport) + "# ";
s+=message;
sendto(sockfd,s.c_str(),s.size(),0,(struct sockaddr*)&client,sizeof(client));
}
}
我们在回调方法中还应该加入用户下线的信息,一旦下线我们就将这个用户从map中移除,上面服务端的代码我们就写完了,下面开始修改客户端的代码:
客户端的代码中我们只需要让每次客户输入前有#的标记,最后消息打印完换行即可(注意:上面代码中我们用recvfrom这个函数的时候将结构体进行了填充,实际上recvfrom这个函数并不需要填充结构体)。下面我们引入多线程的知识,让我们的消息变成有一个线程专门读消息,另一个线程专门发送消息,这样的话即使有人不想说话也依旧能看到其他用户的聊天内容,如果我们不加入多进程或多线程的概念,那么就会出现用户不说话就无法看到其他用户的消息:
首先加入一个线程的变量,然后我们让这个读线程去执行客户端中读取服务端发送消息的过程,主线程去给服务端发消息:
static void *readMessage(void* args)
{
int sockfd = *(static_cast(args));
pthread_detach(pthread_self());
while (true)
{
char buffer[1024];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
if (n >= 0)
{
buffer[n] = 0;
}
cout << buffer << endl;
}
return nullptr;
}
void run()
{
pthread_create(&reader,nullptr,readMessage,(void*)&_sockfd);
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
server.sin_port = htons(_serverport);
string message;
while (!_quit)
{
cout<<"# ";
char commandbuffer[1024];
fgets(commandbuffer,sizeof(commandbuffer)-1,stdin);
message = commandbuffer;
sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
}
创建线程后让reader去执行read方法,将文件描述符传过去,由于主线程是死循环的发送消息,所以我们就不让主线程去等待reader线程了,直接将reader线程分离即可。分离后read线程就执行接收服务器消息的代码,主线程执行向服务器发送消息的代码。
下面我们演示一下:
首先我们可以搞一个小玩法,先创建一个管道文件,然后让服务器启动起来,将所有服务器发给我们客户端的消息我们都重定向到管道文件中,然后用另一个窗口使用cat命名查看管道文件中的内容,这样的话就实现了一个窗口我们只发送消息,一个窗口只查看服务器给我们发的消息,群聊消息可以在服务器看到。但是因为我们客户端run函数中是cout打印,cout会向一号描述符去打印,这就导致打印的消息也被重定向到管道了,所以我们将客户端中的消息用cerr打印,cerr会打印到二号描述符,这样就不会被重定向到管道文件了:
上面重定向有什么作用呢?,作用其实就是让我们单独接收服务器给我们发送的消息,这也是重定向的一种玩法。当我们运行起来后发现我们不能上线输入online不识别:
原因是我们输入的时候会带有回车,所以我们直接将回车去掉就好了:
去掉后我们重新运行:
我们发现这次可以正常的上线了,我们再试试下线:
先开启服务端:
然后我们已经用另一个xshell发了消息,客户端收到了消息:
现在我们让这个用户上线:
这个时候我们自己还没有上线:
然后让自己上线:
可以看到是没有问题的,下面可以看一下多人一起聊天的成果:
以上就是我们udp服务器实现大型网络聊天室的全部内容了。