基于KCP的聊天室项目代码的主要逻辑
其中kcp的讲解在kcp协议介绍与解析
一、主要逻辑
封装一个ChatServer类,该类保存服务器端正在进行的会话的指针,并实现消息处理和会话处理业务。
ChatServer类的run函数定义一个主循环,该主循环中包含两个线程,一个线程循环检测现有的会话是否失效。另一个线程循环接收消息,根据消息中的会话id找到或者添加新会话,然后判断是保活信息还是聊天信息并进行相应处理
二、相关结构
主程序中建立一个ChatServer类对象并设置它的kcp属性,然后调用run方法进入主循环
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
KcpOpt opt;
opt.conv = 0;
opt.is_server = true;
opt.keep_alive_timeout = 5000; // 超过5秒没有收到客户端的信息则认为其断开
ChatServer server(opt, ip, port);
server.Run();
其中KcpOpt是存储KCP的设置属性的结构,我们先看它的内容
class KcpOpt {
public:
bool is_server = true;
int64_t keep_alive_timeout = 5000; // 超时时间
// 如果服务器端超过这个时间没接收到新的数据就移除这个会话
// 如果客户端超过这个时间没接收到新的数据就发送保活数据
uint32_t conv = 0; // 会话id,该设置对服务端有效,服务端主要解析client的conv并创建同样的会话
int sndwnd = 32; // 默认发送窗口
int rcvwnd = 128; // 默认接收窗口
int nodelay = 0; // 无延迟是否开启 0未开启,1/2开启
int interval = 10; // 调度间隔时间, 默认10毫秒
int resend = 0; // 快速重传指标,0未开启,1/2开启
int nc = 0; // 流控 0开启,1关闭
};
ChatServer类是对TCPServer类的扩展,我们注意TCPServer类主要的成员变量
class KcpServer {
KcpOpt kcp_opt_; //存储KCP的属性
UdpSocket::ptr socket_; //指向UdpSocket变量的共享指针
using SessionMap = std::unordered_map<uint32_t, KcpSession::ptr>;
SessionMap sessions_; //保存正在通信会话的指针map
std::vector<char> buf_; //缓冲区
};
ChatServer类实现新的客户端逻辑接入和断开,以及与客户端收发信息等业务
class ChatServer : public KcpServer {
public:
using KcpServer::KcpServer;
// 接收客户端发送过来的消息
virtual void HandleMessage(const KcpSession::ptr &session,
const std::string &msg); //收到客户端发来的信息的处理: 通知其他人
void HandleConnection(const KcpSession::ptr &session); //检测到新的客户端: 添加新的会话并通知其它人
void HandleClose(const KcpSession::ptr &session); //检测到会话断开后的后续处理: 删除该会话以及通知其它人
void Notify(const std::string &str); //向所有保存的会话发送信息
public:
using Lock = std::unique_lock<std::mutex>;
private:
std::unordered_set<KcpSession::ptr> users_; // 会话保存
std::mutex mtx_;
};
KcpSession类的主要成员变量
class KcpSession {
public:
using ptr = std::shared_ptr<KcpSession>;
private:
KcpOpt kcp_opt_; //该会话的属性
uint32_t conv_; //会话的标志(id)
sockaddr_in addr_; //会话对象的地址、端口
UdpSocket::ptr socket_; //udp的封装,用来接收发送信息
ikcpcb *kcp_; //kcp控制块,用于控制包的发送,接收,重传等操作
int64_t recv_latest_time = 0; //上次接收的时间
int64_t send_latest_time = 0; //上次发送的时间
};
其中UdpSocket是对UDP通信的封装
class UdpSocket // udp的封装
{
//封装的接口略
private:
std::string ip_; //服务器地址
uint16_t port_; //服务器端口
int fd_; //服务器监听的socket
int family_; //服务器使用的协议
sockaddr_in addr_; //目标对象的地址、端口等
};
三、主循环
我们来看主循环,主要包含两个线程
void KcpServer::Run() {
std::thread t([this]() {
while (true) {
usleep(10);
Update();
}
});
do {
sockaddr_in addr;
int len = socket_->RecvFrom(buf_.data(), body_size, addr); // 采用block的方式 读取到udp
// if (len != -1)
// trace("recvfrom:len = ", len);
if (!Check(len)) // 检测是否有数据可以读取 header 24字节
continue;
uint32_t conv = GetConv(buf_.data());
{
Lock lock(mtx_); // 加锁
// TRACE("conv:", conv);
KcpSession::ptr session = GetSession(conv, addr);
HandleSession(session, len);
}
} while (1);
t.join(); // 等待线程退出
}
线程1循环检查是否有失效的会话,如果超时就移除会话和指向它的指针
void KcpServer::Update() {
Lock lock(mtx_); // session的管理
for (auto it = sessions_.begin(); it != sessions_.end();) {
if (!it->second->Update(iclock64())) {
++it;
continue;
}
HandleClose(it->second);
it = sessions_.erase(it);
}
}
线程2循环阻塞式接收给它的报文,根据会话id(conv)找到会话。
如果是新会话id就新建会话,否则就更新会话的客户端地址和端口。
KcpSession::ptr KcpServer::GetSession(uint32_t conv, const sockaddr_in &addr) {
auto it = sessions_.find(conv);
if (it == sessions_.end())
sessions_[conv] = NewSession(conv, addr);
KcpSession::ptr session = sessions_[conv];
const sockaddr_in &session_addr = session->GetAddr();
if (session_addr.sin_port != addr.sin_port ||
session_addr.sin_addr.s_addr != addr.sin_addr.s_addr) {
session->SetAddr(addr); // 保存的客户端的地址 ip + port
}
return session;
}
然后判断是保活信息还是聊天信息
如果是保活信息就更新该会话的上次接收时间
如果是聊天信息就调用信息处理
void KcpServer::HandleSession(const KcpSession::ptr &session, int length) {
int ret = session->Input(buf_.data(), length);
if (ret != 0) {
TRACE("Input error = ", ret);
return;
}
do {
int len = session->Recv(buf_.data(), body_size); // 读取用户数据
if (len == -3) {
TRACE("body size too small");
exit(-1);
}
if (len <= 0)
break;
// 先检测是不是保活命令
if (memcmp(buf_.data(), KCP_KEEP_ALIVE_CMD,
sizeof(KCP_KEEP_ALIVE_CMD)) == 0) {
// TRACE("recv conv: ", session->conv(), " keep alive cmd");
session->SetKeepAlive(iclock64()); // 通过带宽换cpu效率
continue;
}
std::string msg(buf_.data(), buf_.data() + len);
// TRACE(msg);
HandleMessage(session, msg); // 当前线程读取, 调用业务子类处理业务
} while (1);
}