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

kaidnxhd2023 · · 351 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。
[15章]基于C++从0到1手写Linux高性能网络编程框架 学习地址1:https://pan.baidu.com/s/1AISz1k_2uwYAB41St1HxfA 提取码:t2gy 学习地址2:https://pan.baidu.com/s/1MgD4BdeD6V6HfXkoMAZ5Hw 提取码:l5t4 深度掌握网络编程是逆袭成为高阶开发者的秘密法宝,所以今天给大家深度讲解基于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); }
351 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传