编程

编程

如果再给我一次机会,我希望我从未学过编程

技术交流徐凤年 发表了文章 • 0 个评论 • 31 次浏览 • 2020-11-04 14:39 • 来自相关话题

我是一名程序员。你可能会觉得我是专业的软件工程师,但实际上,软件工程不只是一个专业,还是一种生活方式。连帽衫、乒乓球、吃不完的零食和苏打水……都是这种生活的一部分。但虽然这个职业可以给人带来那么多回报,我还是要坦白一件事:有时我希望自己从来没有学过编程。怪癖自... ...查看全部

我是一名程序员。你可能会觉得我是专业的软件工程师,但实际上,软件工程不只是一个专业,还是一种生活方式。连帽衫、乒乓球、吃不完的零食和苏打水……都是这种生活的一部分。但虽然这个职业可以给人带来那么多回报,我还是要坦白一件事:

有时我希望自己从来没有学过编程。

怪癖

自从我开始学习编程以来,就再也不能像以前那样轻松地浏览网页了。我无法再平静地接受在各种网站上遇到的各种 bug。每当有奇怪的事情发生时,我的好奇心就上了头,然后就会打开浏览器开发工具开始调试网页。

尝试提交表单时出现加密错误消息?看到这样的错误,我会深入 JavaScript 控制台,查找错误消息,深入研究源代码,并仔细观察进进出出的网络请求。页面 UI 看起来有些过时,或者页面布局很漂亮?不管怎样,我都会调整浏览器窗口大小,检查页面响应屏幕尺寸的能力。什么?你竟然选择了 Comic Sans 字体?我可能永远不会再使用你或你们公司的产品了。

微信图片_20201104143349.jpg

请不要使用 ComicSans

职业生涯

觉得我的这些浏览习惯很怪异吗?这还不算啥,我和其他软件工程师的对话会让你印象更深刻的。总体而言,工程师往往固执己见。我们的血液中流淌着迂腐的基因。你喜欢使用空格还是 tabs 缩进代码?你更喜欢 Vim 还是 Emacs?Chrome 还是 Firefox?

聪明点的人会问到,这些对话“真的重要吗?”。Bikeshedding(在琐碎细节上浪费时间)是确实存在的现象,大家都需要经常反省。

微信图片_20201104143403.jpg

xkcd——真正的程序员

随着越来越多的公司采用敏捷方法,我们还得时常考虑在 Waterfall、Scrum 或 Kanban 开发生命周期之间权衡取舍。我们所有人都喜欢鄙视 Waterfall,毕竟这是老式的低效率公司所使用的遗留品。但是 Scrum 与看板之战还在继续。

你竟然是 Scrum 的信徒?我敢打赌,你为了满足那些 time box 会仓促提交混乱的代码,才能在冲刺结束之前赶上假想的时限。

微信图片_20201104143416.jpg

说到敏捷,故事的重点在哪里?时间尺度?努力?复杂性?风险?这些都有?你我可能会花费几小时时间来争论每个定义的优缺点,最后还是无法达成共识。

在某些时候,这种争论可能会演变为更多的 bikeshedding,但能够相互理解和有效沟通是至关重要的,其中就包括了对我们日常使用的术语达成共识。

前端开发带来的争论又是数不清的,其中最典型的是:“我们要到什么时候才会放弃对 Internet Explorer 的支持?”我必须在“讨厌 Internet Explorer,想要放弃它”和了解还在用它的客户需求之间找到完美的平衡点。

微信图片_20201104143428.jpg

持续学习

持续学习是每一位开发者必备的能力,因为技术形势日新月异。每月都有成百上千的新库和框架发布。JavaScript 倦怠不是什么幻觉,“学不动了”也不仅仅是调侃。

我得花一天的时间配置 Webpack、Rollup 和 Babel 以使用最新的 ES6+ 语法,而旁人听我这么说就像在听天书。Angular 和 AngularJS 截然不同?LitElement、Svelte 和 Stencil 似乎是很有前途的 Web 组件解决方案?Deno 可能是下一个 Node?听着都像是谜语。

当有人问我做什么工作时,我总会蹦出来一堆术语:“我通常使用前端技术,例如 HTML、CSS 和 JS。有时我必须使用 PHP 或 SQL,但我更多是 MEAN/MERN 栈开发人员。有时我使用 Heroku 之类的 PaaS 技术,还有时我会使用 AWS 或 GCP 之类的 IaaS 提供商。”

微信图片_20201104143437.jpg

个人生活

在业余时间里,我读的书干货十足,例如《干净代码》《重构》和《领域驱动设计》。我不是在看教科书,就是在阅读里面写着可疑建议的文章,或观看一些编程教程。关于编程的播客更能吸引人,这样我就能在路上听某人谈论写代码的方法,然后我又可以花一天时间谈相关话题了。

除了在线获取内容外,软件工程师还会花费大量个人时间来在网上创建内容。我个人的兴趣是构建简单的应用和游戏,其实没人看得到。这是浪费时间吗?也许是吧,但的确挺有意思的。


微信图片_20201104143447.jpg企业 BS 生成器应用

事实是

可事实是,编程给了我创造和创新的机会。它帮助我将创意变为现实,让我几乎从零开始构筑了很多东西。软件工程使我能够解决有趣而艰巨的挑战,理想情况下还能让人们的生活更轻松一些。编程使我的思维更具逻辑性。编程让我有机会不断学习,我还能以编程为职业获取薪水!

事实是,我喜欢编程。

参考阅读

https://hackernoon.com/i-wish-i-never-learned-to-code-7a1m3wwx

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

技术交流大神庵 发表了文章 • 0 个评论 • 48 次浏览 • 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


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

技术交流admin 发表了文章 • 0 个评论 • 81 次浏览 • 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 视频首帧的显示时间,改善了用户体验。


Ruby会消失吗?盘点十年后将要消失的五种编程语言

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

本文作者从自己的观点出发,介绍了未来 20 年内可能消失的 5 个编程语言,并给出了具体的原因。最后对想要学习编程的初学者给出了学习建议。随着时间的流逝,程序员们发现了更新、更简单的工作方式,新的编程语言如雨后春笋般出现,但只有少数编程语言能成为社区的新宠。这... ...查看全部

ÉãͼÍø_501237513_wx_ÌØÁ¢Äá´ïСÕò£¨·ÇÆóÒµÉÌÓã©.jpg


本文作者从自己的观点出发,介绍了未来 20 年内可能消失的 5 个编程语言,并给出了具体的原因。最后对想要学习编程的初学者给出了学习建议。

随着时间的流逝,程序员们发现了更新、更简单的工作方式,新的编程语言如雨后春笋般出现,但只有少数编程语言能成为社区的新宠。这种进步的一个副作用是一些古老的编程语言必然会跟历史一样被人们遗忘。如果一个编程语言无法随着时间的推移提升其价值,那么它的用户群终将会流失,并逐渐淡出人们的视线,或者成为更新一代编程语言的基础。

最近,古老的 COBOL 编程语言上了热搜。在 1960 年代和 1970 年代,它曾经是许多美国银行和政府机构的首选的编程语言,但最终被更加简单有效的编程语言所取代。但是,使用 COBOL 构建的系统仍然存在,当一些政府机构发现他们需要通过更新代码来全面改革失业系统时,才发现业内没有几个开发人员可以熟练使用该编程语言。

沧海桑田,COBOL 早已物是人非。我们当前的许多编程语言也注定会有相似的下场。本文中,我们将分析未来 20 内最终会消失的 5 种编程语言。我知道这可能会伤害到那些正在使用这几个编程语言的程序员的内心,所以在开始介绍之前首先声明下这只是我个人的看法和预测。

1. Ruby

Ruby 在 1999 年发布后立即受到程序员们的热捧,它能够快速构建应用程序的特性给程序员留下了非常深刻的印象。紧随其后,备受欢迎的 Ruby on Rails 框架于 2004 年发布,由于 Ruby 和 Rails 这两个名称在当时几乎成为了同义词,因此 Ruby 很快地被推到了程序员最喜欢的编程语言排行榜首位。它经历了一个坚实的十年,稳居编程语言排行榜前列,一度成为众人瞩目的焦点,但是最近的十年它并不好过,所以 Ruby 纳进了我的淘汰清单列表。

为什么 Ruby 会逐渐消退呢?导致其排名下降的因素之一是其执行速度。由它构建的应用程序运行速度往往比其他流行的编程语言,比如 JavaScript、Go 和 Python 构建的应用(在某些框架下)运行速度慢。同时期下,后三者已经发展到可以满足当时的需求,而 Ruby 在很多方面一直在原地踏步,没有什么进步。例如,让 Ruby on Rails 名噪一时的 MVC 架构在现在被很多编程人员看来是笨重和过时的。

2. Visual Basic

