[15章]基于C++从0到1手写Linux高性能网络编程框架

赵颜
• 阅读 113

[15章]基于C++从0到1手写Linux高性能网络编程框架

深度掌握网络编程是逆袭成为高阶开发者的秘密法宝,所以今天给大家深度讲解基于C++的Linux高性能事件驱动网络编程框架的设计方法及技巧,我将采取渐进迭代的授课方式,配合C++11新特性的使用,以及网络编程理论的深度讲解,并手把手带着大家落地实现,助力在网络编程领域有更大的技术提升!

TCP/IP协议在设计和实现上并没有客户端和服务器的概念,在通信过程中所有机器都是对等的。但由于资源(视频、新闻、软件等)都被数据提供者所垄断,所以几乎所有的网络应用程序都很自然地采用了C/S(客户端/服务器)模型:所有客户端都通过访问服务器来获取所需的资源。

阻塞 I/O 阻塞 I/O 是指,执行可能会发生阻塞的系统调用后,系统调用不能立即完成并返回,因此操作系统会将其挂起,直到等待的事件(写完成、读完成等)发生为止。

非阻塞 I/O 非阻塞 I/O 执行的系统调用总是立即返回,不管事件是否已经发生。如果事件没有立即发生,这些系统调用返回 -1,然后设置 errno,应用程序需要根据返回的 errno 进行相应的处理。

I/O 复用 I/O 复用是指,应用程序通过 I/O 复用函数(select、poll、epoll)向内核注册一组事件,内核通过 I/O 复用函数把其中就绪的事件通知(发送)给应用程序,应用程序接收并处理这些就绪事件。

