为什么 HTTPS 是安全的?

技术交流徐凤年 发表了文章 • 0 个评论 • 28 次浏览 • 2020-10-20 16:13 • 来自相关话题

1. HTTP 协议在谈论 HTTPS 协议之前,先来回顾一下 HTTP 协议的概念。1.1 HTTP 协议介绍HTTP 协议是一种基于文本的传输协议,它位于 OSI 网络模型中的应用层。HTTP 协议是通过客户端和服务器的请求应答来进行通讯,目前协议由之前的... ...查看全部

1. HTTP 协议

在谈论 HTTPS 协议之前,先来回顾一下 HTTP 协议的概念。

1.1 HTTP 协议介绍

HTTP 协议是一种基于文本的传输协议,它位于 OSI 网络模型中的应用层

1.png

HTTP 协议是通过客户端和服务器的请求应答来进行通讯,目前协议由之前的 RFC 2616 拆分成立六个单独的协议说明(RFC 7230RFC 7231RFC 7232RFC 7233RFC 7234RFC 7235),通讯报文如下:

  • 请求

POST http://www.baidu.com HTTP/1.1Host: www.baidu.comConnection: keep-aliveContent-Length: 7User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36wd=HTTP
  • 响应

HTTP/1.1 200 OKConnection: Keep-AliveContent-Encoding: gzipContent-Type: text/html;charset=utf-8Date: Thu, 14 Feb 2019 07:23:49 GMTTransfer-Encoding: chunked<html>...</html>

<!--more-->

1.2 HTTP 中间人攻击

HTTP 协议使用起来确实非常的方便,但是它存在一个致命的缺点:不安全

我们知道 HTTP 协议中的报文都是以明文的方式进行传输,不做任何加密,这样会导致什么问题呢?下面来举个例子:

小明在 JAVA 贴吧发帖,内容为我爱JAVA
2.png

被中间人进行攻击,内容修改为我爱PHP
3.png

  1. 小明被群嘲(手动狗头)

可以看到在 HTTP 传输过程中,中间人能看到并且修改 HTTP 通讯中所有的请求和响应内容,所以使用 HTTP 是非常的不安全的。

1.3 防止中间人攻击

这个时候可能就有人想到了,既然内容是明文那我使用对称加密的方式将报文加密这样中间人不就看不到明文了吗,于是如下改造:

双方约定加密方式
4.png

使用 AES 加密报文
5.png

这样看似中间人获取不到明文信息了,但其实在通讯过程中还是会以明文的方式暴露加密方式和秘钥,如果第一次通信被拦截到了,那么秘钥就会泄露给中间人,中间人仍然可以解密后续的通信:

6.png

那么对于这种情况,我们肯定就会考虑能不能将秘钥进行加密不让中间人看到呢?答案是有的,采用非对称加密,我们可以通过 RSA 算法来实现。

在约定加密方式的时候由服务器生成一对公私钥,服务器将公钥返回给客户端,客户端本地生成一串秘钥(AES_KEY)用于对称加密,并通过服务器发送的公钥进行加密得到(AES_KEY_SECRET),之后返回给服务端,服务端通过私钥将客户端发送的AES_KEY_SECRET进行解密得到AEK_KEY,最后客户端和服务器通过AEK_KEY进行报文的加密通讯,改造如下:

7.png

可以看到这种情况下中间人是窃取不到用于AES加密的秘钥,所以对于后续的通讯是肯定无法进行解密了,那么这样做就是绝对安全了吗?

所谓道高一尺魔高一丈,中间人为了对应这种加密方法又想出了一个新的破解方案,既然拿不到AES_KEY,那我就把自己模拟成一个客户端和服务器端的结合体,在用户->中间人的过程中中间人模拟服务器的行为,这样可以拿到用户请求的明文,在中间人->服务器的过程中中间人模拟客户端行为,这样可以拿到服务器响应的明文,以此来进行中间人攻击:

8.png

这一次通信再次被中间人截获,中间人自己也伪造了一对公私钥,并将公钥发送给用户以此来窃取客户端生成的AES_KEY,在拿到AES_KEY之后就能轻松的进行解密了。

中间人这样为所欲为,就没有办法制裁下吗,当然有啊,接下来我们看看 HTTPS 是怎么解决通讯安全问题的。

2. HTTPS 协议

2.1 HTTPS 简介

HTTPS 其实是SSL+HTTP的简称,当然现在SSL基本已经被TLS取代了,不过接下来我们还是统一以SSL作为简称,SSL协议其实不止是应用在HTTP协议上,还在应用在各种应用层协议上,例如:FTPWebSocket

其实SSL协议大致就和上一节非对称加密的性质一样,握手的过程中主要也是为了交换秘钥,然后再通讯过程中使用对称加密进行通讯,大概流程如下:

9.png

这里我只是画了个示意图,其实真正的 SSL 握手会比这个复杂的多,但是性质还是差不多,而且我们这里需要关注的重点在于 HTTPS 是如何防止中间人攻击的。

通过上图可以观察到,服务器是通过 SSL 证书来传递公钥,客户端会对 SSL 证书进行验证,其中证书认证体系就是确保SSL安全的关键,接下来我们就来讲解下CA 认证体系,看看它是如何防止中间人攻击的。

2.2 CA 认证体系

上一节我们看到客户端需要对服务器返回的 SSL 证书进行校验,那么客户端是如何校验服务器 SSL 证书的安全性呢。

权威认证机构
在 CA 认证体系中,所有的证书都是由权威机构来颁发,而权威机构的 CA 证书都是已经在操作系统中内置的,我们把这些证书称之为CA根证书
10.png

  • 签发证书
    我们的应用服务器如果想要使用 SSL 的话,需要通过权威认证机构来签发CA证书,我们将服务器生成的公钥和站点相关信息发送给CA签发机构,再由CA签发机构通过服务器发送的相关信息用CA签发机构进行加签,由此得到我们应用服务器的证书,证书会对应的生成证书内容的签名,并将该签名使用CA签发机构的私钥进行加密得到证书指纹,并且与上级证书生成关系链。

    这里我们把百度的证书下载下来看看:

11.png
11.png

  • 可以看到百度是受信于GlobalSign G2,同样的GlobalSign G2是受信于GlobalSign R1,当客户端(浏览器)做证书校验时,会一级一级的向上做检查,直到最后的根证书,如果没有问题说明服务器证书是可以被信任的。

12.png如何验证服务器证书
那么客户端(浏览器)又是如何对服务器证书做校验的呢,首先会通过层级关系找到上级证书,通过上级证书里的公钥来对服务器的证书指纹进行解密得到签名(sign1),再通过签名算法算出服务器证书的签名(sign2),通过对比sign1sign2,如果相等就说明证书是没有被篡改也不是伪造的。

  • 这里有趣的是,证书校验用的 RSA 是通过私钥加密证书签名,公钥解密来巧妙的验证证书有效性。

这样通过证书的认证体系,我们就可以避免了中间人窃取AES_KEY从而发起拦截和修改 HTTP 通讯的报文。

总结

首先先通过对 HTTP 中间人攻击的来了解到 HTTP 为什么是不安全的,然后再从安全攻防的技术演变一直到 HTTPS 的原理概括,希望能让大家对 HTTPS 有个更深刻的了解。


又来?新增一门新的编程语言,真的学不动了

技术交流王叫兽 发表了文章 • 0 个评论 • 23 次浏览 • 2020-10-20 16:13 • 来自相关话题

