🔥个人主页: Milestone-里程碑
❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>
🌟心向往之行必能至
目录
[二. Route ::路由服务--聊天的前提](#二. Route ::路由服务--聊天的前提)
[四. UdpServer.hpp](#四. UdpServer.hpp)
在前面两章中,我们一篇实现了基本的UDP的实现和增加了一个中文查英文的功能,这篇文章,我们将增加群聊功能(支持多个客户端在线聊天)
根据我们前面学习过的通信,传输数据知识,我们知道发消息不在同一局域网,需要通过路由发送,而取消息也不是从发送者那取,而是从中转站,发消息也是发给中转站,这不就是我们前面说过的生产者-消费者模型吗?因此我们可以引入线程池
一.UdpServer.cc
主要提供三个功能的入口
路由服务
线程池
网路服务对象的通信功能
bash
#include <iostream>
#include <memory>
#include <functional>
#include "UdpServer.hpp"
#include "Route.hpp"
#include"ThreadPool.hpp"
using namespace ThreadPoolModule;
using task_t=std::function<void()>;
std::string defaulthandler(const std::string &message)
{
std::string hello = "hello, ";
hello += message;
return hello;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
// std ::string ip =argv[1];
uint16_t port = std::stoi(argv[1]); // xiecuo
Enable_Console_Log_Strategy();
// 1. 路由服务
Route r;
// std::unique_ptr<UdpServer> usvr =std::make_unique<UdpServer>(port,defaulthandler);
//2.线程池
auto tp=ThreadPool<task_t>::GetInstance();
//3.网络服务对象,提供通信功能
std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port,[&r,&tp](int sockfd, const std::string &message, InetAddr&peer){
task_t t=std::bind(&Route::MessageRoute,&r,sockfd,message,peer);
tp->Enqueue(t);
}
);
// std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&r](int sockfd, const std::string &message, InetAddr&peer){
// r.MessageRoute(sockfd, message, peer);
// });
usvr->Init();
usvr->Start();
return 0;
}
二. Route ::路由服务--聊天的前提
注意前提:消息路由转发时要进行加锁,防止一遍在遍历,一边又在删除,导致迭代器失效
bash
#pragma once
#include <iostream>
#include <vector>
#include "InetAddr.hpp"
#include"Mutex.hpp"
using namespace MutexModule;
class Route
{
private:
bool Isonline(InetAddr &user)
{
for (auto &e : _online_user)
{
// 重载运算符
if (e == user)
return true;
}
return false;
}
void Adduesr(InetAddr &user)
{
_online_user.emplace_back(user);
LOG(LogLevel::INFO) << "新用户上线: " << user.StringAddr();
}
void DeleteUser(InetAddr &peer)
{
for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
{
if (*iter == peer)
{
LOG(LogLevel::INFO) << "删除一个在线用户:" << peer.StringAddr() << "成功";
_online_user.erase(iter);
break;
}
}
}
public:
Route() {}
void MessageRoute(int sockfd, std::string message, InetAddr &user)
{
LockGuard lg(mutex);//加锁是为了防止有人在遍历,又有人在删除
if (!Isonline(user))
{
Adduesr(user);
}
std::string sendmessage = user.StringAddr() + "#" + message;
for (auto &users : _online_user)
{
//存在问题,用户必须自己先输入,才能看到其他人发的消息,原因是在客户端设计的问题
sendto(sockfd, sendmessage.c_str(), sendmessage.size(), 0, (const struct sockaddr *)&(users.in_addr()), sizeof(users.in_addr()));
}
if (message == "QUIT")
{
{
LOG(LogLevel::INFO) << "删除一个在线用户: " << user.StringAddr();
DeleteUser(user);
}
}
}
~Route() {}
private:
std::vector<InetAddr> _online_user;
Mutex mutex;
};
三.UdpClient.cc
bash
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Log.hpp"
#include "Thread.hpp"
using namespace LogModule;
using namespace ThreadModlue;
std::string server_ip;
std::uint16_t server_port;
struct sockaddr_in server;
int sockfd;
pthread_t id;
void read()
{
while (true)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
if (m > 0)
{
buffer[m] = 0;
// std::cout << buffer << std::endl;将读取与输入分开
std::cerr << buffer << std::endl; // 开两个终端,一个为显示他人输入,一个为自己输入
}
}
}
void send()
{
while (true)
{
// 接受信息
std::string input;
std::cout << "PLEASE Enter# ";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
(void)n;
if (input == "QUIT")
{
pthread_cancel(id);
break;
}
}
}
int main(int args, char *argv[])
{
if (args != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
// 1. 创建socket
// int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket fail" << std::endl;
return 2;
}
// 2. 本地的ip和端口是什么?要不要和上面的"文件"关联呢?
// 问题:client要不要bind?需要bind.
// client要不要显式的bind?不要!!首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式
// 为什么?一个端口号,只能被一个进程bind,为了避免client端口冲突
// client端的端口号是几,不重要,只要是唯一的就行!
// 填写服务器信息
server_ip = argv[1];
server_port = std::stoi(argv[2]); // 转数字
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
Thread R([]()
{ read(); });
Thread S([]()
{ send(); });
R.Start();
S.Start();
// id = recver.Id();
id=R.Id();
R.Join();
S.Join();
while (true)
{
// 接受信息
std::string input;
std::cout << "PLEASE Enter# ";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
(void)n;
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
四. UdpServer.hpp
bash
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"
// #include "InetAddr.hpp"
using namespace LogModule;
using func_t = std::function<void(int sockfd,const std::string &, InetAddr&peer)>;
int defaultsockfd = -1;
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)
: _sockfd(defaultsockfd), _func(func), _port(port), _isrunning(false)
{
}
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket0 error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket success,sockfd: " << _sockfd;
// 2. 绑定socket信息,ip和端口, ip(比较特殊,后续解释)
// 2.1 填充sockaddr_in结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 初始化
// 我会不会把我的IP地址和端口号发送给对方?
// IP信息和端口信息,一定要发送到网络!
// 本地格式->网络序列
local.sin_family = AF_INET;
local.sin_port = htons(_port);
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
local.sin_addr.s_addr = INADDR_ANY;
// IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);
// 那么为什么服务器端要显式的bind呢?IP和端口必须是众所周知且不能轻易改变的!
// 此时只开在了栈上,要通过系统调用写入内核
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n == -1)
{
perror("bind:");
LOG(LogLevel::FATAL) << "bind fail";
exit(2);
}
LOG(LogLevel::INFO) << "bind success!";
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 收消息, client为什么要个服务器发送消息啊?不就是让服务端处理数据。
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
buffer[n] = 0;
InetAddr client(peer);
_func(_sockfd,buffer,client);
// LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer; // 1. 消息内容 2. 谁发的??
// // 2. 发消息
// std::string echo_string = "server echo#";
// echo_string += buffer;
// 收到单词
// std::string buffer_str(buffer);
// std::string result = _func(buffer, client);
// LOG(LogLevel::INFO)<<"翻译结果:"<<buffer <<"->"<<result.c_str();
// std::string result =_func(buffer,client);
// sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
}
else
{
LOG(LogLevel::FATAL) << "recvfrom fail";
// exit(1);
}
}
}
~UdpServer() {}
private:
int _sockfd;
uint16_t _port;
// std::string _ip;
// 用的是字符串风格,点分十进制, "192.168.1.1"
func_t _func;
bool _isrunning;
};
五.遇到的问题
- 实验发现,上线用户必须先自己发消息,才能收到别人前面发的消息,客户端未读写分离(解决:客户端进行读写分离,再对读写创建线程,互不影响即可同时进行)
- 实验发现,出现了迭代器失效问题,原因是在进行路由转发遍历时,未进行加锁,导致有可能删除用户与遍历同时存在
六.总结
- 写代码先搭出大致框架,再进行完善
- 测试代码数据要广泛 ,才能尽可能测出潜在的问题