考虑到 Visual Basic 是公认的程序员最不喜欢的编程语言之一的事实,将它包含在淘汰列表中是无可厚非的。它于 1991 年由微软发布,作为构建 Windows 的主要工具,确实实现了此目的,但多年来经常出现问题。程序员使用 VB 进行开发的热情差距很大,随着 2000 年 C#的发布,这种差距逐渐进一步扩大。由于 C#更加简洁,提供了更广泛的功能,并且更适合于云服务和移动开发(在当前市场中扮演着重要角色),因此很多开发人员都放弃使用 VB 转而投向 C#怀抱。最重要的是,似乎它的创建者也都无情地放弃了它,因为微软曾表示他们没有进一步发展它的计划。

3. Haskell

Haskell 是另一个古董级别的编程语言,它于几十年前创建,在世纪之交前一直被使用。不幸的是,对于 Haskell 来说,属于它的时代很快就要结束了。研究人员和学者大多将 Haskell 用于构建程序,以对其工作进行复杂的排列和外推计算,普遍认为它很难学习。学习路径困难必然导致非常有限的活跃用户,而 Haskell 的上一个最新的稳定版本是在 2010 年发布,这对于促进它本身的发展无济于事。

4. Perl

与 Visual Basic 命运一样,Perl 不被大部分使用它的开发人员所喜欢。在 Perl 于 1987 年开始流行时,它被誉为是适合任何一个人的编程语言,无论你是编程的初学者还是专业人士都可以使用,同时还被宣传可以用于各种各样的场景。但是,1991 年 Python 的出现,彻底粉碎了 Perl 主导编程世界的幻想。

尽管 Python 的语法相比 Perl 稍显繁琐,更加严格,但 Python 让用户使用脚本实现目标的过程变得更加简单直接,这改变了当时的游戏规则。Python 在 2000 年代开始让 Perl 黯然失色,并一度成为编程入门的首选语言。在过去的 15 年中,Perl 的使用量一直在稳步下降,我们看不到任何它将停止下滑的迹象。

5. Objective-C

Objective-C 与 Apple 的关系类似于运动相机与 GoPro 的密切联系——两者都是他们所属公司专用的工具。尽管 Objective-C 已经在 macOS、iOS 和 OS X 开发中使用了二十多年(1996 年至今)的时间,但如今已经很少有程序员在使用该语言,因为 Swift 横空出世了。

Swift 由 Apple 构建并于 2014 年发布,旨在成为 macOS、iOS 及其他几个 Apple 相关平台的新编码标准。Obj-C 在很大程度上是基于复杂的 C 语言创建的,而 Swift 有效地删除了这些复杂的元素,同时增加了一些新的功能特性,例如自动内存管理,对 Cocoa Touch 的支持以及类型安全的集成。

Swift 设法实现和维护了较高的语言稳定性和用户采用率,因为 Apple 公司仍在坚持重写 Obj-C 库,以使旧语言的使用者可以很容易地进行过渡。尽管仍然有很多关于这两种选择哪个更好的讨论,但当前的趋势表明,Objective-C 终将被淘汰。

总结

尽管上述提到的语言似乎都处于下降趋势,但不能保证它们最终一定会消失。如果他们的创造者和社区投入足够的精力来更新和维护它们,添加新功能以及有用的特性,这些编程可能会卷土重来,恢复生命力。但事实是广大程序员更有可能继续涌向更新更好的选择。

如果你正在考虑开始学习哪种语言,那么有许多因素需要考虑。如果你只是将编程当做一种兴趣爱好,你可以聆听一下自己内心并选择最能激发你兴趣的那个来学习,即便所选的编程语言不再被广泛使用甚至面临淘汰。另一方面,如果你正在从事编程相关的工作,那么不应该只选择最流行的编程语言,也不应该赌一把选择未来可能会成为最流行的编程语言。而应该通过仔细对比各个编程语言的功能特性,市场需求以及可见的未来增长潜力等因素后做出选择,这才是明智之举。


原文地址https://dzone.com/articles/5-coding-languages-that-will-disappear-in-10-years


作者 | Program Ace

译者 | 王坤祥

厉害了!2020最好的 10 大国外编程学习网站(文末有彩蛋)

技术交流antrue 发表了文章 • 0 个评论 • 69 次浏览 • 2020-08-31 10:16 • 来自相关话题

在这篇文章中,我收集了10个最佳的编程学习网站,掌握编程技能可能是帮助你走出舒适区的一大步,新手程序员通常会觉得程序员市场的竞争太激烈,工作太有挑战性等。但是,据统计,学习编程是值得你花时间去做的事情,原因如下:软件开发人员的平均工资是103,620。软件开发... ...查看全部

在这篇文章中,我收集了10个最佳的编程学习网站,掌握编程技能可能是帮助你走出舒适区的一大步,新手程序员通常会觉得程序员市场的竞争太激烈,工作太有挑战性等。

但是,据统计,学习编程是值得你花时间去做的事情,原因如下:

软件开发人员的平均工资是103,620。

软件开发市场上有很多职位空缺。

简而言之,软件开发是一个快速发展的领域,在这个领域找工作相对不会那么费劲,除此之外,还有大量的学习资源可以学习,以下就是我最喜欢的编程学习网站:

1.Codegym

Codegym是一个以Java为中心的平台,它比其他平台上都要更加深入的介绍Java,这个网站将理论和实践知识相结合,你可以在上面找到1200多个练习项目。主要具备以下特点:

互动课程,这部分有500多小时的编程课程

内置IDE,帮助你解决实践问题

活跃的Java社区

这个平台通过游戏化和故事化让学习变得有趣

地址:https://codegym.cc/ (支持Google账号直接登录)

这个网站还有一大亮点是,提供了在线IDE ,并且可以通过解决 Task 的方式来学习。当你没解决完一个 Task ,你就可以获得相应的奖励。

2. Treehouse

Treehouse是另一个将编程知识简单化讲解的平台,它会教你处理特定项目所需的技能,比如创建一个APP,创建一个WordPress博客等。Treehouse支持所有流行的编程语言——Java、Python、c++、Ruby和其他语言,具有以下好处:

现实环境中练习问题

与导师面对面的交流

离线访问

地址: https://teamtreehouse.com/ (需要另外注册账号)

3. Khan Academy

尽管Khan Academy没有Java或Python教程,但该平台是理解计算机科学基本概念的一个很好的起点,完成这些课程后,你将会了解数据结构和算法相关的知识,并获得“程序员思维”,这个网站具有以下特点:

免费的课程

涉及前端开发和数据库管理

涵盖常见的算法解答

易于使用的界面

有助于巩固知识的小测试

并且,这个网站目前已经支持简体中文版,但是中文版支持的课程比较少。

地址:https://www.khanacademy.org/computing/computer-science (支持Google账号直接登录)

4. Udemy

Udemy是一个领先的在线教育平台,提供大多数编程语言课程,用户群体也相当大,课程分为收费和免费,这是由讲师决定,价格从10美元到几百美元不等,主要具有以下特点:

广泛的编程语言选择

可以按照自己的节奏获取学习材料

地址:https://www.udemy.com/(支持Google账号直接登录)

5. Code4Startup

如果您想在创业领域获得更多实际经验,Code4Startup是一个很好的平台,可以很好地利用你的开发技能。这个项目允许初级程序员为实际的初创公司编写代码,同时,企业主可以节省招聘人才的费用。

除了作为一个实践场所,Code4Startup还为编程专业的学生提供了大量的免费课程。就我个人而言,Learn Ruby on Rails、Heroku和Wistia API等多个课程我都很喜欢。

**地址:**https://code4startup.com/(支持Google、Github账号直接登录)

6. One Month

如果你为“我如何在一个月内学会一门编程语言?”,请务必查看这个平台。这个编程网站可以帮助初级程序员快速掌握JS、Python和Ruby的基本概念,遗憾的是,该平台上的大多数课程都是付费,尽管少数免费课程,还有一个讨论编程热点新闻的博客。

地址:https://onemonth.com/ (需要另外注册账号)

7. PluralSight

Pluralsight的课程从入门到高级,涵盖了所有流行的编程语言,以及数据科学的基础知识,主要具有以下特性:

课程范围广泛

有专门针对高级开发人员的课程

广泛的订阅计划

地址:https://www.pluralsight.com/codeschool (需要另外注册账号)

8. Coursera

与Udemy一样,Coursera是另一个值得CS和编程学生学习的资源。与Udemy不同的是,该平台关注的是大学层面的深入研究,而不是实践理念。

这个平台是计算机科学专业的一个强大的资源,你可以通过它学习斯坦福大学、密歇根大学等学校的课程。

地址:https://www.coursera.org/ (支持facebook和apple账号)

9. Freecodecamp

这是最大的技术社区之一,拥有相当广泛的教程和课程,这在我看来是其他网站都没法比的,你会在这里找到实用的、基于项目的作业——这就是为什么一旦你掌握了扎实的编程理论,最好还是去Freecodecamp看看的原因,与此同时,Freecodecamp上拥有大量的免费资源。