I/O复用函数本身是阻塞的,它能提高程序效率的原因是 I/O 复用函数具有同时监听多个 I/O 事件的能力 我们创建scanIdcardBack回调函数,识别身份证背面的有效期。我们需要先按照“-”拆分出来开始和结束日期,然后在格式化成“yyyy-MM-dd”形式。 data() { return { type: null, photoPath: '', btnText: '拍照', showCamera: true, showImage: false }; }, onLoad: function(options) { this.type = options.type; }, methods: { clickBtn: function() { let that = this; if (that.btnText == '拍照') { let ctx = uni.createCameraContext();

        ctx.takePhoto({
            quality: 'high',
            success: function(resp) {
                that.photoPath = resp.tempImagePath;
                that.showCamera = false;
                that.showImage = true;
                that.btnText = '提交';
            }
        });
    } else {
        let pages = getCurrentPages(); //你访问的小程序页面历史记录
        let prevPage = pages[pages.length - 2]; //上一个小程序页面
        //调用上一个页面的updatePhoto函数,回传拍好的照片
        prevPage.$vm.updatePhoto(that.type, that.photoPath);
        //返回上一个页面
        uni.navigateBack({
            delta: 1
        });
    }
},
afresh: function() {
    let that = this;
    that.showCamera = true;
    that.showImage = false;
    that.btnText = '拍照';
}

} 在App.vue页面中,我们先开启实时GPS定位,然后用Ajax提交GPS定位给后端Java程序。 onLaunch: function() { let gps = []; //保持屏幕常亮,避免手机休眠 wx.setKeepScreenOn({ keepScreenOn: true });

//TODO 每隔3分钟触发自定义事件,接受系统消息

//开启GPS后台刷新
uni.startLocationUpdate({
    success(resp) {
        console.log('开启定位成功');
    },
    fail(resp) {
        console.log('开启定位失败');
        uni.$emit('updateLocation', null);
    }
});

//GPS定位变化就自动提交给后端
wx.onLocationChange(function(resp) {
    let latitude = resp.latitude;
    let longitude = resp.longitude;
    let speed = resp.speed;
    // console.log(resp)
    let location = { latitude: latitude, longitude: longitude };


    let workStatus = uni.getStorageSync('workStatus');
    //TODO 先暂时写死,以后要去掉这句话
    workStatus = '开始接单'
    /*
     * 上传司机GPS定位信息
     */
    let baseUrl = 'http://192.168.99.106:8201/hxds-driver';
    if (workStatus == '开始接单') {
        // TODO 只在每分钟的前10秒上报定位信息,减小服务器压力
        // let current = new Date();
        // if (current.getSeconds() > 10) {
        //     return;
        // }
        let settings = uni.getStorageSync('settings');
        //TODO 先暂时写死,以后要去掉这句话
        settings = {
            orderDistance: 0,
            rangeDistance: 5,
            orientation: ''
        }
        let orderDistance = settings.orderDistance;
        let rangeDistance = settings.rangeDistance;
        let orientation = settings.orientation;
        uni.request({
            url: `${baseUrl}/driver/location/updateLocationCache`,
            method: 'POST',
            header: {
                token: uni.getStorageSync('token')
            },
            data: {
                latitude: latitude,
                longitude: longitude,
                orderDistance: orderDistance,
                rangeDistance: rangeDistance,
                orientateLongitude: orientation != '' ? orientation.longitude : null,
                orientateLatitude: orientation != '' ? orientation.latitude : null
            },
            success: function(resp) {
                if (resp.statusCode == 401) {
                    uni.redirectTo({
                        url: '/pages/login/login'
                    });
                } else if (resp.statusCode == 200 && resp.data.code == 200) {
                    let data = resp.data;
                    if (data.hasOwnProperty('token')) {
                        let token = data.token;
                        uni.setStorageSync('token', token);
                    }
                    console.log("定位更新成功")
                } else {
                    console.error('更新GPS定位信息失败', resp.data);
                }
            },
            fail: function(error) {
                console.error('更新GPS定位信息失败', error);
            }
        });
    } else if (workStatus == '开始代驾') {
        //TODO 每凑够20个定位就上传一次,减少服务器的压力
    }

    //触发自定义事件
    uni.$emit('updateLocation', location);
});

}, 在com.example.hxds.bff.customer.controller.form包中创建SearchBefittingDriverAboutOrderForm.java类。 @Data @Schema(description = "查询符合某个订单接单的司机列表的表单") public class SearchBefittingDriverAboutOrderForm { @NotBlank(message = "startPlaceLatitude不能为空") @Pattern(regexp = "^(([1-8]\d?)|([1-8]\d))(\.\d{1,18})|90|0(\.\d{1,18})?$", message = "startPlaceLatitude内容不正确") @Schema(description = "订单起点的纬度") private String startPlaceLatitude;

@NotBlank(message = "startPlaceLongitude不能为空")
@Pattern(regexp = "^(([1-9]\\d?)|(1[0-7]\\d))(\\.\\d{1,18})|180|0(\\.\\d{1,18})?$", message = "startPlaceLongitude内容不正确")
@Schema(description = "订单起点的经度")
private String startPlaceLongitude;

@NotBlank(message = "endPlaceLatitude不能为空")
@Pattern(regexp = "^(([1-8]\\d?)|([1-8]\\d))(\\.\\d{1,18})|90|0(\\.\\d{1,18})?$", message = "endPlaceLatitude内容不正确")
@Schema(description = "订单终点的纬度")
private String endPlaceLatitude;

@NotBlank(message = "endPlaceLongitude不能为空")
@Pattern(regexp = "^(([1-9]\\d?)|(1[0-7]\\d))(\\.\\d{1,18})|180|0(\\.\\d{1,18})?$", message = "endPlaceLongitude内容不正确")
@Schema(description = "订单起点的经度")
private String endPlaceLongitude;

@NotBlank(message = "mileage不能为空")
@Pattern(regexp = "^[1-9]\\d*\\.\\d+$|^0\\.\\d*[1-9]\\d*$|^[1-9]\\d*$", message = "mileage内容不正确")
@Schema(description = "预估里程")
private String mileage;

} 自动抢单的原理非常简单,就是当语音播报订单结束之后,JS代码就立即发出抢单Ajax请求。如果是手动抢单,就是司机点击抢单按钮之后再发出Ajax请求。