来自| 开源中国 编辑 | 可可经万维网联盟 W3C 认证的 Web 语言有 HTML、CSS 与 JavaScript,而近日该联盟正式宣布 WebAssembly 核心规范(WebAssembly Core Specifica... ...查看全部

来自| 开源中国 编辑 | 可可


经万维网联盟 W3C 认证的 Web 语言有 HTML、CSS 与 JavaScript,而近日该联盟正式宣布 WebAssembly 核心规范(WebAssembly Core Specification)成为官方 Web 标准,这意味着 WebAssembly 成为了第 4 种 Web 语言。
微信图片_20201020160807.jpg
WebAssembly 简称 WASM,它是为基于栈的虚拟机设计的二进制指令格式,WASM 作为可移植目标,用于编译高级语言(如 C/C++/Rust),从而可以在 Web 上部署高性能客户端和服务器应用,同时它也可以在许多其它环境中使用。
WebAssembly 描述了一种内存安全的沙箱执行环境,该环境甚至可以在现有 JavaScript 虚拟机内部实现。当嵌入到 Web 中时,WebAssembly 将强制执行浏览器的同源和权限安全策略。
WASM 有多种实现,包括浏览器和独立系统,它可以用于视频和音频编解码器、图形和 3D、多媒体和游戏、密码计算或便携式语言实现等应用。
目前 1.0 版本的 Wasm 已经支持 Chrome、Firefox、Safari 与 Edge 浏览器。
对于 Web 来说,因为其虚拟指令集设计,WebAssembly 可让加载的页面以本地编译代码运行,从而可以提高 Web 性能。
换句话说,WebAssembly 可以实现接近本地的性能,并且优化加载时间,同时最重要的是,它可以作为现有代码库的编译目标。
尽管本地类型数量很少,但相对于 JavaScript 而言,性能的提高大部分归功于其对一致类型的使用。WebAssembly 对编译语言进行了数十年的优化,其字节代码针对紧凑性和流传输进行了优化。在下载其它代码时,网页便可以开始执行。网络和 API 访问通过附带的 JavaScript 库进行,安全模型则与 JavaScript 相同。
WebAssembly(简称 Wasm)是一种为栈式虚拟机设计的二进制指令集。Wasm 被设计为可供类似C/C++/Rust等高级语言的平台编译目标,最初设计目的是解决 JavaScript 的性能问题。Wasm 是由 W3C 牵头正在推进的 Web 标准,并得到了谷歌、微软和 Mozilla 等浏览器厂商的支持。
Wasm 具有运行高效、内存安全、无未定义行为和平台独立等特点,经过了编译器和标准化团队多年耕耘,目前已经有了成熟的社区。

设计目标

Ontology 目前支持的 NeoVM,具有简单轻量的特点,内置了整数、字节、结构、数组和字典等丰富的类型,由宿主完成数据的内存分配管理工作,因此很多功能可以通过少量的字节码完成。目前,很多的实用功能借助于原生合约实现,以系统调用的方式提供。

Runtime API 设计

Wasm 以模块的形式组织,模块内部主要包括类型定义、函数、全局变量、内存段、表和导入导出项。我们提供 Runtime 原生模块作为 Wasm 虚拟机和链交互的桥梁,在虚拟机启动时会默认加载该 Runtime 模块,供 Wasm 合约导入和调用。
由于 Wasm 只定义了内存块,没有内置内存分配使用的逻辑,所以要么由 Runtime 提供 malloc、free 等内存分配管理 API,要么由合约自身进行管理。经过细致比较分析,Runtime 管理会限制内存分配算法的升级:
由于内置在 Runtime 中,可能会导致硬分叉,比如老版本分配在内存10的位置,新版本分配在20的位置,很可能导致合约执行结果的不一致;
另外如果老版本的分配算法不够优化,导致合约执行时内存不足而执行失败,新的分配算法可能使合约执行成功,也会导致合约执行分叉。
因此将内存交由合约自身管理是一个扩展性更好的选项,同时也简化了 Runtime 的 API 设计。由于内存由合约管理,因此在 Runtime 需要向合约传递数据时需要由合约预先进行内存分配。
由于 Wasm 自身只支持 u32、u64等简单的类型,对于 Runtime 需要向 Wasm 传递复杂的数据结构时,我们定义了 Abi Codec 对数据结构序列化为字节数组的形式,写入 Wasm 内存,然后由用户合约还原出原数据结构。
小结
Ending 定律也称为终结者定律,它是 Ending 在 2016 年 Emscripten 技术交流会上给出的断言:所有可以用 WebAssembly 实现的终将会用 WebAssembly 实现。
WebAssembly,理论上能编译成 LLVM 的语言,都能编程成 WASM,从而在浏览器上运行。(这家伙是跑在浏览器上的汇编语言么?这是 Java Applet 的加强版么?)然后那些没有 Web 版的软件,理论上都能在浏览器上跑了。
Go 语言之1.11版本已支持 WASM。然后写了个 GO 程序可以在浏览器跑了,界面直接使用 HTML 输出。再看看那个只有浏览器的 Chrome OS。以前总是有评论说这系统太超前了,现在终于明白了。
有了 WASM,未来的操作系统真的只需装个浏览器了。

server-api请求 /push 全量推送接口,报500:内部逻辑错误

融云集成admin 回复了问题 • 2 人关注 • 1 个回复 • 23 次浏览 • 2020-10-19 10:19 • 来自相关话题

手把手教学|用代码写一个单机五子棋

技术交流大神庵 发表了文章 • 0 个评论 • 35 次浏览 • 2020-10-14 16:26 • 来自相关话题

这篇文章主要为大家详细介绍了python实现单机五子棋,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下。# 简介这是实验室2018年底招新时的考核题目,使用Python编写一个能够完成基本对战的五子棋游戏。面向新手。程序主要包括两... ...查看全部

这篇文章主要为大家详细介绍了python实现单机五子棋,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下。

# 简介

这是实验室2018年底招新时的考核题目,使用Python编写一个能够完成基本对战的五子棋游戏。面向新手。

程序主要包括两个部分,图形创建与逻辑编写两部分。

程序的运行结果:

640.png

# 样式创建

老规矩,先把用到的包导入进来。

'''
@Auther : gaoxin
@Date : 2019.01.01
@Version : 1.0
'''

from tkinter import *
import math
12345678

然后建立一个样式的类,类名称chessBoard。这里加了很多注释,避免新手看不懂函数的作用,说实话我觉得挺别扭的。

#定义棋盘类
class chessBoard() :
    def __init__(self) :
     #创建一个tk对象,即窗口
        self.window = Tk()
        #窗口命名
        self.window.title("五子棋游戏")
        #定义窗口大小
        self.window.geometry("660x470")
        #定义窗口不可放缩
        self.window.resizable(0,0)
        #定义窗口里的画布
        self.canvas=Canvas(self.window , bg="#EEE8AC" , width=470, height=470)
        #画出画布内容
        self.paint_board()
        #定义画布所在的网格
        self.canvas.grid(row = 0 , column = 0)
    def paint_board(self) :
     #画横线
        for row in range(0,15) :
            if row == 0 or row == 14 :
                self.canvas.create_line(25 , 25+row*30 , 25+14*30 , 25+row*30 , width = 2)
            else :
                self.canvas.create_line(25 , 25+row*30 , 25+14*30 , 25+row*30 , width = 1)
        #画竖线
        for column in range(0,15) :
            if column == 0 or column == 14 :
                self.canvas.create_line(25+column*30 ,25, 25+column*30 , 25+14*30 ,width = 2)
            else :
                self.canvas.create_line(25+column*30 ,25, 25+column*30 , 25+14*30 , width = 1)
        #画圆
        self.canvas.create_oval(112, 112, 118, 118, fill="black")
        self.canvas.create_oval(352, 112, 358, 118, fill="black")
        self.canvas.create_oval(112, 352, 118, 358, fill="black")
        self.canvas.create_oval(232, 232, 238, 238, fill="black")
        self.canvas.create_oval(352, 352, 358, 358, fill="black")
1234567891011121314151617181920212223242526272829303132333435363738394041

# 逻辑编写


这里的主要看每个函数的功能就好了。

#定义五子棋游戏类
#0为黑子 , 1为白子 , 2为空位
class Gobang() :
    #初始化
    def __init__(self) :
        self.board = chessBoard()
        self.game_print = StringVar()
        self.game_print.set("")
        #16*16的二维列表,保证不会out of index
        self.db = [([2] * 16) for i in range(16)]
        #悔棋用的顺序列表
        self.order = []
        #棋子颜色
        self.color_count = 0
        self.color = 'black'
        #清空与赢的初始化,已赢为1,已清空为1
        self.flag_win = 1
        self.flag_empty = 1
        self.options()
    #黑白互换
    def change_color(self) :
        self.color_count = (self.color_count + 1 ) % 2
        if self.color_count == 0 :
            self.color = "black"
        elif self.color_count ==1 :
            self.color = "white"
    #落子
    def chess_moving(self ,event) :
        #不点击“开始”与“清空”无法再次开始落子
        if self.flag_win ==1 or self.flag_empty ==0  :
            return
        #坐标转化为下标
        x,y = event.x-25 , event.y-25
        x = round(x/30)
        y = round(y/30)
        #点击位置没用落子,且没有在棋盘线外,可以落子
        while self.db[y][x] == 2 and self.limit_boarder(y,x):
            self.db[y][x] = self.color_count
            self.order.append(x+15*y)
            self.board.canvas.create_oval(25+30*x-12 , 25+30*y-12 , 25+30*x+12 , 25+30*y+12 , fill = self.color,tags = "chessman")
            if self.game_win(y,x,self.color_count) :
                print(self.color,"获胜")
                self.game_print.set(self.color+"获胜")
            else :
                self.change_color()
                self.game_print.set("请"+self.color+"落子")
    #保证棋子落在棋盘上
    def limit_boarder(self , y , x) :
        if x<0 or x>14 or y<0 or y>14 :
            return False
        else :
            return True
    #计算连子的数目,并返回最大连子数目
    def chessman_count(self , y , x , color_count ) :
        count1,count2,count3,count4 = 1,1,1,1
        #横计算
        for i in range(-1 , -5 , -1) :
            if self.db[y][x+i] == color_count  :
                count1 += 1
            else:
                break
        for i in  range(1 , 5 ,1 ) :
            if self.db[y][x+i] == color_count  :
                count1 += 1
            else:
                break
        #竖计算
        for i in range(-1 , -5 , -1) :
            if self.db[y+i][x] == color_count  :
                count2 += 1
            else:
                break
        for i in  range(1 , 5 ,1 ) :
            if self.db[y+i][x] == color_count  :
                count2 += 1
            else:
                break
        #/计算
        for i in range(-1 , -5 , -1) :
            if self.db[y+i][x+i] == color_count  :
                count3 += 1
            else:
                break
        for i in  range(1 , 5 ,1 ) :
            if self.db[y+i][x+i] == color_count  :
                count3 += 1
            else:
                break
        #\计算
        for i in range(-1 , -5 , -1) :
            if self.db[y+i][x-i] == color_count :
                count4 += 1
            else:
                break
        for i in  range(1 , 5 ,1 ) :
            if self.db[y+i][x-i] == color_count :
                count4 += 1
            else:
                break
        return max(count1 , count2 , count3 , count4)
    #判断输赢
    def game_win(self , y , x , color_count ) :
        if self.chessman_count(y,x,color_count) >= 5 :
            self.flag_win = 1
            self.flag_empty = 0
            return True
        else :
            return False
    #悔棋,清空棋盘,再画剩下的n-1个棋子
    def withdraw(self ) :
        if len(self.order)==0 or self.flag_win == 1:
            return
        self.board.canvas.delete("chessman")
        z = self.order.pop()
        x = z
        y = z//15
        self.db[y][x] = 2
        self.color_count = 1
        for i in self.order :
            ix = i
            iy = i//15
            self.change_color()
            self.board.canvas.create_oval(25+30*ix-12 , 25+30*iy-12 , 25+30*ix+12 , 25+30*iy+12 , fill = self.color,tags = "chessman")
        self.change_color()
        self.game_print.set("请"+self.color+"落子")
    #清空
    def empty_all(self) :
        self.board.canvas.delete("chessman")
        #还原初始化
        self.db = [([2] * 16) for i in range(16)]
        self.order = []
        self.color_count = 0
        self.color = 'black'
        self.flag_win = 1
        self.flag_empty = 1
        self.game_print.set("")
    #将self.flag_win置0才能在棋盘上落子
    def game_start(self) :
        #没有清空棋子不能置0开始
        if self.flag_empty == 0:
            return
        self.flag_win = 0
        self.game_print.set("请"+self.color+"落子")
    def options(self) :
        self.board.canvas.bind("<Button-1>",self.chess_moving)
        Label(self.board.window , textvariable = self.game_print , font = ("Arial", 20) ).place(relx = 0, rely = 0 ,x = 495 , y = 200)
        Button(self.board.window , text= "开始游戏" ,command = self.game_start,width = 13, font = ("Verdana", 12)).place(relx=0, rely=0, x=495, y=15)
        Button(self.board.window , text= "我要悔棋" ,command = self.withdraw,width = 13, font = ("Verdana", 12)).place(relx=0, rely=0, x=495, y=60)
        Button(self.board.window , text= "清空棋局" ,command = self.empty_all,width = 13, font = ("Verdana", 12)).place(relx=0, rely=0, x=495, y=105)
        Button(self.board.window , text= "结束游戏" ,command = self.board.window.destroy,width = 13, font = ("Verdana", 12)).place(relx=0, rely=0, x=495, y=420)
        self.board.window.mainloop()
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171

最后,main函数

if __name__ == "__main__":
    game = Gobang()
1234

将以上的所有程序复制粘贴,即为完整的程序了,可以运行。


最后来一个完整程序,一个一个复制粘贴简直不要太麻烦。

'''
@Auther : gaoxin
@Date : 2019.01.01
@Version : 1.0
'''
from tkinter import *
import math
#定义棋盘类
class chessBoard() :
    def __init__(self) :
        self.window = Tk()
        self.window.title("五子棋游戏")
        self.window.geometry("660x470")
        self.window.resizable(0,0)
        self.canvas=Canvas(self.window , bg="#EEE8AC" , width=470, height=470)
        self.paint_board()
        self.canvas.grid(row = 0 , column = 0)
    def paint_board(self) :
        for row in range(0,15) :
            if row == 0 or row == 14 :
                self.canvas.create_line(25 , 25+row*30 , 25+14*30 , 25+row*30 , width = 2)
            else :
                self.canvas.create_line(25 , 25+row*30 , 25+14*30 , 25+row*30 , width = 1)
        for column in range(0,15) :
            if column == 0 or column == 14 :
                self.canvas.create_line(25+column*30 ,25, 25+column*30 , 25+14*30 ,width = 2)
            else :
                self.canvas.create_line(25+column*30 ,25, 25+column*30 , 25+14*30 , width = 1)
        self.canvas.create_oval(112, 112, 118, 118, fill="black")
        self.canvas.create_oval(352, 112, 358, 118, fill="black")
        self.canvas.create_oval(112, 352, 118, 358, fill="black")
        self.canvas.create_oval(232, 232, 238, 238, fill="black")
        self.canvas.create_oval(352, 352, 358, 358, fill="black")
#定义五子棋游戏类
#0为黑子 , 1为白子 , 2为空位
class Gobang() :
    #初始化
    def __init__(self) :
        self.board = chessBoard()
        self.game_print = StringVar()
        self.game_print.set("")
        #16*16的二维列表,保证不会out of index
        self.db = [([2] * 16) for i in range(16)]
        #悔棋用的顺序列表
        self.order = []
        #棋子颜色
        self.color_count = 0
        self.color = 'black'
        #清空与赢的初始化,已赢为1,已清空为1
        self.flag_win = 1
        self.flag_empty = 1
        self.options()
    #黑白互换
    def change_color(self) :
        self.color_count = (self.color_count + 1 ) % 2
        if self.color_count == 0 :
            self.color = "black"
        elif self.color_count ==1 :
            self.color = "white"
    #落子
    def chess_moving(self ,event) :
        #不点击“开始”与“清空”无法再次开始落子
        if self.flag_win ==1 or self.flag_empty ==0  :
            return
        #坐标转化为下标
        x,y = event.x-25 , event.y-25
        x = round(x/30)
        y = round(y/30)
        #点击位置没用落子,且没有在棋盘线外,可以落子
        while self.db[y][x] == 2 and self.limit_boarder(y,x):
            self.db[y][x] = self.color_count
            self.order.append(x+15*y)
            self.board.canvas.create_oval(25+30*x-12 , 25+30*y-12 , 25+30*x+12 , 25+30*y+12 , fill = self.color,tags = "chessman")
            if self.game_win(y,x,self.color_count) :
                print(self.color,"获胜")
                self.game_print.set(self.color+"获胜")
            else :
                self.change_color()
                self.game_print.set("请"+self.color+"落子")
    #保证棋子落在棋盘上
    def limit_boarder(self , y , x) :
        if x<0 or x>14 or y<0 or y>14 :
            return False
        else :
            return True
    #计算连子的数目,并返回最大连子数目
    def chessman_count(self , y , x , color_count ) :
        count1,count2,count3,count4 = 1,1,1,1
        #横计算
        for i in range(-1 , -5 , -1) :
            if self.db[y][x+i] == color_count  :
                count1 += 1
            else:
                break
        for i in  range(1 , 5 ,1 ) :
            if self.db[y][x+i] == color_count  :
                count1 += 1
            else:
                break
        #竖计算
        for i in range(-1 , -5 , -1) :
            if self.db[y+i][x] == color_count  :
                count2 += 1
            else:
                break
        for i in  range(1 , 5 ,1 ) :
            if self.db[y+i][x] == color_count  :
                count2 += 1
            else:
                break
        #/计算
        for i in range(-1 , -5 , -1) :
            if self.db[y+i][x+i] == color_count  :
                count3 += 1
            else:
                break
        for i in  range(1 , 5 ,1 ) :
            if self.db[y+i][x+i] == color_count  :
                count3 += 1
            else:
                break
        #\计算
        for i in range(-1 , -5 , -1) :
            if self.db[y+i][x-i] == color_count :
                count4 += 1
            else:
                break
        for i in  range(1 , 5 ,1 ) :
            if self.db[y+i][x-i] == color_count :
                count4 += 1
            else:
                break
        return max(count1 , count2 , count3 , count4)
    #判断输赢
    def game_win(self , y , x , color_count ) :
        if self.chessman_count(y,x,color_count) >= 5 :
            self.flag_win = 1
            self.flag_empty = 0
            return True
        else :
            return False
    #悔棋,清空棋盘,再画剩下的n-1个棋子
    def withdraw(self ) :
        if len(self.order)==0 or self.flag_win == 1:
            return
        self.board.canvas.delete("chessman")
        z = self.order.pop()
        x = z
        y = z//15
        self.db[y][x] = 2
        self.color_count = 1
        for i in self.order :
            ix = i
            iy = i//15
            self.change_color()
            self.board.canvas.create_oval(25+30*ix-12 , 25+30*iy-12 , 25+30*ix+12 , 25+30*iy+12 , fill = self.color,tags = "chessman")
        self.change_color()
        self.game_print.set("请"+self.color+"落子")
    #清空
    def empty_all(self) :
        self.board.canvas.delete("chessman")
        #还原初始化
        self.db = [([2] * 16) for i in range(16)]
        self.order = []
        self.color_count = 0
        self.color = 'black'
        self.flag_win = 1
        self.flag_empty = 1
        self.game_print.set("")
    #将self.flag_win置0才能在棋盘上落子
    def game_start(self) :
        #没有清空棋子不能置0开始
        if self.flag_empty == 0:
            return
        self.flag_win = 0
        self.game_print.set("请"+self.color+"落子")
    def options(self) :
        self.board.canvas.bind("<Button-1>",self.chess_moving)
        Label(self.board.window , textvariable = self.game_print , font = ("Arial", 20) ).place(relx = 0, rely = 0 ,x = 495 , y = 200)
        Button(self.board.window , text= "开始游戏" ,command = self.game_start,width = 13, font = ("Verdana", 12)).place(relx=0, rely=0, x=495, y=15)
        Button(self.board.window , text= "我要悔棋" ,command = self.withdraw,width = 13, font = ("Verdana", 12)).place(relx=0, rely=0, x=495, y=60)
        Button(self.board.window , text= "清空棋局" ,command = self.empty_all,width = 13, font = ("Verdana", 12)).place(relx=0, rely=0, x=495, y=105)
        Button(self.board.window , text= "结束游戏" ,command = self.board.window.destroy,width = 13, font = ("Verdana", 12)).place(relx=0, rely=0, x=495, y=420)
        self.board.window.mainloop()
if __name__ == "__main__":
    game = Gobang()

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

来源:https://blog.csdn.net/gaosanjin/article/details/108244164


2020年Java语言发展现状

技术交流王叫兽 发表了文章 • 0 个评论 • 52 次浏览 • 2020-10-13 10:31 • 来自相关话题

作者 | Valeriia Karpenko译者 | 刘雅梦策划 | 陈思今年,Java 到达了一个不可思议的里程碑,达到了 25 岁的高龄。我们通过举办一个特别的在线 Java 日活动来庆祝它的诞生,在该活动中,许多专家演讲者分... ...查看全部
作者 | Valeriia Karpenko


译者 | 刘雅梦
策划 | 陈思

今年,Java 到达了一个不可思议的里程碑,达到了 25 岁的高龄。我们通过举办一个特别的在线 Java 日活动来庆祝它的诞生,在该活动中,许多专家演讲者分享了他们的经验,并就如何从这门语言中获得更多收益提供了技巧和窍门。

这引起了我们的思考,我们决定对数据进行深入研究,以彻底发现 Java 的普遍状态是什么,并帮助你回答一些亟待解决的问题。我们发现的某些内容可能是不足为奇的,但也有些见解是令人非常意想不到的。

随着 Java 15 在本周的发布,我们决定把它放在一起,向你展示 Java 的状态。这篇文章是根据不同来源的数据而创建的,其中包括我们的开发人员倡导者 Trisha Gee 的专家评论。

Java 开发者有多少,他们分布在哪里?

第一个问题是:“大多数的 Java 开发人员都在哪里,我们中有多少人是 Java 开发人员呢?”我们通过综合所能获取到的最准确的信息来回答这个问题,然后进行推断,得出一个我们认为非常合理的猜测。

市场研究和分析团队根据开发人员估算模型得出的最佳估计显示,当今世界上有大约 520 万专业 Java 开发人员,他们将 Java 作为主要语言。但是,如果我们将主要使用其他编程语言但同时也做一些 Java 工作的专业开发人员也包含在内的话,这个数字可能 接近 680 万。

至于这些 Java 开发人员都集中在什么地方,在亚洲居住的 Java 开发人员数量最多,那里大约有 250 万开发人员使用 Java 作为主要语言。北美和欧洲的数字远不及亚洲。你可能会问:“为什么呢?”好吧,起初我们有也有同样的想法,因此我们对这些区域进行了更深入的研究,以确切地了解这些数字的来源。


1.png哪个国家的 Java 开发者最多?

我们进一步研究了拥有最多 Java 开发人员的各个国家,然后调查了为什么这些国家在专业开发中特别喜欢 Java 而不是其他语言。

下图显示了每个国家使用 Java 作为主要语言的开发人员的百分比(用于收集此数据的调查对象最多可以选择 3 种主要语言)。中国韩国 的数值最高,分别约为 51%和 50%。数据来自 2020 年开发者生态系统状况调查

专家分析

Java 在前 6 个国家如此流行的原因可能包括 Java 是免费使用的、政府支持和开源。对于 中国、西班牙巴西 尤其是这样。它是在 中国和印度 进行 Android 移动开发的基础,并且雇佣海外人员用 Java 开发手机应用程序非常普遍,这可能是 印度 使用量达到峰值的原因。德国 的使用率也很高,这可以归因于 Java 在 德国 软件工程师中是最流行的语言,因为多种行业都使用 Java 来构建高度可扩展的应用程序。大多数企业服务都依靠 Java 来驱动应用程序支持日常业务的运行,例如工资单、库存管理、报表等。德国还有一个庞大的金融部门,在本国技术上大量使用 Java,例如交易机器人、零售银行系统以及金融业为了保持竞争所需要的其他应用程序。

我们本以为美国会有很高比例的 Java 用户,但是并没有,这也是很合理的。有大量的技术栈可供选择,而且很多技术公司通常处于这些技术栈的最前沿,因此那里的开发人员可能不需要 Java 的强大功能或稳定性,而是使用能让他们进行快速构建和测试等的语言。

2.pngJava 在开发行业中的位置

根据 2020 年开发者生态系统状况调查,有超过三分之一的专业开发人员将 Java 用作主要语言,而 Java 在专业开发人员当中仍然是仅次于 JavaScript 的第二大主要语言。

专家分析

看到 JavaScript 和 Java 处于领先地位,这并不奇怪,因为它们是成对的:使用 Java 的开发人员经常使用 JavaScript 编写前端和任意快速脚本。由于机器学习的普及,Python 可能排名第三。一般来说,我们希望 Web 成为开发人员生态系统的重要组成部分,因此 JavaScript、HTML 和 CSS 以及 PHP 将始终能拥有稳固的地位。SQL 也会一直存在,因为没有多少东西是不需要某种容量的数据库的。C++ 也是一种坚实的语言,因为它被用于许多嵌入式应用程序中,因此它不会很快在图表中消失。虽然 C# 似乎正在逐渐衰落,但我想如果 Java 比例高,C# 就会低,因为它们在功能上非常相似。至于为什么我认为 Java 在专业发展领域如此之高,原因与之前提到的德国类似。大多数企业的业务服务都依靠 Java 来运行。它不仅仅用于 IT 部门,几乎每个公司,无论是分销、制造还是银行业,都将 IT 服务作为其基础设施的一部分,而这些服务,如工资单或库存管理,通常都是在后端使用 Java 构建的。因此,Java 被这些公司的专业开发人员所大量使用。

3.png用 Java 开发的软件类型

快速浏览一下用 Java 开发的软件类型,应该可以了解它的使用统计信息。根据2020 年开发者生态系统状况调查的结果,Java 使用最流行的领域是 Web 服务,占 52%。

专家分析

看到 Java 在商业智能 / 数据科学 / 机器学习中如此流行,真令人惊讶,因为你可能认为这将是 Python 的领域。其他的就不足为奇了,因为 Web 服务的后端通常是 Java,而且使用 Java 编写业务应用程序也很有意义,因为它们也需要使用后端和数据库。

4.png使用 Java 的热门行业

既然我们知道了为什么这么多专业开发人员使用 Java,那么让我们具体看看 Java 应用于哪些行业。

根据 2020 年开发者生态系统状况调查,Java 程序 主要用于 IT 服务(42%)与金融和金融科技领域(44%),但这并不是说 Java 没有用于其他行业。

专家分析

金融和金融科技领域主要涉及金融交易所、零售银行系统、创建计算引擎以及开发本地定制工具和服务,以使公司在市场上具有竞争力。金融和金融科技几乎都是用 Java 建立的,所以在这里没有什么好惊讶的。IT 服务也是如此,因为许多针对非 IT 公司的薪资系统和库存管理服务都是基于 Java 构建的。其他行业也很有趣。由于 Android 的存在,移动开发的比例可能很高,因此 Java 正以这种方式被使用。大数据和数据分析也非常有趣,因为该行业是由 Python 主导的,但是后端可能会使用 Java 和 JVM 语言。当然构建软件开发工具也可以。JetBrains IDE 目前是用 Java 构建的。尽管其他行业有点神秘,但实际上,了解 Java 在这些行业中的使用方式将非常有趣。

5.pngJava 相关工具

Java 版本

Java 8 仍然是最受欢迎的版本。在使用 Java 作为主要语言的专业开发人员当中,有 75%的人使用 Java 8。基于开发人员在 2020 年开发者生态系统状况调查 中选择的几个版本,下图显示了 Java 版本的分布情况。

专家分析

有几个因素导致了 Java 8 的如此流行。首先,它拥有典型的 Java 开发人员所需要的所有语言,它具有 lambda 和流,并且它是一个很好的易于使用的版本。另外,人们一直不愿意迁移到 Java9。Java9 引入了一些重大的架构更改,人们担心这些更改会破坏他们用 Java 8 构建的应用程序。最重要的是,Oracle 还推出了每两年发布一次的版本,因此并不是所有版本都是长期受支持的,因此 Java 9、Java 10、Java 12 和 Java 13 仅受 6 个月的支持,这可能就是为什么它们都只有这么少用户的原因。Java 13 之所以如此之高,是因为当本调查公布时,它是最新的版本,因此你可以预期,该数字将在几个月后下降。

Java 11 发布于 2018 年,它是长期受支持的最新版本。许多企业仍未迁移到它,因为他们担心超过 Java 9(由于其架构的更改)会破坏一切,而且 Java 11 引入了新的许可和新的订阅,因此它带来了一个新的恐惧:更担心如果使用了错误的版本,以错误的方式使用它,Oracle 会对你进行罚款。许多开发人员没有升级到 Java 11 的最后一个主要因素是,它没有很多令人兴奋的新功能,因此该语言的功能并没有降低升级的风险。Java 17 将是下一个拥有长期支持的版本,并带有许多新功能,但是直接从 Java 8 升级到 Java 17 也会带来一些问题。

我的预测是,我认为下一个长期版本 Java 17 将比上一个 LTS(长期支持版本)Java 11 更受欢迎。不过,作为 Java 17 的准备,这一点我再强调也不为过,建议你先将代码库更新到 Java 11,然后再更新为 Java 17,以避免出现大问题。

6.png流行的应用服务器

在过去的 3 年中,Apache Tomcat 仍然是最受欢迎的应用服务器,而 JBoss EAP 和 WildFly 的使用量却减少了一半。给出的数据来自参加 2018 年 和 2020 年 开发者生态系统调查的所有以 Java 为主要语言的开发人员。

专家分析

Jetty 位居第二,但它确实低得令人惊讶。可能是某些正在使用 Spring Boot 和其他微服务框架的开发人员没有意识到他们到底在使用使用,他们可能在不知不觉中使用了 Tomcat 或 Jetty。

7.png排名前 5 的 Web 框架

在 2018 年,Spring Boot 与 Spring MVC 是一样流行的,到了 2020 年,它变得更加流行。给出的数据来自所有使用 Java 作为主要语言的开发人员。

专家分析

这基本上只是在证实 Spring 拥有市场。几乎可以肯定的是,仍然有人在使用 Struts 1,但它只是用于遗留应用程序。

8.png排名前 5 的分析器

2020 年开发者生态系统状况调查 显示,有 24%的用户使用 VisualVM,而一半的用户则没有使用。给出的数据来自所有使用 Java 作为主要语言的开发人员。

9.png

排名前 5 的 IDE/ 编辑器

2018 年 和 2020 年 的开发者生态系统调查显示,IntelliJ IDEA 的份额从 2018 年的 55%增加到 2020 年的 72%,而其他四个的使用率则有所下降。

专家分析

不过,即使我们对调查结果进行了加权,但我们并不否认这些信息可能会有些偏差,因为这是来自 JetBrains 开发者生态系统状态调查,而 JetBrains 的一个主要产品就是 IntelliJ IDEA。然而,这并不是说这并非完全不合理,就好像我们在其他调查中看到的一样,IntelliJ IDEA 通常是使用最多的 IDE 之一,并且通常拥有约 55-60%的用户份额。VS Code 正在增长,这不是从竞争的角度来看的,而是从缺乏对 IDE 的理解的角度来看的。VS Code 是一个代码编辑器,带有一些你可以在 IDE 中找到的特性,并且可以提供附加功能的扩展。因此,如果人们使用 VS Code 进行开发,则可能意味着开发人员不知道一个功能齐全的 IDE 能给他们什么。在 Web 领域,使用编辑器是可以理解的,因为 Web 开发人员通常使用动态语言,并且经常使用其他工具(例如浏览器插件)来满足他们的需求。但是在 Java 中,特别是在专业的 Java 中,你确实可以从一个与应用程序服务器集成的优秀工具中得到很多东西,你可以真正使用分析、重构等功能。

我们专家的最新消息

我发现有很多对本博客文章数据分析进行删减和编辑的版本,这是我无意间造成的混乱。我想澄清一下我对开发人员和 IDE 评论背后的意图。对我来说,如果开发人员不理解 IntelliJ IDEA 作为一个功能齐全的 IDE 给他们带来了什么,那对我来说就是一个失败,因为这六年来我的工作就是让开发人员了解 IDE(特别是 IntelliJ IDEA)能为你做什么。我强烈地认为,人们不应该因为不了解产品而责备用户或潜在用户。

我个人对 IDE 的观点来自拥有 20 多年 Java 开发经验的 Java 开发人员,他们开发过各种大大小小的 Java 项目。如果没有像 IntelliJ IDEA 这样的 IDE 提供大量的帮助,我无法想象如何创建一个复杂的企业应用程序。我也见过很多开发人员使用 VS Code,并且我完全了解代码编辑器所涵盖的用例。在你的工具箱中总有能容纳多于一种的工具,了解某个工具的优点将有助于我们为正确的工作选择合适的工具。

10.png讨论最多的 Java 工具和其他语言

在 IT 社区中会经常讨论 Java,其中的一个社区就是 Stack Overflow。我们从 “问答”部分 中获取了数据,以找出哪些标签与“ java”最相关。纵轴表示提到 Java 的次数,横轴表示标签出现的总次数。

专家分析

对于那些希望确保自己使用了正确的技术或正寻找适合自己的工具的用户来说,这个图表可能很有用。这些语言很有意思,但这可能是因为人们正在寻找 Java 与其他语言之间的比较。正则表达式是人们苦苦挣扎的一个利基领域,但它能上榜也就不足为奇了。

11.pngJava 社区的热门话题

Java 的讨论

我们分析了 Reddit 上“ java”子版块的帖子,并发现了 Java 用户在 Reddit 上讨论最多的主题。

专家分析

这些正是我期望看到的话题。例如,用 Java 编写代码的人总是会对这种语言是否仍然有需求感兴趣,毕竟,这种语言还在发展。我们刚刚庆祝了 Java 诞生 25 周年,因此人们希望检查它是否过时了,以及它是否仍然有效。特别是,如果他们刚从大学毕业,还不知道他们所学的语言是否能为他们提供工作机会。在容器中部署 Java 是一个非常热门的话题,包括我在内,这是每个人都想知道的东西,但几乎找不到任何相关信息。我对性能优化这个主题并不感到惊讶,尽管我认为这个主题有点多余,因为大多数应用程序实际上并不需要开发人员来进行优化,尽管许多开发人员认为这是一项重要的职业技能。使后端和前端协同工作也非常复杂,我可以想到有很多关于这方面的问题。

12.png

原文链接:

https://blog.jetbrains.com/idea/2020/09/a-picture-of-java-in-2020/



跳来跳去,程序员到底去大公司还是小公司?

技术交流大兴 发表了文章 • 0 个评论 • 58 次浏览 • 2020-10-10 18:30 • 来自相关话题

职场生涯总会面临着选择,尤其对我们这些 IT 人来说,跳槽的频率应该是所有行业中相当大的了。那么我们跳来跳去,究竟该选择什么样的公司 ?大 or 小 。工作三年多了,经历一大一小,最近也面试了不少家公司,形态各异,说说自己的感受想法。在小公司中,给我个人的印象... ...查看全部

untitled.png

职场生涯总会面临着选择,尤其对我们这些 IT 人来说,跳槽的频率应该是所有行业中相当大的了。那么我们跳来跳去,究竟该选择什么样的公司 ?大 or 小 。

工作三年多了,经历一大一小,最近也面试了不少家公司,形态各异,说说自己的感受想法。

在小公司中,给我个人的印象大多数环境都不怎么样。定义下这个小公司规模吧,在几人到几十人吧,反正不会超过100人。

亲历一家小公司, 面试见过数家小型公司。我觉得大概分两种:

1.真正的黑穷丑

入职原因:实在没地方去了,毕业什么也不会,来做苦工吧

缺点:加班是家常便饭、工资少的可怜、福利基本没有,事事都要你干

优点:锻炼你顽强的意志力、培养男人的愤怒血性,当然干的多了能力自然也会有提升,不过如果没有牛人带且自己也不是特强的话,你的视野应该是比较窄的

2.有稳定业务、公司盈利还不错,待遇也可以媲美大公司

缺点:还是个人视野的问题,如果你个人能力很好,不是野心很大,在小公司也不错

优点:至少福利待遇不会差,环境也还可以,公司小自己做的贡献领导会看到,做个2,3年可能就是公司的主干力量了,有成就感。

说说自己的第一家公司,那个小公司

自己毕业时选择的是这家小公司,说选择当时是有对比,而最终决定去这家小公司的原因是他给开了3k的工资,比其他两个相对大的公司多,于是就去了,虽然环境不怎么样,自己也忍了,觉得应该锻炼机会很多吧,有家公司环境着实不错,可是帝都1600的工资实在觉得跌面儿。

正像我说的第2种小公司一样,老板有自己的关系公司每年的盈利还不错,几十人的小公司过的也还算舒服。工资每年都会涨,没用自己提过,基本在一年1k左右,毕业刚去的那年发了5k的年终奖,欣喜的不亦乐乎。

在公司的工作就是做一些小项目,很少加班,开始有人带,而后就是自己做项目,整个项目的方方面面,再后来还要带一些刚毕业的小弟弟,实在不敢以师傅挂名,羞愧不敢当,觉得自己的水平不够,于是考虑到在该公司的状况也就如此了,自己又不是视野很宽知道学什么的主,于是有了离开的打算,想去看看国外的月亮。

没见过国外的月亮,都会觉得外面的月亮是更圆的。尽管工作2年的时候工资翻了一倍,那年的年终奖也拿到2w多的地步,老板器重的情况下,还是选择了了离开。

觉得外面的世界很精彩,自己需要出去看看,就这样离开了,来到了一家规模还比较大的互联网公司,虽然工资只比原来多了1k,还是去了,觉得神秘的大公司应该可以学习到不少的东西吧。

很庆幸,刚毕业没有遇见第一种小公司,一些脏乱差,到处摆满东西的公司在后来的面试时还真是见过几个。

一个插曲:一个什么外包公司要我去面试,进去后一看里面安了很多挡板,临时搭的那种,我以为公司发展过来的小分部,随后了解说这是公司总部,汗颜。

来到了大公司

虽然钱没多挣(也许还不如以前的多),但一下子觉得自己牛逼了不少,因为自己的公司耳熟能详啊,可以和别人吹牛逼,也可以给自己的职业生涯贴贴金。

干净的办公环境,每过一会儿就有人清理的卫生间,正版的操作系统、应用软件,公司项目用的新技术,一切的一切都是新鲜的,就像一个村儿逼来到了城里看见了摩天大厦,豁然开朗。周围人也都是4,5年工作经验的牛人……

也就是半年吧,新鲜感过了,也没什么了。唯一感觉的就是觉得工作无聊,整天维护着那么一个小项目,有时很长时间都不知道做什么,也许是自己的问题吧,但是我确定的是这不是我想要的,跟我想的不一样。

不过在该公司的一年多时间里,技术上有一定的提高、见识也增长了不少,但是更大的变化是自己的思想发生了很大的改变。

以前觉得自己是一个.net程序员,就像园子里曾经有人说,这个叫法很蹩脚很奇怪,确实是这样,我们为什么要把自己定义为一个xx程序员?而我们只是一个程序员啊,写代码的程序员,不管是java、c#、php、python、javascript…. 这是一个重要改变。

一直在想,我追求的所谓大公司,到底追求的是什么?

离开第一家小公司想要追求的东西,想要有人带,有高手指导,而这只不过是自己能力的欠缺与知识获取方面能力的不足罢了。

当我们自己这两方面足够强大的时候,我们就成为了高手,不再需要别人的指导,你追求的大公司也就成为了一个空壳。

所以我觉得,大公司、小公司都无所谓,首先我们要让自己牛逼,或者知道怎样牛逼起来,然后再有施展技能的平台就够了,大、小只是一个壳罢了,问题的根源在于你是否能够牛逼起来!


写给年轻程序员的一些心里话

技术交流大兴 发表了文章 • 0 个评论 • 45 次浏览 • 2020-10-09 18:39 • 来自相关话题

每个人的经历都是不同的,也是无法复制的。不要试图复制某个人的成功,你将徒劳无功。但每个人成长过程中总结出来的经验却是可以供他人借鉴的。下面我把自己这么多年的生活经验总结了几条,分享给各位,希望能给大家带来一些有意义的帮助。1保持自信做任何一件事情之前,你都要对... ...查看全部

09d50ad12ab7fe446af8aeb2a2ab05d0.jpg

每个人的经历都是不同的,也是无法复制的。不要试图复制某个人的成功,你将徒劳无功。但每个人成长过程中总结出来的经验却是可以供他人借鉴的。

下面我把自己这么多年的生活经验总结了几条,分享给各位,希望能给大家带来一些有意义的帮助。

1保持自信

做任何一件事情之前,你都要对自己充满信心。如果对自己不自信的话,就会在潜意识了给自己消极的心理暗示,到最后你都不敢去尝试做这件事。如果连尝试的勇气都没有了,就更谈不上把这件事做成了。

关于自信和不自信给一个人带来的不同影响,我在这里就不废话了,很多心理学的专业资料都有论证,相信各位自己也都有亲身经历。

人的自信心其实是可以培养的,我的方法是,每天结束之后都想想自己做成了哪些事情,哪怕是一些特别微不足道的事情。比如今天上班没迟到;比如今天少抽了一根烟等等。通过这些很小的事情,很容易达成的事情,来慢慢积累自己的信心。

2持续学习

社会总是不断在进步,新的事物和概念总是不断涌现,如果我们不持续学习,最终会被这个飞速前进的时代列车所抛弃。各位可以观察一下自己身边优秀的人,无论年龄大小,无一不在持续的学习。咱老祖宗的古语说得好,活到老学到老。

其实很多程序员都是比较爱学习的,因为总是有新的技术和框架出现,逼着大家去学习。这里我想说的不仅仅是对技术的学习。而是所有的东西,比如新的思维方式,新的认知方式等。

所谓的学习就是认知新观念和新想法的过程。我们遇到新的概念和事物,要勇于去尝试,尝试用新的思维方式去思考问题。不要轻易用已有的思维方式给之前没遇到的事物下结论,这样很容易阻止自己的进步。

这里可以给大家举个我生活中的例子。当年我结婚的时候,去家具城买床,我看中了一款特别厚特别软的床垫,大概不到1W块钱。我老娘就说不要睡软的床垫子,对腰不好。因为他们那个年代的人,都是睡硬床板过来的,所以根本不愿意接受这个新的事物,上来就否定这个东西。

前段时间,老娘来我家里,我让她睡在了那个床上,第二天早上醒来,老娘跟我说,这个床垫子还挺舒服的。

我们在不了解一件事物的时候,千万不要根据已有的思维定式去下判断,要接受新的事物,新的观念,不断更新自己的认知,千万不要让自己变成老顽固。

3学会理财

大部分程序员的工资还是很高的,特别是那些大厂程序员。但是跟很多的年轻程序员聊起来,发现他们其实没有多少存款,甚至根本没有存款。

其实理财根本不能让我们实现财富自由,但是理财可以让自己的生活更有计划,更有保障,给你自己和家人带来更多的安全感。比如这次疫情,很多人都失业或者被降薪了,如果手里没有余粮还是挺狼狈的。

特别是随着年龄的增上,你的责任也会越来越大,比如谈恋爱了,想换个有面子的车子,孩子要上补习班,父母生病了,等等。所以劝各位还是要认真规划自己的收入,无论多少。正所谓你不理财,财不理你。

关于理财,我自己的做法是一部分钱放在银行,一部分钱去买基金理财,还有一部分钱用于日常消费。放在银行里面的钱,相当于自己的粮仓,轻易不会动,是自己的靠山。俗话说的好,家中有粮,心中不慌。

而基金理财,是可以让你的财富升值的;其实基金是广大非专业人士最理想的投资方式,简单易操作,而且风险不会像股票那么高。

关于买基金,建议各位选一些大公司的基金产品,采取定投的方式,一定要长期持有(以年为计量单位),不要一看下跌了,心理就发慌,一发慌就开始卖出,这样做你将成为一名优秀的韭菜。

我看过很多材料和数据,大部分基金从长期来看,平均每年都会有12%的收益,这是什么概念呢,就是你买一万块钱的基金,到年底的时候,会盈利1200块。根据我自己的实际经验也是符合这个说法的。关于具体收益算法,以及为什么会是12%的收益率,这里我就不展开了,毕竟我不是卖理财产品的。

最后在多唠叨几句,投资有风险,要拿自己暂时不用的闲钱去投资。千万不要拿自己的吃饭钱去投资,更不要拿父母的养老钱去投资。你这样做不是投资而是赌博。

4幸福生活

每个人对幸福生活的定义都有各自的标准。我所理解的幸福生活,不是拥有了多少财富,也不是开多么豪华的车子,而是你内心的充实,有时间进行自己的业余爱好,有一起闲聊的好朋友,周末的时候可以陪陪父母和家人。

总之我不建议各位为了获取更多的金钱,而失去太多本来可以体验美好生活的时光,毕竟我们赚钱的目的是为了幸福生活。大家不要舍本逐末,要做好平衡。毕竟中国人最讲究中庸之道!相信各位在生活和工作之间都能做好平衡。

原创:花括号MC(微信公众号:huakuohao-mc)。关注JAVA基础编程及大数据,注重经验分享及个人成长。

外卖骑手的解困之策

技术交流大兴 发表了文章 • 0 个评论 • 50 次浏览 • 2020-09-30 10:06 • 来自相关话题

来源:虎嗅APP作者|王海、孙昊头图|CFP.CN背景本月初,一篇针对“困在系统里”的外卖骑手的新闻报道展现了外卖行业中险象环生的现状,激起社会各界的广泛讨论。面对外卖市场的激烈竞争,平台持续地追求提升效率和降低成本,采用大数据技术和人工智能算法,并在发掘人力... ...查看全部


cf1b9d16fdfaaf516a3cab6bf057c8e9f11f7a15.jpg

来源:虎嗅APP

作者|王海、孙昊头图|CFP.CN


背景

本月初,一篇针对“困在系统里”的外卖骑手的新闻报道展现了外卖行业中险象环生的现状,激起社会各界的广泛讨论。面对外卖市场的激烈竞争,平台持续地追求提升效率和降低成本,采用大数据技术和人工智能算法,并在发掘人力极限的过程中,不断降低送餐时限,使得全行业外卖订单单均配送时长在2019年比3年前减少了10分钟[51] ,显著提升了顾客体验,推动外卖成为了劳动密集型和技术密集型模式结合的代表行业。

然而,由于现有共享经济商业模式中服务提供者与平台并没有正式雇佣关系的特殊性,平台对骑手的权益保障和社会福利的重视程度仍有待提高;在现有的商业逻辑、算法规则和考核制度面前,为了完成更多订单以提高收入,一些骑手违反交通法规甚至冒着生命危险“乘风破浪”。据统计,在2017年,仅上海市就发生涉及快递和外卖行业的各类道路交通事故117起,共造成9人死亡,134人受伤[50] 。

当前,激烈的市场竞争环境正推动外卖平台不断改进算法和骑手绩效考核方式。社会在收获良好顾客体验、较低配送成本和极高配送效率的同时,也付出了骑手权益和行人安全降低的代价。面对此外卖骑手困境,我们将围绕其中的问题根源,从平台设计运营和政府监督监管的视角,系统性地提出一系列可以尝试的改进方案。

作为致力于大数据,运筹学,以及人工智能方法在智慧城市领域应用的学者和研究者,我们期望用更加科学的运营流程和算法逻辑,构筑健康,温暖,高效,可持续并且具有社会责任的外卖生态体系。

我们的方案将从平台设计运营和政府监督监管两方面展开——

在平台设计部分,方案将聚焦骑手激励机制、运营流程和算法,以及供需调节机制等内容;

在政府监管部分,方案将聚焦明晰劳资关系、加强资质审核和运营监督、明确平台责任、以及市场竞争与政府干预等内容。由于时间和篇幅所限,本文并没有列举介绍所有可能的改进方向,仅选择我们认为对解决外卖骑手困境最重要并且具有实际可行性的内容。

针对每项的具体内容,我们在“洞察”部分介绍了相关的经济学和管理学原理,或者相关的数据科学、人工智能和优化算法的可能技术实现路线,以供业内人士和专家学者等参考,读者直接跳过该部分内容不会影响对全文的理解。

平台设计

1、骑手激励机制设计

在围绕外卖骑手困境的讨论中,改善骑手的激励和奖惩机制是大家关注的一个焦点。

在基于人工智能算法的外卖配送系统中,从顾客成功下单的时刻起,该系统便会自动化计算最优的订单分派和骑手配送路线,并且预测订单的“预计送达时间”,然后以此考核骑手的“准点率”;一旦订单配送超时,骑手们将面临降低收入甚至淘汰出局的惩罚。

在此过程中,值得重点关注的是,这套“最优”方案只是在给定的历史数据和预设的模型参数下、通过模拟现实得到的“理想值”或者“乐观值”。因此,面对复杂多变的现实场景,外卖骑手的激励机制需要具备容错性和灵活性,帮助外卖骑手抵抗已知或者未知的市场不确定性带来的负面冲击,降低外卖骑手收入的波动,这不仅有助于提升骑手们的福利水平,也将会增强平台的总体运力。

(1)预计送达时间容错

面对无法避免的外卖订单配送超时风险,平台可以考虑引入“超时容错机制”,订单的“预计送达时间”设置和显示为一定的时间段(例如,“17:20~17:25 到达”),并且根据商家的备餐状态和骑手的配送情况动态调整、提前向顾客提醒可能的超时送达,而不是精确到具体的时刻(例如,“预计17:23到达”)甚至故意将顾客端显示的“预计送达时间”设定的短于骑手端。

洞察:从技术层面而言,即便是采用先进的深度学习算法,模型学习到的结果也只是关于“预计送达时间”(Estimated Time of Arrival,简称 ETA)的统计分布,并不能做到对每个样本的预测值都有绝对准确的“信心”[47] 。ETA 参数的区间估计结果(“17:20~17:25”)会比点估计结果(“17:23”)更加具有可信度,因为后者并不能告诉顾客真实的送达时间与它的距离,而前者则可以反映真实的送达时间所处的大致可信范围(confidence interval)。从管理层面而言,区间形式的“预计送达时间”是应对预测算法的误差和外部配送环境的变化的容错策略,体现的是对骑手和顾客负责任的态度。与此相关,平台还可以根据送达时间的区间设置弹性的实际送达时间考核标准:如果实际送达时间在“预计送达时间”前后N分钟,则该笔订单配送记录判定为“正常”,如果实际送达时间超过“预计送达时间”N分钟,则该笔订单配送记录判定为“超时”而纳入超时评估体系;最后,如果实际送达时间早于“预计送达时间”N分钟,则该笔订单配送记录判定为“快速”而纳入高效奖励体系。类似思想已经被外卖平台的ETA预估算法所采纳[47] ,也可以作为超时容错策略体现在骑手激励机制设计中。

(2)弹性的超时奖惩

围绕超时容错机制,平台可以考虑针对骑手“超时率”划分多个区间段、设计更具有弹性的阶梯式超时奖惩规则:超时率被划分成多个区间段,例如,[0%, 5%]、(5%, 10%] 以及10%以上,如果骑手的超时率处于第一个区间段,则骑手的绩效水平不会受到影响;如果骑手的超时率处于第二个区间段,则骑手的绩效水平将受到适当轻微的负面影响;但是,如果骑手的超时率处于更高的区间段,则骑手的绩效水平将受到显著增强的负面影响。

洞察:当前,骑手们的超时率通常不得高于3%,否则,包括骑手、站长甚至区域经理等所有人都将面临绩效的惩罚[51]。那么,从激励机制设计的本质而言,无论是预计送达时间容错机制还是弹性超时奖惩机制,从数学模型的角度,都是在期望构建基于骑手工作绩效(即超时程度或者“超时率”)的非线性激励机制。类似分段阶梯式的非线性惩罚机制在线上多边平台市场中经常出现,例如,在网约车市场,为了杜绝司机的刷单作弊行为,平台会设计阶梯式作弊违规处罚标准:“第一次禁用3天,第二次禁用15天,第三次解除合作;累计非法获利≥1000元时,解除合作永不录用”[39]。在激励骑手们准时配送以最大化日成交金额(Gross Merchandise Volume,简称 GMV)或者日完单量的问题中,订单的送达状态取决于配送过程中环境的不确定性、骑手们的工作业务水平、以及骑手们对承担风险的态度等多方面因素,有一些因素只有骑手们自己知道,而平台并不能完全掌握。那么,采用非线性激励机制是否一定会提升平台整体的运营效率呢?从管理学理论上讲,基于绩效的激励机制包含激励强度的“斜率”(slope of incentive intensity)和激励的“形状”(shape of incentive contract)两个特征:斜率是指激励强度关于绩效的变化率,例如,超时率增加一个单位将会带来的收入惩罚绝对变化值,而形状是指激励关于绩效水平的非线性(或者称凹凸性)。研究表明,高激励斜率有助于改善绩效水平,而强非线性将会增强个体承担风险的偏好;同时,最新的实证结果显示,激励的斜率会影响个体的风险偏好,而激励的形状也会影响绩效水平[11]。因此,构建基于骑手工作绩效的激励机制需要综合上述多方面因素,合理选择激励关于绩效水平的斜率和形状,这样有助于提升配送系统整体的效率。

(3)骑手配送超时互助保险

为了进一步应对外卖订单配送超时风险,以及降低配送超时对骑手收入造成的激烈波动,平台除了在用户端提供配送超时保险和赔偿之外,还可以考虑尝试建立“骑手配送超时互助保险”机制:该保险可由骑手自主选择是否加入,即骑手在选择接单的同时还可以选择是否愿意为这笔订单缴纳较低金额的超时保险费,这笔费用成为骑手之间互助的保险金额池。如果骑手因为任何原因无法按时送达订单,可由金额池向骑手提供经济补偿以降低其收入波动,金额池内的余额最后都退还给骑手。

洞察:在市场经济条件下,保险是一个管理风险的常见工具,例如,在电子商务平台上,保险公司会向第三方卖家推出以商品买家为被保险人的“退换货运费险”,以便降低可能的退换货运费给买家造成的损失[42]。在外卖行业中,67%的骑手家庭人口在3人~5人,四成骑手的爱人选择在家照顾孩子和老人,同时,五成骑手是家庭收入的主要来源,七成骑手月收入在五千元以下,这些数字意味着大多数骑手正面临着来自自身和家庭的双重消费压力[43]。那么,设立“骑手配送超时互助保险”的核心目的在于希望能够建立大数量骑手们互助的保险金额池,缓解超时对个体骑手带来的负面收入冲击,降低个人收入的波动性,最终发挥平滑骑手收入的作用。研究表明,在面对风险时,低收入劳动者或者家庭通常会基于工作和消费等两类经济行为进行调节:一是平滑收入(income smoothing),例如,通过同时兼任多份工作以提高收入来源的多样性和稳定性(在美团平台,35%的骑手拥有包括小生意或者其它外卖平台在内的其它收入来源[43]);二是平滑消费(consumption smoothing),例如,调整自身的劳动力供给水平和使用保险协议[22]。从这两类经济行为可以看出,“骑手配送超时互助保险”可以有助于骑手抵御超时风险,降低收入的波动性,也将会激励骑手提供更好的运力供给。

(4)考核周期延长

为了进一步帮助骑手降低收入的波动性,平台可以考虑延长对骑手的考核周期,将以较短“时间段”(例如,现在平台的冲单奖励活动和考核时间段一般是一天的某个时段),或者以“天”为单位的奖励或者惩罚考核周期适当延长至“星期”甚至“月”,通过实践寻找出最佳的考核周期,以降低骑手对短期高频考核的焦虑,提高系统的整体效率。

洞察:类似基于时间段的冲单奖励看起来有助于满足高峰期时段的旺盛需求,但是,关于共享经济商业模式的实证研究表明,理性的服务提供者为了尽可能获得活动期的高冲单奖励,可能会选择在活动期开始之前策略性的停止工作,以等待高峰期的到来,这种“前瞻”(forward-looking)行为将会导致平台在高峰期到来之前的运力供给出现下降,给整个系统的运行效率造成负面影响[34]。考核周期的延长将有助于平滑骑手在不同时间段内的平均收入,激励骑手们维持稳定的服务质量水平,保障平台总体运力的平稳运行。那么,平台是否应该尽可能延长或者缩短激励机制中的考核周期呢?最近,一份关于产品销售人员薪酬体系设计的研究表明,销售人员的工作绩效将会显著受到考核周期的影响:当平台将激励机制的考核周期缩短时(比如,从“月”度考核调整为“天”度考核),原本优秀的销售人员就表现出了下降的绩效水平[10]。值得关注的是,已有的行为学实验研究和现实商业现象表明,基于考核目标的高频绩效评估会潜在地增加人员的焦虑心理和工作压力,甚至会因为激发人员过于激进而引发欺诈和不道德行为[23][38]。基于上述结论,并结合现实中“外卖小哥在电梯里急哭了”和逆行违章等现象,我们可以总结到,在外卖市场中,虽然商品性质和商业逻辑并不一样,但平台也需要综合骑手们的配送努力程度,配送能力的差异性,以及他们在进行运力供给决策中的前瞻行为等综合因素,非常谨慎地制定出有助于降低骑手收入不确定性和提升平台整体服务能力的最佳的考核周期。

(5)多层次多属性绩效评估

除了上述直接根据“超时率”和“完单量”评估骑手绩效的指标,平台应该建立基于综合服务质量、配送效率和安全保障等多属性的多层次骑手绩效评估体系。当前,系统为骑手设置的积分等级体系是直接基于完单量、准时率或者顾客评价奖励积分[51],可以进一步提升骑手们的安全保障、违章记录、以及公益活动等有助于建设外卖生态体系的多个属性的重要性。

评估体系可以尝试根据政府部门监管法规和平台发展目标为不同评估指标设定优先级,其中,遵守交通安全法规应该成为外卖配送的基本要求。

洞察:在多层次多属性绩效评估体系的设计过程中,在第一层次,考虑到遵守交通安全法规是外卖配送的基本要求,那么,骑手的“违章率”就需要摆在优先级最高的位置,在一定的考核期内,如果骑手的“违章率”超过了阈值,则该骑手将被“一票否决”,不会进入到下一层次的绩效评估;在第二层,维持并且提升市场占用率可能是当前平台运营的关键目标,那么,骑手们的“完单量”可以摆在优先级次高的位置,在对“完单量”合理划分不同等级的奖励区间之后,各骑手再根据实际“完单量”进入相应的评估区间;最后,在特定的基于“完单量”的评估区间内,机制再综合服务质量和顾客评价等属性设定奖励金额。另外,平台可能需要同时实现“违章率”、“日成交单量”和“超时率”等多个关键绩效的管理目标,对此多属性绩效目标导向的决策问题,平台不仅需要考虑到实际绩效水平和管理目标的不确定性,还需要关注多个绩效指标之间的相关性(例如,严格控制“违章率”会减少当前的“日成交单量”)对绩效评估体系的影响[25]。

(6)多向评分反馈

外卖市场主要是由顾客、平台、商家和骑手共同建立起来的生态圈,当前系统采取的是顾客给商家或者骑手的单向评分方式,忽视了骑手们的重要作用。平台可以建立起“多向”评分反馈系统,其中,顾客给商家和骑手分别评分,顾客对商家的菜品健康和口味等进行反馈,对骑手的派送服务进行反馈;骑手也可以自由选择给商家评分,反馈商家出餐及时性等信息;更进一步地,骑手还可以给顾客评分,反馈订单交付难度和顾客接单态度等信息。

与此同时,平台根据商家、顾客和骑手们的权力和义务,并综合天气、路况和其它不可控因素等,设定公平合理的判责系统。

洞察:在实际商业场景中,例如,网约车市场,不仅乘客可以给司机评分,司机也可以向乘客评分。本质上而言,建立多方评分反馈系统(multi-lateral rating systems)的核心目的是增强在线市场中各参与方之间的信任度[12],降低各参与方之间的信息不对称性[17],从而“驱逐”低质量的参与方,并且降低交易成本。为此,平台需要认真思考两个重要问题:一方面,相比于传统的单向评分反馈系统,多向评分反馈系统是否真的会提升市场效率和社会福利?另一方面,由于参与方做出准确的评分是基于自愿且需要付出成本的,在平台没有提供适当奖励的情况下,作为公共产品的用户评分将会供给不足[2];因此,如果采取多向评分反馈系统,平台应该如何激励尽可能多的参与方真实地披露信息并给出可信的评分结果?关于第一个问题,以共享经济商业模式为背景的理论研究表明,相比于仅有顾客的单向评分,顾客和服务提供者之间的双向评分将弱化服务提供者之间的竞争,从而提高市场的均衡价格[17];另外,在一定条件下,双向的评分机制将会提升社会福利[26]。关于第二个问题,平台需要合理设计向各参与方展示彼此评分结果的方式和时间:如果平台在参与方完成评分之后立刻披露结果,那么,相关的被评价方可能在后续采取报复动作,故意压低对方的评分,从而导致顾客评分的“失真”[6];但是,如果平台在双方或者多方都完成评分或者允许评分的时间窗结束之后才披露结果,那么,这种策略将会显著降低评分者进行报复的可能性,并且增加评分的真实性和供给量[13]。

2、运营流程和算法

以上从骑手激励机制方面介绍了外卖平台设计的改进方向,本节将聚焦运营流程和算法设计等社会各界正在热烈讨论的话题。算法本身是中性的,而其蕴含的思想和流程则是由平台的商业逻辑和商业目标决定的,在平台设计中,平台可以进一步让运营流程和算法的目标或者逻辑更加兼顾顾客、骑手和平台等多参与方的目标,提升社会的整体福利水平。

为此,根据通常的外卖配送流程,我们分别提出针对顾客端、骑手端和平台端的运营和算法改进方案,包括餐馆推荐系统、预计送达时间预测算法、派单算法、路径规划算法、算法参数管理、以及基础设施建设等六点建议。

(1)顾客端:餐馆推荐系统

平台可以在实时餐馆推荐系统的设计中采用更多与应用场景相关、反映实时供需情况和骑手空间分布的数据。例如,针对一个商家,如果过去 1个小时的骑手等待时间较短、当前周边分布的骑手较多、店内聚集的正在等待取餐的骑手较少、或者正在准备的订单数量较少等,在其它属性相近的条件下,那么,该商家在推荐排序算法的输出结果中可以被优先推荐。

推荐系统还可以根据实时已有订单的餐馆和骑手位置进行拼单推荐,实现外卖的“顺风车”。例如,如果一位骑手正在顾客周边的商家等待取单,而且预估取餐时间较长,那么,即使该商家不是距离顾客最近或者价格最低,推荐系统也可以在一定程度上提高该商家的推荐权重和优先级。

洞察:在当前实现的推荐系统中,算法输入的应用场景数据至少由三大类组成[44]:一是用户画像,例如,性别、常驻地、价格偏好、食物偏好等;二是食物画像,包含商家、外卖、团单(即团购订单),其中,商家特征包含商家价格、商家好评数、商家地理位置等,外卖特征包含平均价格、配送时间和销量等,团单特征包含适用人数和返购率等;三是场景画像,包含用户当前所在地、时间、定位附近商圈、基于用户的上下文场景信息等。如果基于上述方案改进餐馆推荐系统,系统可以采用基于在线优化(online optimization)和增强学习(reinforcement learning)的算法,实时更新客户端的餐馆推荐结果,最终将餐馆推荐系统打造成调节市场供需平衡状态的重要工具之一。另外,关于实现外卖的“顺风车”,类似思想已经在网约车市场中实现,目的是希望通过将目的地相近或者顺路的订单整合在一起,从而提升配送资源的利用率,其中,需要同时解决订单与骑手的双边匹配问题、以及针对“顺风车”订单的配送费定价问题[19][17]。

(2)顾客端:预计送达时间预测算法

预计送达时间预测算法可以融合来自骑手的手机 GPS 实时定位数据、手机运动传感器移动数据、和安卓操作系统特定应用程序搜集的活动识别数据等多源异质数据集[28],识别骑手在不同时刻所处的活动状态以及状态改变的时间点。这将有助于提升订单交付时间的预测准确性,尤其是和楼层高度、小区内配送和顾客交付相关的时间。同时,为了明确商家和骑手的责任,并且方便顾客对商家和骑手进行公平的评分,平台应该分别预测、显示和评估“商家出餐时间”和“骑手送餐时间”。

洞察:在外卖配送场景中,预计送达时间(ETA)是用户成功下单时刻到骑手将外卖送达到顾客手中的送达时间预测结果,具体可以分解为压单时间(从商家接单到骑手接单)、到店时间(从骑手接单到骑手到店)、取餐时间(从骑手到店到骑手取餐)、送餐时间(从骑手取餐到到达用户)、以及交付时间(从到达用户到完成送达),此过程还包含了出餐时间(从商家接单到商家出餐)[45]。关于骑手送餐时间,一个最大的技术性挑战,也是目前网友们激烈讨论的实际问题是对交付时间的预估,即骑手到达用户附近下车后多久能送到用户手中:一方面,老旧小区没有电梯、或者写字楼难以等到电梯等现实问题给骑手们快速交付订单带来困难;另一方面,在进行交付时间预估时,算法的输入字段较少,重要的维度特征仅包括交付地址(文本数据)、交付点的经纬度、区域、以及城市[48]。对此,如果基于手机GPS实时定位数据、手机运动传感器移动数据(motion sensor data)、以及安卓操作系统ActivityRecognitionClient API搜集的活动识别数据(activity recognition data)等改进ETA预测算法,平台将更清晰地识别骑手运动状态,例如,电动车骑行中、步行中、奔跑中或者原地等待中等,然后采用深度学习方法进行序列建模(sequence modeling),实现对ETA尤其是交付时间的更准确地预测。

(3)骑手端:派单算法

在派单过程中,派单算法应当考虑骑手之间的订单负载均衡,让不同骑手当前累积分配到的订单数量相对比较平均,避免出现个别骑手承载过大的配送任务、而有些骑手被闲置的局面。同时,派单算法还应当考虑骑手在当地区域的熟知程度和配送经验等有助于提升派单效率的多种因素。

洞察:实时派单算法是智能配送系统中的重要组成部分,当前的实时派单问题被描述为以离散马尔可夫决策过程(Markov Decision Process)为核心的动态随机优化问题,其目标是一段时间内的顾客体验(例如,准时率)和骑手效率(例如,单均行驶距离或者单均消耗时间)等指标最优,算法需要计算动态到达的订单分配给骑手的策略、以及每个骑手后续的节点访问顺序(即路径规划,routing optimization)[49]。在对该问题进行合理建模和算法优化之后,派单算法可以实现骑手订单负载均衡(workload balance)和融合骑手当地区域知识和配送经验。对此,建模人员可以通过在目标函数中引入“最大化所有骑手的最小订单负载”(maxmin)或者“最小化所有骑手的最大订单负载”(minmax)的方式进行调整。另外,派单算法引入骑手在当地区域的熟知程度和配送经验等因素,这不仅意味着 ETA 的预测算法需要纳入骑手在当地区域的熟知程度和配送经验等重要指标,也意味着派单算法需要考虑骑手们之间在这两个维度上的差异性。综合以上因素,由多目标优化算法(multi-objective optimization)给出满足多个目标的最佳派单结果[21]。

(4)骑手端:路径规划算法

与派单算法紧密关联的是路径规划算法,算法应该引入与实际路况更为贴切的特征,例如,单行道、限行、机动车道和非机动车道,以及交通管制和交通拥堵等,并且根据实时信息进行调整。

洞察:针对骑手路径规划的问题,平台需要建立基于有向图的实时动态路径规划模型,输入该模型的可行路径集合(set of feasible routes)需要根据离线信息进行缩小[33],例如,许多单行道、限行路线和过街天桥不允许电动车经过的路线就应该作为强约束从集合中剔除;另外,该集合还需要根据实时信息进行调整,例如,出现严重交通拥堵或者临时交通管制的路线也应该被剔除。同时,面对随机的订单需求和配送时长,该问题可以考虑借鉴离线-在线近似动态规划算法[30] (offline-online approximate dynamic programming)或者在线再优化策略[5](online re-optimization strategy)的思想进行求解。

(5)平台端:算法参数管理

平台端对算法参数进行及时审核并且合理设置是解决外卖骑手困境的关键之一,在此应该得到重点关注。这个问题起源于平台在预测订单“预计送达时间”探索实践中的模型迭代过程:在实际的ETA预估场景下,算法的损失函数设计是以“整体的预估结果能够尽量前倾”为目的,而且对于迟到部分会增加数值惩罚[47],这意味着算法在不断“逼迫”骑手缩短实际送达时间,而骑手每一次成功避免超时的历史记录都会让算法“学习”到可能更短的送达时间,即便这个送达时间是骑手通过闯红灯、逆行等违反交通规则甚至冒着生命危险的方式实现的。基于此逻辑,这些历史数据会进一步提高算法对骑手送达时间的“期待”,从而使算法朝着缩短送达时间的方向进行要求和优化。

对此,平台应该通过对实际送达时间等算法的参数进行及时审核和调整以终止上述的恶性循环,也就是赋予算法“底线思维”。遵守交通法规和维护行人安全是不可逾越的底线,是数据预处理环节进行历史数据清洗和校正必须考量的因素,是数学模型中的“硬约束条件”,也是优化算法剔除不可行路径中必须满足的规则。这也意味着订单送达时间应该存在着一个合理的、无法通过算法不断优化而逾越的下界,否则,不管在任何激励机制和评分体系之下,缺乏“底线思维”的算法流程会一直将骑手困在系统之中。

(6)平台端:基础设施建设

针对最后 100米配送问题,平台可以尝试自建或者联合第三方物流公司建设外卖取餐柜[52]。取餐柜的候选位置可以是办公场所、写字楼、医院以及高校,而顾客可以选择线上下单、线下取餐。

洞察:考虑到取餐柜的选址将会影响顾客线上下单、线下取餐(buy-online-pick-up-in-facility)的便利性,平台可以进行两阶段的取餐柜网络设计优化:在第一阶段,平台可以结合各个区域的人口社会统计、经济发展、历史完单、以及配送时长等多维度数据建立机器学习模型和计量经济学模型以预测各区域的潜在需求和建立取餐柜对需求的影响[15];在第二阶段,平台可以建立取餐柜设施选址(facility location optimization)和骑手服务区划分(service region allocation optimization)的多阶段随机规划模型(multi-stage stochastic programming),联合优化取餐柜的位置和骑手的派单服务策略。

除此之外,在需求量较大的办公楼、小区、学校和医院等场所,平台也可以考虑配备专职的终端派送人员,一方面是因为终端配送人员对小区周边和电梯设备更加熟悉,可以帮助降低因骑手对环境陌生而造成的顾客等待时间,另一方面是能够实现对局部区域的订单统一管理和配送、从而避免骑手的重复劳动。

洞察:配备专职终端人员在提升平台整体运营效率的同时也会增加平台的运营成本,需要进行深入的成本-效益分析。分析框架可以将配送过程视为一个排队系统 (queueing system)[32],在指定场所配备专职的终端配送人员将会增加固定人力成本,但是,值得关注的是,根据排队论,配送骑手的每单的平均派送时间和顾客的等待时间等系统的服务质量指标之间存在着强烈非线形的关系,这意味着通过配备专职终端人员以适当降低骑手们在这些场所所花的派送时间可能会带来系统服务质量指标的显著提高,从而使得带来的配送效益可能会高于额外的人力成本。

在未来,平台还可以推广使用机器人和无人机配送。同时,作为互联网公司,平台可以通过在相关核心技术的资本投入和技术积累,转型成为高科技公司。实际上,美团已经开始尝试无人车和无人机的配送:在 2月份疫情期间,外卖平台利用无人配送车为北京市顺义区几个小区的居民做订单配送,截至 9 月初,平台已经累计使用无人车配送了超过6000多用户实际订单,覆盖该站点超过80%的订单需求;目前,平台也在深圳等地进行无人机的运营测试[53]。

洞察:在设计无人车辅助的配送系统中,面对无人车配送和骑手配送的两种模式,考虑到无人车的在成本方面的优势和在灵活性方面的不足,平台可以基于实际配送场景、市场供需状态、以及交通路况,采用马尔可夫决策过程联合优化无人车和骑手之间的分配比例、以及无人车和骑手的配送路线[29]。

3。 供需调节机制

在实际的运营中,外卖平台可以尝试对配送价格进行调整以调节市场供需。平台可以根据配送距离和配送时段等诸多因素合理设计基础的派送价格和骑手端补贴;另外,平台也可以针对突变的供需情况,实时调整骑手的配送费用来缓解供需不平衡的问题[46][36]。

其实,作为企业收益管理的重要工具,动态定价已经在多个行业得到广泛使用,包括在上个世纪 80 年代开始得到采纳的航空业、90 年代开始得到采纳的酒店业和租车业[31]、以及当前新兴的共享出行行业[3]。

在外卖行业,平台面临的市场供需不协调的问题更加突出,而且市场供需状态随时间变化剧烈。采取不同形式的配送价格可以区分顾客对等待时间的实际需求和时间敏感性,这对提高和使用供需弹性,缓解供不应求带来的负面影响具有一定积极作用;然而,基于配送价格的动态定价机制并不能解决所有问题,无论是在现实生活还是学术研究中仍然存在争议,这依赖于平台对该策略的价值进行更加深入的探究。

(1)基于区域和时间段的时空动态定价

在调节市场供需平衡状态的过程中,平台可以针对每笔订单的配送费实行基于配送区域、下单时间或者送达时间的动态定价,并且对愿意等待的顾客提供愿等打折。

洞察:基于区域和时间段的时空动态定价(dynamic & surging pricing)通常可以分为“乘积溢价”(multiplicative surge)和“加和溢价”(additive surge)两类:在特定区域和时间段内,前者是指在基础价格上乘以一定倍数(例如,1.5倍),美国的网约车平台Uber早期就是使用该类溢价策略;而后者是指在基础价格上加上与距离无关的常数(例如,10元),这是 Uber 平台当前采用的最新溢价策略。研究表明,在供需动态变化的环境下,相比于乘积溢价策略,加和溢价策略是激励相容 (incentive-compatible) 的定价机制[14],这为外卖平台设计基于配送费的实时动态定价策略提供了新的方向。值得提醒的是,实时动态定价策略在社会各界中仍然存在争议,理论研究结果显示,在共享出行市场的背景下,理性的乘客和司机会策略性地等待更合适的价格或者收入,如果市场状态比较平稳,那么,平台没有必要采用实时动态定价策略,尤其是该策略可能造成乘客、司机、监管者之间对立的局面[9]。

(2) 顾客灵活充值账户

为了弥补实时动态定价在区分顾客对等待时间的实际需求和时间敏感性方面的不足,平台可以建立灵活的用户充值账户:在高峰期时段,如果需要外卖尽快送到,顾客可以向个人在平台上的账户充入额外的金额,这些资金并不会流入骑手或者平台,而是作为用户的充值余额,可以在低峰期时段点餐使用。

洞察:顾客在高峰时段的充值行为将传递出其对派送的等待时间比较敏感的信号,作为系统派单算法的依据,并且,顾客在低峰期使用账户余额订餐,可以激发低峰时段的整体需求,充分利用低峰期时段闲散的备餐和配送资源,发挥平滑市场需求的作用。针对网约车市场的研究结果表明,在一定条件下,合理设计用户充值账户机制 (integrated reward scheme with surge pricing) 可以实现多方共赢,提升顾客、司机和平台的整体社会福利[37]。

(3)增加兼职和众包骑手

平台提升兼职和众包骑手的运力占比有助于提高运力调整的空间和弹性,从而更有效的调节市场的供需平衡状态。

洞察:平台的骑手分为专送全职骑手和众包兼职骑手,前者的工作时间固定并且接受系统派单,后者灵活决定工作时间并且可以有限次拒绝派单[51]。为了提升整体的运力供给,尤其是满足高峰期时段的配送需求,平台通常会采用现金奖励的方式补贴骑手。实证研究表明,在网约车市场中,平台补贴将会从劳动者是否选择工作和工作时长两个维度影响劳动力供给,而且,兼职劳动者的收入供给弹性 (supply elasticity) 更高[24]。这意味着外卖平台需要更加严密地研究骑手们的运力供给行为,分析骑手们对期望收入水平发生变化而进行的运力供给调整,然后合理设计平台上全职骑手和兼职骑手的比例,并精准地向骑手、尤其是兼职骑手提供补贴奖励。

4。 政府监管

精准的政府监管对外卖行业实现可持续健康发展至关重要。深入而全面地理解平台的商业逻辑和运营流程有助于制定高效精准的监管措施,这不仅依赖于平台向监管者真实而全面地披露公司运营相关的信息,也依赖于监管部门联合第三方专业研究机构共同完成对信息的分析和总结[8]。基于全面而系统的专业分析之后,监管者再对平台和骑手的劳资关系、运营过程中的资质审核和监管、以及平台责任以法律法规形式进行统一规范。

(1)明晰劳资关系

相关部门需要加快出台法律法规,明晰包括外卖骑手在内的自由职业者与共享经济/零工经济平台之间的劳资关系,对全职骑手与兼职骑手的法律地位进行清晰的分类,界定相应的权利和义务[16]。

洞察:针对相关的劳资关系,国家发展改革委于2020年7月14日发布文件要求,强化灵活就业劳动权益保障,探索适应跨平台、多雇主间灵活就业的权益保障、社会保障等政策[40]。作为参考,2019年9月18日,美国加州正式签署了AB5法案[7],并于2020年1月1日正式生效。该法案要求将临时合同工(例如,网约车司机)纳入雇主的正式受雇员工,这意味着相关企业的业务成本(例如,最低工资、保险和员工福利等)将大幅增加,并将影响公司的员工管理机制和定价机制。实际上,为了满足该法案的要求,从2020年7月9日开始,Uber平台已经开始允许司机自己根据服务时间和距离设定价格[27]。目前,围绕AB5法案、以及自由职业者与平台之间的劳资关系仍然存在非常大的争议,一部分学者建议创立一个新的、介于雇员和合同工之间的第三类工作类别,该类别将保留自由职业者的一部分工作灵活度,同时,赋予他们部分全职雇员可以享受到的福利待遇[16][35]。

(2)加强资质审核和运营监督

交管、人社、应急管理等相关部门应当督促和加强对平台和骑手的资质审核和安全培训。在劳动者申请加入平台过程中,平台需要对骑手电动车的准入标准进行统一登记管理;与此同时,加强对平台运营流程的监督,比如,可以借助骑手 APP 的渠道对平台的配送路径规划等算法进行监管审查,严禁对骑手提供违反交通规则(例如,逆行)的推荐路线;相关部门还可以对平台上骑手每天的连续配送时长设置上限并进行合理监督,避免出现因为骑手疲劳工作而造成的交通事故。

(3)明确平台责任

在对涉及快递和外卖行业的道路交通安全进行监管的过程中,监管部门应该把平台所属骑手发生的交通事故和违章次数、以及相应后果的严重程度作为对平台重要的监管指标,按月度或者季度等进行考核和追责,从而督促平台和骑手共同维护配送安全。

针对已经发生的交通事故,已有的法院判决为相关的责任划分提供了具有现实意义的指导。近日,关于浙江省湖州市吴兴区的外卖骑手撞伤行人一案,法院最后宣判,外卖骑手虽然没有与平台签订法律劳务合同,但对外是以“平台网上订餐配送”的名义为客户提供服务,且在提供配送服务时受平台管理制度的约束,报酬由平台发放,因此,无论是否与公司签合同,在其接受配送任务后均与配送平台建立了雇佣关系,在送餐中发生事故,作为雇主的公司应承担赔偿责任[41]。这一结果无疑明确了外卖平台必须加快运营管理流程的调整,以保障骑手安全和行人安全。

(4)市场竞争与政府干预

监管部门需要在遵循平台竞争规律和市场调节作用的条件下制定提升社会福利的监管法规。在市场经济环境下,平台之间的自由竞争将会影响平台选择最优的骑手激励机制以及运营流程和算法。但是,竞争形成的市场结果并不一定能够实现社会各参与方的福利最大化,可能陷入“囚徒困境”的局面。

例如,如果一个平台没有严格遵守法规制度,漠视骑手权益,那么,为了维持甚至扩大市场份额和利润,其竞争对手很有可能也不会选择严格遵守法规制度和提升骑手权益。为此,监管部门应该充分考虑多平台竞争的市场环境,以问题为导向制定相关法规,一视同仁地对所有平台进行严格的监管。

洞察:平台之间的市场竞争虽然会有助于提升市场效率,但在一定的情况下,也有可能出现市场失灵,竞争无法达到最优的纳什均衡(Nash equilibrium)。对此,监管者可以考虑根据博弈论(game theory)和机制设计(mechanism design)的理论和工具来刻画骑手、顾客、商家和多个平台之间的交互行为,并研究市场中的平台竞争结果[1]。特别是针对竞争的外部性(externalities)可能导致出现的市场失灵的情况,监管部门需要考虑相应的监管制度规则以引导相互竞争的平台向更有利于提升社会福利的方向发展。

总结

作为致力于大数据,运筹学,以及人工智能方法在智慧城市领域应用的学者和研究者,我们从平台设计运营和政府监督监管两方面为解决外卖骑手困境提供了可能的解决方案,探讨了骑手激励机制、运营流程和算法、供需调节机制等平台可以采取的运营策略,并分析了明晰劳资关系、加强资质审核和运营监督、明确平台责任、以及市场竞争与政府干预等监管者面临的挑战。

构筑健康、温暖、高效、可持续并且具有社会责任的外卖生态体系,需要我们从社会伦理、法律制度以及经济学和管理学原理出发,融合大数据技术和人工智能算法等工具,充分满足不同目标下多参与方的核心利益。

最终,让每一方都释放出最大的善意以实现共赢,只有这样才能提升社会整体的福利水平,让人们真正享受到科技为生活带来的便利。

作者简介:

王海:清华大学学士,麻省理工学院运筹学博士,现为新加坡管理大学决策分析方向助理教授,美国卡内基梅隆大学信息系统与公共政策学院访问教授;研究方向为运筹学,大数据,优化算法,以及人工智能的方法论及其在智慧城市场景的应用;主要领域包括智能交通,共享经济,智慧物流,以及智慧医疗等。个人主页,http://wang-hai.net/

孙昊:华中科技大学学士,清华大学管理科学与工程博士,现为香港大学经济及工商管理学院博士后;研究方向为银行与金融中介,在线市场设计,统计学习方法与应用;主要领域包括金融科技与创新,共享经济等。

参考文献:

[1] Ahmadinejad, AmirMahdi, Hamid Nazerzadeh, Amin Saberi, Nolan Skochdopole, and Kane Sweeney。 (2019)。 Competition in ride-hailing markets。 Available at SSRN 3461119。

[2] Avery, Christopher, Paul Resnick, and Richard Zeckhauser。 (1999)。 The market for evaluations。 American Economic Review , 89(3),564-584。

[3] Bai, Jiaru, Kut C。 So, Christopher S。 Tang, Xiqun (Michael) Chen, and Hai Wang。 (2019)。 Coordinating supply and demand on an on-demand service platform with impatient customers。 Manufacturing & Service Operations Management , 21(3), 556-570。

[4] Basu, Amiya K。, Rajiv Lal, V。 Srinivasan, and Richard Staelin。 (1985)。 Salesforce compensation plans: An agency theoretic perspective。 Marketing Science , 4(4), 267-291。

[5] Bertsimas, Dimitris, Patrick Jaillet, and Sébastien Martin。 (2019)。 Online vehicle routing: The edge of optimization in large-scale applications。 Operations Research , 67(1), 143-162。

[6] Bolton, Gary, Ben Greiner, and Axel Ockenfels。 (2013)。 Engineering trust: reciprocity in the production of reputation information。 Management Science , 59(2), 265-285。

[7] California Legislative Information。 Assembly Bill No。 5。 September 18, 2019。 https://leginfo.legislature.ca.gov/faces/billTextClient.xhtml?bill_id=201920200AB5。

[8] Calo, Ryan, and Alex Rosenblat。 (2017)。 The taking economy: Uber, information, and power。 Columbia Law Review , 117, 1623。

[9] Chen, Yiwei, and Ming Hu。 (2020)。 Pricing and matching with forward-looking buyers and sellers。 Manufacturing & Service Operations Management , 22(4), 717-734。

[10] Chung, Doug J。, Das Narayandas, and Dongkyu Chang。 (2020) The effects of quota frequency: Sales performance and product focus。 Management Science 。

[11] de Figueiredo Jr, Rui JP, Evan Rawley, and Orie Shelef。 (2019)。 Bad bets: Nonlinear incentives, risk, and performance。 Strategic Management Journal 。 https://doi.org/10.1002/smj.3111

[12] Einav, Liran, Chiara Farronato, and Jonathan Levin。 (2016)。 Peer-to-peer markets。 Annual Review of Economics , 8, 615-635。

[13] Fradkin, Andrey, Elena Grewal, and David Holtz。 (2020)。 Reciprocity in two-sided reputation systems: Evidence from an experiment on Airbnb。 Working Paper, MIT Sloan School of Management。 https://andreyfradkin。 com。

[14] Garg, Nikhil, and Hamid Nazerzadeh。 (2019)。 Driver surge pricing。 arXiv preprint arXiv:1905.07544。

[15] Glaeser, Chloe Kim, Marshall Fisher, and Xuanming Su。 (2019)。 Optimal Retail Location: Empirical Methodology and Application to Practice: Finalist–2017 M&SOM Practice-Based Research Competition。 Manufacturing & Service Operations Management , 21(1), 86-102。

[16] Hagiu, Andrei, and Julian Wright。 (2019)。 The status of workers and platforms in the sharing economy。 Journal of Economics &Management Strategy , 28(1), 97-108。

[17] Ke, Jintao, Hai Yang, Xinwei Li, Hai Wang, and Jieping Ye。 (2020)。 Pricing and equilibrium in on-demand ride-pooling markets。 Transportation Research Part B: Methodological 139: 411-431。

[18] Ke, T。 Tony, Baojun Jiang, and Monic Sun。 (2017)。 Peer-to-peer markets with bilateral ratings。 In MIT Sloan Research Paper No。 5236-17; NET Institute Working Paper No (pp。 17-101)。

[19] Li, Ruijie, Yu (Marco) Nie, and Xiaobo Liu。 (2020) Pricing carpool rides based on schedule displacement。 Transportation Science 54(4):1134-1152。

[20] Liu, Sheng, Long He, and Zuo-Jun Max Shen。 (2018)。 On-time last mile delivery: Order assignment with travel time predictors。 Management Science 。

[21] Lyu, Guodong, Wang Chi Cheung, Chung-Piaw Teo, and Hai Wang。 (2019)。 Multi-objective online ride-matching。 Working paper, National University of Singapore。 Available at SSRN 3356823。

[22] Morduch, Jonathan。 (1995)。 Income smoothing and consumption smoothing。 Journal of Economic Perspectives , 9(3), 103-114。

[23] Schweitzer, Maurice E。, Lisa Ordóez, and Bambi Douma。 (2004)。 Goal setting as a motivator of unethical behavior。 Academy of Management Journal , 47(3), 422-432。

[24] Sun, Hao, Hai Wang, and Zhixi Wan。 (2019)。 Model and analysis of labor supply for ride-sharing platforms in the presence of sample self-selection and endogeneity。 Transportation Research Part B: Methodological , 125, 76-93。

[25] Tsetlin, Ilia, and Robert L。 Winkler。 (2007)。 Decision making with multiattribute performance targets: The impact of changes in performance and target distributions。 Operations Research , 55(2), 226-233。

[26] Tunc, Murat M。, Huseyin Cavusoglu, and Srinivasan Raghunathan。 (2019)。 Two-sided adverse selection and bilateral reviews in sharing economy。 Available at SSRN 3499979。

[27] Uber Blog。 California drivers: Set your own fares when you drive with Uber。 June 25, 2020。

https://www.uber.com/blog/california/set-your-fares/。

[28] Uber Engineering。 How trip inferences and machine learning optimize delivery times on Uber Eats。 (2018)。 https://eng.uber.com/uber-eats-trip-optimization/。

[29] Ulmer, Marlin W。, and Barrett W。 Thomas。 (2018)。 Same-day delivery with heterogeneous fleets of drones and vehicles。 Networks ,72(4), 475-505。

[30] Ulmer, Marlin W。 Justin C。 Goodson, Dirk C。 Mattfeld, and Marco Hennig。 (2019)。 Offline–online approximate dynamic programming for dynamic vehicle routing with stochastic requests。 Transportation Science , 53(1), 185-202。

[31] Wall Street Journal。 (2015)。 Now prices can change from minute to minute。 December 14, 2015.https://www.wsj.com/articles/now-prices-can-change-from-minute-to-minute-1450057990。

[32] Wang, Hai, and Amedeo Odoni。 (2016)。 Approximating the performance of a “last mile” transportation system。 Transportation Science , 50(2), 659-675。

[33] Wang, Hai。 (2019)。 Routing and scheduling for a last-mile transportation system。 Transportation Science , 53(1), 131-147。

[34] Wang, Hai, and Hao Sun。 (2020)。 Peak-hour incentive design with strategic driver behavior。 Singapore Management University, working paper。

[35] Wang, Hai, and Hao Sun。 (2020)。 The optimal labor supply flexibility on ride-sharing platforms。 Singapore Management University, working paper。

[36] Wang, Hai, and Hai Yang。 (2019)。 Ridesourcing systems: A framework and review。 Transportation Research Part B: Methodological , 129, 122-155。

[37] Yang, Hai, Chaoyi Shao, Hai Wang, and Jieping Ye。 (2020)。 Integrated reward scheme and surge pricing in a ridesourcing market。 Transportation Research Part B: Methodological , 134, 126-142。

[38] Zoltners Andris A, PK Sinha, and Sally E。 Lorimer。 (2016)。 Wells Fargo and the slippery slope of sales incentives。 Harvard Business Review 。 (September), https://hbr.org/2016/09/wells-fargo-and-the-slippery-slope-of -sales-incentives。

[39] 滴滴出行。 滴滴专车司机作弊违规处罚标准。 2019 年 04 月 24 日。 https://www.eycen.com/post/545.html。

[40] 发展改革委网站。 关于支持新业态新模式健康发展 激活消费市场带动扩大就业的意见。 2020 年 07 月 14 日。 https://www.ndrc.gov.cn/xxgk/zcfb/tz/202007/t20200715_1233793_ext.html。

[41] 金融界。 浙江湖州外卖骑手撞伤行人,法院判未签劳动合同的平台需担责。 2020 年 09 年 15 日。

https://baijiahao.baidu.com/s?id=1677901965178599201&wfr=spider&for=pc。

[42] 京东。 什么是退换货运费险?2019 年 12 月 10 日。 https://help.jd.com/user/issue/430-499.html

[43] 美团研究院。 新时代 新青年:2018 年外卖骑手群体研究报告。 2019 年 1 月 17 日。

https://s3plus.meituan.net/v1/mss_531b5a3906864f438395a28a5baec011/official website/c21d0443-decf-41d5-9813-ef8eaa6516d0。

[44] 美团技术团队。 深度学习在美团推荐平台排序中的运用。 2017 年 07 月 28 日。 https://tech.meituan.com/2017/07/28/dl.html。

[45] 美团技术团队。 即时配送的 ETA 问题之亿级样本特征构造实践。 2017 年 11 月 24 日。

https://tech.meituan.com/2017/11/24/gbdt.html。

[46] 美团技术团队。 美团外卖骑手背后的 AI 技术。 2018 年 03 月 29 日。 https://tech.meituan.com/2018/03/29/herenqing-ai-con.html。

[47] 美团技术团队。 深度学习在美团配送 ETA 预估中的探索与实践。 2019 年 02 月 21 日。

https://tech.meituan.com/2019/02/21/meituan-delivery-eta-estimation-in-the-practice-of-deeplearning.html。

[48] 美团技术团队。 配送交付时间轻量级预估实践。 2019 年 10 月 10 日。 https://tech.meituan.com/2019/10/10/distribution-time-prediction-practice.html。

[49] 美团技术团队。 美团智能配送系统的运筹优化实战。 2020 年 02 月 20 日。 https://tech.meituan.com/2020/02/20/meituan-delivery-operations-research.html。

[50] 青年报。 快递、外卖行业交通事故频发 去年 117 起事故致 9 死 134 伤。 2018 年 02 月 10 日。

http://app.why.com.cn/epaper/webpc/qnb/html/2018-02/10/content_51905.html。

[51] 人物。 外卖骑手, 困在系统里。 2020 年 9 月 8 日。 https://mp.weixin.qq.com/s/Mes1RqIOdp48CMw4pXTwXw。

[52] 搜狐网。 高校为什么要布局智能外卖柜?对学校有什么好处。 2019 年 6 月 21 日。

https://www.sohu.com/a/322101528_120167191。

[53] 中国日报中文网。 美团夏华夏:不断深化现实物理场景应用 美团 AI 助力产业智能化转型。 2020 年 9 月 8 日。http://cn.chinadaily.com.cn/a/202009/08/WS5f56f6faa310084978423e53.html。


融云 WebRTC 首帧显示优化策略到底有多强?

技术交流admin 发表了文章 • 0 个评论 • 65 次浏览 • 2020-09-29 17:47 • 来自相关话题

作者:融云 WebRTC 高级工程师 苏道音视频实时通话首帧的显示是一项重要的用户体验标准。本文主要通过对接收端的分析来了解和优化视频首帧的显示时间。流程介绍发送端采集音视频数据,通过编码器生成帧数据。这数据被打包成 RTP 包,通过 ICE 通道发送到接收端... ...查看全部

timg.jpg

作者:融云 WebRTC 高级工程师 苏道


音视频实时通话首帧的显示是一项重要的用户体验标准。本文主要通过对接收端的分析来了解和优化视频首帧的显示时间。


流程介绍

发送端采集音视频数据,通过编码器生成帧数据。这数据被打包成 RTP 包,通过 ICE 通道发送到接收端。接收端接收 RTP 包,取出 RTP payload,完成组帧的操作。之后音视频解码器解码帧数据,生成视频图像或音频 PCM 数据。

640.png


本文参数调整谈论的部分位于上图中的第 4 步。因为是接收端,所以会收到对方的 Offer 请求。先设置 SetRemoteDescription 再 SetLocalDescription。如下图蓝色部分:

2.png


参数调整

视频参数调整

当收到 Signal 线程 SetRemoteDescription 后,会在 Worker 线程中创建 VideoReceiveStream 对象。具体流程为 SetRemoteDescription

VideoChannel::SetRemoteContent_w 创建 WebRtcVideoReceiveStream。

WebRtcVideoReceiveStream 包含了一个 VideoReceiveStream 类型 stream_ 对象,通过 webrtc::VideoReceiveStream* Call::CreateVideoReceiveStream 创建。创建后立即启动 VideoReceiveStream 工作,即调用 Start() 方法。此时 VideoReceiveStream 包含一个 RtpVideoStreamReceiver 对象准备开始处理 video RTP 包。接收方创建 createAnswer 后通过 setLocalDescription 设置 local descritpion。对应会在 Worker 线程中 setLocalContent_w 方法中根据 SDP 设置 channel 的接收参数,最终会调用到 WebRtcVideoReceiveStream::SetRecvParameters。


WebRtcVideoReceiveStream::SetRecvParameters 实现如下:

void WebRtcVideoChannel::WebRtcVideoReceiveStream::SetRecvParameters(
    const ChangedRecvParameters& params) {
  bool video_needs_recreation = false;
  bool flexfec_needs_recreation = false;
  if (params.codec_settings) {
    ConfigureCodecs(*params.codec_settings);
    video_needs_recreation = true;
  }
  if (params.rtp_header_extensions) {
    config_.rtp.extensions = *params.rtp_header_extensions;
    flexfec_config_.rtp_header_extensions = *params.rtp_header_extensions;
    video_needs_recreation = true;
    flexfec_needs_recreation = true;
  }
  if (params.flexfec_payload_type) {
    ConfigureFlexfecCodec(*params.flexfec_payload_type);
    flexfec_needs_recreation = true;
  }
  if (flexfec_needs_recreation) {
    RTC_LOG(LS_INFO) << "MaybeRecreateWebRtcFlexfecStream (recv) because of "
                        "SetRecvParameters";
    MaybeRecreateWebRtcFlexfecStream();
  }
  if (video_needs_recreation) {
    RTC_LOG(LS_INFO)
        << "RecreateWebRtcVideoStream (recv) because of SetRecvParameters";
    RecreateWebRtcVideoStream();
  }
}

根据上图中 SetRecvParameters 代码,如果 codec_settings 不为空、rtp_header_extensions 不为空、flexfec_payload_type 不为空都会重启 VideoReceiveStream。video_needs_recreation 表示是否要重启 VideoReceiveStream。重启过程为,把先前创建的释放掉,然后重建新的 VideoReceiveStream。以 codec_settings 为例,初始 video codec 支持 H264 和 VP8。若对端只支持 H264,协商后的 codec 仅支持 H264。SetRecvParameters 中的 codec_settings 为 H264 不空。其实前后 VideoReceiveStream 的都有 H264 codec,没有必要重建 VideoReceiveStream。可以通过配置本地支持的 video codec 初始列表和 rtp extensions,从而生成的 local SDP 和 remote SDP 中影响接收参数部分调整一致,并且判断 codec_settings 是否相等。如果不相等再 video_needs_recreation 为 true。这样设置就会使 SetRecvParameters 避免触发重启 VideoReceiveStream 逻辑。在 debug 模式下,修改后,验证没有“RecreateWebRtcVideoStream (recv) because of SetRecvParameters”的打印, 即可证明没有 VideoReceiveStream 重启。


音频参数调整

和上面的视频类似,音频也会有因为 rtp extensions 不一致导致重新创建 AudioReceiveStream,也是释放先前的 AudioReceiveStream,再重新创建 AudioReceiveStream。参考代码:

bool WebRtcVoiceMediaChannel::SetRecvParameters(
    const AudioRecvParameters& params) {
  TRACE_EVENT0("webrtc", "WebRtcVoiceMediaChannel::SetRecvParameters");
  RTC_DCHECK(worker_thread_checker_.CalledOnValidThread());
  RTC_LOG(LS_INFO) << "WebRtcVoiceMediaChannel::SetRecvParameters: "
                   << params.ToString();
  // TODO(pthatcher): Refactor this to be more clean now that we have
  // all the information at once.
  if (!SetRecvCodecs(params.codecs)) {
    return false;
  }
  if (!ValidateRtpExtensions(params.extensions)) {
    return false;
  }
  std::vector<webrtc::RtpExtension> filtered_extensions = FilterRtpExtensions(
      params.extensions, webrtc::RtpExtension::IsSupportedForAudio, false);
  if (recv_rtp_extensions_ != filtered_extensions) {
    recv_rtp_extensions_.swap(filtered_extensions);
    for (auto& it : recv_streams_) {
      it.second->SetRtpExtensionsAndRecreateStream(recv_rtp_extensions_);
    }
  }
  return true;
}

AudioReceiveStream 的构造方法会启动音频设备,即调用 AudioDeviceModule 的 StartPlayout。AudioReceiveStream 的析构方法会停止音频设备,即调用 AudioDeviceModule 的 StopPlayout。因此重启 AudioReceiveStream 会触发多次 StartPlayout/StopPlayout。经测试,这些不必要的操作会导致进入视频会议的房间时,播放的音频有一小段间断的情况。解决方法同样是通过配置本地支持的 audio codec 初始列表和 rtp extensions,从而生成的 local SDP 和 remote SDP 中影响接收参数部分调整一致,避免 AudioReceiveStream 重启逻辑。另外 audio codec 多为 WebRTC 内部实现,去掉一些不用的 Audio Codec,可以减小 WebRTC 对应的库文件。


音视频相互影响

WebRTC 内部有三个非常重要的线程,woker 线程、signal 线程和 network 线程。

调用 PeerConnection 的 API 的调用会由 signal 线程进入到 worker 线程。

worker 线程内完成媒体数据的处理,network 线程处理网络相关的事务,channel.h 文件中有说明,以 _w 结尾的方法为 worker 线程的方法,signal 线程的到 worker 线程的调用是同步操作。如下图中的 InvokerOnWorker 是同步操作,setLocalContent_w 和 setRemoteContent_w 是 worker 线程中的方法。

bool BaseChannel::SetLocalContent(const MediaContentDescription* content,
                                  SdpType type,
                                  std::string* error_desc) {
  TRACE_EVENT0("webrtc", "BaseChannel::SetLocalContent");
  return InvokeOnWorker<bool>(
      RTC_FROM_HERE,
      Bind(&BaseChannel::SetLocalContent_w, this, content, type, error_desc));
}
bool BaseChannel::SetRemoteContent(const MediaContentDescription* content,
                                   SdpType type,
                                   std::string* error_desc) {
  TRACE_EVENT0("webrtc", "BaseChannel::SetRemoteContent");
  return InvokeOnWorker<bool>(
      RTC_FROM_HERE,
      Bind(&BaseChannel::SetRemoteContent_w, this, content, type, error_desc));
}

setLocalDescription 和 setRemoteDescription 中的 SDP 信息都会通过 PeerConnection 的 PushdownMediaDescription 方法依次下发给 audio/video RtpTransceiver 设置 SDP 信息。举例,执行 audio 的 SetRemoteContent_w 执行很长(比如音频 AudioDeviceModule 的 InitPlayout 执行耗时), 会影响后面的 video SetRemoteContent_w 的设置时间。PushdownMediaDescription 代码:

RTCError PeerConnection::PushdownMediaDescription(
    SdpType type,
    cricket::ContentSource source) {
  const SessionDescriptionInterface* sdesc =
      (source == cricket::CS_LOCAL ? local_description()
                                   : remote_description());
  RTC_DCHECK(sdesc);
  // Push down the new SDP media section for each audio/video transceiver.
  for (const auto& transceiver : transceivers_) {
    const ContentInfo* content_info =
        FindMediaSectionForTransceiver(transceiver, sdesc);
    cricket::ChannelInterface* channel = transceiver->internal()->channel();
    if (!channel || !content_info || content_info->rejected) {
      continue;
    }
    const MediaContentDescription* content_desc =
        content_info->media_description();
    if (!content_desc) {
      continue;
    }
    std::string error;
    bool success = (source == cricket::CS_LOCAL)
                       ? channel->SetLocalContent(content_desc, type, &error)
                       : channel->SetRemoteContent(content_desc, type, &error);
    if (!success) {
      LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER, error);
    }
  }
  ...
}

