文章目录
UDP介绍
UDP(User Datagram Protocol)是一种无连接的传输层协议,它提供了一种简单的、不可靠的数据传输服务。
UDP 提供了不面向连接的通信,且不对传送的数据报进行可靠的保证,适用于一次传送少量的数据,不适用于传输大量的数据。
特点一:无连接
两台主机在使用UDP进行数据传输时,不需要建立连接,只需知道对端的IP和端口号即可把数据发送过去。
特点二:不可靠
UDP协议没有没有确认重传机制,如果因为网络故障导致报文无法发到对方,或者对方收到了报文,但是传输过程中乱序了,对方校验失败后把乱序的包丢了,UDP协议层也不会给应用层任何错误反馈信息。
PS:在网络中,“不可靠”是个中性词,因为可靠就意味着要付出更多的代价去维护可靠,实现起来会复杂很多;而“不可靠”的话,实现起来会更简单。
特点三:面向数据报
UDP传输数据时,是以数据报文为单位一个个地发出去,然后一个个地接收的,这导致上面应用层无法灵活控制数据数据的读写次数和数量。
TCP与UDP如何选择
UDP具有优点:
1,UDP无需连接;
2,无连接状态;TCP需要在端系统中维护连接状态。连接状态包括接收和发送缓存、拥塞控制参数和序号与确认号的参数。UDP不维护连接状态,也不跟踪这些参数。
3,应用层能更好控制要发送的数据和发送时间。UDP没有拥塞控制,因此网络中的拥塞不会影响主机的发送效率。某些实时应用要求以稳定的速度发送,能容忍一些数据的丢失,但不允许有较大的时延,而UDP正好满足这些应用的需求。比如直播。
4,UDP支持一对一,一对多,多对一,多对多的通信。
UDP是面向报文的,一次交付一整个完整的报文。报文是不可分割的,是UDP数据报处理最小单位。应用程序必须选择合适大小的报文,若报文太长,UDP把它交给IP层后,可能会导致分片;若报文太短,UDP把它交给IP层后,会使IP数据报的首部的相对长度太大,两者都会降低IP层的效率。
TCP的优点:
1,TCP是面向连接的传输层协议;
2,每条TCP连接只能有两个端点,每条TCP连接只能是端到端的;
3,TCP提供可靠交付服务;
4,TCP提供全双工通信;
5,TCP面向字节流,虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序交下来的数据仅视为一连串的无结构的字节流。
基于UDP的服务端和客户端实现
UDP内部工作原理
IP的作用是让离开主机B的UDP数据包准确传递到主机A。但把UDP包最终交给主机A某一个UDP套接字的过程由UDP完成。在这个过程中,UDP不会进行流控制。
服务器端和客户端实现
1,在UDP服务器端和客户端不像TCP那样要在连接状态下交换数据,也就意味着不用像TCP调用listen和accept函数。
2,UDP服务器端和客户端均只需要一个套接字。TCP中套接字之间是一对一关系,但在UDP中,只需要1个UDP套接字可以向任何主机传输数据。
下面是基于UDP的数据IO函数:
ssize_t sendto(int sock,void *buf,size_t nbytes,int flags,struct sockaddr *to,socklen_t addrlen);
ssize_t recvfrom(int sock,void *buf,size_t nbytes,int flags,struct sockaddr *from,socklen_t *addrlen);
客户端实现:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in serv_addr,from_addr;
char message[BUF_SIZE];
int str_len;
socklen_t addr_size;
if (argc != 3)
{
printf("usage:%s IP and port\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1)
{
printf("socket() error");
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[2]));
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
while (1)
{
fputs("inuput message:(Q to quit)", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
sendto(sock,message,strlen(message),0,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
addr_size=sizeof(from_addr);
str_len=recvfrom(sock,message,BUF_SIZE,0,(struct sockaddr*)&from_addr,&addr_size);
message[str_len]=0;
printf("mesage from server: %s\n", message);
}
close(sock);
return 0;
}
服务器端实现
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[])
{
int serv_sock;
struct sockaddr_in serv_addr,clnt_addr;
char message[BUF_SIZE];
int str_len;
socklen_t clnt_addr_size;
if (argc != 2)
{
printf("usage:%s IP and port\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if (serv_sock == -1)
{
printf("socket() error");
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[1]));
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
{
printf("bind error");
}
while (1)
{
clnt_addr_size=sizeof(clnt_addr);
str_len=recvfrom(serv_sock,message,BUF_SIZE,0,(struct sockaddr*)&clnt_addr,&clnt_addr_size);
sendto(serv_sock,message,strlen(message),0,(struct sockaddr *)&clnt_addr,clnt_addr_size);
message[str_len]=0;
}
close(serv_sock);
return 0;
}
UDP如何做到可靠传输
ARQ协议
ARQ协议主要有3中模式:
(1)停止-等待ARQ
(2)后退N帧ARQ
(3)选择性重传ARQ
停止-等待ARQ
在停止-等待协议中,源站发送单个帧后必须等待确认,在目的站的回答到达源站之前,源站不能发送其他的数据帧。
工作原理:
1、发送方对接收方发送数据包,然后等待接收方回复ACK并且开始计时。
2、在等待过程中,发送方停止发送新的数据包。
3、当数据包没有成功被接收方接收,接收方不会发送ACK.这样发送方在等待一定时间后,重新发送数据包。
4、反复以上步骤直到收到从接收方发送的ACK.
后退N帧ARQ
在后退N帧式ARQ中,发送方无须在收到上一个帧的ACK后才能开始发送下一帧,而是可以连续发送帧。当接收方检测出失序的信息帧后,要求发送方重发最后一个正确接收的信息帧之后的所有未被确认的帧;或者当发送方发送了N个帧后,若发现该N个帧的前一个帧在计时器超时后仍未返回其确认信息,则该帧被判为出错或丢失,此时发送方就不得不重传该出错帧及随后的N个帧。换句话说,接收方只允许按顺序接收帧。
选择重传ARQ
在选择重传协议中,每个发送缓冲区对应一个计时器,当计时器超时时,缓冲区的帧就会重传。另外,该协议使用了比上述其他协议更有效的差错处理策略,即一旦接收方怀疑帧出错,就会发一个否定帧NAK给发送方,要求发送方对NAK中指定的帧进行重传。
流量控制
为什么需要流量控制?
1,发送方的速率和接收方的速率不一定相等
2,如果缓冲区满了发送方还在疯狂发送数据,接收方只能把收到的数据包丢掉,大量的丢掉的包会极大地浪费网络资源。
3,对发送方发送速率的控制称之为流量控制。
对于流量控制,TCP是不用自己管,UDP是需要自己控制发送速率的。
如何流量控制
在任意时刻,发送方都维持一组连续的允许发送的帧的序号,称为发送窗口;同时接收方也维持一组连续的允许接收帧的序号,称为接收窗口。发送窗口用来对发送方进行流量控制,而发送窗口的大小 代表在还未收到对方确认信息的情况下发送方最多还可以发送多少个数据帧。发送端每收到一个确认帧,发送窗口就向前滑动一个帧的位置,当发送窗口内没有可以发送的帧(即窗口内的帧全部是已发送但未收到确认的帧)时,发送方就会停止发送,直到收到接收方发送的确认帧使窗口移动,窗口内有可以发送的帧后,才开始继续发送。
流量控制机制是基于滑动窗口协议实现,在通信过程中,接收方根据自己的接收缓存的大小,动态调整发送方的发送窗口大小。同时,发送方根据当前网络拥塞程度估计来确定窗口值。
总结
1,接收窗口的大小是随着网络情况动态调整,丢包多的时候需要减少接收窗口
2,一般来说,接收窗口大于等于发送窗口
拥塞控制
拥塞控制用于防止过多的数据注入网络,保证网络中的路由器不至于过载。
拥塞控制和流量控制的区别:拥塞控制是让网络能够承受现有的网络负荷,是一个全局性的过程,涉及所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是指点对点的通信量的控制,是个端到端的问题(接收端控制发送端),它所要做的是抑制发送端发送数据的速率,以使使接收端来得及接收。
拥塞控制主要在于维护接收窗口和拥塞窗口。
接收窗口:接收方根据目前接收缓存大小更新窗口值,反应接收方的容量。
拥塞窗口:发送方根据自己估算的网络拥堵程度设置窗口,反映网络的当前容量。
发送窗口的上限值应该取接收窗口和拥塞窗口中的较小的一个。
拥塞控制主要有四种算法:慢开始,拥塞避免,快重传和快恢复
KCP协议
KCP官方:https://github.com/skywind3000/kcp
KCP概述
KCP是一种网络传输协议(ARQ,自动重传请求),可以视他为TCP的代替品,但是它运行于用户空间,不管底层的发送和接收,这是一个纯算法实现的可靠传输。KCP可以理解为可靠的UDP协议。UDP面向无连接的协议,由于实时性较好,通常用于游戏或音视频通话中。为了提高UDP的可靠性,在UDP协议上封装一层可靠性传输机制(类似TCP的ACK机制,重传机制,序号机制,重排机制,窗口机制),就做到了兼具TCP的安全性(流量控制和拥塞控制等)和UDP的实时性,并且具备一定的灵活性(超时重传,ACK等),其中一个代表是KCP协议。
TCP的特点是可靠传输(累积确认,超时重传,选择确认),流量控制(滑动窗口),拥塞控制(慢开始,拥塞避免,快重传,快恢复),面向连接。
KCP对这参数基本都可配置,也没有使用建立/关闭连接的过程。
KCP优势
KCP是一个基于UDP的快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%的效果。
KCP纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback的方式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何一次系统调用。
RTO翻倍vs不翻倍
TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖,而KCP启 动快速模式后不x2,只是x1.5,提高了传输 速度。
延迟ACK和非延迟ACK
TCP为了充分利用带宽,延迟发送ACK(NODELAY都没有用),这样超时计算会算出较大RTT时间,延长了丢包时的判断。KCP的ACK是否延迟发送可以调节。
UNA vs ACK+UNA
ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK作为丢失成本太高,以往协议都是二选其一,而KCP协议中,除去单独的ACK包外,所有的包都有UNA信息。
选择性重传vs全部重传
TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。
快速重传(跳过多少个包马上重传)
发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时, KCP知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失, 不用等超时,直接重传2号包,大大改善了丢包时的传输速度。fastresend = 2
非退让流控
KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、 接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要 求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。 以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果。
KCP基本使用
创建 KCP对象:
// 初始化 kcp对象,conv为一个表示会话编号的整数,和tcp的 conv一样,通信双
// 方需保证 conv相同,相互的数据包才能够被认可,user是一个给回调函数的指针
ikcpcb *kcp = ikcp_create(conv, user);
设置回调函数:
// KCP的下层协议输出函数,KCP需要发送数据时会调用它
// buf/len 表示缓存和长度
// user指针为 kcp对象创建时传入的值,用于区别多个 KCP对象
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{
....
}
// 设置回调函数
kcp->output = udp_output;
循环调用 update:
// 以一定频率调用 ikcp_update来更新 kcp状态,并且传入当前时钟(毫秒单位)
// 如 10ms调用一次,或用 ikcp_check确定下次调用 update的时间不必每次调用
ikcp_update(kcp, millisec);
输入一个下层数据包:
// 收到一个下层数据包(比如UDP包)时需要调用:
ikcp_input(kcp, received_udp_packet, received_udp_size);
发送数据:
ikcp_send(kcp1,buffer,8);
接收数据:
hr=ikcp_recv(kcp2,buffer,10);
下面的是协议配置
协议默认模式是一个标准的 ARQ,需要通过配置打开各项加速开关:
工作模式:
int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)
nodelay :是否启用 nodelay模式,0不启用;1启用。
interval :协议内部工作的 interval,单位毫秒,比如 10ms或者 20ms
resend :快速重传模式,默认0关闭,可以设置2(2次ACK跨越将会直接重传)
nc :是否关闭流控,默认是0代表不关闭,1代表关闭。
普通模式: ikcp_nodelay(kcp, 0, 40, 0, 0);
极速模式: ikcp_nodelay(kcp, 1, 10, 2, 1);
最大窗口:
int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd);
该调用将会设置协议的最大发送窗口和最大接收窗口大小,默认为32. 这个可以理解为 TCP的 SND_BUF 和 RCV_BUF,只不过单位不一样 SND/RCV_BUF 单位是字节,这个单位是包。
最大传输单元:
纯算法协议并不负责探测 MTU,默认 mtu是1400字节,可以使用ikcp_setmtu来设置该值。该值将会影响数据包归并及分片时候的最大传输单元。
最小RTO:
不管是 TCP还是 KCP计算 RTO时都有最小 RTO的限制,即便计算出来RTO为40ms,由于默认的 RTO是100ms,协议只有在100ms后才能检测到丢包,快速模式下为30ms,可以手动更改该值:
kcp->rx_minrto = 10;
每个通道客户端-服务端kcp是一一对应,conv由客户端产生,存在产生一致的会话id的可能,这个问题解决方法:1,由uuid算法生成唯一conv的id值
2,先使用http向服务器请求获得conv的id值
KCP源码流程图
1,每个KCP内部都有状态,每个连接创建一个KCP,kcp是独立的,服务端有多个kcp对象;
2,在某个线程实时循环调用kcp_update
3,应用层调用kcp_send,将数据发送到kcp内部,调用kcp_update,经过编码后,调用output发送包
4,接收数据:首先需要recvfrom读取udp数据,调用kcp_input发送数据到kcp_update,它会应答发送端,并且内部组帧。KCP有报文和流模式两种。经过kcp_update处理后,调用kcp_recv。
KCP原理
KCP协议头
conv:连接号。UDP是无连接的,conv用于表示来自于哪个客户端。对连接的一种替代
cmd:命令字。如,IKCP_CMD_PUSH数据推送命令,IKCP_CMD_ACK确认命令,IKCP_CMD_WASK接收窗口大小询问命令,IKCP_CMD_WINS接收窗口大小告知命令
frg:分片,用户数据可能会被分成多个KCP包,发送出去;比如用户数据被分成4个分片:报文模式的值如:3,2,1,0;流模式如:0,0,0,0
wnd:接收窗口大小,发送方的发送窗口不能超过接收方给出的数值
ts:时间序列;计算RTT,计算重发包时间点
sn:序列号
una:下一个可接收的序列号。其实就是确认号,收到sn=10的包,una为11
len:数据长度
data:用户数据
KCP发送数据过程
1,ikcp_send发送用户数据
2,将数据放入发送队列中,ikcp_update调度的时候才会将数据放入发送缓存
3,缓冲中有已经确认的包;待确认的包;第一次发送的包;需要注意重传包,重传包需要从发送缓存中取出再发送。
KCP实战
在开始实战之前先对KCP和UDP之间的的联系和区别做一些了解
1,KCP是一个在UDP之上实现的协议,它利用UDP提供的无连接,轻量级的特性来构建自己的可靠传输协议;
2,KCP通过UDP套接字发送和接收数据。在KCP内部维护自己的数据包格式和重传机制,但最终还是通过UDP进行发送;
3,KCP负责处理数据可靠性和顺序,而UDP负责数据的底层传输;
下面的是项目的总体框架
我们先来实现Udp_socket,这个类涉及的主要是UDP相关的方法
//udp_socket.cpp
UdpSocket::UdpSocket(const std::string &ip, uint16_t port, int family)
: ip_(ip), port_(port), family_(family) {
addr_.sin_family = family_;
addr_.sin_port = htons(port_);
addr_.sin_addr.s_addr = inet_addr(ip.c_str());
TRACE("addr:", GetAddrToString());
fd_ = socket(family_, SOCK_DGRAM, IPPROTO_UDP);
if (fd_ == -1)
throw strerror(errno);
}
std::string UdpSocket::GetAddrToString() const {
std::stringstream ss;
ss << inet_ntoa(addr_.sin_addr) << ":" << ntohs(addr_.sin_port);
return ss.str();
}
UdpSocket::~UdpSocket() {
TRACE("close fd");
close(fd_);
}
bool UdpSocket::SetNoblock() {
int flag = fcntl(fd_, F_GETFL);
return fcntl(fd_, F_SETFL, flag | O_NONBLOCK) == 0;
}
bool UdpSocket::Bind() {
int ret = bind(fd_, (sockaddr *)&addr_, sizeof(addr_));
if (ret == -1) {
perror("bind error");
return false;
}
return true;
}
int UdpSocket::RecvFrom(void *buf, std::size_t size, sockaddr_in &addr,
int flag) {
socklen_t len = sizeof(addr);
return recvfrom(fd_, buf, size, flag, (sockaddr *)&addr, &len);
}
int UdpSocket::SendTo(const void *buf, std::size_t size, sockaddr_in &addr,
int flag) {
return sendto(fd_, buf, size, flag, (sockaddr *)&addr, sizeof(addr));
}
KCP是在UDP之上的协议,构建一个封装了Udp_socket的KCP会话:KcpSession
//kcp_session.cpp
#define KCP_KEEP_ALIVE_CMD "KCP_KEEP_ALIVE_CMD"
//回调函数使用sendTo发送数据
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user) {
KcpSession *ptr = (KcpSession *)user;
int ret = ptr->sendTo((const void *)buf, len);
return ret;
}
//这个函数调用UDP的sendto发送封装好的KCP数据包到目标地址
int KcpSession::sendTo(const void *buf, std::size_t size, int flag) {
int ret = socket_->SendTo(buf, size, addr_, flag);
return ret;
}
//KCP会话初始化,配置部分参数
KcpSession::KcpSession(KcpOpt &kcp_opt, const sockaddr_in &addr,
const UdpSocket::ptr &socket)
: kcp_opt_(kcp_opt), addr_(addr), socket_(socket) {
kcp_ = ikcp_create(kcp_opt_.conv, this);
//当KCP需要发送数据时调用回调函数
kcp_->output = udp_output;
TRACE("addr:", GetAddrToString());
TRACE("kcp_opt.conv:", kcp_opt.conv);
TRACE("kcp_opt.is_server:", kcp_opt.is_server);
TRACE("kcp_opt.keep_alive_timeout: ", kcp_opt_.keep_alive_timeout);
TRACE("kcp_opt.sndwnd:", kcp_opt.sndwnd);
TRACE("kcp_opt.rcvwnd:", kcp_opt.rcvwnd);
TRACE("kcp_opt.nodelay:", kcp_opt.nodelay);
TRACE("kcp_opt.resend:", kcp_opt.resend);
TRACE("kcp_opt.nc:", kcp_opt.nc);
//设置窗口大小
ikcp_wndsize(kcp_, kcp_opt.sndwnd, kcp_opt.rcvwnd);
//设置工作模式
ikcp_nodelay(kcp_, kcp_opt.nodelay, kcp_opt.interval, kcp_opt.resend, kcp_opt.nc);
}
KcpSession::~KcpSession() {
if (kcp_) {
ikcp_release(kcp_);
kcp_ = NULL;
}
}
//以ip:prot格式输出addr_数据
std::string KcpSession::GetAddrToString() const {
std::stringstream ss;
ss << inet_ntoa(addr_.sin_addr) << ":" << ntohs(addr_.sin_port);
return ss.str();
}
//更新 KCP会话状态
bool KcpSession::Update(int64_t current) {
{
Lock lock(mtx_);
ikcp_update(kcp_, (uint32_t)current);
}
if (recv_latest_time == 0)
recv_latest_time = current;
// 客户端的心跳检测,比如服务端 5000ms, 客户端1000ms发送一次
if (kcp_opt_.is_server &&
(current - recv_latest_time > kcp_opt_.keep_alive_timeout)) {
TRACE("conv: ", kcp_opt_.conv, " timeout");
return true;
}
// 客户端检测到长时间没有发送数据则发送保活数据
if (!kcp_opt_.is_server &&
(current - send_latest_time > kcp_opt_.keep_alive_timeout)) {
// 独立发送保活信息
send_latest_time = current;
int ret = Send(KCP_KEEP_ALIVE_CMD, sizeof(KCP_KEEP_ALIVE_CMD));
}
return false;
}
//接收方通过UDP套接字接收数据,通常使用recvfrom读取数据,读取到的数据传递给ikcp_input
//ikcp_input将从UDP套接字收到的KCP数据包输入到KCP内部处理,其中包括拆解协议字段,更新KCP内部接收窗口,ACK信息,丢包重传等等;
//把有效的数据存储在KCP接收缓存区
int KcpSession::Input(void *data, std::size_t size) {
Lock lock(mtx_);
int ret = ikcp_input(kcp_, (const char *)data, size);
return ret;
}
//ikcp_recv将从KCP的接收缓冲区读取数据,更新KCP内部ACK确认等信息
//整个过程其实先由ikcp_input处理接收到的数据,再由ikcp_recv读取处理完的接收数据
int KcpSession::Recv(void *buf, uint32_t size) {
Lock lock(mtx_);
int ret = ikcp_recv(kcp_, (char *)buf, size);
// TRACE("ikcp_recv ret:", ret);
return ret;
}
// 用户数据发送给kcp内部处理,由kcp内部调用sendto发送给对端
int KcpSession::Send(const void *data, uint32_t size) {
Lock lock(mtx_);
//KCP使用ikcp_send函数发送数据时,ikcp_send将要发送的KCP数据包,可能分片,重排处理,然后调用udp_output。
int ret = ikcp_send(kcp_, (const char *)data, size);
return ret;
}
接下来,我们先写服务器端由KCP协议构建的代码
//kcp_server.cpp
static constexpr uint32_t body_size = 1024 * 4;
using SessionMap = std::unordered_map<uint32_t, KcpSession::ptr>;
SessionMap sessions_;
static inline void itimeofday(long *sec, long *usec) {
struct timeval time;
gettimeofday(&time, NULL);
if (sec)
*sec = time.tv_sec;
if (usec)
*usec = time.tv_usec;
}
/* get clock in millisecond 64 */
static inline int64_t iclock64() {
long s, u;
int64_t value;
itimeofday(&s, &u);
value = ((int64_t)s) * 1000 + (u / 1000);
return value;
}
//对KCP基础配置,服务器端口绑定
KcpServer::KcpServer(KcpOpt &kcp_opt, const std::string &ip, uint16_t port)
: kcp_opt_(kcp_opt),
socket_(std::make_shared<UdpSocket>(ip, port, AF_INET)), buf_(body_size) {
if (!socket_->Bind()) {
exit(-1);
}
// socket_->SetNoblock();
}
//KCP的会话和客户端之间通常是一一对应关系,每个客户端在KCP服务器上都有独特的conv,用于区别不同客户端会话
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);
}
}
//检查返回值
bool KcpServer::Check(int ret) {
if (ret == -1) {
if (errno != EAGAIN)
perror("recvform error!");
return false;
}
if(ret < 24) {
return false;
}
return true;
}
//获取session
KcpSession::ptr KcpServer::GetSession(uint32_t conv, const sockaddr_in &addr) {
auto it = sessions_.find(conv);
//不是已有session则添加
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;
}
//创建新的session
KcpSession::ptr KcpServer::NewSession(uint32_t conv, const sockaddr_in &addr) {
KcpOpt opt = kcp_opt_;
opt.conv = conv;
KcpSession::ptr session = std::make_shared<KcpSession>(opt, addr, socket_);
//虚函数
HandleConnection(session);
return session;
}
uint32_t KcpServer::GetConv(const void *buf) { return *(uint32_t *)(buf); }
//在整个过程中,主线程与子线程是并行执行的。子线程负责定期调用 Update(),而主线程处理接收到的网络数据。
void KcpServer::Run() {
std::thread t([this]() {
while (true) {
usleep(10);
Update();
}
});
do {
sockaddr_in addr;
// 采用block的方式 从udp读取数据
int len = socket_->RecvFrom(buf_.data(), body_size, addr);
// 检测是否有数据可以读取 header 24字节
if (!Check(len))
continue;
//获取会话标识符conv
uint32_t conv = GetConv(buf_.data());
{
Lock lock(mtx_); // 加锁
//获取kcp会话
KcpSession::ptr session = GetSession(conv, addr);
HandleSession(session, len);
}
} while (1);
t.join(); // 等待线程退出
}
//处理KCP会话
void KcpServer::HandleSession(const KcpSession::ptr &session, int length) {
//获取从udp读取到的数据进行处理,放入接收缓冲区
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) {
session->SetKeepAlive(iclock64()); // 通过带宽换cpu效率
continue;
}
std::string msg(buf_.data(), buf_.data() + len);
//虚函数
HandleMessage(session, msg); // 当前线程读取, 调用业务子类处理业务
} while (1);
}
下面是服务器端实现代码
//chat_server.cpp
class ChatServer : public KcpServer {
public:
//声明,在ChatServer类中引入KcpServer的构造函数
using KcpServer::KcpServer;
// 接收客户端发送过来的消息
//虚函数的重写
virtual void HandleMessage(const KcpSession::ptr &session,
const std::string &msg) override {
std::stringstream ss;
ss << "user from [" << session->GetAddrToString() << "] : " << msg;
Notify(ss.str());
}
//连接时会话处理
void HandleConnection(const KcpSession::ptr &session) {
{ // 注意锁的粒度
Lock lock(mtx_);
users_.insert(session);
}
std::stringstream ss;
//构建一个字符串,ss是一个stringstream对象,把字符串写入这个字符串流中
ss << "user from [" << session->GetAddrToString()
<< "] join the ChatRoom!";
Notify(ss.str()); // 通知房间的所有人
}
void HandleClose(const KcpSession::ptr &session) {
TRACE("close ", session->GetAddrToString());
{
Lock lock(mtx_);
users_.erase(session);
}
std::stringstream ss;
ss << "user from [" << session->GetAddrToString()
<< "] left the ChatRoom!";
Notify(ss.str());
}
//发送字符串数据
void Notify(const std::string &str) {
Lock lock(mtx_);
for (auto &user : users_)
user->Send(str.data(), str.size());
}
public:
using Lock = std::unique_lock<std::mutex>;
private:
std::unordered_set<KcpSession::ptr> users_; // 业务保存
std::mutex mtx_;
};
int main(int argc, char *argv[]) {
// arg : ip + port
if (argc != 3) {
exit(-1);
}
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();
return 0;
}
客户端和服务器端KCP实现有相似之处,下面是客户端KCP实现
//kcp_client
//初始化
KcpClient::KcpClient(const std::string &ip, uint16_t port, KcpOpt &kcp_opt) {
server_addr_.sin_family = AF_INET;
server_addr_.sin_port = htons(port);
server_addr_.sin_addr.s_addr = inet_addr(ip.c_str());
socket_ = std::make_shared<UdpSocket>(ip.c_str(), port, AF_INET);
socket_->SetNoblock();
session_ = std::make_shared<KcpSession>(kcp_opt, server_addr_, socket_);
}
bool KcpClient::Run() {
//每个客户端只有一个kcpsession
//更新session的值
if (session_->Update(iclock64())) {
//虚函数
HandleClose();
return false;
}
std::string buf(1024 * 4, 'a');
sockaddr_in addr;
//使用 RecvFrom 函数接收来自 UDP 套接字的原始数据包。这些数据包是未经过 KCP 处理的原始网络数据。
int len =
socket_->RecvFrom(const_cast<char *>(buf.data()), buf.length(), addr);
if (addr.sin_port != server_addr_.sin_port ||
addr.sin_addr.s_addr != server_addr_.sin_addr.s_addr) {
if (len != -1)
TRACE("run ret:", len);
return true;
}
if (!CheckRet(len))
return true;
buf.resize(len);
//将接收到的原始数据输入到 KCP 会话中,处理后把数据放入接收缓冲中
len = session_->Input(const_cast<char *>(buf.data()), len);
if (len != 0) {
TRACE("input error = ", len);
return false;
}
std::string body(max_body_size, 'a');
do {
//从 KCP 会话中读取接收缓冲区的数据
len = session_->Recv(const_cast<char *>(body.data()), max_body_size);
if (len == -3) {
TRACE("body_size too small");
exit(-1);
}
if (len <= 0)
break;
body.resize(len);
//虚函数
HandleMessage(body); // 调用子类,处理业务
} while (1);
return true;
}
//发送数据
int KcpClient::Send(const void *data, uint32_t size) {
int ret = session_->Send(data, size);
return ret;
}
bool KcpClient::CheckRet(int len) {
if (len == -1) {
if (errno != EAGAIN)
perror("recvform error!");
return false;
}
if (len < KCP_HEADER_SIZE)
return false;
return true;
}
下面是客户端代码实现
class ChatClient : KcpClient
{
public:
using KcpClient::KcpClient;
void Start()
{
std::thread t([this]()
{
while(true)
{
usleep(10);
if(!Run()){
std::cout<<"error occur"<<std::endl;
break;
}
} });
while (true)
{
std::string msg;
std::getline(std::cin, msg);
Send(msg.data(), msg.length());
}
t.join();
}
// 服务端发送过来的消息
virtual void HandleMessage(const std::string &msg) override
{
std::cout << msg << std::endl;
}
virtual void HandleClose() override
{
std::cout << "close kcp connection!" << std::endl;
exit(-1);
}
};
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "args error please input [ip][port]" << std::endl;
exit(-1);
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
srand(time(NULL));
KcpOpt opt;
opt.conv = rand() * rand(); // uuid的函数
opt.is_server = false;
opt.keep_alive_timeout = 1000; // 保活心跳包间隔,单位ms
std::cout << "conv = " << opt.conv << std::endl;
ChatClient client(ip, port, opt);
client.Start();
return 0;
}
总结,KCP是在UDP之上的协议,所有数据的收和发都是有UDP完成的,只是这些数据都需要经过KCP处理,才能保证可靠性传输。