在工作台页面的showNewOrder()函数中,我们补充上自动抢单和手动抢单的代码。 showNewOrder: function(ref) { …… /* * 执行自动抢单 / if (ref.settings.autoAccept) { ref.ajax(ref.url.acceptNewOrder, 'POST', { orderId: orderId },function(resp) { let result = resp.data.result; //自动抢单成功 if (result == '接单成功') { uni.showToast({ title: '接单成功' }); audio = uni.createInnerAudioContext(); ref.audio = audio; audio.src = '/static/voice/voice_3.mp3'; audio.play(); audio.onEnded(function() { //停止接单 ref.audio = null; ref.ajax(ref.url.stopWork, 'POST', null, function(resp) {}); //初始化新订单和列表变量 ref.newOrder = null; ref.newOrderList.length = 0; ref.executeOrder.id = orderId; clearInterval(ref.reciveNewOrderTimer); ref.reciveNewOrderTimer = null; ref.playFlag = false; //隐藏了工作台页面底部操作条之后,需要重新计算订单执行View的高度 ref.contentStyle = width: 750rpx;height:${ref.windowHeight - 200 - 0}px;; //TODO 加载订单执行数据 }); } else { //自动抢单失败 audio = uni.createInnerAudioContext(); ref.audio = audio; audio.src = '/static/voice/voice_4.mp3'; audio.play(); audio.onEnded(function() { //抢单失败就切换下一个订单 ref.playFlag = false; if (ref.newOrderList.length > 0) { ref.showNewOrder(ref); //递归调用 } else { ref.newOrder = null; } }); } }, false ); } else { /** * 每个订单在页面上停留3秒钟,等待司机手动抢单 / ref.playFlag = false; setTimeout(function() { //如果用户不是正在手动抢单中,就播放下一个新订单 if (!ref.accepting) { ref.canAcceptOrder = false; if (ref.newOrderList.length > 0) { ref.showNewOrder(ref); //递归调用 } else { ref.newOrder = null; } } }, 3000); } }, #include <sys/signal.h> #include <event.h> void signal_cb(int fd, short event, void *argc) { struct event_base *base = (event_base *)argc; struct timeval delay = {2, 0}; printf("Caught an interrupt signal;exiting cleanly in two seconds...\n"); event_base_loopexit(base, &delay); } void timeout_cb(int fd, short event, void *argc) { printf("timeout\n"); } int main() { struct event_base *base = event_init(); struct event *signal_event = evsignal_new(base, SIGINT, signal_cb, base); event_add(signal_event, NULL); timeval tv = {1, 0}; struct event *timeout_event = evtimer_new(base, timeout_cb, NULL); event_add(timeout_event, &tv); event_base_dispatch(base); event_free(timeout_event); event_free(signal_event); event_base_free(base); } 最后,讨论一下 Libevent 的“动力”,即事件循环。Libevent 中实现事件循环的函数是 event_base_loop 。该函数首先调用 I/O 事件多路分发器的事件监听函数,以等待事件;当有事件发生时,就依次处理之: int event_base_loop(struct event_base *base, int flags) { const struct eventop *evsel = base->evsel; struct timeval tv; struct timeval *tv_p; int res, done, retval = 0; EVBASE_ACQUIRE_LOCK(base, th_base_lock); /一个event_base仅允许运行一个事件循环/ if (base->running_loop) { event_warnx("%s:reentrant invocation.Only one event_base_loop" "can run on each event_base at once.", func); EVBASE_RELEASE_LOCK(base, th_base_lock); return -1; } base->running_loop = 1; /标记该event_base已经开始运行/ clear_time_cache(base); /清除event_base的系统时间缓存/ /设置信号事件的event_base实例/ if (base->sig.ev_signal_added && base->sig.ev_n_signals_added) evsig_set_base(base); done = 0; #ifndef_EVENT_DISABLE_THREAD_SUPPORT base->th_owner_id = EVTHREAD_GET_ID(); #endif base->event_gotterm = base->event_break = 0; while (!done) { base->event_continue = 0; if (base->event_gotterm) { break; } if (base->event_break) { break; } timeout_correct(base, &tv); /校准系统时间/ tv_p = &tv; if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) { /获取时间堆上堆顶元素的超时值,即I/O复用系统调用本次应该设置的超时值/ timeout_next(base, &tv_p); } else { /如果有就绪事件尚未处理,则将I/O复用系统调用的超时时间“置0”。这样I/O复用系 统调用直接返回,程序也就可以立即处理就绪事件了/ evutil_timerclear(&tv); } /如果event_base中没有注册任何事件,则直接退出事件循环/ if (!event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) { event_debug(("%s:no events registered.", func)); retval = 1; goto done; } /更新系统时间,并清空时间缓存/ gettime(base, &base->event_tv); clear_time_cache(base); /调用事件多路分发器的dispatch方法等待事件,将就绪事件插入活动事件队列/ res = evsel->dispatch(base, tv_p); if (res == -1) { event_debug(("%s:dispatch returned unsuccessfully.", func)); retval = -1; goto done; } update_time_cache(base); /将时间缓存更新为当前系统时间/ /检查时间堆上的到期事件并依次执行之/ timeout_process(base); if (N_ACTIVE_CALLBACKS(base)) { /调用event_process_active函数依次处理就绪的信号事件和I/O事件/ int n = event_process_active(base); if ((flags & EVLOOP_ONCE) && N_ACTIVE_CALLBACKS(base) == 0 && n != 0) done = 1; } else if (flags & EVLOOP_NONBLOCK) done = 1; } event_debug(("%s:asked to terminate loop.", func)); done: /事件循环结束,清空时间缓存,并设置停止循环标志/ clear_time_cache(base); base->running_loop = 0; EVBASE_RELEASE_LOCK(base, th_base_lock); return (retval); }

点赞
收藏
评论区
推荐文章
威尔we 威尔we
3年前
Netty 高性能网络协议服务器开发
本文通过一个实例来讲解如何使用框架来开发网络协议服务器,项目使用工具来构建和运行,并且支持部署。项目代码已在GitHub开源,。Netty简介Netty是一个异步、事件驱动的网络应用框架,使用它可以快速开发出可维护良好的、高性能的网络协议服务器。它大幅简化和流程化了网络编程,比如TCP和UDP套接字服务器开发。难能
Wesley13 Wesley13
2年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
2年前
Netty堆外内存泄露排查与总结
导读Netty是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了TCP和UDP套接字服务器等网络编程。Netty底层基于JDK的NIO,我们为什么不直接基于JDK的NIO或者其他NIO框架:1.使用JDK自带的NIO需要了解太多的概念,编程复杂。2
荀勗 荀勗
4个月前
基于C++从0到1手写Linux高性能网络编程框架-15章
参考资料地址1:https://pan.baidu.com/s/1i8FuLluEUV3BJFphjKWvhQ提取码:zvet参考资料地址2:https://pan.baidu.com/s/1MgD4BdeD6V6HfXkoMAZ5Hw提取码:l5t4网络
笑面虎 笑面虎
4个月前
【完结12章】基于C++从0到1手写Linux高性能网络编程框架
【完结12章】基于C从0到1手写Linux高性能网络编程框架分享一套课程——基于C从0到1手写Linux高性能网络编程框架,已完结12章,附源码电子书。大家下载学习。Socket网络编程框架Socket(套接字)是一个网络编程概念,描述了一个通信
赵嬷嬷 赵嬷嬷
4个月前
[完结12章]基于C++从0到1手写Linux高性能网络编程框架(附电子书)
学习地址1:https://pan.baidu.com/s/1AISz1k2uwYAB41St1HxfA提取码:t2gy学习地址2:https://share.weiyun.com/XNELQdHP密码:bdmum6今天我将给大家讲解基于C的Linux
荀勗 荀勗
4个月前
[完结12章+电子书]基于C++从0到1手写Linux高性能网络编程框架
学习地址1:https://pan.baidu.com/s/1yXZMBwdAbtW635Lws9Efiw提取码:erbx学习地址2:https://share.weiyun.com/XNELQdHP密码:bdmum6LinuxSocket网络编程框架主要
光之守卫 光之守卫
2星期前
C++从0实现百万并发Reactor服务器
C从0实现百万并发Reactor服务器download》quangneng.com/4979/一、C从0实现百万并发Reactor服务器实现一个百万并发的Reactor服务器是一个非常复杂的任务,需要深入了解C、网络编程、并发编程以及操作系统等
深度学习与图神经网络研修
深度学习与图神经网络研修时间2022年10月13日—2022年10月17日直播特色:1、采用深入浅出的方法,结合实例并配以大量代码练习,重点讲解深度学习框架模型、科学算法、训练过程技巧。2、能够把握深度学习的技术发展趋势,可以熟练掌握深度学习核心技术、实践技巧,同时针对工作中存在的疑难问题进行分析讲解和专题讨论,有效的提升学员解决复杂问题的能力;3
笑面虎 笑面虎
4个月前
基于C++从0到1手写Linux高性能网络编程框架(2023新课)
基于C从0到1手写Linux高性能网络编程框架(2023新课)分享课程——基于C从0到1手写Linux高性能网络编程框架,2023年新课,附源码电子书,课程包更新。Linux在服务器领域有着强大的优势,网络编程便是其中一项重要组成部分,运用合理的