地址:https://www.freecodecamp.org/ (支持Google、Github等账号直接登录)

10. Codewars

Codewars是一个代码练习网站,能够培养程序员,特别是编程新手的逻辑思维能力,如果你经常对编程提不起动力学习,那么,Codewars你值得拥有,砌墙式进阶的方式会让你对编程更加感兴趣。除了刷题进阶之外,它的社区也值得关注,在这里你可以看到许多困扰着程序员的问题,还有别人解答的解决方案

地址:https://www.codewars.com/


11. GeekOnline

1.png

让我们把目光回到国内,由通信云技术领导者融云推出的开发者社区 GeekOnline 正式与全球极客们见面啦!崇尚科技、自由和创造力的极客精神,GeekOnline 致力于成为一个创意与价值兼备、兴趣和温度并存的技术社区。

这是一个开发者们的技术乐园,也是国内首个全面覆盖了 IM、RTC 等通信云技术的极客社区。在移动互联网概念中,设想所有的客户端都“永远在线”,即每条消息发出必有回响。Online 既是一种态度,也是一种精神,正如 IM 所连接的每一个用户,极客永远在线。

地址:https://geekonline.rongcloud.cn/


原文地址:https://levelup.gitconnected.com/its-time-to-start-learning-coding-top-20-best-websites-to-learn-programming-in-2020-9c5105c76c96


养成好的编码习惯受益终生

活动王叫兽 发表了文章 • 1 个评论 • 46 次浏览 • 2020-08-27 11:51 • 来自相关话题

前言我经常能听到一些对话狗腿子A:哇 我刚刚去改**项目的代码,看的我有点怀疑人生狗腿子B: 我现在项目的跟屎山一样狗腿子C: 我隔壁那哥们每天写代码都特别随性,我有点按耐不住我的刀.....今天跟大家聊聊一些 我眼中 好孩子的编码习惯,而不是代码风格习惯 ,... ...查看全部

前言

我经常能听到一些对话

狗腿子A:哇 我刚刚去改**项目的代码,看的我有点怀疑人生

狗腿子B: 我现在项目的跟屎山一样

狗腿子C: 我隔壁那哥们每天写代码都特别随性,我有点按耐不住我的刀

.....


今天跟大家聊聊一些 我眼中 好孩子的编码习惯,而不是代码风格习惯 ,当然还是强烈建议大家代码风格跟psr-12和psr-1靠齐。

psr-1基础编码规范 、 psr-12编码规范托充 

This specification extends, expands and replaces PSR-2, the coding style guide and requires adherence to PSR-1, the basic coding standard. 


推荐一本《代码整洁之道》,这本书我已经书都快翻烂了,墙裂推荐!!! 


不过度的if嵌套判断

案例背景

有个函数需要判断用户是否参与活动


案例代码

    if (用户 == VIP) {
        if (用户的过期时间 <= 1个月内) {
            if (用户没参加过任务) {
                return true;
            }
        } else {
            return false;
        }
    } else {
        return true
    }

面对这种多条件的判断可以试着用拦截法和逆向思维

拦截法只要符合条件立马返回结果,不再嵌套的if。可以理解成横向判断变成纵向判断。

舒适感

从上往下看 > 从左往右看

逆向思维 大家上学的时候都了解过,与其漫天去找符合的条件还不如找不符合条件,这样的逻辑代码可以少很多。

    if (用户 != VIP) {
        return true;
    } 
    
    if (用户参加过任务) {
        return false;
    }
    
    if (用户的过期时间 <= 1个月内) {
        return true;    
    }
    
    return false;

    

不过度的try-catch嵌套

我遇到过很多项目都过度嵌套try-catch导致最上层的try-catch catch了寂寞。


案例代码

function insertUser($data)
{
    try {
        userIsInValid();
    } catch (Exception $exception) {
    }
}
function userIsInValid()
{
    try {
        //逻辑判断
    } catch (Exception $exception) {
        return true;
    }
    return true;
    
}

这样的代码没有问题,但是如果假设userIsInValid真的发生代码级的错误没法知道那里出问题,虽然不会破坏业务的健壮性。

可能有人说了在Excetion加个日志,但是如果嵌套的try-catch多了,排查日志也是一件很痛苦的事情。

1.尽可能业务最上层包裹异常 除非网络IO请求函数。

2.如果非要异常嵌套 需要定义每个异常的类型。

3.尽可能根据特定的异常进行catch 不建议直接catch Exception。

4.异常和日志是个cp,还是不要忘记了。



不要用if-else做错误类型判断

案例代码 (来源某个网民前段时间咨询)


这样的代码可能写起来特别舒服,但是后期进行业务的增加改写和时间的沉淀,容易变成让人害怕的屎山代码。


我们用mapping错误码来调整下

function packApiDataByOrderError($code)
{
    $errorCodeMappins = [
        "NOTENOUGH" => [
            "code" => 400014,
            "wx_message" => "Company have no enough money to pay",
            "error_message" => "企业余额不足"
        ],
        "AMOUNT_LIMIT" => [
            "code" => 400015,
            "wx_message" => "Amount limit",
            "error_message" => "金额超限或被微信风控拦截"
        ],
        .....
    ];
    if (array_key_exists($code, $errorCodeMappins)) {
        packApiData(
            $errorCodeMappins[$code]['code'],
            $errorCodeMappins[$code]['wx_message'],
            [],
            $errorCodeMappins[$code]['error_message']
        );
    }
    packApiData(
        999999,
        "undefined message",
        [],
        "未知错误"
    );
}

建议errorCodeMappins不要放在函数内,可以放在类顶部或者专门枚举类。

通过errorCode 可以避免调整主流程代码,能够保证主流程的代码比较精简也能对不同的code进行错误的定义

if ($code == "SEND_FAILED") {
    // 付款错误,要查单来看最终结果
    if ($orderInfo[1]['status'] == 'SUCCESS') {
        // 还是成功给了,扣回余额
        PDOQuery($dbcon, 'UPDATE user SET money=money-? WHERE open_id=?', [$payAmount, $openId], [PDO::PARAM_INT, PDO::PARAM_STR]);
        packApiData(200, 'success', [$orderInfo[1]]);
    } else {
        packApiData(400017, 'Weixin pay failed', [], '微信支付付款失败');
    }
}
packApiDataByOrderError($code);

在合适的场景使用设计模式

上述可能只能针对错误码进行改造,如果万一我们需要不同的错误进行逻辑处理还怎么办。这时候可以考虑用设计模式 (比如用以多态取代条件表达式)

设计模式固好但不要过度使用,不然整个项目更难维护,你要坚信未来的你队友不知道是什么样的生物 


$callbackCodeMappings = [
    "SEND_FAILED" => OrderSendFailed::class,
];
if (array_key_exists($code, $callbackCodeMappings)) {
    $class = new $callbackCodeMappings[$code];
    $class->handle();
}
interface OrderStateImp
{
    public function handle($context);
}
class OrderSendFailed implements  OrderStateImp
{
    public function handle($context)
    {
    }
}

$callbackCodeMappings同样建议配置专门枚举文件内。

给出得代码比较粗糙,其实可以更加健壮性的做一些判断

统一处理浮点运算结果

由于php是弱对象语言,所以面对一堆情况总能出现,这个订单数据怎么不对了,接口有问题。

$int = 0.58; var_dump(intval($int * 100));
output:57

在浮点数里面 58是被视为57.999999999999999999999……9999无限接近58 

再intval强制转换乘整型的时候就默认采用截取法取整

所以最好养成一个好习惯每次在计算浮点数的时候用

BC Math

$int = 0.58;
intval(strval($int * 100))

或者使用BC MATH

bcmul(0.58, 100, 0);

鼓励用全局错误码来控制错误

写接口的我们对以下的json格式特别熟悉

{
    "success": true,
    "error_code": 0,
    "message": "",
    "results": []
}

对以下的代码也已经熟悉

if (***) {
    $this->error(999,"****", []);
}

这样的结果的错误码容易重复没有统一管理,事实上唯一错误码应该有以下帮助。

1.前端可以根据错误码做逻辑处理

2.根据错误码能直接快速定位到错误代码

建议

 1050001,
        "message" => "用户已被停用"
    ];
}
$this->error(UserErrorCode::USER_DISABLE_ERROR);

错误码建议

1-2位 - 项目码 | 3-4位 - 模块码 | 5-7位具体业务错误码

可靠的命名规范

不可靠的命名总会让人误导。

比如变量命名为userArrayList 我以为是个数组列表变量,事实上这个特么是个对象列表。

1.做有意义的区分

比如 singleUserItem跟userItem有啥区别 

比如 getUserList跟getUsers有啥区别


2.可以通过搜索翻译能知道的变量含义 

不要把变量贴入搜索翻译会出现七七八八的东西

3.如果真的不知道该怎么翻试试用拼音把别硬凹了