其他影响首帧显示的问题


Android图像宽高16字节对齐


AndroidVideoDecoder 是 WebRTC Android 平台上的视频硬解类。AndroidVideoDecoder 利用 MediaCodec API 完成对硬件解码器的调用。

MediaCodec 有已下解码相关的 API:

  •  dequeueInputBuffer:若大于 0,则是返回填充编码数据的缓冲区的索引,该操作为同步操作。

  • getInputBuffer:填充编码数据的 ByteBuffer 数组,结合 dequeueInputBuffer 返回值,可获取一个可填充编码数据的 ByteBuffer。

  • queueInputBuffer:应用将编码数据拷贝到 ByteBuffer 后,通过该方法告知 MediaCodec 已经填写的编码数据的缓冲区索引。

  • dequeueOutputBuffer:若大于 0,则是返回填充解码数据的缓冲区的索引,该操作为同步操作。

  • getOutputBuffer:填充解码数据的 ByteBuffer 数组,结合 dequeueOutputBuffer 返回值,可获取一个可填充解码数据的 ByteBuffer。

  • releaseOutputBuffer:告诉编码器数据处理完成,释放 ByteBuffer 数据。

在实践当中发现,发送端发送的视频宽高需要 16 字节对齐。因为在某些 Android 手机上解码器需要 16 字节对齐。Android 上视频解码先是把待解码的数据通过 queueInputBuffer 给到 MediaCodec。然后通过 dequeueOutputBuffer 反复查看是否有解完的视频帧。若非 16 字节对齐,dequeueOutputBuffer 会有一次 MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED。而不是一上来就能成功解码一帧。经测试发现,帧宽高非 16 字节对齐会比 16 字节对齐的慢 100 ms 左右。


