开源C/C++跨平台网络库,底层功能由C实现(60%),使用C++封装(30%)。 基础模块:原子操作,内存分配与释放,字符串操作,文件夹/路径操作,随机数,大小端字节序,数学函数,线程锁,跨平台宏,日期时间,文件操作,文件缓存,dir/ls,url,常用宏定义; 数据结构:动态数组宏,堆,缓存,链表,队列,红黑树,MultiMap,JSON; 功能模块:日志,网络日志,错误,命令行参数,配置文件,信号处理,软件版本,自动析构,单例宏,线程私有变量,资源池,线程池,拆包组包; 网络模块:socket通用接口,ifconfig,心跳,重连,转发,tcp客户端/服务端,udp客户端/服务端,http客户端/服务端,WebSocket客户端/服务端; 事件循环:事件,事件循环,事件循环线程,事件循环线程池,定时器,网络IO,异步自定义事件,epoll,poll,select,iocp(win),kqueue(OS_BSD/OS_MAC),evport(OS_SOLARIS); 协议编码:gRPC,rudp,kcp,ssl,tls,mqtt,dns,ftp,icmp,smtp,base64,md5,sha1。 |
Github:https://github.com/ithewei/libhv 文档:https://github.com/ithewei/libhv/tree/master/docs/cn 接口手册:https://hewei.blog.youkuaiyun.com/article/details/103976875 教程目录:libhv教程00--目录-优快云博客 |
一、网络通信概念
1、OSI七层体系结构
(虽然细致完善,但过于复杂,得不到实际应用) |
应用层、表示层、会话层、传输层、网络层、数据链路层、物理层; |
2、TCP/IP四层体系结构
(简化OSI) | ||
应用层、传输层、网际(网络)层、网络接口层; | ||
![]() | ||
TCP/IP不是一个单独的协议,而是一个协议簇,是一组不同层次上的多个协议的组合,TCP/IP分层模型每一层负责不同的通信功能,整体联动合作,就可以完成互联网的大部分传输要求; | ||
TCP/IP层次 | 相关网络协议 | 作用 |
应用层 | HTTP协议(超文本传输协议)、NTP协议(网络时间协议)、TELNET协议(虚拟终端协议)、DNS协议(域名解析协议)、FTP协议(文件传输协议)、TFTP协议(简单文件传输协议)、SMTP协议(简单邮件传输协议)、POP协议(邮局协议) | 负责应用程序网络访问,通过端口号识别不同进程 |
传输层 | TCP协议(传输控制协议)、UDP协议(用户数据报协议) | 负责端对端之间通信会话连接和建立(TCP UDP) |
网络层 | IP协议(网际协议)、ICMP协议(互联网控制报文协议) | 负责将数据帧封装成IP数据报,并运行必要的路由算法 |
网络接口层 | ARP协议(地址解析协议),以太网帧 | 将数据帧转换为二进制流,并进行数据帧的发送和接收(使用ARP协议目的:发送之前需要根据IP地址获取MAC地址,才能将数据包发送到正确的目标主机[通过MAC地址定位]) |
数据传输流程: | ||
![]() |
3、IP地址(计算机标识号)
- IPv4
IPv4的地址位数为32位(0—42亿(4,294,967,296)),最多2的32次方电脑可以连接 |
个人PC:动态IP |
百度/阿里:静态IP - 1211739(不管所处何地都能进行访问) |
(域名(www.baidu.com)经过域名解析(DNS)可以转换为IP地址) (IPV4资源有限->收费购买) |
网络类型 | 作用 | 特征 | 地址范围 | |
A类地址 | 保留给政府机构 | 8位网络号,24位主机号,且网络号第一位为0 | 0.0.0.0 ~ 127.255.255.255 | |
B类地址 | 分配给中等规模公司 | 16位网络号,16位主机号,且网络号前两位为10 | 128.0.0.0 ~ 191.255.255.255 | |
C类地址 | 分配给任何需要的人 | 24位网络号,8位主机号,且网络号的前三位为110 | 192.0.0.0 ~ 223.255.255.255 | |
D类地址 | 用于组播 | 32位都是组播号,且组播号前四位为1110 | 224.0.0.0 ~ 239.255.255.255 | |
E类地址 | 用于实验 | 32位都是组播号,前组播号前五位为11110 | 240.0.0.0 ~ 247.255.255.255 | |
根据用途和安全性级别不同,IP地址还可以大致分为两类: | ||||
公共地址在Internet中使用,可以在Internet中随意访问 | ||||
私有地址只能在内部网络使用,通过代理服务器才能与Internet通信 | ||||
网络类型 | 私有IP地址范围 | |||
A类地址 | 10.0.0.0 ~10.255.255.255 | |||
B类地址 | 172.16.0.0~172.31.255.255 | |||
C类地址 | 192.168.0.0~192.168.255.255 |
(在同一局域网下的上网设备共享一个公有IP)
点分十进制: |
IPv4地址:2345678988(真实IP数值); 将2345678988转为16进制:8BD0 388C; 将16进制转为点分十进制格式:8B D0 38 8C -> 13205140; |
- IPv6
IPv6地址位数为128位,几乎可以不受限制地提供地址; |
IPv6是下一个版本互联网协议,IPv4定义的有限地址空间将被耗尽,为了扩大地址空间,拟通过IPv6重新定义地址空间; |
IPv6的设计除了一劳永逸地解决地址短缺问题以外,还考虑了在IPv4中解决不好的其它问题,主要有端到端IP连接、服务质量、安全性、多播、移动性、即插即用等; |
4、端口号(进程标识号)
一台服务器(或计算机)有256*256个端口。 | |
端口:2个字节 | |
端口号范围:0---65535 | 0-1023是公认端口号,即已经公认定义或为将要公认定义软件保留 |
1024-65535是没有公认定义的端口号,用户自定义这些端口作用 | |
端口与协议有关(TCP和UDP的端口互不相干) |
不同体系结构的主机使用不同的字节序来存储保存多字节整数; |
(以32位整数0x01020304为例) | |
端口:2个字节; | |
端口与协议有关(TCP和UDP的端口互不相干); | |
大端字节序 | 高位数据存放在低地址处,低位数据在高地址处; |
小端字节序 | 高位数据存放在高地址处,低位数据存放在低地址处; |
主机字节序 | 主机内部内存中数据的处理方式,Intel机器采用小端排序方式; |
网络字节序 | 网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用big endian(大端)排序方式; |
5、子网掩码
子网掩码只有一个作用,将某个IP地址划分成网络地址和主机地址两部分。 子网掩码为一个32位地址,用于屏蔽IP地址的一部分以区别网络标识和主机标识,通过子网掩码,就可以判断两个IP在不在一个局域网内部; |
子网掩码:屏蔽一个IP地址的网络部分的“全1”比特模式。 对于A类地址来说,默认的子网掩码是255.0.0.0; 对于B类地址来说默认的子网掩码是255.255.0.0; 对于C类地址来说默认的子网掩码是255.255.255.0; |
![]() |
局域网中某主机的IP地址为202.116.1.12/21,该局域网的子网掩码为(C) (注:21表示网络号的位数,也就是子网掩码中前多少位为1) A.255.255.255.0 11111111 11111111 11111000 00000000 B.255.255.252.0 C.255.255.248.0 D.255.255.240.0 |
6、网关/路由表
在计算机网络中,网关(gateway)是连接不同网络之间的设备,用于转发数据包。 连接不同网络:网关连接两个或多个不同类型的网络,如连接局域网和因特网或连接以太网和无线网络; 转发数据包:当两个网络之间的数据包需要交换时,网关接收来自一个网络的数据包,并根据目的地址将其转发到另一个网络; 协议转换:如果两个网络使用不同的协议,网关可以将一个协议的数据包转换为另一个协议的数据包,以确保数据的正确传输; 访问控制:网关可以实现访问控制策略,允许或阻止特定的数据包通过,以保护网络安全; 网络地址转换(NAT):网关在进行数据包转发时,可能会对数据包的源地址或目的地址进行转换,以隐藏网络的真实地址或实现多个主机共享一个公共 IP 地址。 |
路由表是路由器用来决定如何转发IP数据报的核心,一个IP路由表通常包含以下主要信息: 目的网络地址:表示数据报要到达的目标网络; 地址掩码:用于确定目的网络的范围; 下一跳路由器:数据报需要转发到的下一个路由器的地址; 接口:数据报通过哪个接口发送; |
7、NAT映射
网络地址转换(NAT)是一种用于访问Internet访问模式广域网(WAN)的技术,用于将私有(保留)地址转换为合法IP地址,NAT不仅能够有效地额抵抗外部网络攻击,还能够在IP地址分配不理想,不足时有效、合理化的分配IP地址,从而能够进行互联网访问; |
优点: |
极大节省合法IP地址; 能够处理地址重复情况,避免了地址的重新编号,增加了编址的灵活性; 隐藏内部网络地址,增强安全性; 可以使多个使用TCP负载特性的服务器之间实现基本的数据包负载均衡; |
缺点: |
由于NAT要在边界路由器上进行地址转换,增大了传输延迟; 由于NAT改动了IP地址,失去了跟踪端到端IP流量的能力,当出现恶意流量时,会使故障排除和流量跟踪变的更加棘手; 不支持一些特定的应用程序,如早期版本的MSN; 增大了资源开销,处理NAT进程增加了CPU的负荷,并需要更多内存来存储NAT表项; |
![]() |
8、内网穿透
内网计算机(LowID),通过至少一层网关连接互联网,没有自己的独立IP和端口(别人看到的IP为网关),无法主动建立连接,两个内网用户也就无法连通,更无法实现传输; 外网的路由需要进行内网ip和端口的映射或者使用内网穿透产品(内网主机软件和第三方服务器进行通信,其它用户通过第三方服务器的转发和内网主机通信); |
![]() |
二、Libhv环境搭建(Windows)
1、Vcpkg包安装
.\vcpkg install libhv |
2、CMakeLists
# this is heuristically generated, and may not be correct find_package(libhv CONFIG REQUIRED) target_link_libraries(main PRIVATE hv) |
三、Libhv环境搭建(Linux)
cmake -S . -B build cmake --build build/ |
编译完成会生成到build目录: include:头文件 lib:静态库、动态库 |
四、Libhv环境搭建(交叉编译)
cmake -S . -B build -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ cmake --build build/ |
注:只需要指定交叉编译器即可,目前已测试通过arm-linux-gnueabihf、aarch64-linux-gnu。 |
五、Libhv Utils
1、Libhv Logfile
hlogd(fmt, ...) hlogi(fmt, ...) hlogw(fmt, ...) hloge(fmt, ...) hlogf(fmt, ...) |
#include <stdio.h> #include "hv/hlog.h" int main() { // 设置日志级别 hlog_set_level(LOG_LEVEL_DEBUG); // 设置日志文件路径和名称 hlog_set_file("iotgateway.log"); // 设置日志文件大小 - 5MB hlog_set_max_filesize(5*1024*1024); // 设置日志文件保留天数 hlog_set_remain_days(1); // 输出日志到文件 for(int i = 0; i < 500000; ++i) { hlogd("This is a debug message"); hlogi("This is an info message"); hlogw("This is a warning message"); hloge("This is an error message"); } return 0; } |
注:此方式只能1天一个日志文件,并做单个日志文件大小限制。 |
2、Libhv Sysinfo
int get_ncpu() 描述: 获取系统中可用的 CPU 核心数量; 返回值: 返回 CPU 核心数量。 int get_meminfo(meminfo_t* mem) 描述: 获取系统内存信息,包括总内存和可用内存; 参数: mem: 用于存储内存信息的结构体指针; 返回值: 返回 0 表示成功,返回负值表示失败。 |
meminfo_t 描述: 存储内存信息的结构体; 成员变量: total: 总内存大小(单位:KB); free: 可用内存大小(单位:KB)。 |
#include <iostream> #include "hv/hsysinfo.h" int main(int argc, char** argv) { // 获取 CPU 核心数量并打印 int ncpu = get_ncpu(); std::cout << "Number of CPU cores: " << ncpu << std::endl; // 获取内存信息并打印 meminfo_t mem; int ret = get_meminfo(&mem); if (ret == 0) { std::cout << "Total memory: " << mem.total << " KB" << std::endl; std::cout << "Free memory: " << mem.free << " KB" << std::endl; } else { std::cerr << "Failed to get memory info. Error code: " << ret << std::endl; }
return 0; } |
3、Libhv Time
数据结构: |
datetime_t: 表示日期和时间的结构体。 typedef struct datetime_s { int year; int month; int day; int hour; int min; int sec; int ms; } datetime_t; |
时间获取与格式化函数: |
|
#include <iostream>
#include "hv/htime.h"
int main(int argc, char** argv)
{
// 获取并打印当前时间
datetime_t now = datetime_now();
char buf[DATETIME_FMT_BUFLEN];
datetime_fmt(&now, buf);
std::cout << "Current time: " << buf << std::endl;
// 获取并打印未来3天的时间
datetime_t future = now;
datetime_future(&future, 3);
datetime_fmt(&future, buf);
std::cout << "Time 3 days in the future: " << buf << std::endl;
// 将时间戳转换为本地时间并打印
time_t timestamp = time(NULL);
datetime_t local = datetime_localtime(timestamp);
datetime_fmt(&local, buf);
std::cout << "Local time: " << buf << std::endl;
// 计算并打印系统启动到现在的毫秒数
unsigned long long ms = gettick_ms();
std::cout << "Milliseconds since system start: " << ms << std::endl;
// 打印编译时间
datetime_t compile_time = hv_compile_datetime();
datetime_fmt(&compile_time, buf);
std::cout << "Compile time: " << buf << std::endl;
return 0;
}
4、Libhv Hash
HV_SHA1Init:SHA1初始化 HV_SHA1Update: SHA1更新 HV_SHA1Final:SHA1结束 |
HV_MD5Init:MD5初始化 HV_MD5Update:MD5更新 HV_MD5Final:MD5结束 |
六、Libhv EventLoop
事件循环线程是一个网络框架的核心, libhv 把事件循环(EventLoopPtr)和线程(std::thread)分开封装,可以先创建 EventLoopPtr ,再创建 EventLoopThread 启动线程; EventLoopThread 构建时可以传入已有的 EventLoopPtr ,如果不传则自动创建一个 EventLoopPtr ;也可传入Functor pre post ,分别在循环前和循环后执行; EventLoopThread 可以暂停(HLOOP_STATUS_PAUSE 线程还在,只是不处理任务),或无激活事件时退出线程,或只执行一次(HLOOP_FLAG_RUN_ONCE),或一直执行。 |
libhv 事件循环线程主要处理3种任务: 定时器,网络IO,自定义事件。 定时器:支持创建单次或循环定时器,存放在小顶堆(timers)中,堆顶即是最先超时的定时器,每次事件循环检测堆顶,超时的定时器会先出堆再重新入堆, libhv 会优先处理定时器任务。事件循环检测堆顶定时器后,会把下一次定时器超时时长 blocktime_ms 传入 epoll_wait ,避免过渡执行循环,如果没有定时器则 epoll_wait 默认100ms超时; 自定义任务:使用 epoll 实现事件监听,事件循环刚启动时创建事件(eventfds)和 epoll,调用 runInLoop 把自定义事件加入队列 customEvents ,并通过 eventfds 唤醒 epoll 执行循环; 网络IO:使用和自定义事件同一个 epoll 监听网络IO事件,比如创建tcp客户端时把fd传入epoll监听。 |
1、hloop
延时宏: |
#define hv_sleep(s) Sleep((s) * 1000) #define hv_msleep(ms) Sleep(ms) #define hv_usleep(us) Sleep((us) / 1000) #define hv_delay(ms) hv_msleep(ms) |
2、EventLoop类
class EventLoop { // 返回底层的loop结构体指针 hloop_t* loop(); // 运行 void run(); // 停止 void stop(); // 暂停 void pause(); // 继续 void resume(); // 设置定时器 TimerID setTimer(int timeout_ms, TimerCallback cb, uint32_t repeat = INFINITE, TimerID timerID = INVALID_TIMER_ID); // 设置一次性定时器 TimerID setTimeout(int timeout_ms, TimerCallback cb); // 设置永久性定时器 TimerID setInterval(int interval_ms, TimerCallback cb); // 杀掉定时器 void killTimer(TimerID timerID); // 重置定时器 void resetTimer(TimerID timerID, int timeout_ms = 0); // 返回事件循环所在的线程ID long tid(); // 是否在事件循环所在线程 bool isInLoopThread(); // 断言在事件循环所在线程 void assertInLoopThread(); // 运行在事件循环里 void runInLoop(Functor fn); // 队列在事件循环里 void queueInLoop(Functor fn); // 投递一个事件到事件循环 void postEvent(EventCallback cb); }; |
示例代码: |
#include "hv/hv.h" #include "hv/EventLoop.h" using namespace hv; static void onTimer(TimerID timerID, int n) { printf("tid=%ld timerID=%lu time=%lus n=%d\n", hv_gettid(), (unsigned long)timerID, (unsigned long)time(NULL), n); } int main(int argc, char* argv[]) { HV_MEMCHECK; printf("main tid=%ld\n", hv_gettid()); auto loop = std::make_shared<EventLoop>(); // runEvery 1s loop->setInterval(1000, std::bind(onTimer, std::placeholders::_1, 100)); // runAfter 10s loop->setTimeout(10000, [&loop](TimerID timerID){ loop->stop(); }); loop->queueInLoop([](){ printf("queueInLoop tid=%ld\n", hv_gettid()); }); loop->runInLoop([](){ printf("runInLoop tid=%ld\n", hv_gettid()); }); // run until loop stopped loop->run(); return 0; } |
3、EventLoopThread类
class EventLoopThread { // 返回事件循环指针 const EventLoopPtr& loop(); // 返回底层的loop结构体指针 hloop_t* hloop(); // 是否运行中 bool isRunning(); /* 开始运行 * wait_thread_started: 是否阻塞等待线程开始 * pre: 线程开始后执行的函数 * post: 线程结束前执行的函数 */ void start(bool wait_thread_started = true, Functor pre = Functor(), Functor post = Functor()); // 停止运行 void stop(bool wait_thread_stopped = false); // 等待线程退出 void join(); }; |
示例代码: |
#include "hv/hv.h" #include "hv/EventLoopThread.h" using namespace hv; static void onTimer(TimerID timerID, int n) { printf("tid=%ld timerID=%lu time=%lus n=%d\n", hv_gettid(), (unsigned long)timerID, (unsigned long)time(NULL), n); } int main(int argc, char* argv[]) { HV_MEMCHECK; printf("main tid=%ld\n", hv_gettid()); EventLoopThread loop_thread; const EventLoopPtr& loop = loop_thread.loop(); // 创建定时器 - runEvery 1s loop->setInterval(1000, std::bind(onTimer, std::placeholders::_1, 100)); // runAfter 10s loop->setTimeout(10000, [&loop](TimerID timerID){ loop->stop(); }); loop_thread.start(); // 创建异步自定义事件 loop->queueInLoop([](){ printf("queueInLoop tid=%ld\n", hv_gettid()); }); loop->runInLoop([](){ printf("runInLoop tid=%ld\n", hv_gettid()); }); // wait loop_thread exit loop_thread.join(); return 0; } |
4、EventLoopThreadPool类
class EventLoopThreadPool { // 获取线程数量 int threadNum(); // 设置线程数量 void setThreadNum(int num); // 返回下一个事件循环对象 // 支持轮询、随机、最少连接数等负载均衡策略 EventLoopPtr nextLoop(load_balance_e lb = LB_RoundRobin); // 返回索引的事件循环对象 EventLoopPtr loop(int idx = -1); // 返回索引的底层loop结构体指针 hloop_t* hloop(int idx = -1); /* 开始运行 * wait_threads_started: 是否阻塞等待所有线程开始 * pre: 线程开始后执行的函数 * post: 线程结束前执行的函数 */ void start(bool wait_threads_started = false, std::function<void(const EventLoopPtr&)> pre = NULL, std::function<void(const EventLoopPtr&)> post = NULL); // 停止运行 void stop(bool wait_threads_stopped = false); // 等待所有线程退出 void join(); }; |
示例代码: |
#include "hv/hv.h" #include "hv/EventLoopThreadPool.h" using namespace hv; static void onTimer(TimerID timerID, int n) { printf("tid=%ld timerID=%lu time=%lus n=%d\n", hv_gettid(), (unsigned long)timerID, (unsigned long)time(NULL), n); } int main(int argc, char* argv[]) { HV_MEMCHECK; hlog_set_level(LOG_LEVEL_DEBUG); printf("main tid=%ld\n", hv_gettid()); EventLoopThreadPool loop_threads(4); loop_threads.start(true); int thread_num = loop_threads.threadNum(); for (int i = 0; i < thread_num; ++i) { EventLoopPtr loop = loop_threads.nextLoop(); printf("worker[%d] tid=%ld\n", i, loop->tid()); loop->runInLoop([loop](){ // runEvery 1s loop->setInterval(1000, std::bind(onTimer, std::placeholders::_1, 100)); }); loop->queueInLoop([](){ printf("queueInLoop tid=%ld\n", hv_gettid()); }); loop->runInLoop([](){ printf("runInLoop tid=%ld\n", hv_gettid()); }); } // runAfter 10s loop_threads.loop()->setTimeout(10000, [&loop_threads](TimerID timerID){ loop_threads.stop(false); }); // wait loop_threads exit loop_threads.join(); return 0; } |
七、Libhv Process
结构体: |
proc_ctx_t 描述: 进程或线程的上下文结构体。 成员变量: pid: 进程或线程的 ID。 start_time: 进程或线程启动的时间。 spawn_cnt: 进程或线程的创建次数。 init: 初始化函数,用于初始化进程或线程。 init_userdata: 初始化函数的用户数据。 proc: 主函数,用于执行进程或线程的主要任务。 proc_userdata: 主函数的用户数据。 exit: 退出函数,用于清理进程或线程。 exit_userdata: 退出函数的用户数据。 |
函数: |
void hproc_run(proc_ctx_t* ctx) 描述: 运行进程或线程。 参数: ctx: 进程或线程的上下文结构体。 int hproc_spawn(proc_ctx_t* ctx) 描述: 创建并运行新的进程或线程。 参数: ctx: 进程或线程的上下文结构体。 返回值: 返回进程或线程的 ID,如果失败则返回 -1。 |
#include <iostream>
#include "hv/hproc.h"
#include "hv/hloop.h"
// 初始化函数
void init_func(void* userdata) {
std::cout << "Initializing process" << std::endl;
}
// 主函数
void main_func(void* userdata) {
std::cout << "Running main process" << std::endl;
// 模拟一些工作
for (int i = 0; i < 5; ++i) {
std::cout << "Working..." << std::endl;
hv_sleep(1);
}
}
// 退出函数
void exit_func(void* userdata) {
std::cout << "Exiting process" << std::endl;
}
int main() {
// 创建进程或线程的上下文结构体
proc_ctx_t ctx;
ctx.init = init_func;
ctx.proc = main_func;
ctx.exit = exit_func;
// 创建并运行新的进程或线程
int ret = hproc_spawn(&ctx);
if (ret != -1) {
std::cout << "Process spawned with ID: " << ctx.pid << std::endl;
std::cout << "Process start time: " << ctx.start_time << std::endl;
std::cout << "Process spawn count: " << ctx.spawn_cnt << std::endl;
// 等待一段时间
hv_sleep(10);
} else {
std::cerr << "Failed to spawn process" << std::endl;
}
return 0;
}
八、Libhv Tcp
1、Tcp协议概述
传输层控制协议,面向连接,传输安全可靠,适用一次传输大批数据,速度较慢; |
# 三次握手
三次握手成功,则连接建立成功,可以开始传送数据信息; | ||
第一次握手 | 客户端向服务器端发送连接请求包SYN(syn=j),等待服务器回应 | |
第二次握手 | 服务器端收到客户端连接请求包SYN(syn=j)后,将客户端请求包SYN(syn=j)放到未连接队列,此时服务器需要发送两个包给客户端: | 向客户端发送确认收到其连接请求确认包ACK(ack=j+1),表明已知其连接请求 |
向客户端发送连接询问请求包SYN(syn=k),询问客户端是否已准备建立连接,进行数据通信; | ||
即在第二次握手时向客户端发送ACK(ack=j+1)和SYN(syn=k)包,此时服务器进入SYN_RECV状态 | ||
第三次握手 | 客户端收到服务器ACK(ack=j+1)和SYN(syn=k)包,得知服务器同意建立连接,此时需要发送连接已建立的消息给服务器;向服务器发送连接建立的确认包ACK(ack=k+1),回应服务器的SYN(syn=k)告诉服务器已建立连接,可以进行数据通信 |
# 四次挥手
TCP关闭,由客户端或服务端任一方执行close来触发; TCP连接为全双工(同时既可以发送也可以接收),可以同时发送和接受数据,关闭的时候要关闭这两个方向的通道,每个方向必须单独关闭(全双工à半双工); 原则:当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向上的连接,当一端收到一个FIN后,必须通知应用层另一端已经终止了那个方向的数据传送,即收到一个FIN意味着在该方向上没有数据流动;(命令) | |
第一次挥手 | Client给Server发送FIN,请求关闭连接; |
第二次挥手 | Server收到FIN之后给Client返回确认ACK,同时关闭Receive通道; Client收到FIN确认后,关闭Send通道; |
第三次挥手 | Server给Client发送FIN,请求关闭连接; |
第四次挥手 | Client收到FIN之后给Server返回确认ACK,同时关闭Receive通道,进入TIME_WAIT状态; Server收到FIN确认后,关闭Send通道; |
2、Tcp服务端
class TcpServer {
// 返回索引的事件循环
EventLoopPtr loop(int idx = -1);
// 创建套接字
int createsocket(int port, const char* host = "0.0.0.0");
// 关闭套接字
void closesocket();
// 设置最大连接数
void setMaxConnectionNum(uint32_t num);
// 设置负载均衡策略
void setLoadBalance(load_balance_e lb);
// 设置线程数
void setThreadNum(int num);
// 开始运行
void start(bool wait_threads_started = true);
// 停止运行
void stop(bool wait_threads_stopped = true);
// 设置SSL/TLS加密通信
int withTLS(hssl_ctx_opt_t* opt = NULL);
// 设置拆包规则
void setUnpack(unpack_setting_t* setting);
// 返回当前连接数
size_t connectionNum();
// 遍历连接
int foreachChannel(std::function<void(const TSocketChannelPtr& channel)> fn);
// 广播消息
int broadcast(const void* data, int size);
int broadcast(const std::string& str);
// 连接到来/断开回调
std::function<void(const TSocketChannelPtr&)> onConnection;
// 消息回调
std::function<void(const TSocketChannelPtr&, Buffer*)> onMessage;
// 写完成回调
std::function<void(const TSocketChannelPtr&, Buffer*)> onWriteComplete;
};
#include <iostream>
#include "hv/TcpServer.h"
using namespace hv;
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s port\n", argv[0]);
return -10;
}
int port = atoi(argv[1]);
// 实例化TCP服务端
TcpServer srv;
int listenfd = srv.createsocket(port, "127.0.0.1");
if (listenfd < 0) {
return -20;
}
printf("server listen: %d, listenfd=%d ...\n", port, listenfd);
// 客户端连接回调
srv.onConnection = [](const SocketChannelPtr& channel) {
std::string peeraddr = channel->peeraddr();
if (channel->isConnected()) {
printf("%s connected! connfd=%d id=%d tid=%ld\n", peeraddr.c_str(), channel->fd(), channel->id(), currentThreadEventLoop->tid());
} else {
printf("%s disconnected! connfd=%d id=%d tid=%ld\n", peeraddr.c_str(), channel->fd(), channel->id(), currentThreadEventLoop->tid());
}
};
// 客户端消息回调
srv.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) {
// echo
printf("< %.*s\n", (int)buf->size(), (char*)buf->data());
channel->write(buf);
};
// 设置IO线程数
srv.setThreadNum(4);
// 设置负载均衡策略
srv.setLoadBalance(LB_LeastConnections);
srv.start();
// 控制台输入
std::string str;
while (std::getline(std::cin, str)) {
if (str == "close") {
srv.closesocket();
} else if (str == "start") {
srv.start();
} else if (str == "stop") {
srv.stop();
break;
} else {
// 广播消息
srv.broadcast(str.data(), str.size());
}
}
return 0;
}
3、Tcp客户端
class TcpClient {
// 返回所在的事件循环
const EventLoopPtr& loop();
// 创建套接字
int createsocket(int remote_port, const char* remote_host = "127.0.0.1");
int createsocket(struct sockaddr* remote_addr);
// 绑定端口
int bind(int local_port, const char* local_host = "0.0.0.0");
int bind(struct sockaddr* local_addr);
// 关闭套接字
void closesocket();
// 开始运行
void start(bool wait_threads_started = true);
// 停止运行
void stop(bool wait_threads_stopped = true);
// 是否已连接
bool isConnected();
// 发送
int send(const void* data, int size);
int send(Buffer* buf);
int send(const std::string& str);
// 设置SSL/TLS加密通信
int withTLS(hssl_ctx_opt_t* opt = NULL);
// 设置连接超时
void setConnectTimeout(int ms);
// 设置重连
void setReconnect(reconn_setting_t* setting);
// 是否是重连
bool isReconnect();
// 设置拆包规则
void setUnpack(unpack_setting_t* setting);
// 连接状态回调
std::function<void(const TSocketChannelPtr&)> onConnection;
// 消息回调
std::function<void(const TSocketChannelPtr&, Buffer*)> onMessage;
// 写完成回调
std::function<void(const TSocketChannelPtr&, Buffer*)> onWriteComplete;
};
#include <iostream>
#include "hv/TcpClient.h"
#include "hv/htime.h"
#define TEST_RECONNECT 1
#define TEST_TLS 0
using namespace hv;
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s remote_port [remote_host]\n", argv[0]);
return -10;
}
int remote_port = atoi(argv[1]);
const char* remote_host = "127.0.0.1";
if (argc > 2) {
remote_host = argv[2];
}
// 实例化TCP客户端
TcpClient cli;
int connfd = cli.createsocket(remote_port, remote_host);
if (connfd < 0) {
return -20;
}
printf("client connect to port %d, connfd=%d ...\n", remote_port, connfd);
// 连接状态回调
cli.onConnection = [&cli](const SocketChannelPtr& channel) {
std::string peeraddr = channel->peeraddr();
if (channel->isConnected()) {
printf("connected to %s! connfd=%d\n", peeraddr.c_str(), channel->fd());
// send(time) every 3s
setInterval(3000, [channel](TimerID timerID) {
if (channel->isConnected()) {
if (channel->isWriteComplete()) {
char str[DATETIME_FMT_BUFLEN] = {0};
datetime_t dt = datetime_now();
datetime_fmt(&dt, str);
channel->write(str);
}
} else {
killTimer(timerID);
}
});
} else {
printf("disconnected to %s! connfd=%d\n", peeraddr.c_str(), channel->fd());
}
if (cli.isReconnect()) {
printf("reconnect cnt=%d, delay=%d\n", cli.reconn_setting->cur_retry_cnt, cli.reconn_setting->cur_delay);
}
};
// 消息回调
cli.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) {
printf("< %.*s\n", (int)buf->size(), (char*)buf->data());
};
#if TEST_RECONNECT
// reconnect: 1,2,4,8,10,10,10...
reconn_setting_t reconn;
reconn_setting_init(&reconn);
reconn.min_delay = 1000;
reconn.max_delay = 10000;
reconn.delay_policy = 2;
cli.setReconnect(&reconn);
#endif
cli.start();
// 控制台输入
std::string str;
while (std::getline(std::cin, str)) {
if (str == "close") {
cli.closesocket();
} else if (str == "start") {
cli.start();
} else if (str == "stop") {
cli.stop();
break;
} else {
if (!cli.isConnected()) break;
cli.send(str);
}
}
return 0;
}
九、Libhv Udp
1、Udp协议概述
用户数据报协议,面向非连接协议,适用于一次传输数据量少、对可靠性要求不高或对实时性要求高(监控)的应用场景; UDP无需建立三次握手的连接,通信效率高, 不与对方建立连接,而是直接将数据报发给对方(发送短信); |
2、Udp服务端
class UdpServer {
// 返回所在的事件循环
const EventLoopPtr& loop();
// 创建套接字
int createsocket(int port, const char* host = "0.0.0.0");
// 关闭套接字
void closesocket();
// 开始运行
void start(bool wait_threads_started = true);
// 停止运行
void stop(bool wait_threads_stopped = true);
// 发送
int sendto(const void* data, int size, struct sockaddr* peeraddr = NULL);
int sendto(Buffer* buf, struct sockaddr* peeraddr = NULL);
int sendto(const std::string& str, struct sockaddr* peeraddr = NULL);
// 设置KCP
void setKcp(kcp_setting_t* setting);
// 消息回调
std::function<void(const TSocketChannelPtr&, Buffer*)> onMessage;
// 写完成回调
std::function<void(const TSocketChannelPtr&, Buffer*)> onWriteComplete;
};
#include <iostream>
#include "hv/UdpServer.h"
using namespace hv;
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s port\n", argv[0]);
return -10;
}
int port = atoi(argv[1]);
// 实例化UDP服务端
UdpServer srv;
int bindfd = srv.createsocket(port, "192.168.0.108");
if (bindfd < 0) {
return -20;
}
printf("server bind on port %d, bindfd=%d ...\n", port, bindfd);
// 消息回调
srv.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) {
// echo
printf("< %.*s\n", (int)buf->size(), (char*)buf->data());
channel->write(buf);
};
srv.start();
// 控制台输入
std::string str;
while (std::getline(std::cin, str)) {
if (str == "close") {
srv.closesocket();
} else if (str == "start") {
srv.start();
} else if (str == "stop") {
srv.stop();
break;
} else {
srv.sendto(str);
}
}
return 0;
}
3、Udp客户端
class UdpClient {
// 返回所在的事件循环
const EventLoopPtr& loop();
// 创建套接字
int createsocket(int remote_port, const char* remote_host = "127.0.0.1");
// 绑定端口
int bind(int local_port, const char* local_host = "0.0.0.0");
// 关闭套接字
void closesocket();
// 开始运行
void start(bool wait_threads_started = true);
// 停止运行
void stop(bool wait_threads_stopped = true);
// 发送
int sendto(const void* data, int size, struct sockaddr* peeraddr = NULL);
int sendto(Buffer* buf, struct sockaddr* peeraddr = NULL);
int sendto(const std::string& str, struct sockaddr* peeraddr = NULL);
// 设置KCP
void setKcp(kcp_setting_t* setting);
// 消息回调
std::function<void(const TSocketChannelPtr&, Buffer*)> onMessage;
// 写完成回调
std::function<void(const TSocketChannelPtr&, Buffer*)> onWriteComplete;
};
#include <iostream>
#include "hv/UdpClient.h"
#include "hv/htime.h"
using namespace hv;
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s remote_port [remote_host]\n", argv[0]);
return -10;
}
int remote_port = atoi(argv[1]);
const char* remote_host = "192.168.0.108";
if (argc > 2) {
remote_host = argv[2];
}
// 实例化UDP客户端
UdpClient cli;
int sockfd = cli.createsocket(remote_port, remote_host);
if (sockfd < 0) {
return -20;
}
printf("client sendto port %d, sockfd=%d ...\n", remote_port, sockfd);
// 消息回调
cli.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) {
printf("< %.*s\n", (int)buf->size(), (char*)buf->data());
};
cli.start();
// sendto(time) every 3s
cli.loop()->setInterval(3000, [&cli](TimerID timerID) {
char str[DATETIME_FMT_BUFLEN] = {0};
datetime_t dt = datetime_now();
datetime_fmt(&dt, str);
cli.sendto(str);
});
// 控制台输入
std::string str;
while (std::getline(std::cin, str)) {
if (str == "close") {
cli.closesocket();
} else if (str == "start") {
cli.start();
} else if (str == "stop") {
cli.stop();
break;
} else {
cli.sendto(str);
}
}
return 0;
}
十、Libhv Http
1、Http协议概述
HTTP(Hyper Text Transfer Protocol)全称超文本传输协议,应用层协议,基于TCP/IP通信协议传递数据,默认端口80,使用 http:// 和 https:// 协议前缀,半双工通信; HTTPS:通过SSL/TLS建立全信道加密数据包(非对称加密),提供对网站服务器身份认证,保护交换数据的隐私与完整性,默认端口443; 证书用来证明公钥拥有者身份的凭证; server.crt :服务器证书文件,包含了网站的公钥以及相关的证书信息,这个文件通常是在证书颁发机构 (CA) 上申请证书后获得的; server.key :服务器的私钥文件,它用于对传输的数据进行加密和解密,私钥应该是服务器密钥对的一部分,通常与证书一起生成。 支持客户/服务器模式,无连接/无状态; |
# HTTP通信流程
客户端:主动发起网络请求的一端; 服务器:被动接收网络请求的一端; 请求:客户端给服务器发送的数据; 响应:服务器给客户端返回的数据。 # 整体流程: ## 请求(URL): [URL字段 HTTP协议版本 请求方法[GET、POST等]] GET参数直接拼接在URL后,POST参数放到请求体中 ## 请求头部(header): [关键字:值] ## 请求正文(body) ## 响应:[状态码 响应信息 HTTP协议] # HTTP请求示例: ## 连接TCP服务器(127.0.0.1:8080) ## 请求: GET /ping HTTP/1.0\r\n Host: 127.0.0.1:8080\r\n Content-Type: octet-stream\r\n Content-Length: 15\r\n \r\n your data here(GET不需要) ## 响应: HTTP/1.0 200 OK\r\n Date: Wed, 21 Oct 2015 07:28:00 GMT\r\n Content-Type: text/html; charset=UTF-8\r\n Content-Length: 137\r\n \r\n <html> <head> <title>An Example Page</title> </head> <body> <p>Hello World, this is a very simple HTML document.</p> </body> </html> ## 断开TCP连接 |
# HTTP网址(URL)
URL(Uniform Resource Locator),翻译为统一资源定位符; 协议类型:[//[访问资源需要的凭证信息@]服务器地址[:端口号]][/资源层级 UNIX 文件路径]文件名[?查询字符串][#片段标识符] |
# HTTP请求
HTTP请求由三个主要部分组成:请求行、请求报头、请求体。 |
## HTTP请求行
<Method> <Request-URL> <HTTP-Version> 示例:GET /ping HTTP/1.0\r\n <Method> 请求方法: <Request-URL>:请求URL <HTTP-Version> HTTP版本: HTTP1.0、HTTP1.1、HTTP2.0 均为 TCP 实现,HTTP3.0 基于 UDP 实现,主流使用 HTTP1.0 和 HTTP3.0。 |
## HTTP请求报头
请求报头由一系列的键值对组成,用于传递额外的信息和元数据,每个键值对一行,用冒号(:)分隔键和值。 Host:表示服务器主机的地址和端口(地址可以是域名,也可以是 IP,端口号可以省略或者手动指定); Content-Length:表示 body 的数据长度,长度单位是字节; Content-Type:表示 body 的数据格式(在有请求体的情况下); text/plain:纯文本格式 application/json:此时 body 数据为 json 格式; application/octet-stream : 二进制流数据(如常见的文件下载) multipart/form-data:上传文件使用,multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW; Range:用于请求资源的部分内容,而不是整个资源,这在需要下载大文件时尤其有用,例如恢复中断的下载或在流媒体中请求特定的字节范围(注意:分片下载通过客户端发送一个 HEAD 请求以获取文件的元数据,然后发起多次带有Range头部请求实现); bytes=start-end:请求从start字节到end字节的部分; 示例(获取前5个字节):Range: bytes=0-4 User-Agent:标识客户端的软件和版本信息的字段。 |
## HTTP请求体
请求体用于在POST、PUT等方法中传递数据,GET请求通常没有请求体,请求体的内容类型由Content-Type报头指定。 示例(JSON格式 - application/json): { "username": "user", "password": "pass" } 示例(文件上传 - multipart/form-data)[假设我们要上传一个名为example.txt的文件,文件内容为Hello, world!,并且还包含一个文本字段description,其值为Sample file]: 注:边界字符串以--结尾表示请求体结束 ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="description" Sample file ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="file"; filename="example.txt" Content-Type: text/plain Hello, world! ------WebKitFormBoundary7MA4YWxkTrZu0gW-- |
# HTTP响应
HTTP响应也由三个主要部分组成:响应行、响应报头、响应体。 |
## HTTP响应行
<HTTP-Version> <Status-Code> <Reason-Phrase> 示例:HTTP/1.0 200 OK\r\n <HTTP-Version> HTTP版本:参考请求行; <Status-Code> 状态码:表示访问一个页面的结果(如访问成功、失败,还是其它一些情况等等),3位整数,从 1xx、2xx、3xx、4xx、5xx,分为五个大类; 200 OK:表示访问成功; 404 Not Found:表示没有找到资源; 403 Forbidden:表示访问被拒绝(有的页面通常需要用户有一定的权限才能访问,如未登录); 405 Method Not Allowed:表示访问的服务器不能支持请求中的方法或者不能使用该请求中的方法; 500 Internal Server Error:表示服务器出现内部错误; <Reason-Phrase> 状态文本:描述状态码的简短文本说明。 |
## HTTP响应报头
响应报头也是一系列的键值对,用于传递关于响应的额外信息和元数据。 Content-Length:表示 body 的数据长度,长度单位是字节; Content-Type:表示 body 的数据格式; Content-Range:指定在响应中返回的资源部分范围,bytes start-end/total; Accept-Ranges 分片下载前的HEAD请求中表示服务器支持范围请求,bytes为支持; Content-Disposition:指定内容的呈现方式,主要用于指示浏览器如何处理返回的内容,最常用于文件下载和附件的处理; inline:指示浏览器直接在页面中显示内容(通常用于图像或HTML内容); attachment:指示浏览器将内容作为附件下载,而不是直接显示,在下载文件时通常会用到这个值; filename:指定下载文件的默认文件名。 |
## HTTP响应体
响应体包含实际的响应数据,根据请求的不同可以是HTML、JSON、图像等; 示例(HTML内容 - text/html): <!DOCTYPE html> <html> <head> <title>Example</title> </head> <body> <h1>Hello, World!</h1> </body> </html> |
2、Http服务端
// HTTP服务类
class HttpServer {
// 注册HTTP业务类
void registerHttpService(HttpService* service);
// 设置监听主机
void setHost(const char* host = "0.0.0.0");
// 设置监听端口
void setPort(int port = 0, int ssl_port = 0);
// 设置监听文件描述符
void setListenFD(int fd = -1, int ssl_fd = -1);
// 设置IO进程数 (仅`linux`下有效)
void setProcessNum(int num);
// 设置IO线程数
void setThreadNum(int num);
// 设置SSL/TLS
int setSslCtx(hssl_ctx_t ssl_ctx);
// 新建SSL/TLS
int newSslCtx(hssl_ctx_opt_t* opt);
// hooks
// 事件循环开始时执行的回调函数
std::function<void()> onWorkerStart;
// 事件循环结束时执行的回调函数
std::function<void()> onWorkerStop;
// 占用当前线程运行
int run(bool wait = true);
// 不占用当前线程运行
int start();
// 停止服务
int stop();
};
// HTTP业务类
class HttpService {
// 添加静态资源映射
void Static(const char* path, const char* dir);
// 允许跨域访问
void AllowCORS();
// 添加可信代理 (代理白名单)
void AddTrustProxy(const char* host);
// 添加不可信代理 (代理黑名单)
void AddNoProxy(const char* host);
// 开启正向转发代理
void EnableForwardProxy();
// 添加反向代理映射
void Proxy(const char* path, const char* url);
// 添加中间件
void Use(Handler handlerFunc);
// 添加路由处理器
void Handle(const char* httpMethod, const char* relativePath, Handler handlerFunc);
// 添加`HEAD`路由
void HEAD(const char* relativePath, Handler handlerFunc);
// 添加`GET`路由
void GET(const char* relativePath, Handler handlerFunc);
// 添加`POST`路由
void POST(const char* relativePath, Handler handlerFunc);
// 添加`PUT`路由
void PUT(const char* relativePath, Handler handlerFunc);
// 添加`DELETE`路由
void Delete(const char* relativePath, Handler handlerFunc);
// 添加`PATCH`路由
void PATCH(const char* relativePath, Handler handlerFunc);
// 添加任意`HTTP method`路由
void Any(const char* relativePath, Handler handlerFunc);
// 返回注册的路由路径列表
hv::StringList Paths();
// 处理流程:前处理器 -> 中间件 -> 处理器 -> 后处理器
// preprocessor -> middleware -> processor -> postprocessor
// 数据成员
http_handler preprocessor; // 前处理器
http_handlers middleware; // 中间件
http_handler processor; // 处理器
http_handler postprocessor; // 后处理器
std::string base_url; // 基本路径
std::string document_root; // 文档根目录
std::string home_page; // 主页
std::string error_page; // 默认错误页
std::string index_of; // 目录
http_handler errorHandler; // 错误处理器
int proxy_connect_timeout; // 代理连接超时
int proxy_read_timeout; // 代理读超时
int proxy_write_timeout; // 代理写超时
int keepalive_timeout; // 长连接保活超时
int max_file_cache_size; // 文件缓存最大尺寸
int file_cache_stat_interval; // 文件缓存stat间隔,查询文件是否修改
int file_cache_expired_time; // 文件缓存过期时间,过期自动释放
int limit_rate; // 下载速度限制
};
/* 几种`handler`处理函数区别说明: */
// 同步`handler`运行在IO线程
typedef std::function<int(HttpRequest* req, HttpResponse* resp)> http_sync_handler;
// 异步`handler`运行在`hv::async`全局线程池,可通过`hv::async::startup`设置线程池属性
typedef std::function<void(const HttpRequestPtr& req, const HttpResponseWriterPtr& writer)> http_async_handler;
// 上下文`handler`运行在IO线程,可以很方便的将`HttpContextPtr`智能指针抛到消费者线程/线程池处理
typedef std::function<int(const HttpContextPtr& ctx)> http_ctx_handler;
// 中间状态`handler`运行在IO线程,用来实现大数据量的边接收边处理
typedef std::function<int(const HttpContextPtr& ctx, http_parser_state state, const char* data, size_t size)> http_state_handler;
#include "hv/HttpServer.h"
#include "hv/hthread.h" // import hv_gettid
#include "hv/hasync.h" // import hv::async
#include <iostream>
#include <fstream>
using namespace hv;
/*
* #define TEST_HTTPS 1
* @server bin/http_server_test 8080
*
* @client curl -v http://127.0.0.1:8080/pings
* curl -v https://127.0.0.1:8443/ping --insecure
* bin/curl -v http://127.0.0.1:8080/ping
* bin/curl -v https://127.0.0.1:8443/ping
*
*/
#define TEST_HTTPS 0
int main(int argc, char** argv) {
HV_MEMCHECK;
int port = 0;
if (argc > 1) {
port = atoi(argv[1]);
}
if (port == 0) port = 8080;
HttpService router;
/* Static file service */
// curl -v http://ip:port/
router.Static("/", "./html");
/* Forward proxy service */
router.EnableForwardProxy();
// curl -v http://httpbin.org/get --proxy http://127.0.0.1:8080
router.AddTrustProxy("*httpbin.org");
/* Reverse proxy service */
// curl -v http://ip:port/httpbin/get
router.Proxy("/httpbin/", "http://httpbin.org/");
/* API handlers */
// curl -v http://ip:port/ping
router.GET("/ping", [](HttpRequest* req, HttpResponse* resp) {
return resp->String("pong");
});
// curl -v http://ip:port/data
router.GET("/data", [](HttpRequest* req, HttpResponse* resp) {
static char data[] = "0123456789";
return resp->Data(data, 10 /*, false */);
});
// curl -v http://ip:port/paths
router.GET("/paths", [&router](HttpRequest* req, HttpResponse* resp) {
return resp->Json(router.Paths());
});
// curl -v http://ip:port/get?env=1
router.GET("/get", [](const HttpContextPtr& ctx) {
hv::Json resp;
resp["origin"] = ctx->ip();
resp["url"] = ctx->url();
resp["args"] = ctx->params();
resp["headers"] = ctx->headers();
return ctx->send(resp.dump(2));
});
// curl -v http://ip:port/user/123
router.GET("/user/{id}", [](const HttpContextPtr& ctx) {
hv::Json resp;
resp["id"] = ctx->param("id");
return ctx->send(resp.dump(2));
});
// curl -v http://ip:port/async
router.GET("/async", [](const HttpRequestPtr& req, const HttpResponseWriterPtr& writer) {
writer->Begin();
writer->WriteHeader("X-Response-tid", hv_gettid());
writer->WriteHeader("Content-Type", "text/plain");
writer->WriteBody("This is an async response.\n");
writer->End();
});
// curl -v http://ip:port/echo -d "hello,world!"
router.POST("/echo", [](const HttpContextPtr& ctx) {
return ctx->send(ctx->body(), ctx->type());
});
// curl.exe -v http://127.0.0.1:8080/upload?filename=6.txt -d '@C:\Users\Administrator\Desktop\6.txt'
router.POST("/upload", [](const HttpContextPtr& ctx) {
std::cout << "/upload " << std::endl;
int status_code = 200;
// 注意该路径必须存在
std::string save_path = "html/uploads/";
// 检查请求是否为多部分表单数据(用于文件上传)
if (ctx->is(MULTIPART_FORM_DATA)) {
// 保存上传的文件到指定路径
status_code = ctx->request->SaveFormFile("file", save_path.c_str());
} else {
// 获取请求参数中的文件名,如果未提供则使用默认的"unnamed.txt"
std::string filename = ctx->param("filename", "unnamed.txt");
std::cout << "filename: " << filename << std::endl;
std::string filepath = save_path + filename;
std::cout << "filepath: " << filepath << std::endl;
// 保存请求体中的文件内容到指定文件路径
status_code = ctx->request->SaveFile(filepath.c_str());
}
// 设置响应的状态码和消息
ctx->response.get()->Set("code", status_code);
ctx->response.get()->Set("message", http_status_str((enum http_status)status_code));
// 发送响应
ctx->send();
std::cout << "status_code: " << status_code << std::endl;
// 返回状态码
return status_code;
});
// curl.exe -v http://127.0.0.1:8080/upload/6.txt -d '@C:\Users\Administrator\Desktop\6.txt'
router.POST("/upload/{filename}", [](const HttpContextPtr& ctx, http_parser_state state, const char* data, size_t size) {
printf("recvLargeFile state=%d\n", (int)state);
int status_code = HTTP_STATUS_UNFINISHED;
HFile* file = (HFile*)ctx->userdata;
switch (state) {
case HP_HEADERS_COMPLETE: {
if (ctx->is(MULTIPART_FORM_DATA)) {
// NOTE: You can use multipart_parser if you want to use multipart/form-data.
ctx->close();
return HTTP_STATUS_BAD_REQUEST;
}
std::string save_path = "html/uploads/";
std::string filename = ctx->param("filename", "unnamed.txt");
std::string filepath = save_path + filename;
file = new HFile;
if (file->open(filepath.c_str(), "wb") != 0) {
ctx->close();
return HTTP_STATUS_INTERNAL_SERVER_ERROR;
}
ctx->userdata = file;
}
break;
case HP_BODY: {
if (file && data && size) {
if (file->write(data, size) != size) {
ctx->close();
return HTTP_STATUS_INTERNAL_SERVER_ERROR;
}
}
}
break;
case HP_MESSAGE_COMPLETE: {
status_code = HTTP_STATUS_OK;
ctx->setContentType(APPLICATION_JSON);
ctx->response.get()->Set("code", status_code);
ctx->response.get()->Set("message", http_status_str((enum http_status)status_code));
ctx->send();
if (file) {
delete file;
ctx->userdata = NULL;
}
}
break;
case HP_ERROR: {
if (file) {
file->remove();
delete file;
ctx->userdata = NULL;
}
}
break;
default:
break;
}
return (enum http_status)status_code;
});
// curl.exe --head http://127.0.0.1:8080/download/6.txt
// 在实现分片下载(也称为断点续传)时,通常需要先发送一个 HEAD 请求来获取文件的元信息
router.HEAD("/download/{filename}", [](const HttpContextPtr& ctx) {
std::string filename = ctx->param("filename");
std::string filepath = "html/uploads/" + filename;
// 打开并检查文件是否存在
HFile file;
if (file.open(filepath.c_str(), "rb") != 0) {
ctx->setStatus(HTTP_STATUS_NOT_FOUND);
return HTTP_STATUS_NOT_FOUND;
}
// 获取文件大小
file.seek(0, SEEK_END);
long filesize = file.tell();
file.seek(0, SEEK_SET);
ctx->setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
// 精确返回类型
http_content_type content_type = CONTENT_TYPE_NONE;
const char* suffix = hv_suffixname(filepath.c_str());
if (suffix) {
content_type = http_content_type_enum_by_suffix(suffix);
}
if (content_type == CONTENT_TYPE_NONE || content_type == CONTENT_TYPE_UNDEFINED) {
content_type = APPLICATION_OCTET_STREAM;
}
ctx->writer->WriteHeader("Content-Type", http_content_type_str(content_type));
ctx->writer->WriteHeader("Content-Length", std::to_string(filesize));
// 支持分片下载
ctx->writer->WriteHeader("Accept-Ranges", "bytes");
ctx->writer->End();
ctx->setStatus(HTTP_STATUS_OK);
return HTTP_STATUS_OK;
});
// curl.exe -o C:\Users\Administrator\Desktop\6.txt http://127.0.0.1:8080/download/6.txt
// curl.exe -o C:\Users\Administrator\Desktop\6.txt --header "Range: bytes=0-99" http://127.0.0.1:8080/download/6.txt
router.GET("/download/{filename}", [](const HttpContextPtr& ctx) {
std::string filename = ctx->param("filename");
std::string filepath = "html/uploads/" + filename;
// 打开并检查文件是否存在
HFile file;
if (file.open(filepath.c_str(), "rb") != 0) {
ctx->setStatus(HTTP_STATUS_NOT_FOUND);
ctx->send("File not found");
return HTTP_STATUS_NOT_FOUND;
}
// 精确返回类型
http_content_type content_type = CONTENT_TYPE_NONE;
const char* suffix = hv_suffixname(filepath.c_str());
if (suffix) {
content_type = http_content_type_enum_by_suffix(suffix);
}
if (content_type == CONTENT_TYPE_NONE || content_type == CONTENT_TYPE_UNDEFINED) {
content_type = APPLICATION_OCTET_STREAM;
}
size_t filesize = file.size();
size_t start = 0;
size_t end = filesize - 1;
bool is_partial = false;
// 处理 Range 头部
auto range_header_iter = ctx->request->headers.find("Range");
if (range_header_iter != ctx->request->headers.end()) {
std::string range_header = range_header_iter->second;
if (!range_header.empty() && range_header.find("bytes=") == 0) {
range_header = range_header.substr(6); // 移除 "bytes="
size_t dash_pos = range_header.find('-');
if (dash_pos != std::string::npos) {
std::string start_str = range_header.substr(0, dash_pos);
std::string end_str = range_header.substr(dash_pos + 1);
if (!start_str.empty()) {
start = std::stoul(start_str);
}
if (!end_str.empty()) {
end = std::stoul(end_str);
}
if (start <= end && end < filesize) {
printf("start=%d,end=%d\n", start, end);
is_partial = true;
}
}
}
}
// 设置响应头和状态
ctx->writer->Begin();
if (is_partial) {
ctx->writer->WriteStatus(HTTP_STATUS_PARTIAL_CONTENT);
ctx->writer->WriteHeader("Content-Range", "bytes " + std::to_string(start) + "-" + std::to_string(end) + "/" + std::to_string(filesize));
} else {
ctx->writer->WriteStatus(HTTP_STATUS_OK);
}
ctx->writer->WriteHeader("Content-Type", http_content_type_str(content_type));
ctx->writer->WriteHeader("Content-Length", std::to_string(end - start + 1));
ctx->writer->WriteHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
ctx->writer->EndHeaders();
file.seek(start);
char* buf = new char[4096];
size_t total_readbytes = 0;
while (total_readbytes < (end - start + 1)) {
size_t to_read = (sizeof(buf) < (end - start + 1 - total_readbytes)) ? sizeof(buf) : (end - start + 1 - total_readbytes);
size_t readbytes = file.read(buf, to_read);
if (readbytes <= 0) {
ctx->writer->close();
break;
}
int nwrite = ctx->writer->WriteBody(buf, readbytes);
if (nwrite < 0) {
// 未连接
break;
}
total_readbytes += readbytes;
}
ctx->writer->End();
delete[] buf;
return HTTP_STATUS_OK;
});
router.GET("/downloadeasy/{filename}", [](const HttpContextPtr& ctx) {
std::string filename = ctx->param("filename");
std::string filepath = "html/uploads/" + filename;
HFile file;
if (file.open(filepath.c_str(), "rb") != 0) {
ctx->setStatus(HTTP_STATUS_NOT_FOUND);
ctx->send("File not found");
return HTTP_STATUS_NOT_FOUND;
}
ctx->setHeader("Content-Type", "text/plain");
ctx->setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
ctx->sendFile(filepath.data());
return HTTP_STATUS_OK;
});
// middleware
router.AllowCORS();
router.Use([](HttpRequest* req, HttpResponse* resp) {
resp->SetHeader("X-Request-tid", hv::to_string(hv_gettid()));
return HTTP_STATUS_NEXT;
});
HttpServer server;
server.service = &router;
server.port = port;
#if TEST_HTTPS
server.https_port = 8443;
hssl_ctx_opt_t param;
memset(¶m, 0, sizeof(param));
param.crt_file = "cert/server.crt";
param.key_file = "cert/server.key";
param.endpoint = HSSL_SERVER;
if (server.newSslCtx(¶m) != 0) {
fprintf(stderr, "new SSL_CTX failed!\n");
return -20;
}
#endif
// uncomment to test multi-processes
// server.setProcessNum(4);
// uncomment to test multi-threads
// server.setThreadNum(4);
server.start();
// press Enter to stop
while (getchar() != '\n');
hv::async::cleanup();
return 0;
}
3、Http客户端
class HttpClient {
// 设置超时
int setTimeout(int timeout);
// 设置SSL/TLS
int setSslCtx(hssl_ctx_t ssl_ctx);
// 新建SSL/TLS
int newSslCtx(hssl_ctx_opt_t* opt);
// 清除全部请求头部
int clearHeaders();
// 设置请求头部
int setHeader(const char* key, const char* value);
// 删除请求头部
int delHeader(const char* key);
// 获取请求头部
const char* getHeader(const char* key);
// 设置http代理
int setHttpProxy(const char* host, int port);
// 设置https代理
int setHttpsProxy(const char* host, int port);
// 添加不走代理
int addNoProxy(const char* host);
// 同步发送
int send(HttpRequest* req, HttpResponse* resp);
// 异步发送
int sendAsync(HttpRequestPtr req, HttpResponseCallback resp_cb = NULL);
// 关闭连接 (HttpClient对象析构时会自动调用)
int close();
};
namespace requests {
// 同步请求
Response request(Request req);
Response request(http_method method, const char* url, const http_body& body = NoBody, const http_headers& headers = DefaultHeaders);
// 上传文件
Response uploadFile(const char* url, const char* filepath, http_method method = HTTP_POST, const http_headers& headers = DefaultHeaders);
// 通过 `multipart/form-data` 格式上传文件
Response uploadFormFile(const char* url, const char* name, const char* filepath, std::map<std::string, std::string>& params = hv::empty_map, http_method method = HTTP_POST, const http_headers& headers = DefaultHeaders);
// 上传大文件(带上传进度回调)
Response uploadLargeFile(const char* url, const char* filepath, upload_progress_cb progress_cb = NULL, http_method method = HTTP_POST, const http_headers& headers = DefaultHeaders);
// 下载文件 (更详细的断点续传示例代码见`examples/wget.cpp`)
size_t downloadFile(const char* url, const char* filepath, download_progress_cb progress_cb = NULL);
// HEAD 请求
Response head(const char* url, const http_headers& headers = DefaultHeaders);
// GET 请求
Response get(const char* url, const http_headers& headers = DefaultHeaders);
// POST 请求
Response post(const char* url, const http_body& body = NoBody, const http_headers& headers = DefaultHeaders);
// PUT 请求
Response put(const char* url, const http_body& body = NoBody, const http_headers& headers = DefaultHeaders);
// PATCH 请求
Response patch(const char* url, const http_body& body = NoBody, const http_headers& headers = DefaultHeaders);
// DELETE 请求
Response Delete(const char* url, const http_headers& headers = DefaultHeaders);
// 异步请求
int async(Request req, ResponseCallback resp_cb);
}
#include "hv/requests.h"
#include "hv/axios.h"
using namespace hv;
#include "hv/hthread.h"
static void test_http_async_client(HttpClient* cli, int* resp_cnt) {
printf("test_http_async_client request thread tid=%ld\n", hv_gettid());
auto req = std::make_shared<HttpRequest>();
req->method = HTTP_POST;
req->url = "http://127.0.0.1:8080/echo";
req->headers["Connection"] = "keep-alive";
req->body = "This is an async request.";
req->timeout = 10;
cli->sendAsync(req, [resp_cnt](const HttpResponsePtr& resp) {
printf("test_http_async_client response thread tid=%ld\n", hv_gettid());
if (resp == NULL) {
printf("request failed!\n");
} else {
printf("%d %s\r\n", resp->status_code, resp->status_message());
printf("%s\n", resp->body.c_str());
}
*resp_cnt += 1;
});
}
static void test_http_sync_client(HttpClient* cli) {
HttpRequest req;
req.method = HTTP_POST;
req.url = "http://127.0.0.1:8080/echo";
req.headers["Connection"] = "keep-alive";
req.body = "This is a sync request.";
req.timeout = 10;
HttpResponse resp;
int ret = cli->send(&req, &resp);
if (ret != 0) {
printf("request failed!\n");
} else {
printf("%d %s\r\n", resp.status_code, resp.status_message());
printf("%s\n", resp.body.c_str());
}
}
static void test_requests() {
// auto resp = requests::get("http://www.example.com");
//
// make clean && make WITH_OPENSSL=yes
// auto resp = requests::get("https://www.baidu.com");
// get请求
auto resp = requests::get("http://127.0.0.1:8080/ping");
if (resp == NULL) {
printf("request failed!\n");
} else {
printf("%d %s\r\n", resp->status_code, resp->status_message());
printf("%s\n", resp->body.c_str());
}
// Content-Type: application/json
hv::Json jroot;
jroot["user"] = "admin";
jroot["pswd"] = "123456";
http_headers headers;
headers["Content-Type"] = "application/json";
resp = requests::post("http://127.0.0.1:8080/echo", jroot.dump(), headers);
if (resp == NULL) {
printf("request failed!\n");
} else {
printf("%d %s\r\n", resp->status_code, resp->status_message());
printf("%s\n", resp->body.c_str());
}
// 文件上传
printf("upload File\n");
resp = requests::uploadFile("http://192.168.0.108:8080/upload?filename=1.txt", "C:/Users/19717/Desktop/1.txt");
if (resp == NULL) {
printf("request failed!\n");
} else {
printf("%d %s\r\n", resp->status_code, resp->status_message());
printf("%s\n", resp->body.c_str());
}
printf("upload LargeFile\n");
resp = requests::uploadFile("http://192.168.0.108:8080/upload/2.txt", "C:/Users/19717/Desktop/2.txt");
if (resp == NULL) {
printf("request failed!\n");
} else {
printf("%d %s\r\n", resp->status_code, resp->status_message());
printf("%s\n", resp->body.c_str());
}
// 文件下载
size_t filesize = requests::downloadFile("http://192.168.0.108:8080/download/2.txt", "2.txt");
if (filesize == 0) {
printf("downloadFile failed!\n");
} else {
printf("downloadFile success!\n");
}
// async
/*
// auto req = std::make_shared<HttpRequest>();
req->url = "http://127.0.0.1:8080/echo";
req->method = HTTP_POST;
req->body = "This is an async request.";
req->timeout = 10;
requests::async(req, [](const HttpResponsePtr& resp) {
if (resp == NULL) {
printf("request failed!\n");
} else {
printf("%d %s\r\n", resp->status_code, resp->status_message());
printf("%s\n", resp->body.c_str());
}
});
*/
}
static void test_axios() {
const char* strReq = R"(
{
"method": "POST",
"url": "http://127.0.0.1:8080/echo",
"timeout": 10,
"params": {
"page_no": "1",
"page_size": "10"
},
"headers": {
"Content-Type": "application/json"
},
"body": {
"app_id": "123456",
"app_secret": "abcdefg"
}
}
)";
// sync
auto resp = axios::axios(strReq);
// auto resp = axios::post("http://127.0.0.1:8080/echo", strReq);
if (resp == NULL) {
printf("request failed!\n");
} else {
printf("%d %s\r\n", resp->status_code, resp->status_message());
printf("%s\n", resp->body.c_str());
}
// async
/*
axios::axios(strReq, [](const HttpResponsePtr& resp) {
if (resp == NULL) {
printf("request failed!\n");
} else {
printf("%d %s\r\n", resp->status_code, resp->status_message());
printf("%s\n", resp->body.c_str());
}
});
*/
}
int main(int argc, char* argv[]) {
int req_cnt = 0;
if (argc > 1) req_cnt = atoi(argv[1]);
if (req_cnt == 0) req_cnt = 1;
HttpClient sync_client;
HttpClient async_client;
int resp_cnt = 0;
for (int i = 0; i < req_cnt; ++i) {
test_http_async_client(&async_client, &resp_cnt);
test_http_sync_client(&sync_client);
test_requests();
test_axios();
}
// demo wait async finished
while (resp_cnt < req_cnt) hv_delay(100);
printf("finished!\n");
return 0;
}
十一、Libhv Websocket
1、Websocket协议概述
HTTP同一时间里,客户端和服务器只能有一方主动发数据,是半双工通信; 这种由客户端主动请求,服务器响应的方式满足大部分网页的功能场景,服务器不会主动给客户端发消息,而类似网页游戏这样的场景,是需要客户端和服务器之间互相主动发大量数据。 |
WebSocket 是一种用于在单个 TCP 连接上进行全双工通信的网络协议,使用 ws:// 和 wss:// 协议前缀,这些前缀可以与任何域名或 IP 地址结合使用; WebSocket 建立连接:客户端发送一个 HTTP 请求来建立连接,然后服务器返回一个确认消息,表示已建立连接,之后,客户端和服务器可以通过这个连接进行双向通信,消息可以是任意的字节数组,并且可以使用任意的格式进行编码。 |
注意:WebSocket不是基于HTTP的新协议,因为WebSocket只有在建立连接时才用到了HTTP,升级完成之后就跟HTTP不再有任何关系。 |
2、Websocket服务端
// WebSocketServer 继承自 HttpServer
class WebSocketServer : public HttpServer {
// 注册WebSocket业务类
void registerWebSocketService(WebSocketService* service);
};
// WebSocket业务类
struct WebSocketService {
// 打开回调
std::function<void(const WebSocketChannelPtr&, const HttpRequestPtr&)> onopen;
// 消息回调
std::function<void(const WebSocketChannelPtr&, const std::string&)> onmessage;
// 关闭回调
std::function<void(const WebSocketChannelPtr&)> onclose;
// 心跳间隔
int ping_interval;
};
/*
* @server bin/websocket_server_test 9999
* @client bin/websocket_client_test ws://127.0.0.1:9999/
*/
#include "hv/WebSocketServer.h"
#include "hv/EventLoop.h"
#include "hv/htime.h"
using namespace hv;
/*
* #define TEST_WSS 1
* @server bin/websocket_server_test 9999
*
* @client bin/websocket_client_test ws://127.0.0.1:9999/
* bin/websocket_client_test wss://127.0.0.1:10000/
*/
#define TEST_WSS 0
using namespace hv;
class MyContext {
public:
MyContext() {
printf("MyContext::MyContext()\n");
timerID = INVALID_TIMER_ID;
}
~MyContext() {
printf("MyContext::~MyContext()\n");
}
// 消息处理
int handleMessage(const std::string& msg, enum ws_opcode opcode) {
printf("onmessage(type=%s len=%d): %.*s\n", opcode == WS_OPCODE_TEXT ? "text" : "binary",
(int)msg.size(), (int)msg.size(), msg.data());
return msg.size();
}
TimerID timerID;
};
int main(int argc, char** argv) {
if (argc < 2) {
printf("Usage: %s port\n", argv[0]);
return -10;
}
int port = atoi(argv[1]);
// 实例化HTTP服务端
HttpService http;
http.GET("/ping", [](const HttpContextPtr& ctx) {
return ctx->send("pong");
});
WebSocketService ws;
// 打开回调
ws.onopen = [](const WebSocketChannelPtr& channel, const HttpRequestPtr& req) {
printf("onopen: GET %s\n", req->Path().c_str());
auto ctx = channel->newContextPtr<MyContext>();
// send(time) every 1s
ctx->timerID = setInterval(1000, [channel](TimerID id) {
if (channel->isConnected() && channel->isWriteComplete()) {
char str[DATETIME_FMT_BUFLEN] = {0};
datetime_t dt = datetime_now();
datetime_fmt(&dt, str);
channel->send(str);
}
});
};
// 消息回调
ws.onmessage = [](const WebSocketChannelPtr& channel, const std::string& msg) {
auto ctx = channel->getContextPtr<MyContext>();
ctx->handleMessage(msg, channel->opcode);
};
// 关闭回调
ws.onclose = [](const WebSocketChannelPtr& channel) {
printf("onclose\n");
auto ctx = channel->getContextPtr<MyContext>();
if (ctx->timerID != INVALID_TIMER_ID) {
killTimer(ctx->timerID);
ctx->timerID = INVALID_TIMER_ID;
}
};
WebSocketServer server;
server.port = port;
#if TEST_WSS
server.https_port = port + 1;
hssl_ctx_opt_t param;
memset(¶m, 0, sizeof(param));
param.crt_file = "cert/server.crt";
param.key_file = "cert/server.key";
param.endpoint = HSSL_SERVER;
if (server.newSslCtx(¶m) != 0) {
fprintf(stderr, "new SSL_CTX failed!\n");
return -20;
}
#endif
// 注册服务
server.registerHttpService(&http);
server.registerWebSocketService(&ws);
server.start();
// press Enter to stop
while (getchar() != '\n');
return 0;
}
3、Websocket客户端
class WebSocketClient {
// 打开回调
std::function<void()> onopen;
// 关闭回调
std::function<void()> onclose;
// 消息回调
std::function<void(const std::string& msg)> onmessage;
// 打开
int open(const char* url, const http_headers& headers = DefaultHeaders);
// 关闭
int close();
// 发送
int send(const std::string& msg);
int send(const char* buf, int len, enum ws_opcode opcode = WS_OPCODE_BINARY);
// 设置心跳间隔
void setPingInterval(int ms);
// 设置WebSocket握手阶段的HTTP请求
void setHttpRequest(const HttpRequestPtr& req);
// 获取WebSocket握手阶段的HTTP响应
const HttpResponsePtr& getHttpResponse();
};
#include "hv/WebSocketClient.h"
using namespace hv;
class MyWebSocketClient : public WebSocketClient {
public:
MyWebSocketClient(EventLoopPtr loop = NULL) : WebSocketClient(loop) {}
~MyWebSocketClient() {}
int connect(const char* url) {
// 打开回调
onopen = [this]() {
const HttpResponsePtr& resp = getHttpResponse();
printf("onopen\n%s\n", resp->body.c_str());
};
// 消息回调
onmessage = [this](const std::string& msg) {
printf("onmessage(type=%s len=%d): %.*s\n", opcode() == WS_OPCODE_TEXT ? "text" : "binary",
(int)msg.size(), (int)msg.size(), msg.data());
};
// 关闭回调
onclose = []() {
printf("onclose\n");
};
// ping
setPingInterval(10000);
// reconnect: 1,2,4,8,10,10,10...
reconn_setting_t reconn;
reconn_setting_init(&reconn);
reconn.min_delay = 1000;
reconn.max_delay = 10000;
reconn.delay_policy = 2;
setReconnect(&reconn);
/*
auto req = std::make_shared<HttpRequest>();
req->method = HTTP_POST;
req->headers["Origin"] = "http://example.com";
req->json["app_id"] = "123456";
req->json["app_secret"] = "abcdefg";
printf("request:\n%s\n", req->Dump(true, true).c_str());
setHttpRequest(req);
*/
http_headers headers;
headers["Origin"] = "http://example.com/";
return open(url, headers);
};
};
typedef std::shared_ptr<MyWebSocketClient> MyWebSocketClientPtr;
int TestMultiClientsRunInOneEventLoop(const char* url, int nclients) {
auto loop_thread = std::make_shared<EventLoopThread>();
loop_thread->start();
std::map<int, MyWebSocketClientPtr> clients;
for (int i = 0; i < nclients; ++i) {
MyWebSocketClient* client = new MyWebSocketClient(loop_thread->loop());
client->connect(url);
clients[i] = MyWebSocketClientPtr(client);
}
// press Enter to stop
while (getchar() != '\n');
loop_thread->stop();
loop_thread->join();
return 0;
}
int main(int argc, char** argv) {
if (argc < 2) {
printf("Usage: %s url\n", argv[0]);
return -10;
}
const char* url = argv[1];
int nclients = 0;
if (argc > 2) {
nclients = atoi(argv[2]);
}
if (nclients > 0) {
return TestMultiClientsRunInOneEventLoop(url, nclients);
}
MyWebSocketClient ws;
ws.connect(url);
std::string str;
while (std::getline(std::cin, str)) {
if (str == "close") {
ws.close();
} else if (str == "open") {
ws.connect(url);
} else if (str == "stop") {
ws.stop();
break;
} else {
if (!ws.isConnected()) break;
ws.send(str);
}
}
return 0;
}
十二、Libhv Mqtt
1、Mqtt协议概述
物联网传输协议,用于轻量级的发布/订阅式消息传输,旨在为低带宽和不稳定的网络环境中物联网设备提供可靠网络服务,它的核心设计思想是开源、可靠、轻巧、简单,具有以下主要的几项特性: 非常小的通信开销(最小的消息大小为 2 字节); 支持各种流行编程语言(包括C(中级语言),Java,Ruby, Python 等等)且易于使用的客户端; 支持发布/预定模型,简化应用程序的开发; 提供三种不同消息传递等级,让消息能按需到达目的地,适应在不稳定工作的网络传输需求。 |
# Mqtt通信流程
|
服务器处理客户端网络连接、发布信息、处理订阅/退订请求、向订阅客户端推送信息; 客户端发布信息,其他客户端可以订阅该信息; 客户端退订或删除信息; |
# Mqtt服务质量(Qos)
![]() |
QoS 是消息的发送方(Sender)和接受方(Receiver)之间达成的一个协议(一般使用等级0或1,等级2太消耗带宽,会一直发送直到接收方收到为止): QoS 0: Sender发送方发送一条消息,接受方Receiver最多只接受一次,发送方尽力发送,发送成功接收方接到,但是发送失败,忽略; QoS 1: Sender发送方发送一条消息,接受方Receiver至少能接受一次,也就是说sender向receive发送消息,如果发送失败,继续发送,直到接收方接到消息,但是因为重传的原因,receive有可能会接到重复的消息; QoS 2(注意现在主流服务器云服务器应用MQTT服务等级一般采用等级0或者等级1(等级2基本没有 太消耗带宽))代表: Sender发送方发送一条消息,接受方确保能接收到而且只接到一次,也就是发送方尽力发送,发送失败继续发送,直到接收方接到消息为止,同时保证接收方不会因为消息重发而接到重复消息。 |
# Mqtt主题通配符
MQTT 主题通配符包含单层通配符 + 及多层通配符 #,主要用于客户端一次订阅多个主题。 注意:通配符只能用于订阅,不能用于发布。 |
加号 (“+”) 用于单个主题层级匹配的通配符。 在使用单层通配符时,单层通配符必须占据整个层级,例如: 有效sensor/+ 有效sensor/+/temperature 有效sensor+ 无效(没有占据整个层级) 如果客户端订阅了主题 sensor/+/temperature,将会收到以下主题的消息: sensor/1/temperaturesensor/2/temperature...sensor/n/temperature 但是不会匹配以下主题: sensor/temperaturesensor/bedroom/1/temperature |
# Mqtt报文数据帧格式(根据报文类型会有所区别)
固定报头+可变报头+有效负载。 |
## CONNECT(连接服务器)
固定报头:Byte1:报文类型,Byte2:剩余长度(可变报头长度+有效负载长度); 可变报头:协议名、协议级别、连接标志(是否需要用户名密码登录,遗嘱报文[服务器检测到异常断开会推送给订阅者])、保持连接(客户端发送PING报文最大间隔[以秒为单位]); 有效负载:由可变报头中标志决定是否包含包含前缀字段。 |
## CONNACK(确认连接请求,服务器发送给客户端第一个报文)
固定报头:Byte1:报文类型,Byte2:剩余长度(可变报头长度+有效负载长度); 可变报头:Byte1:连接确认标志,Byte2:连接返回码; |
## DISCONNECT(断开连接,客户端发给服务器的最后一个报文)
固定报头:Byte1:报文类型,Byte2:剩余长度(可变报头长度+有效负载长度); |
## PINGREQ(心跳请求,客户端在设定保持连接时间内发送)
固定报头:Byte1:报文类型,Byte2:剩余长度(可变报头长度+有效负载长度); |
## PINGRESP(心跳响应,客户端发送多次PING服务器无回复则重新连接)
固定报头:Byte1:报文类型,Byte2:剩余长度(可变报头长度+有效负载长度); |
## SUBSCRIBE(订阅主题)
固定报头:Byte1:报文类型,Byte2:剩余长度(可变报头长度+有效负载长度); 可变报头:0x00+0x0A; 有效负载:包含主题过滤器列表,表示客户端想要订阅的主题;每个过滤器后跟着一个QoS等级字节; |
## SUBACK(订阅主题确认,用于确认服务器已收到并且正在处理订阅报文)
固定报头:Byte1:报文类型,Byte2:剩余长度(可变报头长度+有效负载长度); 可变报头:报文标识符为订阅主题时发送的可变报头; 有效负载:返回码清单,每个返回码对应等待确认订阅报文中的主题过滤器; |
## UNSUBSCRIBE(取消订阅主题)
固定报头:Byte1:报文类型,Byte2:剩余长度(可变报头长度+有效负载长度); 可变报头:报文标识符; 有效负载:取消订阅主题过滤器列表; |
## UNSUBACK(取消订阅确认)
固定报头:Byte1:报文类型,Byte2:剩余长度(可变报头长度+有效负载长度); 可变报头:等待确认的 UNSUBSCRIBE 报文的报文标识符; |
## PUBLISH(发布消息)
固定报头:Byte1:报文类型+ DUP首次请求+Qos等级,Byte2:剩余长度(可变报头长度+有效负载长度); 可变报头:顺序包含主题名和报文标识符; 有效负载:消息(长度=固定报头剩余长度-可变报头长度); |
2、Mqtt服务搭建(Linux)
使用apt-get 安装mosquitto,命令如下:apt-get install mosquitto 安装mosquitto 客户端, 命令如下:apt-get install mosquitto-clients |
Mqtt代理、客户端API。 mosquitto:代理器主程序 mosquitto.conf:配置文件【路径:/etc/mosquitto】 mosquitto_passwd:用户密码管理工具 mosquitto_pub:用于发布消息的命令行客户端 mosquitto_sub:用于订阅消息的命令行客户端 mqtt:MQTT的后台进程 libmosquitto:客户端编译的库文件 |
3、Mqtt服务搭建(交叉编译)
# 交叉编译uuid库
源码下载:wget http://nchc.dl.sourceforge.net/project/libuuid/libuuid-1.0.3.tar.gz, |
./configure CC=aarch64-linux-gnu-gcc --host=aarch64-linux-gnu --prefix=/opt/mosquitto/mosquitto-aarch/libuuid-1.0.3 |
make make install |
# 交叉编译openssl库
源码下载:wget https://github.com/openssl/openssl/releases/download/openssl-3.3.0-beta1/openssl-3.3.0-beta1.tar.gz |
setarch i386 ./config no-asm shared --prefix=/opt/mosquitto/mosquitto-aarch/openssl-1.0.2g/ |
vim Mkefile 修改编译器前缀: CC=aarch64-linux-gnu-gcc AR=aarch64-linux-gnu-ar $(ARFLAGS) r RANLIB=aarch64-linux-gnu-gcc-ranlib NM=aarch64-linux-gnu-gcc-nm |
make make install |
# 交叉编译mosquitto库
源码下载:wget http://mosquitto.org/files/source/mosquitto-2.0.2.tar.gz |
make WITH_STATIC_LIBRARIES=yes WITH_SRV=no CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ CFLAGS="-I /opt/mosquitto/mosquitto-aarch/openssl-1.0.2g/include -I /opt/mosquitto/mosquitto-aarch/libuuid-1.0.3/include -I /opt/mosquitto/mosquitto-aarch/openssl-1.0.2g/lib -I /opt/mosquitto/mosquitto-aarch/libuuid-1.0.3/lib" LDFLAGS="-L /opt/mosquitto/mosquitto-aarch/openssl-1.0.2g/lib -L /opt/mosquitto/mosquitto-aarch/libuuid-1.0.3/lib -lssl -lcrypto -luuid" |
make DESTDIR=/opt/mosquitto/mosquitto-aarch/mosquitto-1.5 install |
# 拷贝到文件系统
制作安装包: cp /tools/mosquitto/arm-mosquitto/mosquitto-1.5/etc/mosquitto/mosquitto.conf.example /tools/mosquitto/arm-install/ cp /tools/mosquitto/arm-mosquitto/mosquitto-1.5/usr/local/bin/* /tools/mosquitto/arm-install/ cp /tools/mosquitto/arm-mosquitto/mosquitto-1.5/usr/local/sbin/mosquitto /tools/mosquitto/arm-install/ cp /tools/mosquitto/arm-mosquitto/libuuid-1.0.3/ /tools/mosquitto/arm-install/ -r cp /tools/mosquitto/arm-mosquitto/openssl/ /tools/mosquitto/arm-install/ -r cp /tools/mosquitto/arm-mosquitto/mosquitto-1.5/ /tools/mosquitto/arm-install/ -r |
拷贝到文件系统: cd /tools/mosquitto/arm-install cp libuuid-1.0.3/lib/* openssl/lib/* mosquitto-1.5/usr/local/lib/* /mnt/nfs/rootfs/lib/ -r cp mosquitto.conf.example /mnt/nfs/rootfs/etc/mosquitto.conf cp mosquitto mosquitto_* /mnt/nfs/rootfs/bin/ |
修改mosquitto.conf 文件,修改里面的第40 行为user root 并取消注释; |
# 联合测试
以开发板作为服务,Ubuntu 开两个终端作为订阅者/发布者测试: Arm启动服务:mosquitto -d -c /etc/mosquitto.conf Arm查询状态:ps -ef |grep mosquito Ubuntu订阅主题(注意IP地址):mosquitto_sub -h 192.168.0.190 -t "mqtt" -v Ubuntu发布消息(注意IP地址):mosquitto_pub -h 192.168.0.190 -t "mqtt" -m "Hello MQTT" |
以开发板作为消息的发布者,Ubuntu 开俩个两端作为服务器/订阅者测试: Ubuntu启动服务:service mosquitto start Ubuntu查询状态:ps -ef | grep mosquitto Ubuntu订阅主题(注意IP地址):mosquitto_sub -h 192.168.0.128 -t "mqtt" -v Arm发布消息(注意IP地址):mosquitto_pub -h 192.168.0.128 -t "mqtt" -m "Hello MQTT" |
4、Mqtt服务使用
# Mosquitto服务端
Mosquitto 是一个轻量级的开源 MQTT(Message Queuing Telemetry Transport)代理,广泛用于物联网(IoT)和其他需要低带宽、低延迟通信的应用。它遵循发布/订阅模式,允许设备(客户端)通过主题进行消息的发布和订阅。以下是一些 Mosquitto 的关键特性和基本用法。 |
启动服务:sudo service mosquitto start; 查看服务状态:sudo service mosquitto status 关闭服务:sudo service mosquitto stop |
/etc/mosquitto/mosquitto.conf |
pid_file /var/run/mosquitto.pid # 消息持久存储 persistence true persistence_location /var/lib/mosquitto/ # 日志文件 log_dest file /var/log/mosquitto/mosquitto.log # 其他配置 include_dir /etc/mosquitto/conf.d |
通过配置文件启动服务: mosquitto -c /etc/mosquitto/mosquitto.conf -d |
# Mosquitto客户端订阅
订阅者通过mosquitto_sub 订阅指定主题的消息; |
打开一个终端,订阅主题,命令如下: mosquitto_sub -h localhost -t "mqtt" -v 其中参数-h 是指定要连接的MQTT 服务器,这里使用的是本机,也可以直接使用本机的IP,-t 订阅主 题,此处为mqtt,所以主题为mqtt,-v 打印更多的调试信息; |
# Mosquitto客户端发布
发布者通过mosquitto_pub 发布指定主题的消息; |
打开另一个终端,发布主题,命令如下: mosquitto_pub -h localhost -t "mqtt" -m "Hello MQTT" 其中参数-h 是指定要连接的MQTT 服务器,这里连接的是本机,所以是localhost,也可以是要连接的 设备的IP 地址,-t 订阅主题,此处为mqtt,-m 指定消息内容,这里发送的是Hello MQTT; |
发送成功以后,mqtt 的订阅端会收到我们的发布的信息Hello MQTT; |
5、Mqtt客户端
# 基于Libhv实现 需要将源码目录下mqtt文件夹拷贝到工程: 注:4个文件中引用的头文件可能需要修改,视情况而定,比如hv/xxx.h |
class MqttClient { public: mqtt_client_t* client; MqttClient(hloop_t* loop = NULL); ~MqttClient(); void run(); void stop(); // 设置客户端ID void setID(const char* id); void setWill(mqtt_message_t* will); void setAuth(const char* username, const char* password); void setPingInterval(int sec); int lastError(); // SSL/TLS int setSslCtx(hssl_ctx_t ssl_ctx); int newSslCtx(hssl_ctx_opt_t* opt); // 连接/断开服务器 void setReconnect(reconn_setting_t* reconn); void setConnectTimeout(int ms); int connect(const char* host, int port = DEFAULT_MQTT_PORT, int ssl = 0); int reconnect(); int disconnect(); bool isConnected(); // 发布 int publish(mqtt_message_t* msg, MqttCallback ack_cb = NULL); int publish(const std::string& topic, const std::string& payload, int qos = 0, int retain = 0, MqttCallback ack_cb = NULL); // 订阅/取消订阅 int subscribe(const char* topic, int qos = 0, MqttCallback ack_cb = NULL); int unsubscribe(const char* topic, MqttCallback ack_cb = NULL); // 连接回调 MqttCallback onConnect; // 关闭回调 MqttCallback onClose; // 消息回调 MqttMessageCallback onMessage; } |
示例代码: |
/* * @test bin/mqtt_client_test 127.0.0.1 1883 topic payload */ #include "libhv/mqtt/mqtt_client.h" using namespace hv; /* * @test MQTTS * #define TEST_SSL 1 * * @build ./configure --with-mqtt --with-openssl && make clean && make * */ #define TEST_SSL 0 #define TEST_AUTH 0 #define TEST_RECONNECT 1 #define TEST_QOS 0 int main(int argc, char** argv) { if (argc < 5) { printf("Usage: %s host port topic payload\n", argv[0]); return -10; } const char* host = argv[1]; int port = atoi(argv[2]); const char* topic = argv[3]; const char* payload = argv[4]; MqttClient cli; cli.onConnect = [topic, payload](MqttClient* cli) { printf("connected!\n"); #if TEST_QOS cli->subscribe(topic, 1, [topic, payload](MqttClient* cli) { printf("subscribe OK!\n"); cli->publish(topic, payload, 1, 0, [](MqttClient* cli) { printf("publish OK!\n"); }); }); #else cli->subscribe(topic); cli->publish(topic, payload); #endif }; cli.onMessage = [](MqttClient* cli, mqtt_message_t* msg) { printf("topic: %.*s\n", msg->topic_len, msg->topic); printf("payload: %.*s\n", msg->payload_len, msg->payload); cli->disconnect(); cli->stop(); }; cli.onClose = [](MqttClient* cli) { printf("disconnected!\n"); }; #if TEST_AUTH cli.setAuth("test", "123456"); #endif #if TEST_RECONNECT reconn_setting_t reconn; reconn_setting_init(&reconn); reconn.min_delay = 1000; reconn.max_delay = 10000; reconn.delay_policy = 2; cli.setReconnect(&reconn); #endif cli.setPingInterval(10); int ssl = 0; #if TEST_SSL ssl = 1; #endif cli.connect(host, port, ssl); cli.run(); return 0; } |
十三、Libhv Ftp
1、Ftp协议概述
FTP(File Transfer Protocol)是一种用于在计算机网络上传输文件的标准协议,它允许用户通过网络在客户端和服务器之间传输文件,包括上传(从客户端到服务器)和下载(从服务器到客户端)文件。 |
数据端口(接收数据(主动模式下数据端口20 被动模式>1024)) 命令端口(接收控制命令,固定21端口),必须建立两个套接字; |
# 主动模式
(服务器主动连接客户端数据端口) |
客户端随机开启一个大于1024的端口N向服务器的21号端口发起连接,然后开放N+1号端口进行监听,并向服务器发出PORT N+1命令; |
服务器接收到命令后会用其本地的FTP数据端口(通常是20)来连接客户端指定的端口N+1,进行数据传输; |
![]() |
主动模式服务器端主动连接时有可能被客户端的防火墙墙掉(对于客户端防火墙是从外部到内部的连接); |
# 被动模式
(服务器被动等待客户端连接自己的数据端口) |
客户端随机开启一个大于1024的端口N向服务器的21号端口发起连接,同时会开启N+1号端口,然后向服务器发送PASV命令,通知服务器自己处于被动模式; |
服务器收到命令后,会开放一个大于1024的端口P进行监听,然后用PORT P命令通知客户端,自己的数据端口是P,客户端收到命令后,会通过N+1号端口连接服务器的端口P,然后在两个端口之间进行数据传输; |
![]() |
2、Ftp服务搭建(Linux)
apt-get install vsftpd |
修改配置文件(vim /etc/vsftpd.conf): 31行把#去除; |
如果把145行改为ftp然后重启服务器就可以允许任意用户登录; |
重启服务:/etc/init.d/vsftpd restart |
3、Ftp服务使用
# 连接服务器
在linux命令行下输入:ftp <服务器ip地址> (ftp -p <服务器ip地址> 被动模式) 服务器询问:分别输入用户名、用户密码认证后即可登录 |
# 将文件从远端主机中传送至本地主机
get [remote-file] [local-file] |
( FTP服务器文件路径 本地路径[不能在根目录下] ) |
# 从远端主机接收一批文件至本地主机
mget [remote-files] |
# 将本地一个文件传送至远端主机中
put local-file [remote-file] |
# 断开连接
bye:中断与服务器连接; |
4、Ftp命令
命令 | 描述 |
ABOR | 中断数据连接程序 |
ACCT <account> | 系统特权帐号 |
ALLO <bytes> | 为服务器上的文件存储器分配字节 |
APPE <filename> | 添加文件到服务器同名文件 |
CDUP <dir path> | 改变服务器上的父目录(回到上一层目录) |
CWD <dir path> | 改变服务器上的工作目录(跳转) |
DELE <filename> | 删除服务器上的指定文件 |
HELP <command> | 返回指定命令信息 |
LIST <name> | 如果是文件名列出文件信息,如果是目录则列出文件列表 |
MODE <mode> | 传输模式(S=流模式,B=块模式,C=压缩模式) |
MKD <directory> | 在服务器上建立指定目录 |
NLST <directory> | 列出指定目录内容,用数据通道接收服务器返回内容 |
NOOP | 无动作,除了来自服务器上的承认 |
PASS <password> | 系统登录密码(口令) |
PASV | 请求服务器等待数据端口连接(被动模式) |
PORT <address> | IP 地址和两字节的端口 ID(数据端口,主动模式) |
PWD | 显示当前工作目录 |
QUIT | 从 FTP 服务器上退出登录 |
REIN | 重新初始化登录状态连接 |
REST <offset> | 由特定偏移量重启文件传递 |
RETR <filename> | 从服务器上找回(复制)文件(下载功能) |
RMD <directory> | 在服务器上删除指定目录 |
RNFR <old path> | 对旧路径重命名 |
RNTO <new path> | 对新路径重命名 |
SITE <params> | 由服务器提供的站点特殊参数 |
SMNT <pathname> | 挂载指定文件结构 |
STAT <directory> | 在当前程序或目录上返回信息 |
STOR <filename> | 储存(复制)文件到服务器上 |
STOU <filename> | 储存文件到服务器名称上 |
STRU <type> | 数据结构(F=文件,R=记录,P=页面) |
SYST | 返回服务器使用的操作系统 |
TYPE <data type> | 数据类型(A=ASCII,E=EBCDIC,I=binary) |
USER <username>> | 系统登录用户名(控制连接后第一个发出的命令) |
5、Ftp响应码
(三位数字+文本信息(与服务器相关,用户可能得到不同的文本信息)) |
![]() |
# 响应码概述
客户端每发送一个FTP命令,服务器就会返回一个响应码,命令/响应码都是在命令通道进行传输,传送文件内容是数据通道,FTP命令的响应是为了对数据传输请求和过程进行同步,让用户了解服务器状态; |
# 数字位含义
第一位数字(命令状态的一般性指示): 1 :服务器正确接收信息,还未处理; 2 :服务器已经正确处理信息; 3 :服务器正确接收信息,正在处理; 4 :信息暂时错误; 5 :信息永久错误(FTP服务器和物理主机是ping不通的); |
第二位数字(响应类型分类): 0 :语法; 1 :系统状态和信息; 2 :连接状态; 3 :与用户认证有关的信息; 4 :未定义; 5 :与文件系统相关信息; |
第三位数字: 在第二个数字的基础上对应答内容进一步细化; |
# 标准响应码列表
响应代码 | 解释说明 |
110 | 新文件指示器上的重启标记 |
120 | 服务器准备就绪的时间(分钟数) |
125 | 打开数据连接,开始传输 |
150 | 打开连接 |
200 | 成功 |
202 | 命令没有执行 |
211 | 系统状态回复 |
212 | 目录状态回复 |
213 | 文件状态回复 |
214 | 帮助信息回复 |
215 | 系统类型回复 |
220 | 服务就绪 |
221 | 退出网络 |
225 | 打开数据连接 |
226 | 结束数据连接 |
227 | 进入被动模式(IP 地址、ID 端口) |
230 | 登录因特网 |
250 | 文件行为完成 |
257 | 路径名建立 |
331 | 要求密码 |
332 | 要求帐号 |
350 | 文件行为暂停 |
421 | 服务关闭 |
425 | 无法打开数据连接 |
426 | 结束连接 |
450 | 文件不可用 |
451 | 遇到本地错误 |
452 | 磁盘空间不足 |
500 | 无效命令 |
501 | 错误参数 |
502 | 命令没有执行 |
503 | 错误指令序列 |
504 | 无效命令参数 |
530 | 未登录网络 |
532 | 存储文件需要帐号 |
550 | 文件不可用 |
551 | 不知道的页类型 |
552 | 超过存储分配 |
553 | 文件名不允许 |
6、Ftp客户端
# 基于Linux Socket实现
## 客户端连接FTP服务器
![]() |
当客户端与服务器建立连接后,服务器会返回220响应码和一些欢迎信息。; 步骤(本过程在命令端口21实现): 创建通信套接字:socket; 编写服务器信息:IP、端口号、协议; 读取响应码:read; |
/* 链接FTP服务器 - FTP服务器固定端口21 */ unsigned char Connect_Ftp_Server(char *IP) { char tempBuf[128] = {0}; struct sockaddr_in ftp_addr; socklen_t addrlen = sizeof(struct sockaddr_in); // 初始化socket套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0){ perror("socket"); return -1; } // 编写服务器信息 ftp_addr.sin_family = AF_INET; ftp_addr.sin_port = htons(21); ftp_addr.sin_addr.s_addr = inet_addr(IP); // 连接服务器 if(connect(sockfd, (struct sockaddr *)&ftp_addr, addrlen)<0){ perror("链接失败:"); return 0; } // 接收服务器端相关欢迎信息 read(sockfd, tempBuf, sizeof(tempBuf)); printf("%s\n",tempBuf); memset(tempBuf,0,sizeof(tempBuf)); return 1; } |
## 客户端登录FTP服务器
![]() |
客户端发送用户名和密码,服务器验证通过后,会返回230响应码表示登录成功。 步骤: 发送USER登录名:Sprintf(buff,”USER %s\r\n”,username); Write(sockfd, buff,stlen(buff)); 客户端进行接收消息:read(sockfd, buff,sizeof(buff)); 判定接收消息是否为331(本过程可以省略); 发送密码:Sprintf(buff,”PASS %s\r\n”,password); Write(sockfd, buff,stlen(buff)); 客户端进行接收消息:read(sockfd, buff,sizeof(buff)); 判定接收消息是否为230(登陆成功标志为230); |
/* 登录ftp服务器 */ unsigned char Login_Ftp_Server(char Login_name[],char Login_passwd[]) { char tempBuf[128] = {0}; // ---------------------------命令 ”USER username\r\n” sprintf(tempBuf,"USER %s\r\n",Login_name); // ---------------------------客户端发送用户名到服务器端 write(sockfd,tempBuf,strlen(tempBuf)); memset(tempBuf,0,sizeof(tempBuf)); // ---------------------------客户端接收服务器响应码和信息 read(sockfd,tempBuf,sizeof(tempBuf)); printf("%s",tempBuf); memset(tempBuf,0,sizeof(tempBuf)); // ---------------------------命令 ”PASS password\r\n” sprintf(tempBuf,"PASS %s\r\n",Login_passwd); write(sockfd,tempBuf,strlen(tempBuf)); memset(tempBuf,0,sizeof(tempBuf)); // ---------------------------客户端接收服务器响应码和信息 read(sockfd,tempBuf,sizeof(tempBuf)); printf("%s",tempBuf); // ---------------------------判断tempBuf是否有230响应码 if(strstr(tempBuf, "230")==NULL){ perror("链接失败:"); return 0; } memset(tempBuf,0,sizeof(tempBuf)); return 1; } |
## 客户端退出FTP服务器
![]() |
## 客户端控制FTP服务器进入被动模式
![]() |
客户端在下载/上传文件前,要先发送命令让服务器进入被动模式。服务器会打开数据端口并监听,并返回响应码227和数据连接的端口号; 步骤: 客户端发送PASV:Write(socket_fd,”PASV\r\n”,6); 客户端接收:227 Entering Passive Mode (192,168,1,86,端口号高8位,低8位) 提取端口号: sscanf(buff,”227 Entering Passive Mode (192,168,1,86,%d, %d)”,&port1,&port2); 端口号应该为Short Prot=prot1<<8|port2; |
/* 被动模式回传端口号 */ unsigned char Passive(unsigned short * Port) { int IP1,IP2,IP3,IP4,Port1,Port2; char tempBuf[128] = {0}; // ---------------------------命令 "PASV\r\n" sprintf(tempBuf,"PASV\r\n"); // ---------------------------客户端告诉服务器用被动模式 write(sockfd,tempBuf,strlen(tempBuf)); memset(tempBuf,0,sizeof(tempBuf)); // ---------------------------客户端接收服务器的响应码和新开的端口号 // 正常为 "227 Entering passive mode (<h1,h2,h3,h4,p1,p2>)" read(sockfd, tempBuf, sizeof(tempBuf)); printf("%s\n",tempBuf); // ---------------------------判断并提取端口号 if(sscanf(tempBuf, "227 Entering Passive Mode (%d,%d,%d,%d,%d,%d)", &IP1,&IP2,&IP3,&IP4,&Port1,&Port2)==EOF) { printf("对比失败\n"); return 0; } // 把分裂的两个字节端口号进行整合 *Port = Port1<<8|Port2; printf("端口号%d\n",*Port); memset(tempBuf,0,sizeof(tempBuf)); return 1; } |
## 客户端发送CWD实现路径跳转
![]() |
客户端发送命令下载文件,服务器会返回响应码150,并向数据连接发送文件内容。 步骤: 客户端发送SWD 目录名:sprintf(buff,”CWD %s\r\n”,dirname); 客户端给服务器发送命令:write(sockt_fd,buff,strlen(buff)); 客户端等待响应码返回:read(sockt_fd,buff,sizeof (buff)); 对比是都为250:strstr(buff,”250”)==NULL,失败; |
unsigned char Cwd_Dirname(char dirname[]) { char tempBuf[128]={0}; // 跳转路径 sprintf(tempBuf,"CWD %s\r\n",dirname); write(sockfd,tempBuf,strlen(tempBuf)); memset(tempBuf,0,sizeof(tempBuf)); // 接收250 read(sockfd, tempBuf, sizeof(tempBuf)); printf("CWD:%s\n",tempBuf); if(strstr(tempBuf, "250")==NULL){ perror("跳转失败:"); return 0; } memset(tempBuf,0,sizeof(tempBuf)); return 1; } |
## 客户端发送SIZE查看文件大小
![]() |
判定文件是否传输结束:1、依靠本身大小信息;2、read读到长度为<=0则判定接收完成; 步骤: 文件名:sprintf(buff,”SIZE %s\r\n”,filename); 客户端发送指令:write(socke_fd,buff,strlen(buff)); 接收213 大小:read(socek_fd,buff,sizeof(buff)); 对比字符串,提取出文件大小; |
unsigned char SIZE_Filename(char Filename[],int * File_Size) { char tempBuf[128]={0}; sprintf(tempBuf,"SIZE %s\r\n",Filename); write(sockfd,tempBuf,strlen(tempBuf)); memset(tempBuf,0,sizeof(tempBuf)); // 接收213 read(sockfd, tempBuf, sizeof(tempBuf)); printf("SIZE:%s\n",tempBuf); if(strstr(tempBuf, "213")==NULL){ perror("接收失败:"); return 0; } // 提取文件大小 sscanf(tempBuf,"213 %d", File_Size); memset(tempBuf,0,sizeof(tempBuf)); return 1; } |
## 客户端下载(RETR)/上传文件(STOR)
下载或上传前需要进入被动模式,必须连接第二端口(数据端口): |
/* 链接FTP服务器 - 数据端口 */ unsigned char Connect_Ftp_Server_Data(char *IP,unsigned short Data_Prot) { char tempBuf[128] = {0}; struct sockaddr_in ftp_data_addr = {0}; socklen_t Data_addrlen = sizeof(struct sockaddr); /* 初始化socket */ Datasockfd = socket(AF_INET, SOCK_STREAM, 0); if(Datasockfd < 0){ perror("Datasockfd"); return -1; } printf("DATA:%s %d\n",IP,Data_Prot); memset(&ftp_data_addr,0,sizeof(struct sockaddr_in)); ftp_data_addr.sin_family = AF_INET; ftp_data_addr.sin_port = htons(Data_Prot); ftp_data_addr.sin_addr.s_addr = inet_addr(IP); /* 连接到服务器 */ if(connect(Datasockfd, (struct sockaddr *)&ftp_data_addr, Data_addrlen)<0) { perror("数据通道链接失败:"); return 0; } memset(tempBuf,0,sizeof(tempBuf)); return 1; } |
下载文件: |
![]() |
发送下载文件命令: RETR Filename:sprintf(buff,”RETR %s\r\n”,filename); 客户端通过命令端口进行发送命令:write(socket,buff,strlen(buff)); 服务通过命令端口发送给客户端:read(sock_fd,buff,sizeof(buff)); 对比150响应码:strstr(buff,”150”)==NULL 失败; |
unsigned char Download_File_Cmd(char Filename[]) { char tempBuf[128]={0}; sprintf(tempBuf,"RETR %s\r\n",Filename); write(sockfd,tempBuf,strlen(tempBuf)); memset(tempBuf,0,sizeof(tempBuf)); // 接收150 read(sockfd, tempBuf, sizeof(tempBuf)); printf("RETR %s\n",tempBuf); if(strstr(tempBuf, "150")==NULL) { perror("接收失败:"); return 0; } memset(tempBuf,0,sizeof(tempBuf)); return 1; } |
客户端通过被动模式下载文件: |
当客户端发送命令下载文件,服务器会返回响应码150,并向数据连接发送文件内容; |
// 创建文件 int FD = open("/root/55mp3",O_CREAT|O_RDWR,0777); while(1) { // 接收FTP文件数据并保存到本地文件中 int len = read(Datasockfd,buff,sizeof(buff)); write(FD,buff,len); if(len <= 0) break; } |
上传文件: |
![]() |
发送上传文件命令: STOR Filename:sprintf(tempBuf,"STOR %s\r\n",Filename); 客户端通过命令端口进行发送命令:write(sockfd,tempBuf,strlen(tempBuf)); 服务通过命令端口发送给客户端:read(sockfd, tempBuf, sizeof(tempBuf)); 对比150响应码:strstr(buff,”150”)==NULL 失败; |
unsigned char Put_File_Cmd(char dirname[],char Filename[]) { char tempBuf[128] = {0}; /* 跳转到要保存到服务器位置 */ Cwd_Dirname(dirname); /* 发送指令 */ sprintf(tempBuf,"STOR %s\r\n",Filename); write(sockfd,tempBuf,strlen(tempBuf)); memset(tempBuf,0,sizeof(tempBuf)); // 接收150 read(sockfd, tempBuf, sizeof(tempBuf)); printf("RETR %s\n",tempBuf); if(strstr(tempBuf, "150")==NULL){ perror("接收失败:"); return 0; } // 清空缓存数组 memset(tempBuf,0,sizeof(tempBuf)); return 1; } |
客户端通过被动模式上传文件: |
当客户端发送命令上传文件,服务器会从数据连接接收文件; |
// 打开文件 int fd = open(file_pwd_buff,O_RDONLY,0777); if(fd < 0){ printf("文件打开失败!\n"); } // 获取文件字节 int len = 0; len = lseek(fd,-2,SEEK_END); if(len < 0){ printf("读取失败!\n"); } printf("文件字节为:%d\n",len); // 发送文件数据到FTP服务器 while(1) { // 每次读1字节 read(fd,buff,1); write(Datasockfd,buff,1); if(len <= 0) break; len--; memset(buff,0,512); } |
# 基于Libhv Socket实现
将需要将源码目录下protocol文件夹ftp.c、ftp.h拷贝到工程: 注:2个文件中引用的头文件可能需要修改,视情况而定,比如hv/xxx.h |
示例代码: |
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "libhv/protocol/ftp.h" void print_help() { printf("Usage:\n\ help\n\ login <username> <password>\n\ download <remote_filepath> <local_filepath>\n\ upload <local_filepath> <remote_filepath>\n\ quit\n"); } int main(int argc, char** argv) { if (argc < 2) { printf("Usage: ftp host [port]\n"); return 0; } const char* host = argv[1]; int port = FTP_COMMAND_PORT; if (argc >= 3) { port = atoi(argv[2]); } int ret = 0; ftp_handle_t hftp; ret = ftp_connect(&hftp, host, port); if (ret != 0) { printf("ftp connect failed!\n"); return ret; } print_help(); char cmd[256] = {0}; char param1[256] = {0}; char param2[256] = {0}; while (1) { printf("> "); scanf("%s", cmd); if (strncmp(cmd, "help", 4) == 0) { print_help(); } else if (strncmp(cmd, "login", 5) == 0) { scanf("%s", param1); scanf("%s", param2); //printf("cmd=%s param1=%s param2=%s\n", cmd, param1, param2); const char* username = param1; const char* password = param2; ret = ftp_login(&hftp, username, password); printf("%s", hftp.recvbuf); if (ret != 0) break; } else if (strncmp(cmd, "upload", 6) == 0) { scanf("%s", param1); scanf("%s", param2); //printf("cmd=%s param1=%s param2=%s\n", cmd, param1, param2); const char* localfile = param1; const char* remotefile = param2; ret = ftp_upload(&hftp, localfile, remotefile); printf("%s", hftp.recvbuf); if (ret != 0) break; } else if (strncmp(cmd, "download", 8) == 0) { scanf("%s", param1); scanf("%s", param2); printf("cmd=%s param1=%s param2=%s\n", cmd, param1, param2); const char* remotefile = param1; const char* localfile = param2; ret = ftp_download(&hftp, remotefile, localfile); printf("%s", hftp.recvbuf); if (ret != 0) break; } else if (strncmp(cmd, "quit", 4) == 0) { break; } else { scanf("%s", param1); //printf("cmd=%s param=%s\n", cmd, param1); ret = ftp_exec(&hftp, cmd, param1); printf("%s", hftp.recvbuf); } } printf("QUIT\n"); ftp_quit(&hftp); printf("%s", hftp.recvbuf); return 0; } |