比如之前做百度的一个接口对接

变量命名为hundredDegree而不是baidu


其他的可以参照《代码简洁之道》


擅用middleware

middleware可以理解成观察者模式,我们开发的接口总会遇到很多同样操作,比如

1.身份检测

2.权限判断

3.请求参数filter调整

4.记录接口信息

5.接口限流

我见过挨个接口去实现、也见过初始化一个ControllerBase的类,实现这些,子类的Controller去继承这些。

其实我们可以抽离成middleware去实现


好处可以根据不同接口对middleware进行组合选择,而不是对代码进行各特殊化处理.


函数的单一职责

最最最最后也是最重要的,代码的恶心大多数来源于函数的职责不清晰,有全都塞在一起的、东一块西一块的。

其实关于单一职责有很多文章在描述,如何去检验或者去写符合标准的单一职责。

画流程图

如果你能把业务的流程图画的特别清晰,那么你的函数的职责也就定下来了。



最后

上述为洪光光心中的好孩子的习惯,也有可能是你眼中坏孩子的习惯。如果你认为是坏孩子的习惯或者认为还有其他好孩子的习惯欢迎评论撕逼讨论。

毕竟


留个彩蛋 看看大家怎么实现 

写一个函数returnScoreResult,请根据输入的分数,返回对应的成绩的等级。

1.如果分数小于0或者大于100 返回 【无效分数】

2.如果分数>=0,<60 返回 【不及格】

3.如果分数>=60,<70 返回【及格】

4.如果分数>=70,<80 返回 【一般】

5.如果分数>=80, <90 返回 【良好】

5.如果分数>=90, <100 返回 【优秀】

6.如果分数=100 返回【满分】


融云“拍了拍”你,8.13 直播钜惠!福利大派送!

科技前线admin 发表了文章 • 0 个评论 • 92 次浏览 • 2020-08-07 15:32 • 来自相关话题

恭喜你,关注到了一场线上送福利的活动!融云6周年,IM+RTC套餐冰点价,6折起!携手 趣配音、极客邦科技、三基同创带着 趣配音VIP月卡、游戏机+技术图书套装、极客时间&QCON+课程包、智能手表等好礼向你大步走来◆ 参... ...查看全部

恭喜你,关注到了一场线上送福利的活动!


融云6周年IM+RTC套餐冰点价,6折起


携手 趣配音极客邦科技三基同创


带着 趣配音VIP月卡游戏机+技术图书套装极客时间&QCON+课程包智能手表等好礼


向你大步走来


 参与方式很简单:成功报名就可以


◆ 送福利的方式很直接:报名成功后,即可进入活动群,多轮抽奖,一键操作,即开即中 


如果你够幸运,还能 解锁惊喜福利 


8月13日 晚 19:00 - 20:30


在线开奖!一年就一次!

(悄悄地说,中奖率超高)


别犹豫,快上车

0807-年中大促直播长图-含奖品.png

点击参与活动立即报名参与

编程太无聊?来当导演吧~

活动融云那些事 发表了文章 • 22 个评论 • 2877 次浏览 • 2020-08-06 15:44 • 来自相关话题

对不起~十分抱歉地通知你——当看到此贴的时候,你的“码农”生涯也许就快结束了~曾经~你为之脱发不止的 PHP~JAVA~C,都将离你而去~那个充满名利的娱乐圈,已经敞开了大门从此,你将失去自由,因为每时每刻都有狗仔跟拍~所以~你只能被逼无奈的开着法拉利,带上蛤... ...查看全部

对不起~十分抱歉地通知你——

当看到此贴的时候,你的“码农”生涯也许就快结束了~

曾经~你为之脱发不止的 PHP~JAVA~C,都将离你而去~

那个充满名利的娱乐圈,已经敞开了大门

从此,你将失去自由,因为每时每刻都有狗仔跟拍~

所以~你只能被逼无奈的开着法拉利,带上蛤蟆镜~(也许身边还有一个当演员的女朋友)

因为~你将成为一名~大导演!

 

都说“码农是最具有艺术天赋的群体”~

虽然那一句“hello world”深深地禁锢了你们的肉体,但你们的灵魂永远属于斯坦尼!

我敬爱的“猿”类们——

看着那些自以为是的外行,永远以“格子衫、地中海、单身狗”来固化你们

难道,你的愤怒还要继续隐藏在“//”之后吗?

 

事情是这样的

上半年,融云为展现其“实时音视频”的产品优势,

寻找了多家创意公司甚至影视公司拍摄宣传片~

但他们对开发者真实工作场景的无知,让我们愁眉不展

正应了那句——再牛逼的编剧,也编不出开发者的人生轨迹~

所以,与其让别人“瞎黑”,不如让程序员来“自黑”~

~融云重金邀请不仅会编程,而且还会编剧的“开发文青”

为其全新升级的“实时音视频”编写或执导产品短剧——

 

不是谁都能参加——仅限开发者(不懂编程的,请绕道~)

 

周期还比较紧张——故事大纲选拔截止到20208月31

 

不玩一二三等奖——同时选定10部优秀剧本

 

你最想了解的那些俗事——

1. 选中者将获得5位数或以上的酬劳//别多想~不会超过7位数;

2. 你最想和哪位女演员聊剧本,我们来协调(除奥斯卡影后~);

3. 也许还能参加个电影节,蹭个红地毯,对着镜头说声“hello world”;

 

激动完毕,先看看需求文档吧——

1. 结合开发者真实工作场景,撰写有共鸣度、有笑点(有泪点也OK)的故事;

2. 在故事中,巧妙植入融云“实时音视频”产品优势(植入的越巧妙越好);

3. 单集故事时长控制在3-5分钟;

4. 如果还会画“分镜头剧本”,那就更完美了!

5. 创作者必须保证故事原创性,如出现版权纠纷,由创作者自行解决,融云无连带责任;

 

这次的开发路径是由我们设计的——

1. 开发者先将原创故事的大纲发到本帖的评论区;

2. 融云将对大纲内容进行挑选,选中后将私信与作者沟通详细剧本;

3. 作者将剧本发至指定活动邮箱,融云将安排专业的编剧顾问配合作者对剧本进行优化;

4. 剧本优化后,融云将根据作者(此时应称为“导演”)的要求协调拍摄所需的有关资源;

5. 确定拍摄时间、拍摄地点,并如期拍摄制作;

6. 影片将在B站等视频网站进行首页置顶播放,融云还将协调影片参加相关的评选活动;

 

不知道有木有把事情说清楚,

如果少侠有疑问,可在“评论区”留言,我们将及时解答~

最后,来个“定场诗”收尾——

编程生涯苦无岸,需求文档似高山;

Java&C+亦无趣,秃顶单身格子衫;

日复一日无牵绊,是否也想起波澜?

索性挥毫三千字,当个导演来玩玩!

乐哉!乐哉!

 

(融云“实时音视频”产品功能详见“融云”官网~)


编程太无聊?来当导演吧~

活动融云那些事 发表了文章 • 22 个评论 • 2877 次浏览 • 2020-08-06 15:44 • 来自相关话题

对不起~十分抱歉地通知你——当看到此贴的时候,你的“码农”生涯也许就快结束了~曾经~你为之脱发不止的 PHP~JAVA~C,都将离你而去~那个充满名利的娱乐圈,已经敞开了大门从此,你将失去自由,因为每时每刻都有狗仔跟拍~所以~你只能被逼无奈的开着法拉利,带上蛤... ...查看全部

对不起~十分抱歉地通知你——

当看到此贴的时候,你的“码农”生涯也许就快结束了~

曾经~你为之脱发不止的 PHP~JAVA~C,都将离你而去~

那个充满名利的娱乐圈,已经敞开了大门

从此,你将失去自由,因为每时每刻都有狗仔跟拍~

所以~你只能被逼无奈的开着法拉利,带上蛤蟆镜~(也许身边还有一个当演员的女朋友)

因为~你将成为一名~大导演!

 

都说“码农是最具有艺术天赋的群体”~

虽然那一句“hello world”深深地禁锢了你们的肉体,但你们的灵魂永远属于斯坦尼!

我敬爱的“猿”类们——

看着那些自以为是的外行,永远以“格子衫、地中海、单身狗”来固化你们

难道,你的愤怒还要继续隐藏在“//”之后吗?

 

事情是这样的

上半年,融云为展现其“实时音视频”的产品优势,

寻找了多家创意公司甚至影视公司拍摄宣传片~

但他们对开发者真实工作场景的无知,让我们愁眉不展

正应了那句——再牛逼的编剧,也编不出开发者的人生轨迹~

所以,与其让别人“瞎黑”,不如让程序员来“自黑”~

~融云重金邀请不仅会编程,而且还会编剧的“开发文青”

为其全新升级的“实时音视频”编写或执导产品短剧——

 

不是谁都能参加——仅限开发者(不懂编程的,请绕道~)

 

