思路
对于服务端来说,除了要接收消息之外,还要实现一个路由转发模块,该路由转发模块可以将相应发送给所有连接的客户端。而对于客户端来说,除了要发送消息给聊天室,还要能实时看到其它所有客户端的消息。
下面来看看具体实现的代码,代码只给出核心模块,其余代码模块代码可以借鉴我之前的博客。具体来说,服务器类实现的功能只有收消息。
路由功能实现
Route.hpp头文件
该头文件实现了一个路由类,该路由类维护了一张用户表和一把锁。该类向外提供转发消息功能的接口。为了实现并发转发消息,这里使用到了之前博客写的线程池,将转发功能的函数提供给线程池里的线程去处理。
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include <cstring>
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include "LockGuard.hpp"
using namespace std;
using task_t = function<void()>;
class Route
{
private:
public:
Route()
{
pthread_mutex_init(&_mutex, nullptr);
}
void CheckOnlineUser(InetAddr &who)
{
// 用户表是临界资源,需要加锁
LockGuard lockguard(&_mutex);
for (auto &user : _online_user)
{
if (user == who)
{
LOG(DEBUG, "%s is exists!\n", who.AddrStr().c_str());
return;
}
}
LOG(DEBUG, "%s is not exists,add it!\n", who.AddrStr().c_str());
_online_user.push_back(who);
}
// 下线
void Offline(InetAddr &who)
{
LockGuard lockguard(&_mutex);
vector<InetAddr>::iterator st = _online_user.begin();
while (st != _online_user.end())
{
if (*st == who)
{
_online_user.erase(st);
LOG(DEBUG, "%s is offline\n", who.AddrStr().c_str());
break;
}
st++;
}
}
void ForwardHelper(int sockfd, const string &message, InetAddr who)
{
LockGuard lockguard(&_mutex);
string send_message = "[" + who.AddrStr() + "]# " + message;
// 遍历在线用户表,转发给他们
for (auto &user : _online_user)
{
struct sockaddr_in peer = user.Addr();
LOG(DEBUG, "Forward message to %s\n", user.AddrStr().c_str());
sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
}
}
// 转发消息给在线用户表
void Forward(int sockfd, const string &message, InetAddr &who)
{
// 查看该用户是否在线,不在线就添加到在线用户中
CheckOnlineUser(who);
// 如果用户退出
if (message == "QUIT" || message == "quit")
{
// 下线
Offline(who);
return;
}
// 创建单例线程池帮助转发
task_t t = bind(&Route::ForwardHelper, this, sockfd, message, who);
ThreadPool<task_t>::GetInstance()->Push(t);
}
~Route()
{
pthread_mutex_destroy(&_mutex);
}
private:
vector<InetAddr> _online_user; // 在线用户表
pthread_mutex_t _mutex;
};
服务端
UdpServerMain.cpp
服务器的处理逻辑
#include "UdpServer.hpp"
#include <iostream>
#include <string>
#include <memory>
#include "Route.hpp"
using namespace std;
int main(int argc, char *argv[])
{
if (argc != 2)
{
cerr << "Usage:" << argv[0] << " local-port" << endl;
exit(0);
}
uint16_t port = stoi(argv[1]);
EnableScreen();
Route messageRoute;
server_t message_route = bind(&Route::Forward,
&messageRoute, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(message_route, port); // C++14的标准
usvr->InitServer();
usvr->Start();
return 0;
}
UdpServer.hpp
实现一个服务器类
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <cstring>
#include "LockGuard.hpp"
#include "Log.hpp"
#include "nocopy.hpp"
#include "InetAddr.hpp"
#include <functional>
using namespace log_ns;
using namespace std;
static const int gsockfd = -1;
static const uint16_t glocalport = 8888;
using server_t = function<void(int, const string &, InetAddr &)>;
enum
{
SOCKET_ERROR = 1,
BIND_ERROR
};
class UdpServer : public nocopy
{
private:
public:
UdpServer(server_t func, uint16_t localport = glocalport)
: _localport(localport), _isrunning(false), _sockfd(gsockfd), _fun(func)
{
}
void InitServer()
{
// 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4协议和UDP协议的套接字
if (_sockfd < 0)
{
LOG(FATAL, "socket create error!\n");
exit(1);
}
LOG(DEBUG, "socket create success,sockfd is %d\n", _sockfd);
// 绑定端口号和IP
// 绑定之前创建一个sockaddr_in对象,存储本地地址信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定
local.sin_port = htons(_localport);
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(FATAL, "sockfd bind error!\n");
exit(1);
}
LOG(DEBUG, "sockfd bind success!\n");
}
void Start()
{
_isrunning = true;
char inbuff[1024];
while (_isrunning)
{
// 获取数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, inbuff, sizeof(inbuff) - 1, 0, (struct sockaddr *)&peer, &len);
if (n < 0)
{
LOG(FATAL, "recvfrom error!\n");
}
else
{
LOG(DEBUG, "recvfrom success!\n");
InetAddr addr(peer);
inbuff[n] = '\0';
std::string message = inbuff;
_fun(_sockfd, message, addr);
}
}
_isrunning = false;
}
~UdpServer()
{
if (_sockfd >= 0)
close(_sockfd);
}
private:
int _sockfd;
uint16_t _localport;
bool _isrunning;
server_t _fun;
};
客户端
UdpClientMain.cpp
该文件实现了客户端发送消息和接收消息的逻辑,由于发送消息和接收消息需要异步进行,所以可以考虑使用两个线程,分别查看聊天室的消息和向聊天室发送消息
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Thread.hpp"
#include "InetAddr.hpp"
#include <memory>
#include <functional>
using namespace std;
using namespace ThreadMoudle;
int InitClient()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
cerr << "create client failed" << endl;
exit(1);
}
return sockfd;
}
// 发送消息
void SendMessage(int sockfd, string serverip, uint16_t serverport, const string &name)
{
// 客户端一般不用绑定套接字,操作系统会在第一次发送消息时自动绑定本机ip和一个随机的port
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);
while (true)
{
string line;
cout << name << " #: ";
getline(cin, line);
ssize_t res = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
if (res <= 0)
{
break;
}
}
}
// 接收消息
void RecvMessage(int sockfd, const string &name)
{
while (true)
{
struct sockaddr_in peer;
char buff[1024];
socklen_t len = 0;
int n = recvfrom(sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
buff[n] = 0;
cout << buff << endl;
}
else
{
cerr << "recvfrom error\n"
<< endl;
break;
}
}
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
cerr << "Usage:" << argv[0] << " local-port" << endl;
exit(0);
}
string ip = argv[1];
uint16_t port = stoi(argv[2]);
int sockfd = InitClient();
// auto ref = std::bind(&RecvMessage, sockfd, placeholders::_1);
// auto senf = std::bind(&SendMessage, sockfd, ip, port, placeholders::_1, placeholders::_2, placeholders::_3);
Thread recver("recver-thread", std::bind(&RecvMessage, sockfd, std::placeholders::_1));
Thread sender("sender-thread", std::bind(&SendMessage, sockfd, ip, port, std::placeholders::_1));
recver.Start();
sender.Start();
recver.Join();
sender.Join();
close(sockfd);
return 0;
}