[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);
}
- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码`
- 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传