服务器需转发关键帧请求

iOS 移动设备上,WebRTC App应用进入后台后,视频解码由 VTDecompressionSessionDecodeFrame 返回 kVTInvalidSessionErr,表示解码session 无效。从而会触发观看端的关键帧请求给服务器。这里要求服务器必须转发接收端发来的关键帧请求给发送端。若服务器没有转发关键帧给发送端,接收端就会长时间没有可以渲染的图像,从而出现黑屏问题。这种情况下只能等待发送端自己生成关键帧,发送个接收端,从而使黑屏的接收端恢复正常。


WebRTC内部的一些丢弃数据逻辑举例


Webrtc从接受报数据到、给到解码器之间的过程中也会有很多验证数据的正确性。

举例1

PacketBuffer 中记录着当前缓存的最小的序号 first_seq_num_(这个值也是会被更新的)。当 PacketBuffer 中 InsertPacket 时候,如果即将要插入的 packet 的序号 seq_num 小于 first_seq_num,这个 packet 会被丢弃掉。如果因此持续丢弃 packet,就会有视频不显示或卡顿的情况。

举例2

正常情况下 FrameBuffer 中帧的 picture id,时间戳都是一直正增长的。如果 FrameBuffer 收到 picture_id 比最后解码帧的 picture id 小时,分两种情况:

  • 1. 时间戳比最后解码帧的时间戳大,且是关键帧,就会保存下来;

  • 2. 除情况 1 之外的帧都会丢弃掉;

     

