目录
编辑
一,引入
二,在Server端修改的代码
1,保存用户信息功能实现
2,拼接消息
3,广播消息
三, Client端要修改的代码
四,效果演示
一,引入
在上一篇文章udp网络服务器中,我实现了一个简易版的网络服务器。而在今天的这篇文章中,我要实现的便是基于这个udp网络服务器而实现的多人聊天室。这个聊天室的新增功能如下:
1,添加用户。
2,广播消息。
3,使用不同的线程来接收消息和和发送消息。
二,在Server端修改的代码
1,保存用户信息功能实现
这个功能的实现其实就像我们日常生活中的通讯录的作用一样。我们可以用线性表等方式实现。但是为了提高查找效率,我采用的方式是使用哈希表的方式实现。
std::unordered_map<int,sockaddr_in> online_user_;//建立一个用户上线表,用哈希表的方式存储
哈希表参数类型解释:
int:因为要存放的是接收方的ip地址。
sockaddr_in:套接字类型,可以通过这个参数来获取接收方的端口号。
有了表以后,就要来想想一个常识性问题了。我们的表里面需要有重复信息吗?答案当然是不需要。所以在将数据插入到表里时我们要检查这个表里面的数据是否和当前要插入的数据重复,暂时通过ip的方式识别。
bool Check_user(int clientId)//检查是否为新用户
{
if(online_user_.find(clientId) == online_user_.end())
{
return true;
}
return false;
}
为什么是在Run函数里面修改?
因为在这个函数内部实现了接收消息的功能所以发送方的端口号和ip等消息便可以在这个函数内获得。
2,拼接消息
在得到发送方的端口号和ip以后,为了标识显示发送方。那我们便要将发送方的ip和port以及发送的消息拼接在一起
//拼接消息
inbuf[r1] = 0;
std::string ip = inet_ntoa(si.sin_addr);
std::string port = std::to_string(si.sin_port);
std::string message = inbuf;
std::string tostring = "["+ip+":"+port+"]" + message;
3,广播消息
在做完用户表的添加和消息的拼接以后,我们便知道了消息是什么,消息要发给谁。所以我们便可以开始广播消息,让所有人看到消息了。
void Broad_cast(std::string& message)//广播函数
{
for(auto e:online_user_)//广播
{
socklen_t len = sizeof(e.second);
int r = sendto(socketfd_, message.c_str(), message.size(), 0, (sockaddr*)&e.second, len);
if(r<0)
{
std::cout << "broad cast error!" << std::endl;
continue;
}
}
}
采用循环的方式将消息发送给用户表里的所有人。
所以我们在run函数里面要修改的全部代码便是:
void Run(const func&fun)//加入远程操作
{
char inbuf[inbufSize] = {0};
sockaddr_in si;
socklen_t len = sizeof(si);//一定要初始化
while (true)
{
int r1 = recvfrom(socketfd_, inbuf, sizeof inbuf-1, 0, (sockaddr *)&si, &len);//收消息
if(r1<0)//读取消息失败
{
perror("recvfrom error");
exit(10);
}
if (Check_user(si.sin_addr.s_addr)) // 检查是否是新用户
{
std::cout << "welcome......." << std::endl;
online_user_.insert({si.sin_addr.s_addr, si});//加入到用户表里
}
//将消息发送给用户
//拼接消息
inbuf[r1] = 0;
std::string ip = inet_ntoa(si.sin_addr);
std::string port = std::to_string(si.sin_port);
std::string message = inbuf;
std::string tostring = "["+ip+":"+port+"]" + message;
//将消息广播
Broad_cast(tostring);
//std::cout << tostring << std::endl;
// std::cout <<"收到消息,正在处理"<<std::endl;
// std::string command = inbuf;
// std::string cip = inet_ntoa(si.sin_addr);
// int cport = si.sin_port;
// std::string message = fun(command,cip,cport );
// // std::cout << message << std::endl;
// int r2 = sendto(socketfd_, message.c_str(), message.size(), 0, (sockaddr *)&si, sizeof si);//将处理结果返回给发送方
// std::cout << std::endl<<"处理完成";
// if (r2 < 0)
// {
// perror("server send message error");
// continue;
// }
}
}
对于server端的代码我们只需要修改Run函数里面的代码即可。
三, Client端要修改的代码
在平时的生活中,我们很容易的便可以知道。收发消息是可以同时的运行的。所以,在实现Client端的代码时,我们最好创建两个线程实现收发消息的并发执行。
void Run()
{
// 客户端接收函数,分两个线程执行
// 定义线程变量
pthread_t Send_thread;
pthread_t Receve_thread;
pthread_create(&Send_thread, nullptr, Send, this);
pthread_create(&Receve_thread, nullptr, Receve, this);
pthread_join(Send_thread, nullptr);
pthread_join(Receve_thread, nullptr);
// char outbuf[outbufSize];
// while (true)
// {
// std::cout << "请输入内容>> ";
// std::getline(std::cin, requestes); // client输入内容
// if (sendto(socketfd_, requestes.c_str(), sizeof requestes, 0, (sockaddr *)&si, sizeof si) < 0) // 发送消息
// {
// continue;
// }
// int r3 = recvfrom(socketfd_, outbuf, sizeof outbuf, 0, (sockaddr *)&si, &len);
// if(r3<0)
// {
// continue;
// }
// outbuf[r3] = 0;
// std::cout << outbuf << std::endl;
// memset(outbuf, 0, sizeof outbuf);
// }
}
在这段代码中,我将Client端的Run函数修改如上。Run函数的作用便只是创建两个线程,然后再执行对应的方法。这两个方法便是接收消息和发消息。
接收消息:
static void *Receve(void *args) // 收消息的线程
{
Client *C = static_cast<Client *>(args);
char outbuf[outbufSize];
sockaddr_in si;
socklen_t len = sizeof(si);
C->Dup(); // 重定向到别的窗口
while (true)
{
int r3 = recvfrom(C->socketfd_, outbuf, sizeof outbuf-1, 0, (sockaddr *)&si, &len);
if (r3 < 0)
{
continue;
}
outbuf[r3] = 0;
std::cerr << outbuf << std::endl;
memset(outbuf, 0, sizeof outbuf);
}
}
发消息:
static void *Send(void *args) // 发消息的线程
{
Client *C = static_cast<Client *>(args);
std::string requestes;
sockaddr_in si;
socklen_t len;
bzero(&si, sizeof si);
si.sin_family = AF_INET;
si.sin_port = htons(C->port_);
si.sin_addr.s_addr = inet_addr(C->ip_.c_str());
while (true)
{
std::cout << "请输入内容>> "; // client输入内容
std::string requestes;
std::getline(std::cin, requestes);
if (sendto(C->socketfd_, requestes.c_str(), requestes.size(), 0, (sockaddr *)&si, sizeof si) < 0) // 发送消息
{
continue;
}
}
}
解释:
1,为什么要使用static函数来修饰这两个方法?
因为pthread_create函数里面的方法类型是void*(void*),但是类里面的成员方法的参数里面有一个隐藏的this指针。所以只能使用static修饰让成员方法变成静态成员方法进而去掉前面隐藏的this指针。
2,pthread_create函数中为什么要传入this指针?
因为在这两个函数的内部要使用类的私有成员。但是没有this指针不能直接调用。所以便传入this指针来进行调用私有成员。
3,为什么*Receve方法中的Dup()函数是什么?
其实这是一个重定向的函数。主要是为了实现输入和输出的分离。让输入和输出打印在不同的终端。
代码实现如下:
void Dup() { int fd = open("/dev/pts/17", O_WRONLY|O_CREAT);//这是一个终端文件路径 //可以使用w命令查看自己打开的终端号 if(fd<0) //终端号是数字,前面部分的路径是一样的 { perror("open error"); exit(30); } dup2(fd, 2);//重定向,重定向的是2号标准错误。因为我的消息是用cerr输出的。 }
补充:
如果不想实现重定向的功能,也可以在启动可执行程序时直接重定向到不同的终端:
首先要使用w来查看自己的终端号是什么:
然后使用重定向操作:
./Server 8080 >/dev/pts/17 ./Client 111.230.60.61 8080 >/dev/pts/18