周期还比较紧张——故事大纲选拔截止到20208月31

 

不玩一二三等奖——同时选定10部优秀剧本

 

你最想了解的那些俗事——

1. 选中者将获得5位数或以上的酬劳//别多想~不会超过7位数;

2. 你最想和哪位女演员聊剧本,我们来协调(除奥斯卡影后~);

3. 也许还能参加个电影节,蹭个红地毯,对着镜头说声“hello world”;

 

激动完毕,先看看需求文档吧——

1. 结合开发者真实工作场景,撰写有共鸣度、有笑点(有泪点也OK)的故事;

2. 在故事中,巧妙植入融云“实时音视频”产品优势(植入的越巧妙越好);

3. 单集故事时长控制在3-5分钟;

4. 如果还会画“分镜头剧本”,那就更完美了!

5. 创作者必须保证故事原创性,如出现版权纠纷,由创作者自行解决,融云无连带责任;

 

这次的开发路径是由我们设计的——

1. 开发者先将原创故事的大纲发到本帖的评论区;

2. 融云将对大纲内容进行挑选,选中后将私信与作者沟通详细剧本;

3. 作者将剧本发至指定活动邮箱,融云将安排专业的编剧顾问配合作者对剧本进行优化;

4. 剧本优化后,融云将根据作者(此时应称为“导演”)的要求协调拍摄所需的有关资源;

5. 确定拍摄时间、拍摄地点,并如期拍摄制作;

6. 影片将在B站等视频网站进行首页置顶播放,融云还将协调影片参加相关的评选活动;

 

不知道有木有把事情说清楚,

如果少侠有疑问,可在“评论区”留言,我们将及时解答~

最后,来个“定场诗”收尾——

编程生涯苦无岸,需求文档似高山;

Java&C+亦无趣,秃顶单身格子衫;

日复一日无牵绊,是否也想起波澜?

索性挥毫三千字,当个导演来玩玩!

乐哉!乐哉!

 

(融云“实时音视频”产品功能详见“融云”官网~)


如果再给我一次机会,我希望我从未学过编程

技术交流徐凤年 发表了文章 • 0 个评论 • 31 次浏览 • 2020-11-04 14:39 • 来自相关话题

我是一名程序员。你可能会觉得我是专业的软件工程师,但实际上,软件工程不只是一个专业,还是一种生活方式。连帽衫、乒乓球、吃不完的零食和苏打水……都是这种生活的一部分。但虽然这个职业可以给人带来那么多回报,我还是要坦白一件事:有时我希望自己从来没有学过编程。怪癖自... ...查看全部

我是一名程序员。你可能会觉得我是专业的软件工程师,但实际上,软件工程不只是一个专业,还是一种生活方式。连帽衫、乒乓球、吃不完的零食和苏打水……都是这种生活的一部分。但虽然这个职业可以给人带来那么多回报,我还是要坦白一件事:

有时我希望自己从来没有学过编程。

怪癖

自从我开始学习编程以来,就再也不能像以前那样轻松地浏览网页了。我无法再平静地接受在各种网站上遇到的各种 bug。每当有奇怪的事情发生时,我的好奇心就上了头,然后就会打开浏览器开发工具开始调试网页。

尝试提交表单时出现加密错误消息?看到这样的错误,我会深入 JavaScript 控制台,查找错误消息,深入研究源代码,并仔细观察进进出出的网络请求。页面 UI 看起来有些过时,或者页面布局很漂亮?不管怎样,我都会调整浏览器窗口大小,检查页面响应屏幕尺寸的能力。什么?你竟然选择了 Comic Sans 字体?我可能永远不会再使用你或你们公司的产品了。

微信图片_20201104143349.jpg

请不要使用 ComicSans

职业生涯

觉得我的这些浏览习惯很怪异吗?这还不算啥,我和其他软件工程师的对话会让你印象更深刻的。总体而言,工程师往往固执己见。我们的血液中流淌着迂腐的基因。你喜欢使用空格还是 tabs 缩进代码?你更喜欢 Vim 还是 Emacs?Chrome 还是 Firefox?

聪明点的人会问到,这些对话“真的重要吗?”。Bikeshedding(在琐碎细节上浪费时间)是确实存在的现象,大家都需要经常反省。

微信图片_20201104143403.jpg

xkcd——真正的程序员

随着越来越多的公司采用敏捷方法,我们还得时常考虑在 Waterfall、Scrum 或 Kanban 开发生命周期之间权衡取舍。我们所有人都喜欢鄙视 Waterfall,毕竟这是老式的低效率公司所使用的遗留品。但是 Scrum 与看板之战还在继续。

你竟然是 Scrum 的信徒?我敢打赌,你为了满足那些 time box 会仓促提交混乱的代码,才能在冲刺结束之前赶上假想的时限。

微信图片_20201104143416.jpg

说到敏捷,故事的重点在哪里?时间尺度?努力?复杂性?风险?这些都有?你我可能会花费几小时时间来争论每个定义的优缺点,最后还是无法达成共识。

在某些时候,这种争论可能会演变为更多的 bikeshedding,但能够相互理解和有效沟通是至关重要的,其中就包括了对我们日常使用的术语达成共识。

前端开发带来的争论又是数不清的,其中最典型的是:“我们要到什么时候才会放弃对 Internet Explorer 的支持?”我必须在“讨厌 Internet Explorer,想要放弃它”和了解还在用它的客户需求之间找到完美的平衡点。

微信图片_20201104143428.jpg

持续学习

持续学习是每一位开发者必备的能力,因为技术形势日新月异。每月都有成百上千的新库和框架发布。JavaScript 倦怠不是什么幻觉,“学不动了”也不仅仅是调侃。

我得花一天的时间配置 Webpack、Rollup 和 Babel 以使用最新的 ES6+ 语法,而旁人听我这么说就像在听天书。Angular 和 AngularJS 截然不同?LitElement、Svelte 和 Stencil 似乎是很有前途的 Web 组件解决方案?Deno 可能是下一个 Node?听着都像是谜语。

当有人问我做什么工作时,我总会蹦出来一堆术语:“我通常使用前端技术,例如 HTML、CSS 和 JS。有时我必须使用 PHP 或 SQL,但我更多是 MEAN/MERN 栈开发人员。有时我使用 Heroku 之类的 PaaS 技术,还有时我会使用 AWS 或 GCP 之类的 IaaS 提供商。”

微信图片_20201104143437.jpg

个人生活

在业余时间里,我读的书干货十足,例如《干净代码》《重构》和《领域驱动设计》。我不是在看教科书,就是在阅读里面写着可疑建议的文章,或观看一些编程教程。关于编程的播客更能吸引人,这样我就能在路上听某人谈论写代码的方法,然后我又可以花一天时间谈相关话题了。

除了在线获取内容外,软件工程师还会花费大量个人时间来在网上创建内容。我个人的兴趣是构建简单的应用和游戏,其实没人看得到。这是浪费时间吗?也许是吧,但的确挺有意思的。


微信图片_20201104143447.jpg企业 BS 生成器应用

事实是

可事实是,编程给了我创造和创新的机会。它帮助我将创意变为现实,让我几乎从零开始构筑了很多东西。软件工程使我能够解决有趣而艰巨的挑战,理想情况下还能让人们的生活更轻松一些。编程使我的思维更具逻辑性。编程让我有机会不断学习,我还能以编程为职业获取薪水!

事实是,我喜欢编程。

参考阅读

https://hackernoon.com/i-wish-i-never-learned-to-code-7a1m3wwx

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

技术交流大神庵 发表了文章 • 0 个评论 • 48 次浏览 • 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


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

技术交流admin 发表了文章 • 0 个评论 • 81 次浏览 • 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 视频首帧的显示时间,改善了用户体验。


Ruby会消失吗?盘点十年后将要消失的五种编程语言

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

本文作者从自己的观点出发,介绍了未来 20 年内可能消失的 5 个编程语言,并给出了具体的原因。最后对想要学习编程的初学者给出了学习建议。随着时间的流逝,程序员们发现了更新、更简单的工作方式,新的编程语言如雨后春笋般出现,但只有少数编程语言能成为社区的新宠。这... ...查看全部

ÉãͼÍø_501237513_wx_ÌØÁ¢Äá´ïСÕò£¨·ÇÆóÒµÉÌÓã©.jpg


本文作者从自己的观点出发,介绍了未来 20 年内可能消失的 5 个编程语言,并给出了具体的原因。最后对想要学习编程的初学者给出了学习建议。

随着时间的流逝,程序员们发现了更新、更简单的工作方式,新的编程语言如雨后春笋般出现,但只有少数编程语言能成为社区的新宠。这种进步的一个副作用是一些古老的编程语言必然会跟历史一样被人们遗忘。如果一个编程语言无法随着时间的推移提升其价值,那么它的用户群终将会流失,并逐渐淡出人们的视线,或者成为更新一代编程语言的基础。