代码如下:

auto last_decoded_frame = decoded_frames_history_.GetLastDecodedFrameId();
auto last_decoded_frame_timestamp =
 decoded_frames_history_.GetLastDecodedFrameTimestamp();
if (last_decoded_frame && id <= *last_decoded_frame) {
if (AheadOf(frame->Timestamp(), *last_decoded_frame_timestamp) &&
   frame->is_keyframe()) {
 // If this frame has a newer timestamp but an earlier picture id then we
 // assume there has been a jump in the picture id due to some encoder
 // reconfiguration or some other reason. Even though this is not according
 // to spec we can still continue to decode from this frame if it is a
 // keyframe.
 RTC_LOG(LS_WARNING)
     << "A jump in picture id was detected, clearing buffer.";
 ClearFramesAndHistory();
 last_continuous_picture_id = -1;
} else {
 RTC_LOG(LS_WARNING) << "Frame with (picture_id:spatial_id) ("
                     << id.picture_id << ":"
                     << static_cast<int>(id.spatial_layer)
                     << ") inserted after frame ("
                     << last_decoded_frame->picture_id << ":"
                     << static_cast<int>(last_decoded_frame->spatial_layer)
                     << ") was handed off for decoding, dropping frame.";
 return last_continuous_picture_id;
}
}

