实现原理
只有当客户端先对服务器发送online消息的时候,服务器才会把客户端加入到在线列表。当在线列表的用户发消息的时候,服务器会把消息广播给在线列表中的所有用户。而当用户输入offline时,表明自己要下线了,此时服务器把该用户踢出在线列表。此时的用户看不到公屏的信息也无法在发送信息。
上线步骤:
通信步骤:
下线步骤
只要把用户踢出在线列表,那么它就是离线了,因为服务器只关心在线列表中的客户。
服务器要做的事
1. 判断收到的消息是否是online或者offline
2. 收到online则把用户添加进在线列表,offline则移除在线列表。
3. 如果发送的消息不是offline,切用户在线,则对发送的消息进行广播,广播给在线列表的所有用户
客户端要做的事
1. 向服务器发送online申请上线
2. 主线程负责发送消息,不发也可以
3. 创建一个线程时时刻刻接收消息,收到消息即显示到自己的公屏上
server服务端代码实现
服务端需要有一个UserManage类,来管理在线用户,这也是我们的在线列表。这个类只有一个哈希表成员,用来管理在线的用户。还要提供四个成员函数,分别有上线,下线,判断是否在线,以及广播功能。
server.cc代码:
cpp
#include "server.hpp"
#include <memory>
#include <unistd.h>
#include <fcntl.h>
#include <vector>
#include <sys/wait.h>
#include <cstring>
#include "User.hpp"
UserManage um;
void ChatRoomMessage(int _sock,std::string ip,uint16_t port,std::string message)
{
//如果用户输入online,那么就把用户添加到在线列表
if(message == "online") um.online(port,ip);
//如果用户输入offline,那么把用户移除在线列表
if(message == "offline") um.offline(port,ip);
//用户在线才能广播消息
if(um.isonline(port,ip))
um.broadcastMessage(message,_sock,ip,port); //广播消息
}
int main(int argc , char* argv[])
{
if(argc != 2) //命令行参数不为2就退出
{
std::cout << "Usage : " << argv[0] << " bindport" << std::endl; //打印使用手册
exit(1);
}
uint16_t port = atoi(argv[1]); //命令行传的端口转换成16位整形
std::unique_ptr<UdpServer> s(new UdpServer(port,ChatRoomMessage)); //创建UDP服务器
s->init(); //初始化服务器,创建 + 绑定
s->start(); //运行服务器
}
server.hpp代码:
这个类主要是对服务器的封装,在收到消息后通过用户传入的callback函数进行回调处理。
cpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <functional>
typedef std::function<void(int,std::string,uint16_t,std::string)> func_t;
class UdpServer
{
private:
int _sock;
uint16_t _port;
func_t _callback;
public:
UdpServer(uint16_t port,func_t callback): _port(port) ,_callback(callback){ }
~UdpServer() { close(_sock); }
void init()
{
_sock = socket(AF_INET,SOCK_DGRAM,0); //创建套接字
if(_sock < 0)
{
//创建失败
std::cout << "create socket failed...." << std::endl;
abort();
}
//绑定
struct sockaddr_in ser;
ser.sin_port = htons(_port); //填入端口
ser.sin_family = AF_INET; // 填入域
ser.sin_addr.s_addr = INADDR_ANY; //填入IP地址
if(bind(_sock,(sockaddr*)&ser,sizeof ser) != 0) //绑定
{
//绑定失败
std::cout << "bind socket failed...." << std::endl;
abort();
}
}
void start()
{
struct sockaddr_in peer; //对端
socklen_t peer_len = sizeof peer;
char buff[1024] = {0};
while(1)
{
int n = recvfrom(_sock,buff,1023,0,(struct sockaddr*)&peer,&peer_len);
buff[n] = 0;
if(read == 0)
{
std::cout << "one client quit..." << std::endl;
continue;
}else if(read < 0)
{
std::cout << "read error..." << std::endl;
break;
}
//获取客户端的端口和IP
std::string clientip = inet_ntoa(peer.sin_addr);
uint16_t clientport = ntohs(peer.sin_port);
std::cout << buff << std::endl; //回显客户端信息
//调用回调函数处理数据
_callback(_sock,clientip,clientport,buff);
}
}
};
User.hpp代码:
这个头文件有2个类,User类是对用户的一层抽象,如果你用户还有其他的信息也可以加入到User类中。UserManage是对在线用户的管理,提供了增删查操作,以及消息广播。
cpp
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class User
{
public:
User(uint16_t port , const std::string& ip) :_port(port),_ip(ip){}
std::string ip(){return _ip;}
uint16_t port(){return _port;}
private:
uint16_t _port;
std::string _ip;
};
class UserManage
{
public:
//上线
bool online(uint16_t port,const std::string& ip)
{
std::string id = ip + "-" + std::to_string(port);
auto it = _users.find(id);
User u(port,ip);
//如果不在上线列表中,加入到上线列表
if(it == _users.end())
{
_users.insert(std::make_pair(id,u));
std::cout <<"[" << id << " is online]" << std::endl;
}
else return false;
return true;
}
//下线
bool offline(uint16_t port,const std::string& ip)
{
std::string id = ip + "-" + std::to_string(port);
auto it = _users.find(id);
if(it != _users.end())
{
_users.erase(id); //移除在线列表
std::cout <<"[" << id << " is offline]" << std::endl;
return true;
}
//没找到该用户,下线错误
return false;
}
//用户是否在线
bool isonline(uint16_t port,const std::string& ip)
{
std::string id = ip + "-" + std::to_string(port);
auto it = _users.find(id);
return it != _users.end();
}
//消息转发,把消息转发给用户列表的所有人
void broadcastMessage(const std::string& message, int _sock,std::string ip,uint16_t port)
{
for(auto& u : _users)
{
//构建客户端sockaddr_in
uint16_t u_port = u.second.port(); //要广播的客户端端口
std::string u_ip = u.second.ip(); //要广播的客户端ip
struct sockaddr_in client;
client.sin_family = AF_INET;
client.sin_port = htons(u_port);
client.sin_addr.s_addr = inet_addr(u_ip.c_str());
//这里的ip和port是发送消息人的端口和port
std::string response = ip + "-" + std::to_string(port) + " :" + message;
//发送消息
sendto(_sock,response.c_str(),response.size(),0,(struct sockaddr*)&client,sizeof client);
}
}
private:
std::unordered_map<std::string,User> _users; //记录在线用户
};
client客户端实现
客户端必须要保证至少2个线程,因为读消息和发送消息在一个线程里进行的话,会发送IO阻塞。除非你用多路转接=,=这里暂时不使用这种方法。
client.cc代码:
cpp
#include "client.hpp"
#include <memory>
int main(int argc , char* argv[])
{
if(argc != 3)
{
std::cout << "Usage : " << argv[0] << " serverip serverport" << std::endl;
exit(1);
}
uint16_t port = atoi(argv[2]);
std::string ip = argv[1];
std::unique_ptr<UdpClient> cli(new UdpClient(port,ip));
cli->init();
cli->start();
}
client.hpp代码:
start函数负责处理用户发送的消息,RecvMessageThread函数是线程的执行函数,负责收服务器广播回来的消息,并把消息打印在公屏上,注意回显消息要用cerr打印!因为我们测试的时候会把cout重定向到一个命名管道中。
cpp
#pragma once
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <arpa/inet.h>
#include <cstdio>
#include <cstring>
#include <pthread.h>
class UdpClient
{
public:
UdpClient(uint16_t port, const std::string &ip) : _port(port), _svr_ip(ip) {}
~UdpClient() { close(_sock); }
void init()
{
// 套接字创捷
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock < 0)
{
std::cout << "create socket failed...." << std::endl;
abort();
}
}
// 线程执行函数,负责接收消息
static void *RecvMessageThread(void *args)
{
while (1)
{
int *sock = (int *)args; //提取sock套接字
// 收服务器广播来的消息
char recvbuff[1024 * 4] = {0};
recvfrom(*sock, recvbuff, sizeof recvbuff - 1, 0, nullptr, nullptr);
// 打印回收到的消息
std::cout << recvbuff << std::endl;
}
return nullptr;
}
void start()
{
// 创建服务器的 sockaddr结构
struct sockaddr_in svr;
svr.sin_port = htons(_port);
svr.sin_addr.s_addr = inet_addr(_svr_ip.c_str());
svr.sin_family = AF_INET;
// 发送消息的缓冲区
char sendbuff[1024] = {0};
// 创建一个线程来接收别人发送的信息
pthread_t tid;
pthread_create(&tid, nullptr, RecvMessageThread, (void *)&_sock);
// 该线程负责发送消息
while (1)
{
// 输入消息
std::cerr << "Enrty # ";
fgets(sendbuff, sizeof sendbuff - 1, stdin);
sendbuff[strlen(sendbuff) - 1] = 0;
std::string message = sendbuff;
// 发送消息
sendto(_sock, message.c_str(), message.size(), 0, (struct sockaddr *)&svr, sizeof svr);
}
}
private:
int _sock;
uint16_t _port;
std::string _svr_ip;
};
测试代码:
首先我们启动服务器,绑定端口8080(这个绑定其他的也可以)。
随后启动一个客户端,创建一个管道,这里的管道就相当于聊天室中的公屏,而自己在命令行里输入的是自己的输入窗口。而不是输入栏和接收栏都用一个窗口,这样显得十分怪异,因为自己的消息会回显2次。
随后一个窗口启动客户端并把内容重定向到管道,一个窗口监视管道。
此时的客户端是没有在线的,我们输入online即可上线。
此时我们再创建一个客户端,进行同样的操作,但是暂时不要上线,看看没上线的客户端是否可以和上线的客户端通信。我们会发现没上线的客户端发消息,上线的客户端是看不见的。
我们让上线的客户端也发送消息,我们发现客户端2是无法收到的。
我们让客户端2输入online登录,随即两个客户端进行通信,而一旦客户端2下线后,客户端2的消息将无法被送达客户端1。
因为命令行,只有使用ctrl+退格键才能退格,而退格之后会产生乱码.....这些都是小事情,和程序本身无关。