最近,古老的 COBOL 编程语言上了热搜。在 1960 年代和 1970 年代,它曾经是许多美国银行和政府机构的首选的编程语言,但最终被更加简单有效的编程语言所取代。但是,使用 COBOL 构建的系统仍然存在,当一些政府机构发现他们需要通过更新代码来全面改革失业系统时,才发现业内没有几个开发人员可以熟练使用该编程语言。

沧海桑田,COBOL 早已物是人非。我们当前的许多编程语言也注定会有相似的下场。本文中,我们将分析未来 20 内最终会消失的 5 种编程语言。我知道这可能会伤害到那些正在使用这几个编程语言的程序员的内心,所以在开始介绍之前首先声明下这只是我个人的看法和预测。

1. Ruby

Ruby 在 1999 年发布后立即受到程序员们的热捧,它能够快速构建应用程序的特性给程序员留下了非常深刻的印象。紧随其后,备受欢迎的 Ruby on Rails 框架于 2004 年发布,由于 Ruby 和 Rails 这两个名称在当时几乎成为了同义词,因此 Ruby 很快地被推到了程序员最喜欢的编程语言排行榜首位。它经历了一个坚实的十年,稳居编程语言排行榜前列,一度成为众人瞩目的焦点,但是最近的十年它并不好过,所以 Ruby 纳进了我的淘汰清单列表。

为什么 Ruby 会逐渐消退呢?导致其排名下降的因素之一是其执行速度。由它构建的应用程序运行速度往往比其他流行的编程语言,比如 JavaScript、Go 和 Python 构建的应用(在某些框架下)运行速度慢。同时期下,后三者已经发展到可以满足当时的需求,而 Ruby 在很多方面一直在原地踏步,没有什么进步。例如,让 Ruby on Rails 名噪一时的 MVC 架构在现在被很多编程人员看来是笨重和过时的。

2. Visual Basic

考虑到 Visual Basic 是公认的程序员最不喜欢的编程语言之一的事实,将它包含在淘汰列表中是无可厚非的。它于 1991 年由微软发布,作为构建 Windows 的主要工具,确实实现了此目的,但多年来经常出现问题。程序员使用 VB 进行开发的热情差距很大,随着 2000 年 C#的发布,这种差距逐渐进一步扩大。由于 C#更加简洁,提供了更广泛的功能,并且更适合于云服务和移动开发(在当前市场中扮演着重要角色),因此很多开发人员都放弃使用 VB 转而投向 C#怀抱。最重要的是,似乎它的创建者也都无情地放弃了它,因为微软曾表示他们没有进一步发展它的计划。

3. Haskell

Haskell 是另一个古董级别的编程语言,它于几十年前创建,在世纪之交前一直被使用。不幸的是,对于 Haskell 来说,属于它的时代很快就要结束了。研究人员和学者大多将 Haskell 用于构建程序,以对其工作进行复杂的排列和外推计算,普遍认为它很难学习。学习路径困难必然导致非常有限的活跃用户,而 Haskell 的上一个最新的稳定版本是在 2010 年发布,这对于促进它本身的发展无济于事。

4. Perl

与 Visual Basic 命运一样,Perl 不被大部分使用它的开发人员所喜欢。在 Perl 于 1987 年开始流行时,它被誉为是适合任何一个人的编程语言,无论你是编程的初学者还是专业人士都可以使用,同时还被宣传可以用于各种各样的场景。但是,1991 年 Python 的出现,彻底粉碎了 Perl 主导编程世界的幻想。

尽管 Python 的语法相比 Perl 稍显繁琐,更加严格,但 Python 让用户使用脚本实现目标的过程变得更加简单直接,这改变了当时的游戏规则。Python 在 2000 年代开始让 Perl 黯然失色,并一度成为编程入门的首选语言。在过去的 15 年中,Perl 的使用量一直在稳步下降,我们看不到任何它将停止下滑的迹象。

5. Objective-C

Objective-C 与 Apple 的关系类似于运动相机与 GoPro 的密切联系——两者都是他们所属公司专用的工具。尽管 Objective-C 已经在 macOS、iOS 和 OS X 开发中使用了二十多年(1996 年至今)的时间,但如今已经很少有程序员在使用该语言,因为 Swift 横空出世了。

Swift 由 Apple 构建并于 2014 年发布,旨在成为 macOS、iOS 及其他几个 Apple 相关平台的新编码标准。Obj-C 在很大程度上是基于复杂的 C 语言创建的,而 Swift 有效地删除了这些复杂的元素,同时增加了一些新的功能特性,例如自动内存管理,对 Cocoa Touch 的支持以及类型安全的集成。

Swift 设法实现和维护了较高的语言稳定性和用户采用率,因为 Apple 公司仍在坚持重写 Obj-C 库,以使旧语言的使用者可以很容易地进行过渡。尽管仍然有很多关于这两种选择哪个更好的讨论,但当前的趋势表明,Objective-C 终将被淘汰。

总结

尽管上述提到的语言似乎都处于下降趋势,但不能保证它们最终一定会消失。如果他们的创造者和社区投入足够的精力来更新和维护它们,添加新功能以及有用的特性,这些编程可能会卷土重来,恢复生命力。但事实是广大程序员更有可能继续涌向更新更好的选择。

如果你正在考虑开始学习哪种语言,那么有许多因素需要考虑。如果你只是将编程当做一种兴趣爱好,你可以聆听一下自己内心并选择最能激发你兴趣的那个来学习,即便所选的编程语言不再被广泛使用甚至面临淘汰。另一方面,如果你正在从事编程相关的工作,那么不应该只选择最流行的编程语言,也不应该赌一把选择未来可能会成为最流行的编程语言。而应该通过仔细对比各个编程语言的功能特性,市场需求以及可见的未来增长潜力等因素后做出选择,这才是明智之举。


原文地址https://dzone.com/articles/5-coding-languages-that-will-disappear-in-10-years


作者 | Program Ace

译者 | 王坤祥

厉害了!2020最好的 10 大国外编程学习网站(文末有彩蛋)

技术交流antrue 发表了文章 • 0 个评论 • 69 次浏览 • 2020-08-31 10:16 • 来自相关话题

在这篇文章中,我收集了10个最佳的编程学习网站,掌握编程技能可能是帮助你走出舒适区的一大步,新手程序员通常会觉得程序员市场的竞争太激烈,工作太有挑战性等。但是,据统计,学习编程是值得你花时间去做的事情,原因如下:软件开发人员的平均工资是103,620。软件开发... ...查看全部

在这篇文章中,我收集了10个最佳的编程学习网站,掌握编程技能可能是帮助你走出舒适区的一大步,新手程序员通常会觉得程序员市场的竞争太激烈,工作太有挑战性等。

但是,据统计,学习编程是值得你花时间去做的事情,原因如下:

软件开发人员的平均工资是103,620。

软件开发市场上有很多职位空缺。

简而言之,软件开发是一个快速发展的领域,在这个领域找工作相对不会那么费劲,除此之外,还有大量的学习资源可以学习,以下就是我最喜欢的编程学习网站:

1.Codegym

Codegym是一个以Java为中心的平台,它比其他平台上都要更加深入的介绍Java,这个网站将理论和实践知识相结合,你可以在上面找到1200多个练习项目。主要具备以下特点:

互动课程,这部分有500多小时的编程课程

内置IDE,帮助你解决实践问题

活跃的Java社区

这个平台通过游戏化和故事化让学习变得有趣

地址:https://codegym.cc/ (支持Google账号直接登录)

这个网站还有一大亮点是,提供了在线IDE ,并且可以通过解决 Task 的方式来学习。当你没解决完一个 Task ,你就可以获得相应的奖励。

2. Treehouse

Treehouse是另一个将编程知识简单化讲解的平台,它会教你处理特定项目所需的技能,比如创建一个APP,创建一个WordPress博客等。Treehouse支持所有流行的编程语言——Java、Python、c++、Ruby和其他语言,具有以下好处:

现实环境中练习问题

与导师面对面的交流

离线访问

地址: https://teamtreehouse.com/ (需要另外注册账号)

3. Khan Academy

尽管Khan Academy没有Java或Python教程,但该平台是理解计算机科学基本概念的一个很好的起点,完成这些课程后,你将会了解数据结构和算法相关的知识,并获得“程序员思维”,这个网站具有以下特点:

免费的课程

涉及前端开发和数据库管理

涵盖常见的算法解答

易于使用的界面

有助于巩固知识的小测试

并且,这个网站目前已经支持简体中文版,但是中文版支持的课程比较少。

地址:https://www.khanacademy.org/computing/computer-science (支持Google账号直接登录)

4. Udemy

Udemy是一个领先的在线教育平台,提供大多数编程语言课程,用户群体也相当大,课程分为收费和免费,这是由讲师决定,价格从10美元到几百美元不等,主要具有以下特点:

广泛的编程语言选择