因此为了能让收到了流顺利播放,发送端和中转的服务端需要确保视频帧的 picture_id, 时间戳正确性。

WebRTC 还有其他很多丢帧逻辑,若网络正常且有持续有接收数据,但是视频卡顿或黑屏无显示,多为流本身的问题。


Ending


本文通过分析 WebRTC 音视频接收端的处理逻辑,列举了一些可以优化首帧显示的点,比如通过调整 local SDP 和 remote SDP 中与影响接收端处理的相关部分,从而避免 Audio/Video ReceiveStream 的重启。另外列举了 Android 解码器对视频宽高的要求、服务端对关键帧请求处理、以及 WebRTC 代码内部的一些丢帧逻辑等多个方面对视频显示的影响。这些点都提高了融云 SDK 视频首帧的显示时间,改善了用户体验。


摩拜单车微信小程序开发技术总结

技术交流大兴 发表了文章 • 0 个评论 • 92 次浏览 • 2020-09-25 12:00 • 来自相关话题

前言本文主要讲讲摩拜单车小程序技术方向的总结,在段时间的开发周期内内如何一步步从学习到进阶。思维转变微信小程序没有HTML的常用标签,而是类似React的微信自定义组件,比如view、text、map等没有window变量,但微信提供了wx全局方法集没有a标签... ...查看全部

