# g++.shreal_compiler="/usr/bin/g++"filename=$(echo $* | grep -o "\ -c .*")output="/root/stat.log"/usr/bin/time -f "%U-%S $filename" -a -o $output $real_compiler $*# $export CXX=g++.shmkdir b2 && cd b2 && cmake .. -DCMAKE_BUILD_TYPE=debug && make -j24 BigWorldServer
CMake相关
枚举解耦
不喜欢这个方面还是算了, 有开源的不用白不用. 项目里的无锁队列一年半了也没认真看过.
针对开发中遇到的问题, 对于能够自动化的想法提供自动化工具, 优化已有工具的体验.
最近半年也算是没有写新的工具, 主要是维护自动冒烟, 日志分析(可能要负责维护骏鹰).
目前比较排斥写工具类的, 感觉没有学到太多的深度的东西, 写了之后就要负责维护和答疑甚至绑在上面操作.
感觉比较高大上, 而且难点是找到问题在哪里
做起来太难了, 完全没这方面经验, 没有什么大的成果.
Prometheus+Grafana提供的火焰图插件: 感觉这个也没什么深度的样子, 只做了一个火焰图的插件, 感觉虽然能看到 但还是找不到热点的优化方法, 不是很便宜大碗. 受限于采集频率
编译加速: 这个感觉还是挺爽的, 文件变更数量较少情况下, 编译速度大幅优化了. 但是没能做到进一步深入优化.
bpf: 这个最终甚至都没学完, 也是感觉无法落地.
服务器二进制包大小优化: 都是有损优化, 想无损的话还是没啥用途.
高大上, 经验积累性质强, 成本高, 难于出成果.
培养代码的基本功
适量的话倒是没什么缺点
一直在积累相关的经验, 最近半年整理的也发布出去了, 后面也打算继续整理和发布, 也算是正向的吧.
反正需求一直会有的, 这方面还是不太额外花时间了.
记录总结了从7月到11月积累的非技术经验, 涉及添砖加瓦方方面面.
本文是自己从7月到11月积累的部分非技术经验, 总结了工作记录中遇到的问题. 由于是个人经验积累, 适用性不强, 也不是大而全. 发布出来主要还是用于自己自勉, 整理了, 不发出来有感觉浪费.
需求评审
评审时, 如果不是很确定这部分功能, 自己负责的服务器能不能做, 就不要承接下来, 最后发现做不了, 再转手.
需求文档到手
需求文档看完, 开工之前, 先定协议
定完协议之后, 各个参与人就可以独立开发. 如果先开发再定协议, 可能导致后定的协议不便于所有人使用.
开工之前, 进行下预期设计, 同时评估时间
将重复的项目抽取出来
第一次做的时候, 可能考虑到需求比较简单或者时间不足, 一个功能就放在了一起实现, 但是后续又双叒叕用到这个功能了.对于个人提升来说, 就可以考虑抽取出来了.
做好兼容性处理
负责的功能要将兼容性处理做完善, 能够做到其他人无感知是最好的. 不要想着其他人只需要小小的一点处理就能解决, 到时候不管提示再多次都会有人@问什么问题的, 所以最优解是其他人无感知.
预估时间后无法完成后, 要及时周知相关人员
应该及时通知,否则默认你这边完成了, 其他人进行了一些处理, 导致不必要的问题出现.
开发的时候加足DEBUG日志
个人统一的工具仓库
工作中难免会写出来一些小工具, 提供给他人使用的时候一般都是放到公共代码库中, 这个时候工具就会在本地和公共代码库有两份, 注意只在一个地方修改, 否则容易出现不一致.
需求中途变更后简化代码
代码设计初期一般会留有一些考虑能够应对需求变化,但是需求变化后可能会导致实现可以简化,这个时候继续复用复杂的代码还是简化代码就需要考虑了.
代码尽量一步成型
个人感觉修改次数越多, 出问题的概率越高, 修改了这里忘记了同步修改其他地方是重灾区.
避免设计无用的东西
一些本来加个函数就能解决问题的, 是否要抽成一个模块就要仔细考虑下了.
数值类配置化or计算化
减少通用错误码的使用
如果错误码一对一能够及时发现问题, 如果是多个地方使用的, 只能靠日志+看代码路径了.
客户端参数校验
老生常谈了, 基本上每本书里都会提这点, 这次就出现了所有服务器都没校验的一个操作, 甚至是客户端都没检验.
状态校验放错了位置. 写的时候还是没考虑好运行路径
加代码的时候, 仔细考虑好相关的运行路径. 可能某个分支下, 这次加的就是有问题的.
加快调试速度
需求写完之后, 想要测试下自己的代码, 尽量将GM工具完善好, 否则每次修改代码都要重复操作好几步, 导致花费的时间反而比加一下工具要多.
修改前-不确定是bug还是feature时要进行确认
否则可能将feature作为bug处理了, 后续还要改回来, 造成不必要的时间浪费或者麻烦.
修改前-遇到BUG还是感觉留下现场,比重启解决问题更加重要
问题可以后面解决,复现问题可能再也没有机会了
修改前-简单方式解决问题
对于一个小bug, 简单的打个补丁就可以解决的话, 还是偏向于打补丁. 至于刨根问题解决虽然好, 但是一旦牵扯过多, 然后自己对项目不了解, 大刀阔斧重构容易出问题.
查问题中-用物体的事件经过查问题挺方便的
事件驱动的场景下, 可以先查询顶层都有那些事件, 看看这些事件的内容和实际是否正确, 比一下钻到底层查问题要快很多.
影响扩大-确定修改后果, 注意一个函数都有哪些作用
影响扩大-抽取函数注意不要影响到原区域功能
发现一段代码可以复用的时候, 会从其他函数中将这个函数抽取出来. 但是抽出来的函数增加代码的时候, 注意不要影响到原位置. 如在抽取出来的函数中, 由于其他需求, 增加了一层判断, 导致原位置(抽取函数前的位置)功能被影响.
影响扩大-enum中增加新类型
enum中增加新类型后要注意看看这个enum的使用位置, 如果只是关注自己新写的代码忽略旧代码, 可能导致旧代码的判断过不去.
修改后-BUG修复关联BUG单
否则后续会忘记为啥修改这里, 当时是什么BUG做出的这样的处理.
尽可能进行全面的自测
真的有某些代码是依靠BUG运行的, 修复BUG之后反而运行不了了, 所以基本的测试是必需的, 能想到的可能影响到的地方, 最好也是自测一下. 想到的地方不去测一下的话, 可能就是想到的这个地方出了问题.
A通过B通过不等于A+B通过
一个功能还是完整的测试吧,A功能虽然等于B+C功能,但B和C功能分别正确 不一定B+C就是正确的
自动化测试
一些功能测起来可能比较麻烦, 使用自动化工具会方便很多, 比如pyclient完全模拟一个客户端, 写代码有时候比点来点去构建测试环境方便很多.
问题的优先级安排
并行处理任务的时候, 注意优先级的安排. 可能当前的工作进行到一半, 有其他高优先级的工作要做的话, 还是要切过去. 尤其是查BUG到一半的时候, 很容易上头, 导致又多查了半天.
充分关注自己的工作
其他人都开始要联调了, 自己的功能还没发布到服务器上. 虽然当时在处理其他问题, 但还是已经做过的善后优先一些.
不懂就问
文档较少, 全靠口口相传, 遇到不懂得项目, 一定要进行询问, 不能常识性的去做.
重视不起眼的小问题,可能背后的原因是非常离谱的
遇到过不止1-2次了, 看起来就是很小的一个问题, 深究起来问题可能一串一串的.
特殊处理
没有注释的代码, 可能使用了一些特殊规定, 如果错误删掉了就会影响到功能. 比如数值是0的时候, 按照1来处理, 看描述很简单, 但是实际代码可能没有那么直观.
预期下当前所做是不是可以解决最终问题, 而不是当前某一步
当前所做可能确实能解决当前问题, 但是不一定能解决最终问题. 比如传输日志之前需要压缩, 压缩成几个小包也是压缩, 压缩成超大包也是压缩, 但是压缩成超大包可能不利于传输和解压.
去看看别人负责的模块, 不要只顾自己的模块
对于多模块(多服务器)后台, 多去关注下自己负责模块之外的内容, 这样对项目了解的更深.
问题的原因, 不一定是第一印象想到的点
遇到问题即使下意识感觉问题就是那里, 也一定要确认下. 可能并不是那样.
他人回复的内容一定要仔细理解,不要含主观臆断
后台开发代表全部后台, 前台开发代表全部前台
后台有多个服务器,每个服务器不同的人负责,前台可能认为后台是一个整体,所以找你沟通的时候最好不要只考虑本服务器的事情, 有需要就拉上其他服务器的一起建群.
帮忙
帮忙处理东西的时候,要问清楚原因, 搞清楚要做啥, 不能只是直接照做.
对应的事情尽量给对应的人去做
运维可以通过内网传输日志, 比通过sz和rz快和稳定很多.
CR发起前可以自己整一个临时CR看看代码
每次提交代码前, 全局的看一看自己的代码, 可能功能正确, 但是有不小心动到其他地方的代码.
测试代码在最终CR的时候要及时去掉 使用TODO 名字方便检索
使用TODO标记测试代码, 提交前批量查找下, 全部去掉.
不建议手动操作流水线
流水线中某一步错误后, 尽量重新触发流水线. 手动执行错误的一步, 很容易出错, 漏掉某些操作.
自动化操作没有监管人
流水线有的没有人监管, 即使将结果通知到了使用者, 使用者可能也会忽视结果, 导致有错误没有发现, 进而导致其他问题. 所以错误提示要尽可能的明显.
压测和扩容
发现问题比解决问题更重要
性能优化感觉难点是发现性能问题, 包括编译加速. (20%的问题造成了80%的负面影响, 如果去处理另外80%的问题收益就很低)
指定时间点触发的循环定时器, 每轮都计算下时间相比固定时间的更加稳定
尽量统一函数对统一内容进行清除, 比如标志位, 这样方便发现错误清理的地方
测试东西或者搞新东西的时候, 注意不要影响到旧功能
该开测试空间的开测试空间, 防止影响到其他空间
]]>Linux服务器端可以使用profile生成火焰图. profile从运行开始收集数据, 结束运行时将结果输出, 过了这村就没这店.
有没有可能持续使用profile进行采集, 将数据收集起来, 然后查询任意时间段的火焰图?
答案是√, 使用Prometheus存储profile采集的数据, 使用Grafana的火焰图插件(V9.5.2)将结果展示出来. 本文结束, 感谢观看!
profile的原始数据是栈调用(隐含时间信息), 火焰图是树状结构(丢失时间信息), 如果想要查询任意时间段就要存储原始栈调用数据和其对应的时间.
Grafana提供了火焰图插件, 但是只接受火焰图数据, Prometheus中存储的原始栈数据无法直接使用.
Grafana和Prometheus对字符串的处理能力极差
所以需要实现一个Proxy, 接受Grafana的查询请求, 转去请求Prometheus, 将结果进行处理后返回Grafana进行展示.
threadname;stack0;stack1;stack2 num
threadname;stack1;stack4;stack2 num
线程名, 栈帧, 采集到的数量. 这些数据将会上报到Prometheus中进行存储.
上报的格式需要依照Prometheus要求进行下格式化, 这里就不详细介绍了, 只需要知道Prometheus中存储了这些原始的数据(时间, 栈, 对应采集的次数)即可.
# URL/api/v1/query_range# BODYend=1694848740&query=cpu_func_profile{processname="HelloServer"}&start=1694827140&step=15
start 起始时间
end 结束时间
query 向Prometheus请求的表达式
step 步长
// Prometheus请求协议即是上面的Grafana协议// Prometheus回应协议是如下结构对应的json数据type PrometheusReportData struct { Status string `json:"status"` Data struct { ResultType string `json:"resultType"` Result []struct { Metric struct { Name string `json:"__name__"` Instance string `json:"instance"` Job string `json:"job"` Stack string `json:"stack"` } `json:"metric"` Values [][]any `json:"values"` } `json:"result"` } `json:"data"`}
将完整的Grafana请求转发给Prometheus, Prometheus会进行筛选和合并后返回, 所以无需对结果进行额外处理, 只需要从其中拿到汇总后的栈数据即可.
stack: 这个是我们上报的栈数据stack0;stack1;stack2
Values: 是Value的数组, Value有且仅有两个元素, 第一个元素是数据对应的时间戳, 第二个元素是栈数据的num
所以遍历所有数据统计出一个map<stack, num>
, Prometheus的任务就完成了.
type Metric struct { Name string `json:"__name__"` Instance string `json:"instance"` Job string `json:"job"` Label string `json:"label"` Level string `json:"level"` Self string `json:"self"` Value string `json:"value"`}type Result struct { Metric Metric `json:"metric"` Values string `json:"values"`}type PrometheusFlameData struct { Status string `json:"status"` Data struct { ResultType string `json:"resultType"` Result []Result `json:"result"` } `json:"data"`}
PrometheusFlameData的其他数据从Prometheus的回包中拷贝过来即可, 重要的是PrometheusFlameData.Result
Result.Values: 是Value的数组, Value有且仅有两个元素, 第一个元素是数据对应的时间戳, 第二个元素是栈数据的num
, 这个已经不重要了, 火焰图并不需要每个节点的时间, 可以任意填充., 如[[1684829000,"1"]]
Result.Metric: 将栈数据转换成多叉树后, 前序遍历节点后得到的结果.
将Grafana的请求原封不动, 转发给Prometheus进行处理, Prometheus数据返回后Proxy进行处理转换为火焰图插件数据格式, 进而返回给Grafana.
从上文Prometheus协议
的格式来看, stack部分和values部分是分开的, 如果一个stack在多个时间被采集到, 只会存储新数据的时间和次数到values中.
这里提到的stack实际是一个字段, 此外还有线程名, 进程名等字段. 如果一整条上报数据是一样的, Prometheus只会存储时间和次数部分, 如果整条上报数据有一丝一毫差别 就会导致整条数据被记录一次. 导致空间占用暴增(因为stack很长).
说人话就是不要在字段中增加随机值, 否则会导致上报数据无法复用, 每次上报都要存储完成数据, 导致存储量爆炸.
profile打印出完成的栈结构 需要在程序编译的时候 指定-fno-omit-frame-pointer
Grafana查询的URL是/api/v1/query_range
, 对应的标签是cpu_func_profile, 可以只针对这种情况处理.
其他情况使用doProxy直接转发, 否则无法使用自动补全等一些提示
由于profile需要消耗机器CPU, 可以采用每隔X分钟后运行Y分钟的profile, 将数据格式化后供Prometheus提取.
火焰图准确与否与采集频率有很大关系, profile持续运行会消耗很多的CPU, 所以目前是采取了运行X分钟停止Y分钟的做法, 如果能持续运行profile就能解决这个问题.
func1#func2#func3func1#func3func2#func3func4#func3
如上四个调用栈, 从火焰图上无法轻易看出来func3占用了过多的CPU, 换一种思路将栈倒过来, 就能发现func3占用了过多CPU, 这一步操作只需要在PrometheusProxy中调用一个函数倒转切割后的栈,就能实现.
]]>分类
特点
了解之后就难于理解不了解人的想法, 下意识认为对方已经了解某些知识点
检测知识的最终途径, 将其传播给另一个没有背景的人
写文章的时候捉襟见肘, 不要在决定写文章的时候 才去找素材. 素材积累之后可以整理出来.
有些粗粗看过的, 可能含有非常多的点
确定主要范围, 不要歪楼, 不相关的简单介绍.
确定知识边界, 普适性的文章, 不需要从识字教起.
主题上升, 和更大的主题联系起来, 变归纳为聚焦
主题写明动作和动机
结构化思维: 穿线(讲通, 将多个知识点联系起来, 如从电路讲到程序运行), 归纳(小主题归纳到一个大范围,看看范围都有啥), 深挖(逐字逐句), 聚焦(只讨论关注的点, 将关注的点讲明白)
获取主题灵感: 发散思维, 逻辑思维, 同理心. 在平时多去关注一些
确认分享欲, 搜索相关主题(穿线很少), 思考对其他人是否有价值, 属于那种灵感来源, 主题上升, 标题是否是文章主要内容的一句话概括
标题小技巧, 图片中
主题有了 素材有了
有规律 贴近旧有认知 利于大脑记忆
金字塔原理: 结构化思考, 沟通, 写作. 一本大厚书 芭芭拉明托
结构化思维
梅切原则: 相互独立(没有交集), 完全穷尽(没有空隙)
简单性原理: 文章不需要面面俱到, 主线不相关内容, 可以考虑删减. 结构简单, 文字简省.
不要用复杂的东西解释另一个复杂的东西, 文章结构不要太复杂(精简层数 每层中case精简)
字不如数, 数不如表, 表不如图. 人脑处理图片相比文字更快
配图不一定需要时专业的, 主要目的是帮助理解 不一定需要多么严谨, 放在文章中能说明问题就好.
取决于文章主题, 是否是水货
语言晦涩, 排版不好, 多迭代, 反复打磨文章
语言精简(不能无脑删减), 内容分段, 准确(善用专业词汇 鼠标点两下(双击), 准确数字(90%)), 生动, 例子(不求多 命中要害, 极端例子 说明问题)
发表之前至少自己读一次. 善用AI润色, 重新解读不要机翻 机械搬
今天clion写项目 遇到一个问题 明明写了aux_source_directory 却依然提示没有加入. 后来查了下, 并不推荐使用那个命令.
可能会存在一些问题
https://cmake.org/cmake/help/latest/command/aux_source_directory.html
# 必须片段# CMake 最低版本号要求cmake_minimum_required (VERSION 2.8)# 项目信息project (Demo1)# 指定生成目标add_executable(Demo main.cc)# 多文件# 如果一味地在add_executable中添加源文件, 会导致太长了# 将dir目录中所有源文件保存在变量中aux_source_directory(. DIR_SOURCE)# 将变量赋值给Demoadd_executable(Demo ${DIR_SOURCE})# 多文件多目录# 需要在主目录和子文件夹中都编写CMakeLists.txt文件# 主文件添加子目录add_subdirectory(dir1)# 添加链接库target_link_libraries(Demo Foo)# dir1目录中aux_source_directory(. DIR1_SOURCE)# 生成链接库 Foo 在主文件中添加即可add_library(Foo ${DIR1_SOURCE})
自己的理解
通过为cmake编写CMakeList文件, cmake即可按照规则生成相应的Makefile文件
然后make读取Makefile文件就可以按照规则将源代码编译
总的来说cmake就是为make生成Makefile文件, (Makefile文件可以自己编写, 也可以用Cmake生成)
应该是编写CMakeList的代码量少于或者简单于Makefile, 简化了操作
https://zhuanlan.zhihu.com/p/97369704
https://www.jianshu.com/p/a0915895dbbc
find_package 存在两个模式
find_package(GLEW REQUIRED) # 先module模式再config模式find_package(GLEW REQUIRED CONFIG) # 使用config模式
module模式
find_package(GLEW REQUIRED)if (GLEW_FOUND) message("cannot find glew")endif()
通过上述命令查找glew库,首先会去cmake的modules目录下查找对应的cmake文件。
glew对应的cmake文件为FindGLEW.cmake
C:/Application/Code/cmake-3.19.0-rc2-win64-x64/share/cmake-3.19/Modules/FindGLEW.cmake
config模式
如果module模式对应的cmake文件不存在则启动config模式。
config模式会在如下目录搜索对应的配置文件glew-config.cmake
或GLEWConfig.cmake
W代表windows平台 U代表unix平台
<prefix>/ (W)<prefix>/(cmake|CMake)/ (W)<prefix>/<name>*/ (W)<prefix>/<name>*/(cmake|CMake)/ (W)<prefix>/(lib/<arch>|lib*|share)/cmake/<name>*/ (U)<prefix>/(lib/<arch>|lib*|share)/<name>*/ (U)<prefix>/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ (U)<prefix>/<name>*/(lib/<arch>|lib*|share)/cmake/<name>*/ (W/U)<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/ (W/U)<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ (W/U)
prefix生成规则如下
GLEW_ROOT
的cmake变量cmake -DCMAKE_PREFIX_PATH=/tmp/test
GLEW_DIR
CMAKE_PREFIX_PATH
# glew-config.cmakefind_path(GLEW_INCLUDE_DIR glew/include/)find_library(GLEW_LIBRARY NAMES glew32 PATHS glew/lib/Release/x64)if (GLEW_INCLUDE_DIR AND GLEW_LIBRARY) set(GLEW_FOUND TRUE)endif()
## CMake根目录生成Makefile# 使用all走这里all: cmake_check_build_system $(CMAKE_COMMAND) -E cmake_progress_start /tmp/tmp.3r11cPofBP/cmake-build-debug/CMakeFiles /tmp/tmp.3r11cPofBP/cmake-build-debug/CMakeFiles/progress.marks $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 all $(CMAKE_COMMAND) -E cmake_progress_start /tmp/tmp.3r11cPofBP/cmake-build-debug/CMakeFiles 0.PHONY : all# 直接使用目标untitled4untitled4: cmake_check_build_system $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 untitled4.PHONY : untitled4## CMakeFiles/Makefile2# Convenience name for target.untitled4: CMakeFiles/untitled4.dir/rule.PHONY : untitled4# Build rule for subdir invocation for target.CMakeFiles/untitled4.dir/rule: cmake_check_build_system $(CMAKE_COMMAND) -E cmake_progress_start /tmp/tmp.3r11cPofBP/cmake-build-debug/CMakeFiles 3 $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 CMakeFiles/untitled4.dir/all $(CMAKE_COMMAND) -E cmake_progress_start /tmp/tmp.3r11cPofBP/cmake-build-debug/CMakeFiles 0.PHONY : CMakeFiles/untitled4.dir/rule# All Build rule for target.CMakeFiles/untitled4.dir/all: $(MAKE) $(MAKESILENT) -f CMakeFiles/untitled4.dir/build.make CMakeFiles/untitled4.dir/depend $(MAKE) $(MAKESILENT) -f CMakeFiles/untitled4.dir/build.make CMakeFiles/untitled4.dir/build @$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --progress-dir=/tmp/tmp.3r11cPofBP/cmake-build-debug/CMakeFiles --progress-num=1,2,3 "Built target untitled4".PHONY : CMakeFiles/untitled4.dir/all##Shell# 生成depend$ make -f CMakeFiles/untitled4.dir/build.make CMakeFiles/untitled4.dir/dependScanning dependencies of target untitled4# 变更文件./CMakeFiles/untitled4.dir/depend.internal./CMakeFiles/untitled4.dir/depend.make./CMakeFiles/untitled4.dir/CXX.includecache# 构建$make -f CMakeFiles/untitled4.dir/build.make CMakeFiles/untitled4.dir/build[100%] Building CXX object CMakeFiles/untitled4.dir/Base.cpp.o[100%] Linking CXX executable untitled4
# ./CMakeFiles/untitled4.dir/depend.internalCMakeFiles/untitled4.dir/Base.cpp.o /tmp/tmp.3r11cPofBP/Base.cpp /tmp/tmp.3r11cPofBP/Base.hCMakeFiles/untitled4.dir/main.cpp.o /tmp/tmp.3r11cPofBP/Base.h /tmp/tmp.3r11cPofBP/main.cpp# ./CMakeFiles/untitled4.dir/depend.makeCMakeFiles/untitled4.dir/Base.cpp.o: ../Base.cppCMakeFiles/untitled4.dir/Base.cpp.o: ../Base.hCMakeFiles/untitled4.dir/main.cpp.o: ../Base.hCMakeFiles/untitled4.dir/main.cpp.o: ../main.cpp
# 生成依赖CMakeFiles/untitled4.dir/depend: cd /tmp/tmp.3r11cPofBP/cmake-build-debug && $(CMAKE_COMMAND) -E cmake_depends "Unix Makefiles" /tmp/tmp.3r11cPofBP /tmp/tmp.3r11cPofBP /tmp/tmp.3r11cPofBP/cmake-build-debug /tmp/tmp.3r11cPofBP/cmake-build-debug /tmp/tmp.3r11cPofBP/cmake-build-debug/CMakeFiles/untitled4.dir/DependInfo.cmake --color=$(COLOR).PHONY : CMakeFiles/untitled4.dir/depend# 构建CMakeFiles/untitled4.dir/build: untitled4.PHONY : CMakeFiles/untitled4.dir/builduntitled4: CMakeFiles/untitled4.dir/main.cpp.ountitled4: CMakeFiles/untitled4.dir/Base.cpp.ountitled4: CMakeFiles/untitled4.dir/build.makeuntitled4: CMakeFiles/untitled4.dir/link.txt @$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --green --bold --progress-dir=/tmp/tmp.3r11cPofBP/cmake-build-debug/CMakeFiles --progress-num=$(CMAKE_PROGRESS_3) "Linking CXX executable untitled4" $(CMAKE_COMMAND) -E cmake_link_script CMakeFiles/untitled4.dir/link.txt --verbose=$(VERBOSE)CMakeFiles/untitled4.dir/main.cpp.o: CMakeFiles/untitled4.dir/flags.makeCMakeFiles/untitled4.dir/main.cpp.o: ../main.cpp @$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --green --progress-dir=/tmp/tmp.3r11cPofBP/cmake-build-debug/CMakeFiles --progress-num=$(CMAKE_PROGRESS_1) "Building CXX object CMakeFiles/untitled4.dir/main.cpp.o" /usr/bin/g++ $(CXX_DEFINES) $(CXX_INCLUDES) $(CXX_FLAGS) -o CMakeFiles/untitled4.dir/main.cpp.o -c /tmp/tmp.3r11cPofBP/main.cpp
CMake识别变更和重新编译,是通过比对冒号左右两个文件的最后修改时间。
A:B A依赖B,如果B的修改时间晚于A说明A需要重新构建。
set(CurrentProtoGenP2P ${P2P_OUT_PATH_PATH}/${ProtoName}.proto.tars_Api.p2p.h)add_custom_command(OUTPUT ${CurrentProtoGenP2P} COMMAND sh -c "python2 ${P2P_SELECT_AND_BUILD} ${P2P_OUT_PATH_PATH} ${AllDirs} ${PROTOBUF_ROOT}/include/include ${Protoc} ${ProtoFile}" DEPENDS ${Protoc} ${ProtoFile} WORKING_DIRECTORY ${Dir})list(APPEND Srcs ${CurrentProtoGenP2P})
]]>以后写工具应该注意,一个工具放两个代码库维护是容易出问题的,能统一尽量统一,统一不了应该只在一个里面修改
资料学习
TCP为什么可靠
超时重传、流量控制和拥塞控制
TCP使用基于ACK的方式来处理分组丢失。如果每次只发送一个分组,等到ACK到达后再发送后续分组,这种简单的模式下依然存在着下面的问题。
基于窗口的流量控制
为了处理接收方处理效率相对发送方低的问题,使用基于窗口的流量控制。接收方需要告知发送方其接收窗口的大小。这样发送方就可以根据接收方的窗口大小来调整发送窗口的大小。
基于拥塞控制的流量控制
为了处理网络通道之间所有中转设备和线路限制导致的问题,使用拥塞控制来解决。
TCP高延迟原因
TCP协议中规定了,发生超时时,RTO=RTOx2。超时时间的增长,导致数据包在真正丢失时,无法被及时的重传,超时时间越长,无法被及时重传的情况越严重。
TCP使用ARQ模型中此编号K前所有包已收到。当K+1发生了丢失时,虽然K+2可能已经收到,但发送端无法得知K+2和之后数据包的情况,只能全部重传,进而出现不必要的重发。TCP中的SACK?
SACK
https://www.rfc-editor.org/rfc/rfc2018
MTU为1500
MSS最大为1460
TCP额外包头最大为60字节。SACK需要消耗8*n+2字节的额外包头长度,所以理论最多描述4段block,不过由于时间戳消耗10字节的额外包头长度,所以实际为3段block。
UDP为什么不可靠
UDP如何可靠
涉及到功能的选择和切换
KCP协议
https://github.com/skywind3000/kcp
相关工具
丢包模拟
单元生抽工厂
提供克隆接口, 能够对指定的物体进行克隆生成新物体.
相对于工厂模式一次性获取产品, 生成器模式则是可以一步一步对产品进行组装, 组装完成后获取最终的产品.
系工厂, 有多个产品, 每个产品提供一个接口.
一个工厂可以获得本工厂生产的各种产品. 如日系工厂可以提供各种日式家具, 中系工厂可以提供各种中式家具.
苹果工厂, 提供统一的苹果生产接口.
通过向工厂获取接口传入不同的苹果种类, 获取对应的工厂.
要求红富士返回红富士工厂, 要求糖心苹果返回糖心苹果工厂, 由于都是工厂 所以外部不用关注具体是哪个工厂.
元代适外(室外)装桥组
将class中重复很多的字段抽取出来, 在多个类中共享这些重复的字段, 减少内存占用
经理设置秘书代理自己, 外部人员需要通过秘书向经理递交材料, 秘书还可能会对材料进行归纳总结.
给不兼容的物体设置包装类, 包装类能够和原本不兼容的对象协作.
复杂的系统提供简单的接口.
将一个对象放入封装对象中, 为对象添加新的功能.
通过固定的接口, 将一个整体分离成前台和后台, 前后互不关注具体实现, 只关注接口.
UI界面和操作系统通过一些固定的接口进行交互, UI界面不在乎是什么系统 只要能相应这些接口就行. 操作系统不在乎是什么UI, 只要通过固定的接口向自己发送请求即可.
将对象组合成树状结构, 对外一个接口就可能访问到所有叶子.
模访状任命中备迭观策(模仿状任命中被爹观测)
父类中定义算法框架, 子类可以根据需求重写算法特定的步骤.
将算法和所需对象分离开来
不暴露底层数据结构的情况下, 遍历所有元素
将对象需要状态的操作都抽象出来放到状态父类中, 状态子类中根据自身的状态实现这些操作. 对象不需要关心当前的状态, 只需要根据操作调用对应函数即可, 状态的更新由状态子类负责.
将请求沿着处理链进行发送, 每个人都可以处理请求, 或者传递给下个人员.
将请求和相关参数包装成独立对象. 对立对象可以将方法参数化 延迟 或者压入队列, 还能实现撤销
禁止众多的对象随意互相耦合, 将请求发送给中介者由中介者进行转发. 例如一个塔台和多个飞机交流, 飞机只需要和一个塔台交流就能得知其他飞机的信息.
对象需要实现保存状态和恢复状态的函数. 保存状态的函数将当前状态需要保存的数据生成快照. 管理器调用函数生成快照并保存起来, 当恢复状态的时候读取快照, 应用到对象上.
从众多的事件中订阅某个事件, 忽略其他事件.
将众多算法放到不同的类中, 是的可以通过简单的操作更换算法.
]]>游戏中有许多内容是每天凌晨0点更新, 有些玩法是每天0点都会更新, 有的则需要累积到一定天数才会更新, 累计的天数则是需要进行配置.
设计一个通用的配置Proto用来统一配置格式, 触发器模型就可以读取这些统一的配置 统一进行天数判断.
对于每天0点都会更新的, 每天触发一次. 对于累计天数的, 累计天数达到后进行触发.
触发的时候不主动调用业务代码, 而是采用发布指定事件的方式进行通知.
enum TimeTriggerGamePlay{ TIME_TRIGGER_GAME_PLAY_NONE = 0; TIME_TRIGGER_GAME_PLAY_NORMAL = 1; TIME_TRIGGER_GAME_PLAY_REDOUBT = 2;}enum TimeTriggerType{ TIME_TRIGGER_DEFAULT = 0; // 每小时触发 // TIME_TRIGGER_EVERY_HOUR = 1; // 每天触发 // TIME_TRIGGER_EVERY_DAY = 2; // 累计天数触发 从阶段1开始, 阶段2, 阶段3.. TIME_TRIGGER_DAY_ACCUMULATE = 3;}message TimeTriggerConfigItem{ // 所属玩法 TimeTriggerGamePlay trigger_game_play = 1; // 触发器类型 TimeTriggerType trigger_type = 2; // 进入新阶段开服天数 repeated int32 trigger_accumulate_day = 3; // 是否启用 bool is_enable = 4;}message TimeTriggerConfig{ repeated TimeTriggerConfigItem trigger_config_item = 1;}message TimeTriggerMsg{ // 所属玩法 TimeTriggerGamePlay trigger_game_play = 1; // 触发器类型 TimeTriggerType trigger_type = 2; int32 now_hour = 3; int32 now_stage = 4;}
触发器
enum CoreEventMainType{ CEVENT_MT_TIME_TRIGGER}enum CoreEventSubType{ CEVENT_ST_TIME_TRIGGER}// TimeTriggerMsg
对于每小时执行函数, 起初的写法是计算出当前小时剩余时间, 之后直接定时一个小时. 但这种方法一旦有时间偏差就会一直存在, 还会将偏差积累下来
后面改成了每轮循环都要计算下当前小时剩余时间, 这样后面几轮即使有误差 也会被修正过来.
]]># 压缩两个提交git rebase --interactive HEAD~2# 将# pick c6d7bf6 fix namespace bug# pick 1c648d8 fix namespace bug# pick 7c648d8 fix namespace bug# 改为# pick c6d7bf6 fix namespace bug# s 1c648d8 fix namespace bug# s 7c648d8 fix namespace bug# 压缩所有提交git rebase --interactive -root# 更新所有submodule# 首次运行git submodule update --init --recursive# 之后运行git submodule foreach --recursive git pull origin master -f
how-do-i-force-git-to-use-lf-instead-of-crlf-under-windows
git config --global core.eol lfgit config --global core.autocrlf inputgit rm -rf --cached .git reset --hard HEAD
VIM
set expandtabset ts=4set shiftwidth=4set sts=4set tw=100set numberset cinoptions=:0,g0,(0,w1set backspace=indent,eol,start" set smartindentset autoindentset cindentset softtabstop=4set tabstop=4set enc=utf-8set nocompatiblesyntax onset fileencodings=ucs-bom,utf-8,gb18030,latin1set nobackupset undofileset undodir=~/.vim/undodirif !isdirectory(&undodir) call mkdir(&undodir, 'p', 0700)endifif has("autocmd") au BufReadPost * if line("'\"") > 0 && line("'\"") <= line("$") | exe "normal! g`\"" | endifendifset pastetoggle=<F2>
ZSH
plugins=(git zsh-autosuggestions zsh-syntax-highlighting autojump)
make install <name>
for-cmakes-install-command-what-can-the-component-argument-do
install(TARGETS main RUNTIME DESTINATION ./install_dir/ COMPONENT main)add_custom_target(install_main COMMAND ${CMAKE_COMMAND} -DCOMPONENT=main -P ${CMAKE_BINARY_DIR}/cmake_install.cmake)
# lsoflsof -i tcp:10002 -P-P 不解析端口号,直接显示# iftop-P 显示端口号-N 不解析端口号l 输入fileter,可以直接输入ip
adb connect 127.0.0.1:port # (连接指定机器)adb devices # (列出所有设备)adb shell # (进入机器中 打开shell)adb push tcpdump /sdcard/... # (推送文件)adb pull /sdcard/1.pcap . # (拉取文件到本地)# wireshark左下角的灯是可以点的,可以列出来可疑项目
抓取https(没有成功, 也还是记录一下)
# https://github.com/frida/frida/releases 下载frida-server 注意要到非sdcard目录 否则不会给运行权限# adb后运行./frida-server# pip3 install frida-tools# 这条会自动启动目标app 所以tcpdump要提前准备好# 将产出的秘钥保存起来, Wireshark-Protocol-TLS 最下方加载frida -U -f jp.ne.paypay.android.app -l .\1.js
// 1.jsfunction startTLSKeyLogger(SSL_CTX_new, SSL_CTX_set_keylog_callback) { console.log("start----") function keyLogger(ssl, line) { console.log(new NativePointer(line).readCString()); } const keyLogCallback = new NativeCallback(keyLogger, 'void', ['pointer', 'pointer']); Interceptor.attach(SSL_CTX_new, { onLeave: function(retval) { const ssl = new NativePointer(retval); const SSL_CTX_set_keylog_callbackFn = new NativeFunction(SSL_CTX_set_keylog_callback, 'void', ['pointer', 'pointer']); SSL_CTX_set_keylog_callbackFn(ssl, keyLogCallback); } });}startTLSKeyLogger( Module.findExportByName('libssl.so', 'SSL_CTX_new'), Module.findExportByName('libssl.so', 'SSL_CTX_set_keylog_callback'))
docker run -d -p 9090:9090 -v /data/prometheus/prometheus.yml:/data/prometheus/prometheus.yml -v /data/prometheus:/data/prometheus --user root --name prometheus prom/prometheus --config.file=/data/prometheus/prometheus.yml --storage.tsdb.path=/data/prometheus
# -g enables call-graph recording -p pidperf record -F 99 -g -p 28470perf script > perf.unfoldgit clone https://github.com/brendangregg/FlameGraph.git./stackcollapse-perf.pl perf.unfold > perf.fold./flamegraph.pl perf.fold > perf.svg
objdump -dwarf=info XXXServer > XXXServer.inforeadelf -e XXXServer# cmake选项-femit-struct-debug-baseonly
]]>代码整洁之道中内容很多, 不过这里单单只有命名一节. 被提醒过几次命名不合适, 这部分就着重整理出来, 每次命名的时候都思考思考. 能立刻改善自然最好, 不能立刻也要逐步前进.
名副其实、避免误导、使用读的出来的名称、避免可爱(要实用)
看到名字就知道这个变量是个什么东西,theList就不知道是个什么东西,studentList就好一些。
不能变量描述的是A,实际是B。比如给Map类型起名List
做有意义的区分
a1,a2,a3这些就没啥意义,x,y,z就有了
book和theBook也没区别
使用可以搜索的名称
找7就非常难找,但是找DAY_PEER_WEEK就好找了
避免思维映射
不要起只有自己能理解的取巧的名称,其他人可能是理解不了的
方法名使用动词或者动词短语
get、set、is
每个概念对应一个词
tower是地图中编码里塔的名称,瞭望塔最好起名watchTower不要和其他冲突了。
添加有意义的语境
name是名字的意思,petName?playerName?无法区分
firstName,lastName,street,city,state,这些放一起能看出来是地址,但是单单一个state就看不出来了,不如addrState,或者使用Address类
命名统一
命名要统一,多个人合作的时候要提前商量好,复制粘贴代码后也要注意代码的命名。
可以直接用机制来命名
比如模数余数这些名词
避免无意义修饰
阶段是阶段 等级是等级,不要用阶段等级这种东西
GPT
多用用GPT,这个起名字真的好用, 很有参考价值
break和nullptr
switch-case丢失break, 指针没有判空, 写全新模块的时候基本遇不到, 重灾区是在原有位置加代码.
注释
命名的章节混进来一点点注释, 我感觉问题不大.
短小
避免嵌套太多层
只做一件事情,做好一件事情
编写函数是为了将大一些的概念(函数名称),拆分成另一个抽象层上的一系列步骤。
检查函数是否只做了一件事,可以看看函数是否可以再拆出一个函数
使用描述性的名称
长的具有描述性的名称,比短的令人费解的名称要好。
IDE中改名很容易,可以多次修改找到一个最具描述性的名称
函数参数
限制传入bool参数,bool参数表明了,可能存在两个分支,为true一个为false一个,可以考虑拆成函数。
给函数取个名字,用来解释函数的意图、参数和顺序和意图
assertEqual改成assertExpectedEqualsActual(Expected,actual),能够减少记忆参数顺序的负担。
无副作用
函数承诺只做一件事情(函数名描述),但是还会做其他被藏起来的事情(函数名没有描述的),这个就是函数会有的副作用。有人轻信了函数的名称,就会有这些藏起来的事情导致的风险。
减少重复
结构化编程
尽量保证函数只有一个出口,break,continue等语句限制使用。这些在短小函数中影响不是很大,但是在长函数中影响很大。
反复打磨
一步到位写出完美的代码是很难的,开始应该注重功能的实现,后面可以进行打磨(拆解函数、消除重复、修改名称)
最好是使用代码本身当作注释,当一行或者一块代码需要注释的时候,可以考虑简化这些代码。
注释不能美化代码
通过增加变量来进行注释
错误的注释
需要注释的地方
使用指针时,一定要注意进行判空
switch中增加case的时候, 注意补齐break
加了新的子类型后没有适配
方案可能可以用,但是对于目前还能用的代码,这种改动就需要考量下了。
现有的敌我判断,只需要在IsEnemy中加几行代码就可以了,大刀阔斧的去改成方案中的设计,可能会遇到很多的问题。
改动的时候应该考虑下改动小的方式?而不是去整一套新的方案。
复杂的方案为了通用性,后面维护起来也是很麻烦的。
涉及增加Module + 协议 代码量200~300行 略微修改其他地方 1.5D
比如明明可能有很多配置,按理来说应该是用参数索引,但是写死了,应该说明原因。
// 目前只有1条配置,先写死id为1const auto *config = GetResFactory().FindResStrongpointRewardCfg(1);
应该及时通知,否则认为完成了,后面会出现各种问题。这次就是配置全换掉了 导致半成品的怪物组被启用了
以后写工具应该注意,一个工具放两个代码库维护是容易出问题的,能统一尽量统一,统一不了应该只在一个里面修改.
代码设计初期一般会留有一些考虑能够应对需求变化,但是需求变化后可能会导致实现可以简化,这个时候继续服用复杂的代码还是简化代码就需要考虑了
给堡垒加了adaptor,然而是一个函数能搞定的判断,多余了
如果错误码一对一能够及时发现问题, 如果是多个地方使用的, 只能靠日志+看代码路径了.
没想到出现了非联盟成员拆联盟建筑的问题。
最大等级的BUG去掉InitModuleDependency中的广播后,确实去掉了开启时候的广播,但导致ClientConfig中的内容也没有被填写上。
且由于满阶段nextCheckInterval为0时添加的定时器 又掩盖了这个问题的及时发现 导致后面发现后改的挺急的
没有调用OnIdle出现的问题,结果加上OnIdle调用后影响到了回城
这种还是要确认好,不然浪费双倍的时间
新位置因为需要保证运行,所以一般不会出现问题,但是原位置的多余内容,一般不影响运行,不便于发现。
问题可以后面解决,复现问题可能再也没有机会了
之前删掉了一段代码,导致城郊出现问题,忘记为啥删除的了,后面修BUG还是带上BUG单的链接把
抽函数后加东西, 导致相对于原有增加了一些功能, 这种是很危险的抽函数.
如果解决一个小bug, 完美的方式改动很多的话, 感觉不如简单的处理下
长久来说, 还是要考虑下解决这个问题, 算是一个优化点?
改了BUG不好好验证, 而是急着去查下一个BUG.
战斗无法打起来
因为战斗无法打起来,好多地方开发的时候都没有测试到,结果反而这里才是问题所在
一个功能还是完整的测试吧,A功能虽然等于B+C功能,但B和C功能分别正确 不一定B+C就是正确的
pyclient这个还是很方便的
下午差城郊防守建筑的BUG查上头了,虽然查出来了但导致了开战距离BUG的延迟了,进而导致了赏金联调的滞后。下午的时候防守建筑的BUG实际可以延后的
赏金都开始联调了,我的功能还没发上去
帮忙处理东西的时候,一定要搞清楚,问清楚。自己也要看清楚,不能只是不带脑子的执行
新版服务器未更新到目标服务器 出了N个乌龙BUG单
这次自动提单提了2K+, 如果不是提到了临时空间 估计直接爆炸了.
123
# 查看内核版本# 一定要和最后install的版本一致$ uname -r4.18.0-348.7.1.el8_5.x86_64# 搜索包并显示版本$ yum search kernel-devel --showduplicates# 这个版本最匹配kernel-devel-4.18.0-348.7.1.el8_5.x86_64# 使用包含版本的完整包名安装$ yum install kernel-devel-4.18.0-348.7.1.el8_5.x86_64# header和devel版本一致$ yum install kernel-headers-4.18.0-348.7.1.el8_5.x86_64$ yum install -y bcc-tools# 加入环境目录,在~/.bashrc加入:$ export PATH=$PATH:/usr/share/bcc/tools/# 安装好后尝试执行下$ execsnoop
BPF 实际起作用的一些代码,内核提供的接口。
BCC bpftrace BPF前端
$ uptime 10:45:40 up 205 days, 54 min, 1 user, load average: 0.18, 0.16, 0.20
如果1min的负载高于5min或者15min,说明当前正是高负载。
如果1min的负载低于5min或者15min,说明错过了高负载的现场,后续查看的数据都是非高负载情况的数据。
$ dmesg -T | tail -n 10
T 打印可读的时间戳
查看核心数量
$ cat /proc/cpuinfo | grep "cpu cores" | uniq 获取物理核心数量$ cat /proc/cpuinfo | grep "processor" 获取逻辑核心数量
vmstat
$ vmstat 1procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 0 253240 141064 1181400 0 0 79 101 0 0 2 2 96 0 0 0 0 0 252216 141064 1181404 0 0 0 0 881 1401 1 2 98 0 0 0 0 0 254328 141064 1181404 0 0 0 0 916 1397 1 1 98 0 0 0 0 0 254436 141064 1181416 0 0 0 152 1480 1894 3 8 88 1 0 0 1 0 254504 141064 1181412 0 0 8 488 1180 1743 2 2 96 1 0 0 0 0 254568 141064 1181412 0 0 0 128 1059 1667 2 1 97 1 0 0 0 0 254692 141064 1181412 0 0 0 0 1056 1723 1 1 98 0 0
第一行数据由于工具的启动 不准确
$ mpstat -P ALL 102:04:24 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle02:04:25 PM all 0.99 0.00 0.50 0.00 0.00 0.50 0.00 0.00 0.00 98.0202:04:25 PM 0 0.98 0.00 0.98 0.00 0.00 0.98 0.00 0.00 0.00 97.0602:04:25 PM 1 1.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 99.00Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idleAverage: all 4.64 0.00 1.31 0.28 0.53 0.17 0.00 0.00 0.00 93.07Average: 0 3.13 0.00 1.51 0.17 0.56 0.17 0.00 0.00 0.00 94.46Average: 1 6.14 0.00 1.12 0.39 0.50 0.17 0.00 0.00 0.00 91.68
单个核心占用高, 指向单线程的问题
$ pidstat 102:06:42 PM UID PID %usr %system %guest %wait %CPU CPU Command02:06:43 PM 0 50128 1.00 0.00 0.00 0.00 1.00 1 qbittorrent-nox02:06:43 PM 0 1583327 0.00 1.00 0.00 0.00 1.00 1 YDService02:06:43 PM 0 1641467 0.00 1.00 0.00 0.00 1.00 0 barad_agent02:06:43 PM 0 3314936 0.00 1.00 0.00 0.00 1.00 1 pidstatAverage: UID PID %usr %system %guest %wait %CPU CPU CommandAverage: 0 42 0.00 0.12 0.00 0.00 0.12 - kworker/0:1H-kblockdAverage: 0 623 0.00 0.12 0.00 0.00 0.12 - systemd-journalAverage: 0 948 0.38 0.12 0.00 0.12 0.50 - tunedAverage: 0 50128 0.62 0.12 0.00 0.00 0.75 - qbittorrent-noxAverage: 0 1583327 0.38 0.38 0.00 0.00 0.75 - YDServiceAverage: 0 1641467 0.38 0.38 0.00 0.00 0.75 - barad_agentAverage: 0 3202896 0.12 0.00 0.00 0.00 0.12 - nodeAverage: 0 3313606 0.00 0.12 0.00 0.12 0.12 - kworker/1:0-eventsAverage: 0 3314936 0.12 0.38 0.00 0.00 0.50 - pidstat
相比top, pidstat会将解锁保存到shell的缓冲区 方便观察变化趋势
$ iostat -xz 1
%util 磁盘使用率
avgqu-sz 提交的任务数量, 超过1可能代表有问题
异步IO可能导致性能问题, 因为发起后操作系统不会拒绝, 且一个线程因为不被阻塞可以批量发起.
$ free -mh total used free shared buff/cache availableMem: 7.3Gi 5.8Gi 332Mi 3.0Mi 1.1Gi 1.2GiSwap: 0B 0B 0B
$ sar -n DEV 102:21:23 PM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil02:21:24 PM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.0002:21:24 PM eth0 56.00 50.00 17.92 12.95 0.00 0.00 0.00 0.0002:21:24 PM cni-podman0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00Average: IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutilAverage: lo 8.43 8.43 0.75 0.75 0.00 0.00 0.00 0.00Average: eth0 46.50 43.90 7.77 10.48 0.00 0.00 0.00 0.00Average: cni-podman0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
rxkB/s txkB/s 收发速率
$ sar -n TCP,ETCP 102:25:04 PM active/s passive/s iseg/s oseg/s02:25:05 PM 0.00 1.00 5.00 5.0002:25:04 PM atmptf/s estres/s retrans/s isegerr/s orsts/s02:25:05 PM 0.00 0.00 0.00 0.00 0.00Average: active/s passive/s iseg/s oseg/sAverage: 0.50 0.75 10.50 9.75Average: atmptf/s estres/s retrans/s isegerr/s orsts/sAverage: 0.00 0.00 0.00 0.00 0.25
提供的数据很多, 但是一直再刷新, 导致很难看到变化相关.
Ctrl-S pause Ctrl-Q continue
# 追踪write系统调用 且筛选打开了文件描述符2的进程 strace -fe write `lsof -t "/proc/$$/fd/2" |sed 's/^/-p/'` -o write2.log# rdi是系统调用第一个参数 其次为rsi rdx r10 r8 r9b write if $rdi == 2# 8个字节 x 16进制 b以bytes为单位打印 x /8xb ptr
]]>inject替换函数,配置热更
尽量详尽的日志
服务器是否有卡顿的现象
客户端请求的平均响应速度
玩家行为和投放监控
某些操作是否超过了预期的设定,比如奖励获取等
架构容错(服务器和进程挂掉了),外部服务挂掉(DB),异常情况下的保底逻辑,保证服务的整体可用
功能开关,关闭异常功能 修复后再打开。
玩家行为超出预期(所有列表都加个上限,操作频率上限)
系统行为超出预期(封号频率超过预期后需要预警)
不需要实时的系统,可用做成异步。(原神的抽卡记录就是非实时的)
一些排行榜更新也不一定需求实时,可以收集数据后每隔一段排行一次,(小优化?玩家本人的排行榜可以前台坐下处理进行预排行 让玩家以为看到的数据实时更新了?)
集群的承载上限是集群中逻辑单点的承载上限,游戏架构设计中要尽可能的消除单点。存在单点是由于一些数据需要统计进行处理。水平拓展的方式能够增强稳定性,一个node挂掉不影响其他node。
消除单点的方式
游戏服务器中不需要消除所有的单点,只需要服务器的性能满足要求即可,避免过度优化。优化是需要开发和运维成本的。
对于互不影响的功能(登录和创建队伍)可以拆分成多个服务。对于互相影响的功能(玩家个人逻辑,强化装备、锻造装备都需要操作玩家背包的多个数据)存在同一服务中,使用服务中模块的方式将这些操作隔离开。
服务之间进程隔离和线程隔离。
隔离
对于可能卡死服务的调用,应该使用超时机制,否则A服务出现问题B服务卡死,B服务卡死C服务器跟着卡死,就会出现雪崩影响到众多的功能。
超时的情况
超时的处理方法
由于要保证幂等性,重试逻辑很复杂。所以对于重要的服务才需要考虑重试,不重要的服务还是尽量选择忽略。使用超时后应该将游戏系统进行分割,对于核心的功能还是不要使用,非核心功能可以使用超时。
使用了Redis的时候回档怎么解决。
多级灰度环境,范围较大的改动 先在在灰度环境中上线 一段时间后 没有问题就可以正式上线了。
这里就涉及到了如何让部分玩家不进入正式环境而是进行灰度环境(测试服?直接修改玩家连接的服务器?)
如第一级灰度环境-公司内测试人员,第二级灰度-在线随机挑选。
最好的情况是根据负载情况动态缩扩容,但目前的情况是冗余的服务器也是需要开销的,所以需要考虑的还是有部分的。
战斗服多地区混合部署, 玩家自动选择最优地区的服务器进行连接.
战斗机房多Central共享使用
各个Central使用匹配服决定战斗地区(同地区, 同语言文化区域, 同首选战斗, 扩大匹配区域, 同Central)
测不准
合作难
变量多
客户端到服务器
从TCP/IP四层来看
位置
服务器每帧发送物体的速度和位置. AB帧之间, 客户端使用A帧的速度进行平滑移动. 收到B帧的时候, 使用B帧的位置以及时间偏差x速度计算物体真实位置. 时间偏差是服务器B帧结束到客户端收到B帧的时间差当前时间 - (游戏开始时间 + 帧号 * 每帧时间)
.
子弹
只需要子弹的出生位置和速度就可以同步计算. 客户端负责计算特效, 服务器负责计算受伤信息.
优化
减少移动包: 从每个操作都发送 -> 状态改变的时候发送
弱网处理
延迟过大的时候进行提示
游戏开始时间受游戏开始包控制, 然而这个包的延迟是不确定的. 可以反推游戏开始时间, 当前时间 - 帧号 * 每帧时间
, 多次计算求平均值.
表现统一
A输出, 做预表现后, 会导致A的表现时长比B多一段(RTT+帧时间), 预表现要考虑这段的平滑处理.
服务器发送战斗开始, 开始计算服务器帧. 客户端收到开始后计算客户端帧. 客户端将输出和所属帧发送给服务器, 服务器判断是否是当前帧, 如果是则计算输入, 如果过期则丢弃.
服务器接收客户端输入, 收到输入后保存. 如果帧没有结束则继续等待, 结束后则运算此帧, 并将结果发送给客户端.
Wireshark可以显示客户端和服务器之间的数据包交互。对于游戏服务器来说,一般使用的都是自定义的协议,协议中包含protobuf格式的真实数据。这时想让Wireshark显示这些自定义协议就需要单独开发插件。
这里使用相对简单的lua插件的方式进行开发。开发完成后属于有用,但没那么有用,不过胜在开发成本低,还能学习到项目框架协议的编解码.
可以解决的问题
lua插件基本格式
-- 自定义的协议名称local proto_name = "tsf4g"-- 服务器端口 wireshark通过端口选择协议来解析local server_ports = {10000}local proto = Proto(proto_name, proto_name)-- 源端口的获取方式-- 由于C->S和S->C之间的协议格式不一定是一致的 所以需要进行区分srcport = Field.new("tcp.srcport")-- 个性化字段定义 - 位的显示-- 最后的0xf0代表取字节的高4位crypto_type_field_enum = { [1] = "unencrypted", -- 高4位是0001时 将会显示unencrypted [2] = "encrypted" -- 高4位是0002时 将会显示encrypted}crypto_type_field = ProtoField.uint8("crypto_type", "crypto_type", base.DEC, crypto_type_field_enum, 0xf0)-- 个性化字段定义 - 指定格式的显示-- 这里是uint32格式和string两种格式flag_field = ProtoField.uint32("flag", "flag", base.DEC, flag_field_enum)debug1 = ProtoField.string("debug1", "debug1")-- 注册字段proto.fields = { crypto_type_field, flag_field, debug1}-- 算是个工具函数 从数据中以protobuf序列化后的格式读取一个数字function read_number(tvb, begin_sub) local sub = begin_sub local number = 0 local bit_lshift = 0 repeat local num = tvb(sub, 1):uint() local t_num = bit32.band(num, 0x7f) number = bit32.bor(bit32.lshift(t_num, bit_lshift), number) bit_lshift = bit_lshift + 7 sub = sub + 1 if sub >= tvb:len() then break; end until (bit32.band(num, 0x80) == 0) return numberend-- 这里的tvb可以理解为一个数组,tree则是wireshark左下角的树状展示所需要的数据function proto.dissector(tvb, pinfo, tree) if tvb:len() > max_pack_size then return false end pinfo.cols.protocol = proto_name -- 判断是客户端发送到服务器 还是 服务器发送到客户端 -- local srcport = srcport(); -- svr_to_client = false -- for i, port in ipairs(server_ports) do -- if tostring(srcport) == tostring(port) then -- svr_to_client = true -- end -- end local sub = 0 -- 从数据的开始位置读取4个字节,并作与运算 得到包长 local pkg_size = bit32.band(tvb(sub, 4):uint(), 0x00ffffff) -- 在树中创建一个子树,这里的长度会影响选中后的高亮 local subtree = tree:add(proto, tvb(0, pkg_size), "tsf4g") -- 在子树中继续创建子树 local header = subtree:add(proto, tvb(), "header") -- 添加字段用于展示 -- 由于在定义crypto_type_field设置了0xf0,所以这里实际展示内容为 第一个字节的高4位 header:add(crypto_type_field, tvb(sub, 1)) -- 字段判断,这里判断的是有无开启加密 local crypto_type = bit32.rshift(bit32.band(tvb(sub, 4):uint(), 0xf0000000), 28) sub = sub + 1 if not(crypto_type == 1) then return true end -- 添加字段的同时 添加注释 local cmd = tvb(sub, 2):uint() header:add(cmd_field, tvb(sub, 2)):append_text(" [0 (Game Proto), other (tsf4g proto)]") sub = sub + 2 -- 这里已经将头部数据解析完毕 sub开始到data_size就是protobuf数据了。 if msg_type == 2 then -- 获取protobuf解释器 local protobuf_dissector = Dissector.get("protobuf") -- 设置协议名称 pinfo.private["pb_msg_type"] = "message,main.CSMsg" -- 给定protobuf序列化后的数据 进行解析 pcall(Dissector.call, protobuf_dissector, tvb(sub, data_size), pinfo, tree) Dissector.call(protobuf_dissector, tvb(sub, data_size):tvb(), pinfo, subtree) end -- 由于一个包中可能有多条message 这里嵌套进行解析 sub = sub + data_size if sub < tvb:len() then proto.dissector(tvb(sub):tvb(), pinfo, tree) endendlocal udp_port_table = DissectorTable.get("tcp.port")for i, port in ipairs(server_ports) do udp_port_table:add(port,proto)end
Wireshark安装后的根目录中存在init.lua文件
if not running_superuser or run_user_scripts_when_superuser then dofile(DATA_DIR.."console.lua") -- 添加自定义的lua插件 dofile("D:\\tools\\wireshark1.0\\dict.lua")end
已经有很长一段时间没有记录过本周和每天做过什么、遇到什么事情了,一个月过去如果不知道本月做了什么还是会感觉到虚度了。
编程和写作还是十分相似,写作时如果能引用其他名作中的一句或者一段话,相对完全自己写来说文章的质量就会提升一大截。
编程入门初期,看了很多的书,目前来说我看的绝大部分书籍都是初学的时候看的。在这之后就还没有看过相关的书籍,也没有阅读过开源项目的代码了。
没有输入只有输出,只能是消耗之前已有的知识,只能依靠试错来提高效率和质量。
所以还是要坚持阅读相关书籍,就编程通用的来说,代码整洁之道、重构 代码大全 人月神话,没有一本我完整的看完过。所以这些内容还是要补上。
目前每天还是在学习日语,早睡早起也是为了这一点,早上7.30能够起床,收拾完之后7.40到8.20出门能背40分钟的单词,语言学习这点还是贵在坚持了。间歇性突击学习,并不适合语言学习这里,还容易把耐心耗光。
要制定每周的计划,每月的计划,乃至每年的计划。至于每天的计划就是上面计划的拆解了。不然每天想到那里就学哪里,只学每天习惯性学习的内容,其他方面几乎就停止增长了。
目前来说更早睡和更早起,更利于我目前的计划。早上的时间可以用来学习日语,至于晚上的时候,脑子已经迟钝,用来记忆还是很难的。
为了做A事情,其中会用到其他的工具等,如果这些工具有问题如何去处理呢?感觉去处理处理能够学习到其他方面的知识,而不是不去处理。做一点分外的工作还是能加分的。以后可以把所有遇到的问题都记录下来。
理想的编程环境就跟刷Leetcode一样,只用细细解读描述的非常清楚的需求,开发时只用关注这一个功能,功能阶段性开发完之后能够立刻进行自动化测试。
目前遇到的情况中,将一个函数的几部分可以好好考虑考虑是不是要拆开,这几天做了一个将A结构体转换为B结构体的工作,其中某些字段需要做映射,如果把这些映射全部写到一个转换函数中,后续想到单独处理一个字段的映射还要拆出来。
协议的制定还是要考虑好,最好不要有临时协议这个概念,协议message中可以将部分相同含义的内容(如账号信息)放到一个单独的message中。后续进行协议改动还是挺麻烦的。
单元测试在Tsssvr中进行了第一次应用,效果还是非常不错的,能够加快问题的暴露。
最好能够摆脱客户端进行测试,最理想的情况就是只用处理收发协议。
即使项目很复杂,也应该积极采取操作去改善自己的开发环境。
模块化设计或者微服务架构 降低模块之间的影响
自动化测试
尽量缩短从改完代码到获得反馈的等待时间
学习初期,投入100份精力能够获得1000份结果。学习后期,投入100份可能只能获得10份结果。
学习新事物的时候,要想好自己要学到什么程度,知识要学的非常多。既要能做到有自己专精的部分,也要有略懂一二的部分,这样才能面对的海量的学习不完的知识。自己认为不该学习过深的就不要学习过深,腾出时间来学习其他内容,做到准确的分配自己有限的精力。
学习的时候也要选择合适的学习资料,优秀的资料不一定适合自己,浅显易懂的资料没准才是合适的,尤其是跨行学习的时候。
性能够用,避免过度设计,避免过早优化,
代码质量上精益求精时好事,不过并没有完美的代码,能够满足当前的需求,为未来预留一些扩展空间就够了。
如果你的目标就是学本事,那最应该考虑的是半年、一年、两年、三年或者离开这家公司的时候我能带走什么。
刷新简历不一定是为了跳槽,能写到简历上的才是真正敢拿出来的,可以通过内部的活水或者外部的面试的反馈来检验自己。
现在没有课程表,所以就要学到没有课程表的情况下怎么办,没有课程表的话就需要自己指定,不用每天N次课这种 也没有时间,每周每月每季度每年等。
与前辈比较起来还是很难的,毕竟时间花费就不一样。但是可以和同等级(如年龄)的人比较,看看自己到了哪个阶段,能不能到同等级的前列。
到不了的话就要找找原因了,不排除有先天性的优点或者缺点,但不至于你前面的人都是这些有先天性优点的人。
靠谱 这个比较笼统,总的来说就是让别人放心的把需求给你做,从小到大。简单的问题不靠谱经常搞出问题,又何谈复杂的问题呢
独立 刚入职的时候一般都会有人带,此时导师能够或多或少的帮你挡下一部分问题,这些问题你不用考虑都可以很好的完成需求。等后续这些问题终究还是要自己考虑。当然也不是所有问题都要自己解决,解决不了的要及时询问,不要想着独立不懂装懂,出了问题会影响你的靠谱
]]>因此,“その時は証明なんてどうとでもするから”的意思是“那时候为了证明什么东西可以不择手段地去做”。
综上所述,“だって君そうしないと一日中引きこもってそうだし”的意思为“因为如果不这样做的话,你很可能会整天呆在家里不去外面,而且还有这种可能性”。
综上所述,整个句子的意思大概是“如果得到道歉就当做了,那么就不需要警察了吧”
综上所述,整个句子的意思是“我说了对不起吧”
综上所述,整个句子的意思是“因为欺负了这样的女孩子,莫非你是Lolicon吗?”
综上所述,这句话的意思是“我只是想让你们作为成年人的孩子们知道分辨是非的能力”。
たかっただけ たい + だけ
其中,“膀胱”指的是人体内排尿的器官,而“ご当地キャラ”则是日本各地特色的地方形象吉祥物。
在这句话中,“膀胱がご当地キャラ並みに緩くなったこととか”暗示了一个人因为某个原因而无法控制便意或者从容地完成排泄的情况,类似于膀胱宽度变得像是各个地方的吉祥物那样宽松。
緩い
ゆるい ②
並み
なみ ◎
きつ(きつい去い)+ 過ぎない(すぎず)
「過ぎる」本身表示“超过、过量、过于”,当作为后缀时,其定义仍是“~过量,过于~”
1.动词ます连用+すぎる
2.一类形容词(い形容词)去掉「い」+すぎる
3.二类形容词去掉词尾「だ」(な形容词、形容动词)+すぎる
文言说法,相当于ないで,表示同一主体行为的伴随状态
和する连接时变成せずに相当于しないで
いれば 如果存在 如果是这样 いる的假定性
日语在表示主语有某种能力,有条件进行某种行为时,有以下几种方式:
1,直接用「できる」。
「私は日本语ができます。」“我会日语。”
「李さんは料理ができます。」“小李会做菜。”
这里主语用「は」表示,会的内容用「が」表示。
2,用「ことができる」。
「私は日本语を话すことができます。」
「李さんは料理を作ることができます。」
这里「日本语を话す」和「料理を作る」是能做的具体内容,用简体,是「こと」的定语,「话す」和「作る」是连体形。与上面不同的是具体内容的宾语用「を」,而不用「が」。
用这个方式可以把事情说得更具体一些,如:
「日本语を话すことができます。」“能讲日语。”
「日本语を书くことができません。」“不能写日语”
把这两句合在一起,用对比的形式表示,则为:
「日本语は话すことはできますが、书くことはできません。」
“日语能说但不能写。”
这里因为采用了对比方式,所以「日本语」作为主题提出,以「は」表示;「こと」后面的「が」以「は」表示。
3,可能态
形式为: 五段动词未然形+れる 其他动词未然形+られる
句型为:—-は—-が可能态动词。
「私は刺し身が食べられます。」“我能吃生鱼片。”
「明日は8时に来られます。」 “明天8点钟能来。”
五段动词的情况下,动词发生音变:
1.「読む」的未然形「よま」+「れる」变成「読まれる」。
2. 其中「ま」和「れ」约音变成「め」。
3. 于是「読まれる」变成「読める」
4. 因此,在五段动词中动词变成可能态,不必要通过复杂的变化,可以直接把词尾的う段假名变成该行的え段假名,再加「る」就可以了。
如:
「书く」的可能动词是「书ける」;
「游ぶ」的可能动词是「游べる」;
「走る」的可能动词是「走れる」等等。
「私は日本语の新闻が読めます。」“我能读日语报纸。”
「日曜日は休みだから、町へ行けます。」“星期日休息,所以能上街。”
这里说明一下什么是约音:用第一个假名的辅音和第二个假名的元音结合成新的假名。如:「ま」的发音是“ma”,其辅音是“m”,「れ」的发音是“re”,其元音是“e”,把“m”和“e”结合在一起,就成为“me”即「め」。关于约音的概念,不只限于可能动词,其他内容上提到的约音,都可以用此办法处理。
サ变动词的可能态本来应该是「する」的未然形「し」加「られる」而构成。这时「し」和「ら」约音变成「さ」。因此,サ变动词的可能态应该是词干加「される」。但实际上基本不用这个形式,而用词干加「できる」的形式。
如:「勉强できる」、「说明できる」等。
「図书馆は静かだから良く勉强できます。」
“图书馆很安静,能好好学习。”
「私はまだ日本语で论文が発表できません。」
“我还不能用日语发表论文。”
当一个主体受到另外一个事物的动作时,就要用被动态。
形式为: 五段动词未然形+れる
其他动词未然形+られる
这个形式和可能态的基本形式相同,但五段动词没有约音变化。
サ变动词的被动态是「する」的未然形「し」加「られる」而构成。这时「し」和「ら」约音变成「さ」。因此,サ变动词的被动态是词干加「される」。
一段动词和カ变动词的形式,和可能态完全一样,因此必须从句子结构进行区别。
被动态有4种类型:
1,在主动句中宾语是人或动物时:
主动句: 「先生が学生を褒めた。」“老师表扬了学生。”
被动句:「学生は先生に褒められた。」“学生被老师表扬了。”
在这一类被动句中,主动句的主语变成了补语,用「に」表示;主动句的宾语变成了主语,用「は」表示;动词变成了被动式(=未然形+られる)。
又如:主动句: 「猫が鱼を食べてしまった。」“猫把鱼吃掉了。”
被动句:「鱼は猫に食べられてしまった。」“鱼被猫吃掉了。”
2,在主动句中的宾语是带有以人做定语的事物时:
主动句: 「弟が私の时计を壊した。」“弟弟弄坏了我的表。”
被动句:「私は弟に时计を壊された。」“我被弟弟弄坏了表。”
在这一类被动句中,主动句的主语变成了补语,用「に」表示;主动句宾语的定语部分变成了主语,用「は」表示;宾语保留;动词变成了被动式(=未然形+れる)。
又如:主动句: 「バスの中で、隣の人が私の足を踏んだ。」
“在公共汽车里,旁边的人踩了我的脚。”
被动句:「バスの中で、私は隣の人に足を踏まれた。」
“在公共汽车里,我被旁边的人踩了脚。”
3,主动句的主语可以忽略,宾语是非人物时(多用于活动):
主动句:「学校は8时から会议を开きました。」
“学校从8时起开会。”
被动句:「会议は8时から(学校によって)开かれました。」
“会议(由学校主持)从8时开始。”
在这一类被动句中,主动句的主语一般消失,如果一定需要,可用「によって」表示;主动句的宾语变成了主语,用「は」表示;动词变成了被动式(=未然形+れる)。
又如:主动句: 「いつ、何処で、谁が谚を作ったか、分かりません。」
“弄不清楚,在何时何地,谁创造了谚语。”
被动句:「谚は、いつ、何処で、谁によって作られたか、分かりません。」
“弄不清楚谚语是在何时何地,被谁创造的。”
4,自动词的被动式:
有部分自动词,可以用被动式表示,这种情况只用在主语受到损失的情况下,多用来说明后面动作或状态的原因。
主动句:「雨が降って、风邪を引いた。」“因为下雨了,所以感冒了。”
被动句:「雨に降られて、风邪を引いた。」“因为被雨淋了,所以感冒了。”
如果主语没有受到损失,就不能用自动词的被动式。如:
主动句:「雨が降って、木が青くなった。」“下雨了,树变绿了。”
又如:主动句:「友达が来て、楽しく游んだ。」
“朋友来了,我们玩得很开心。”
被动句:「友达に来られて、宿题ができなかった。」
“朋友来了,害得我没有完成作业。”
自动词的被动式的使用范围很受局限,不是任何自动词都可以变成被动式的。下面再举几个例子:
「父に死なれて、进学をあきらめ、就职した。」
“父亲去世了,我只好放弃升学,而参加工作。”
「子供に泣かれて一晩中良く眠れなかった。」
“孩子哭了一晚上,害得我没有睡好觉。”
日语中较多地使用被动式,但在中文中则多主动形式,所以在翻译中不一定全译成被动句,可以根据情况译成主动句。但是特别要注意被动和主动的关系,千万不要译错了
当一个人受到另外一个人的命令或使役时,动作就要变成使役态。
日语的使役态形式为:
五段动词未然形+せる
其他动词未然形+させる
其中,サ变动词的未然形是「し」,+させる变成「しさせる」,在这里「し」和「さ」发生音变成为「さ」。所以サ变动词的使役态是:「词干+させる」。
动词的使役态有2种:
1,当主动句的动词是自动词时。
主动句:「妹は买い物に行った。」“妹妹去买东西。”
使役句:「母は妹を买い物に行かせた。」“母亲让妹妹去买东西。”
在这里,首先动词「行く」是自动词,其使役态是「行かせる」。在使役句中,动作的执行者“妹妹”由原来的主语变成了宾语;主语是下命令的人,因此,在使役句中主语不是动作的执行者。再则,原来为自动词的句子变成了带宾语的他动词句子。因此,如果当一个自动词根本没有对应的他动词而又需要他动词时,用其使役态是个办法。
又如:「学生が病気になったので、先生は彼を帰らせた。」
“学生生了病,所以老师让他回去了。”
「急な仕事なので、会社は山田さんを出张先に飞ばせた。」
“因为是突然的工作,所以公司让山田先生飞到出差的地方。”
在这里还应该注意的是:有些自动词已经有对应的他动词,这样时就不用使役态而用对应的他动词。如:“母亲让孩子起床。”这时一般想到用使役态,有可能译成:「母亲は子供を起きさせる。」但是实际上要译成:「母亲は子供を起こす。」因为「起きる」的对应他动词是「起こす」。
2,当主动句的动词是他动词时。
主动句:「学生は本を読みます。」“学生读书。”
使役句:「先生は学生に本を読ませます。」“老师让学生读书。”
在这里,主动句的动词是他动词,原来句子中就有宾语。在这种情况下变成使役句时,下命令的「先生」作主语,「学生」变成补语,用「に」表示,动词「読む」变成使役态「読ませる」,宾语保留。在他动词的使役句中,主语仍然不是动作的执行者。
又如:「母亲は子供に薬を饮ませた。」
“母亲给孩子吃了药。”
「あの会社は社员に一日10时间も働かせる。」
“那个公司让员工一天工作10个小时。”
「この学校は休みの日にも学生に外出させない。」
“这个学校连假日都不让学生外出。”
由于使役句带有强迫和命令的口气,所以除了「亲に心配させる」等少数句子以外,一般不用于长辈作补语的句子。如果必须让长辈作某件事情,则要用补助动词「てもらう」或者「ていただく」。如:“让老师再讲解一次。”一般不译成:「先生にもう一度说明させる。」而译成:「先生にもう一度说明していただく(てもらう)。」关于这一点,不论是自动词的句子还是他动词的句子都是一样的。
当一个人被迫或不由自主地做某件事情时使用被役态。被役态是一个动词先变成使役态后再变成被动态。
日语的被役态的形式为:
(五段动词未然形+ せる)+られる
(其他动词未然形+させる)+られる
由于变成使役态后动词已经成为下一段动词,所以后面的被动态只用られる。
(五段动词未然形+せる)+られる在实际操作时,先变成:
五段动词未然形+せられる;
然后せら两个假名发生音变,变成さ,因此整个动词变成:
五段动词未然形+される。
但是,五段动词的さ行词尾「す」是特殊的:由于「す」的未然形是「さ」,与「される」的首字さ重复,所以只有以为「す」词尾的五段动词不约音。如:「话す(はなす)」的被役态是「话させられる」,而不是「话さされる」。
主动句:「仆は买い物に行きます。」“我去买东西。”
被役句:「仆は姉に买い物に行かされます。」“我被姐姐逼着去买东西。”
可以看出:被役句的主语是动作的执行者,所以和主动句的结构基本一致,多一个强迫的来源,用「に」表示。
又如:「彼の言ったことについては本当に考えさせられる。」
关于他说的事情,不得不使我认真思考一下。
「私は饮みたくないです、でも饮まされたのです。」
“不是我愿意喝(酒),而是被别人灌的。”
与前一讲被动态里讲到的内容联系起来看,在这里值得注意的是:当动词后面出现「される」时一定要看前面的动词,前面的动词是五段动词时就是被役态,译成“被迫”;当前面的动词是サ变动词时就是被动态,译成“被……”。如:
「私は母に病院へ行かされた。」
「病院で私は医者に検査された。」
这2句话里都有「された」,是「される」的过去时。上面一句中「される」的前面是五段动词的未然形,所以是被役态,整个句子译成:“我被母亲逼着去了医院。”而下面一句的「される」前面是サ变动词的词干,所以是被动态,整个句子译成:“在医院里,我被医生作了检查。”
五段动词: 把词尾「う」段上的假名变成其所在行「あ」段上的假名加「れる」。(这个形式和被动态的基本形式相同,没有“约音便”。)如:
思う→思われる 偲ぶ→偲ばれる(しのばれる)
待つ→待たれる 思い出す→思い出される
一段动词:去掉词尾「る」加「られる」。如:
感じる→感じられる 案じる→案じられる
サ变动词:「する」的未然形「さ」加「れる」。如:
勉强する→勉强される 邪魔する→邪魔される
カ变动词:只有一个词。如:来る→来られる
用法:动词前面的「を」,变成自发动词以后要变成「が」。
例:◇この写真を见ると、学生时代のことが思い出される。
◇昨日の失败が悔やまれてならない。
◇なんだか変だと思われる。
◇外国の息子のことが案じられる。
◇それを见ると、故郷が偲ばれる。
// C++11template<class T1, class T2>auto Add11(T1 t1, T2 t2) -> decltype(t1 + t2){ return t1 + t2;}// C++14template<class T1, class T2>auto Add14(T1 t1, T2 t2){ return t1 + t2;}
template <class F, class ...Args>auto string_format(F f, Args ...args){ auto size = std::vsnprintf(nullptr, 0, f, std::forward(args)...) + 1; std::string rst; rst.resize(size); std::vsnprintf(&rst[0], size, f, std::forward(args)...); return rst;}
]]>