可以按照自己的节奏获取学习材料

地址:https://www.udemy.com/(支持Google账号直接登录)

5. Code4Startup

如果您想在创业领域获得更多实际经验,Code4Startup是一个很好的平台,可以很好地利用你的开发技能。这个项目允许初级程序员为实际的初创公司编写代码,同时,企业主可以节省招聘人才的费用。

除了作为一个实践场所,Code4Startup还为编程专业的学生提供了大量的免费课程。就我个人而言,Learn Ruby on Rails、Heroku和Wistia API等多个课程我都很喜欢。

**地址:**https://code4startup.com/(支持Google、Github账号直接登录)

6. One Month

如果你为“我如何在一个月内学会一门编程语言?”,请务必查看这个平台。这个编程网站可以帮助初级程序员快速掌握JS、Python和Ruby的基本概念,遗憾的是,该平台上的大多数课程都是付费,尽管少数免费课程,还有一个讨论编程热点新闻的博客。

地址:https://onemonth.com/ (需要另外注册账号)

7. PluralSight

Pluralsight的课程从入门到高级,涵盖了所有流行的编程语言,以及数据科学的基础知识,主要具有以下特性:

课程范围广泛

有专门针对高级开发人员的课程

广泛的订阅计划

地址:https://www.pluralsight.com/codeschool (需要另外注册账号)

8. Coursera

与Udemy一样,Coursera是另一个值得CS和编程学生学习的资源。与Udemy不同的是,该平台关注的是大学层面的深入研究,而不是实践理念。

这个平台是计算机科学专业的一个强大的资源,你可以通过它学习斯坦福大学、密歇根大学等学校的课程。

地址:https://www.coursera.org/ (支持facebook和apple账号)

9. Freecodecamp

这是最大的技术社区之一,拥有相当广泛的教程和课程,这在我看来是其他网站都没法比的,你会在这里找到实用的、基于项目的作业——这就是为什么一旦你掌握了扎实的编程理论,最好还是去Freecodecamp看看的原因,与此同时,Freecodecamp上拥有大量的免费资源。

地址:https://www.freecodecamp.org/ (支持Google、Github等账号直接登录)

10. Codewars

Codewars是一个代码练习网站,能够培养程序员,特别是编程新手的逻辑思维能力,如果你经常对编程提不起动力学习,那么,Codewars你值得拥有,砌墙式进阶的方式会让你对编程更加感兴趣。除了刷题进阶之外,它的社区也值得关注,在这里你可以看到许多困扰着程序员的问题,还有别人解答的解决方案

地址:https://www.codewars.com/


11. GeekOnline

1.png

让我们把目光回到国内,由通信云技术领导者融云推出的开发者社区 GeekOnline 正式与全球极客们见面啦!崇尚科技、自由和创造力的极客精神,GeekOnline 致力于成为一个创意与价值兼备、兴趣和温度并存的技术社区。

这是一个开发者们的技术乐园,也是国内首个全面覆盖了 IM、RTC 等通信云技术的极客社区。在移动互联网概念中,设想所有的客户端都“永远在线”,即每条消息发出必有回响。Online 既是一种态度,也是一种精神,正如 IM 所连接的每一个用户,极客永远在线。

地址:https://geekonline.rongcloud.cn/


原文地址:https://levelup.gitconnected.com/its-time-to-start-learning-coding-top-20-best-websites-to-learn-programming-in-2020-9c5105c76c96


养成好的编码习惯受益终生

活动王叫兽 发表了文章 • 1 个评论 • 46 次浏览 • 2020-08-27 11:51 • 来自相关话题

前言我经常能听到一些对话狗腿子A:哇 我刚刚去改**项目的代码,看的我有点怀疑人生狗腿子B: 我现在项目的跟屎山一样狗腿子C: 我隔壁那哥们每天写代码都特别随性,我有点按耐不住我的刀.....今天跟大家聊聊一些 我眼中 好孩子的编码习惯,而不是代码风格习惯 ,... ...查看全部

前言

我经常能听到一些对话

狗腿子A:哇 我刚刚去改**项目的代码,看的我有点怀疑人生

狗腿子B: 我现在项目的跟屎山一样

狗腿子C: 我隔壁那哥们每天写代码都特别随性,我有点按耐不住我的刀

.....


今天跟大家聊聊一些 我眼中 好孩子的编码习惯,而不是代码风格习惯 ,当然还是强烈建议大家代码风格跟psr-12和psr-1靠齐。

psr-1基础编码规范 、 psr-12编码规范托充 

This specification extends, expands and replaces PSR-2, the coding style guide and requires adherence to PSR-1, the basic coding standard. 


推荐一本《代码整洁之道》,这本书我已经书都快翻烂了,墙裂推荐!!! 


不过度的if嵌套判断

案例背景

有个函数需要判断用户是否参与活动


案例代码

    if (用户 == VIP) {
        if (用户的过期时间 <= 1个月内) {
            if (用户没参加过任务) {
                return true;
            }
        } else {
            return false;
        }
    } else {
        return true
    }

面对这种多条件的判断可以试着用拦截法和逆向思维

拦截法只要符合条件立马返回结果,不再嵌套的if。可以理解成横向判断变成纵向判断。

舒适感

从上往下看 > 从左往右看

逆向思维 大家上学的时候都了解过,与其漫天去找符合的条件还不如找不符合条件,这样的逻辑代码可以少很多。

    if (用户 != VIP) {
        return true;
    } 
    
    if (用户参加过任务) {
        return false;
    }
    
    if (用户的过期时间 <= 1个月内) {
        return true;    
    }
    
    return false;

    

不过度的try-catch嵌套

我遇到过很多项目都过度嵌套try-catch导致最上层的try-catch catch了寂寞。


案例代码

function insertUser($data)
{
    try {
        userIsInValid();
    } catch (Exception $exception) {
    }
}
function userIsInValid()
{
    try {
        //逻辑判断
    } catch (Exception $exception) {
        return true;
    }
    return true;
    
}

这样的代码没有问题,但是如果假设userIsInValid真的发生代码级的错误没法知道那里出问题,虽然不会破坏业务的健壮性。

可能有人说了在Excetion加个日志,但是如果嵌套的try-catch多了,排查日志也是一件很痛苦的事情。

1.尽可能业务最上层包裹异常 除非网络IO请求函数。

2.如果非要异常嵌套 需要定义每个异常的类型。

3.尽可能根据特定的异常进行catch 不建议直接catch Exception。

4.异常和日志是个cp,还是不要忘记了。



不要用if-else做错误类型判断

案例代码 (来源某个网民前段时间咨询)


这样的代码可能写起来特别舒服,但是后期进行业务的增加改写和时间的沉淀,容易变成让人害怕的屎山代码。


我们用mapping错误码来调整下

function packApiDataByOrderError($code)
{
    $errorCodeMappins = [
        "NOTENOUGH" => [
            "code" => 400014,
            "wx_message" => "Company have no enough money to pay",
            "error_message" => "企业余额不足"
        ],
        "AMOUNT_LIMIT" => [
            "code" => 400015,
            "wx_message" => "Amount limit",
            "error_message" => "金额超限或被微信风控拦截"
        ],
        .....
    ];
    if (array_key_exists($code, $errorCodeMappins)) {
        packApiData(
            $errorCodeMappins[$code]['code'],
            $errorCodeMappins[$code]['wx_message'],
            [],
            $errorCodeMappins[$code]['error_message']
        );
    }
    packApiData(
        999999,
        "undefined message",
        [],
        "未知错误"
    );
}

建议errorCodeMappins不要放在函数内,可以放在类顶部或者专门枚举类。

通过errorCode 可以避免调整主流程代码,能够保证主流程的代码比较精简也能对不同的code进行错误的定义

if ($code == "SEND_FAILED") {
    // 付款错误,要查单来看最终结果
    if ($orderInfo[1]['status'] == 'SUCCESS') {
        // 还是成功给了,扣回余额
        PDOQuery($dbcon, 'UPDATE user SET money=money-? WHERE open_id=?', [$payAmount, $openId], [PDO::PARAM_INT, PDO::PARAM_STR]);
        packApiData(200, 'success', [$orderInfo[1]]);
    } else {
        packApiData(400017, 'Weixin pay failed', [], '微信支付付款失败');
    }
}
packApiDataByOrderError($code);

在合适的场景使用设计模式

上述可能只能针对错误码进行改造,如果万一我们需要不同的错误进行逻辑处理还怎么办。这时候可以考虑用设计模式 (比如用以多态取代条件表达式)

设计模式固好但不要过度使用,不然整个项目更难维护,你要坚信未来的你队友不知道是什么样的生物 


$callbackCodeMappings = [
    "SEND_FAILED" => OrderSendFailed::class,
];
if (array_key_exists($code, $callbackCodeMappings)) {
    $class = new $callbackCodeMappings[$code];
    $class->handle();
}
interface OrderStateImp
{
    public function handle($context);
}
class OrderSendFailed implements  OrderStateImp
{
    public function handle($context)
    {
    }
}