2739738930-58730c7fc7eb9_articlex.jpg

前言

本文主要讲讲摩拜单车小程序技术方向的总结,在段时间的开发周期内内如何一步步从学习到进阶。

思维转变

  • 微信小程序没有HTML的常用标签,而是类似React的微信自定义组件,比如view、text、map等

  • 没有window变量,但微信提供了wx全局方法集

  • 没有a标签链接,不可嵌套iframe

  • 事件绑定和条件渲染类似Angular,全部写在WXML中

  • 数据绑定采用Mustache双大括号语法

  • 无法操作DOM,通过改变page data(类似React的state)来改变视图展现

所以如果你熟悉以上提到的所有前端技术栈,开发微信小程序你会得心应手。

生命周期

你可以理解小程序就是一个单页面的H5网页,所有元素的加载都是一次性的,这就引来了生命周期的概念:

图片描述

  • 首次打开,小程序初始化

  • 小程序初始化完成后,触发onShow事件

  • 小程序被切换到后台(熄屏,切换APP等),触发onHide

  • 小程序从后台切换到前台,再次触发onShow

  • 小程序出错,触发onError

每个页面也有自己的生命周期:

图片描述

注意:在微信6.5.3版本中,部分Android机触发不了onLoad事件,可以用onReady替代。

事件广播

“单页面结构”的微信小程序,你可以使用事件广播(统一的事件中心)来注册和触发自定义事件,否则到后期事件管理会越来越乱,而且涉及跨页面传输事件,你更需要这种事件触发机制,可以参考broadcast.js。比如在摩拜单车中有这样的场景:

扫码成功后在开锁页面A提示开锁成功,要跳转到骑行页面B并查询用户骑行状态。 

如果没有统一的事件管理中心,你几乎无法完成这样的过程,当然,可以使用Hack的方式解决。因为跳转到页面B会触发B的onShow事件,所以可以在onShow中写业务逻辑:

// Page A
// 开锁成功后,跳转到Page B
wx.redirectTo({
  url: "/pages/riding/index"
})
// Page B
Page({
  onShow() {
    // 检查骑行状态
  }
}
})

但更合理的应该是利用事件广播来处理:

const broadcast = require("libs/broadcast")
// 先注册事件
broadcast.on("check_ride_state", () => {
  // 检查骑行状态
})
const broadcast = require("libs/broadcast")
// Page A
// 开锁成功后,触发事件,再跳转到Page B
broadcast.fire("check_ride_state")
wx.redirectTo({
  url: "/pages/riding/index"
})

数据中心

根目录的app.js很有用,根目录的app.js很有用,根目录的app.js很有用。

因为在它内部注册的变量或方法,都是可以被所有页面获取到,所以利用它也可以处理上面所述的跨页面事件触发问题。而且可以注册globalData供所有页面取用,例如可以把systemInfo直接注册到globalData中,这样就不用在每个页面都获取一遍:

// app.js
const systemInfo = wx.getSystemInfoSync()
App({
  globalData: {
    systemInfo
  }
})

在页面获取:

// Page A
const {
  systemInfo
} = getApp().globalData

性能优化

小程序运行在微信平台,而且可能和众多小程序“共享运行内存”,可想而知,单个小程序的性能极可能遇到瓶颈而Crash或被微信主动销毁!

比如在摩拜单车有这个场景:

首页展示地图找车,扫码成功后跳转到骑行地图。 

简单的逻辑,直接两个页面,两个map组件切换就可以搞定。实际测试场景中,iOS的确如预期,一切正常,但是在Android下,就很有可能会使得小程序Crash,扫码成功后直接退出了小程序。

解决办法就是, 整个小程序只维护一个map组件 ,通过不同的State来改变map的不同展现:

index.wxml

<map id="map" controls="{{controls}}" style="{{style}}"></map>

index/index.js



这样就成功解决了部分Android设备小程序Crash的问题。


原文地址

https://segmentfault.com/a/1190000008056208



友情链接