$callbackCodeMappings同样建议配置专门枚举文件内。

给出得代码比较粗糙,其实可以更加健壮性的做一些判断

统一处理浮点运算结果

由于php是弱对象语言,所以面对一堆情况总能出现,这个订单数据怎么不对了,接口有问题。

$int = 0.58; var_dump(intval($int * 100));
output:57

在浮点数里面 58是被视为57.999999999999999999999……9999无限接近58 

再intval强制转换乘整型的时候就默认采用截取法取整

所以最好养成一个好习惯每次在计算浮点数的时候用

BC Math

$int = 0.58;
intval(strval($int * 100))

或者使用BC MATH

bcmul(0.58, 100, 0);

鼓励用全局错误码来控制错误

写接口的我们对以下的json格式特别熟悉

{
    "success": true,
    "error_code": 0,
    "message": "",
    "results": []
}

对以下的代码也已经熟悉

if (***) {
    $this->error(999,"****", []);
}

这样的结果的错误码容易重复没有统一管理,事实上唯一错误码应该有以下帮助。

1.前端可以根据错误码做逻辑处理

2.根据错误码能直接快速定位到错误代码

建议

 1050001,
        "message" => "用户已被停用"
    ];
}
$this->error(UserErrorCode::USER_DISABLE_ERROR);

错误码建议

1-2位 - 项目码 | 3-4位 - 模块码 | 5-7位具体业务错误码

可靠的命名规范

不可靠的命名总会让人误导。

比如变量命名为userArrayList 我以为是个数组列表变量,事实上这个特么是个对象列表。

1.做有意义的区分

比如 singleUserItem跟userItem有啥区别 

比如 getUserList跟getUsers有啥区别


2.可以通过搜索翻译能知道的变量含义 

不要把变量贴入搜索翻译会出现七七八八的东西

3.如果真的不知道该怎么翻试试用拼音把别硬凹了

比如之前做百度的一个接口对接

变量命名为hundredDegree而不是baidu


其他的可以参照《代码简洁之道》


擅用middleware

middleware可以理解成观察者模式,我们开发的接口总会遇到很多同样操作,比如

1.身份检测

2.权限判断

3.请求参数filter调整

4.记录接口信息

5.接口限流

我见过挨个接口去实现、也见过初始化一个ControllerBase的类,实现这些,子类的Controller去继承这些。

其实我们可以抽离成middleware去实现


好处可以根据不同接口对middleware进行组合选择,而不是对代码进行各特殊化处理.


函数的单一职责

最最最最后也是最重要的,代码的恶心大多数来源于函数的职责不清晰,有全都塞在一起的、东一块西一块的。

其实关于单一职责有很多文章在描述,如何去检验或者去写符合标准的单一职责。

画流程图

如果你能把业务的流程图画的特别清晰,那么你的函数的职责也就定下来了。



最后

上述为洪光光心中的好孩子的习惯,也有可能是你眼中坏孩子的习惯。如果你认为是坏孩子的习惯或者认为还有其他好孩子的习惯欢迎评论撕逼讨论。

毕竟


留个彩蛋 看看大家怎么实现 

写一个函数returnScoreResult,请根据输入的分数,返回对应的成绩的等级。

1.如果分数小于0或者大于100 返回 【无效分数】

2.如果分数>=0,<60 返回 【不及格】

3.如果分数>=60,<70 返回【及格】

4.如果分数>=70,<80 返回 【一般】

5.如果分数>=80, <90 返回 【良好】

5.如果分数>=90, <100 返回 【优秀】

6.如果分数=100 返回【满分】


融云“拍了拍”你,8.13 直播钜惠!福利大派送!

科技前线admin 发表了文章 • 0 个评论 • 92 次浏览 • 2020-08-07 15:32 • 来自相关话题

恭喜你,关注到了一场线上送福利的活动!融云6周年,IM+RTC套餐冰点价,6折起!携手 趣配音、极客邦科技、三基同创带着 趣配音VIP月卡、游戏机+技术图书套装、极客时间&QCON+课程包、智能手表等好礼向你大步走来◆ 参... ...查看全部

恭喜你,关注到了一场线上送福利的活动!


融云6周年IM+RTC套餐冰点价,6折起


携手 趣配音极客邦科技三基同创


带着 趣配音VIP月卡游戏机+技术图书套装极客时间&QCON+课程包智能手表等好礼


向你大步走来


 参与方式很简单:成功报名就可以


◆ 送福利的方式很直接:报名成功后,即可进入活动群,多轮抽奖,一键操作,即开即中 


如果你够幸运,还能 解锁惊喜福利 


8月13日 晚 19:00 - 20:30


在线开奖!一年就一次!

(悄悄地说,中奖率超高)


别犹豫,快上车

0807-年中大促直播长图-含奖品.png

点击参与活动立即报名参与

编程太无聊?来当导演吧~

活动融云那些事 发表了文章 • 22 个评论 • 2877 次浏览 • 2020-08-06 15:44 • 来自相关话题

对不起~十分抱歉地通知你——当看到此贴的时候,你的“码农”生涯也许就快结束了~曾经~你为之脱发不止的 PHP~JAVA~C,都将离你而去~那个充满名利的娱乐圈,已经敞开了大门从此,你将失去自由,因为每时每刻都有狗仔跟拍~所以~你只能被逼无奈的开着法拉利,带上蛤... ...查看全部

对不起~十分抱歉地通知你——

当看到此贴的时候,你的“码农”生涯也许就快结束了~

曾经~你为之脱发不止的 PHP~JAVA~C,都将离你而去~

那个充满名利的娱乐圈,已经敞开了大门

从此,你将失去自由,因为每时每刻都有狗仔跟拍~

所以~你只能被逼无奈的开着法拉利,带上蛤蟆镜~(也许身边还有一个当演员的女朋友)

因为~你将成为一名~大导演!

 

都说“码农是最具有艺术天赋的群体”~

虽然那一句“hello world”深深地禁锢了你们的肉体,但你们的灵魂永远属于斯坦尼!

我敬爱的“猿”类们——

看着那些自以为是的外行,永远以“格子衫、地中海、单身狗”来固化你们

难道,你的愤怒还要继续隐藏在“//”之后吗?

 

事情是这样的

上半年,融云为展现其“实时音视频”的产品优势,

寻找了多家创意公司甚至影视公司拍摄宣传片~

但他们对开发者真实工作场景的无知,让我们愁眉不展

正应了那句——再牛逼的编剧,也编不出开发者的人生轨迹~

所以,与其让别人“瞎黑”,不如让程序员来“自黑”~

~融云重金邀请不仅会编程,而且还会编剧的“开发文青”

为其全新升级的“实时音视频”编写或执导产品短剧——

 

不是谁都能参加——仅限开发者(不懂编程的,请绕道~)

 

周期还比较紧张——故事大纲选拔截止到20208月31

 

不玩一二三等奖——同时选定10部优秀剧本

 

你最想了解的那些俗事——

1. 选中者将获得5位数或以上的酬劳//别多想~不会超过7位数;

2. 你最想和哪位女演员聊剧本,我们来协调(除奥斯卡影后~);

3. 也许还能参加个电影节,蹭个红地毯,对着镜头说声“hello world”;

 

激动完毕,先看看需求文档吧——

1. 结合开发者真实工作场景,撰写有共鸣度、有笑点(有泪点也OK)的故事;

2. 在故事中,巧妙植入融云“实时音视频”产品优势(植入的越巧妙越好);

3. 单集故事时长控制在3-5分钟;

4. 如果还会画“分镜头剧本”,那就更完美了!

5. 创作者必须保证故事原创性,如出现版权纠纷,由创作者自行解决,融云无连带责任;

 

这次的开发路径是由我们设计的——

1. 开发者先将原创故事的大纲发到本帖的评论区;

2. 融云将对大纲内容进行挑选,选中后将私信与作者沟通详细剧本;

3. 作者将剧本发至指定活动邮箱,融云将安排专业的编剧顾问配合作者对剧本进行优化;

4. 剧本优化后,融云将根据作者(此时应称为“导演”)的要求协调拍摄所需的有关资源;

5. 确定拍摄时间、拍摄地点,并如期拍摄制作;

6. 影片将在B站等视频网站进行首页置顶播放,融云还将协调影片参加相关的评选活动;

 

不知道有木有把事情说清楚,

如果少侠有疑问,可在“评论区”留言,我们将及时解答~

最后,来个“定场诗”收尾——

编程生涯苦无岸,需求文档似高山;

Java&C+亦无趣,秃顶单身格子衫;

日复一日无牵绊,是否也想起波澜?

索性挥毫三千字,当个导演来玩玩!

乐哉!乐哉!

 

(融云“实时音视频”产品功能详见“融云”官网~)