谁是虽好的语言 ?- 语言选型闲聊(上)
谁是最好的语言?当然是php了 :) 近期我们公司做架构升级,调研了一下各种语言, 包括TypeScript,c#,rust, 还有java和go。这个过程中有一些个人看法,可能会有些偏颇或者不正确的地方,我就简单一说,大家一乐,无意引战。
我们公司服务端的语言体系也比较多,有python,java,go,还有c#,我们传统使用的是python。现在有新的业务机会,老板也支持来一次重新选择,为以后的后端架构奠定一点基础。对于后端的语言选择,我们的核心需求是高性能,这个一点也不意外,高性能意味着低服务器成本。同样如果可以支持热更会更好,可以不重启服务直接修复bug,对追求糙猛快的游戏服务非常有吸引力。天下武功,唯快不破。如果后端语言能够和客户端统一,那更是极好的。除了工具栈统一,方便交流,还有一个易见的好处:代码重用。有些游戏逻辑,比如战斗功能,客户端需要计算,服务器也需要对客户端结果进行校验,不然外挂横行,会冲垮游戏体系。同样的功能和逻辑,不同的语言各自实现一遍,成本肯定是双杀。
在语言及其生态之外,还得考虑人才市场的情况,架构不只是技术。对后端来说,c/c++肯定性能最高效,但是人难招。我们也要跟随市场上主流的技术方向。
以上是一些主要背景知识,我之前工作中还做过一些网页游戏开发,用的是Flash/ActionScript/html5; 单机游戏开发,用的是cocos-xLua; 基于java的企业系统开发; 基于php的网站和CRM系统.., 对这些语言也是略有了解,虽然杂而不精。回到正题,下面我们正式聊聊各种语言:TypeScript(ts),c#,rust, go 和 java,当然包括绕不过去的python。
python
python毕竟简单轻便,人生苦短,我用python,上手极快,功能强大,在公司里一直霸榜。可以做游戏服务,可以做内部系统,可以做自动化测试,可以做数据分析,可以做自动化运维...。优点很多,但是就游戏服务而言,主要就一个缺点:性能有限。由于GIL锁的原因,导致无法很好的发挥CPU多核的潜力。虽然我们有一些实践,利用pypy协程+twisted的异步IO,可以提高单线程的并发能力和降低编码难度,可是终究无法绕过多进程模型这个槛。举个简单的例子。游戏里有大量的配置文件,在多进程模型下,每个进程都要加载配置并序列化到内存,这样就造成内存浪费。同时因为进程数量庞大,管理和维护成本也在递增。当我们的玩家规模增长后,就出现各种问题,要解决这些问题,又需要做一些复杂的hack设计。
python可以做到比较好的性能,不过我个人认为,这种优化工作,应该是编译器/运行时做的工作,而不是程序编码。举个例子:
# py-amqp-5.0.6/amqp/method_framing.py
def frame_writer(connection, transport,
pack=pack, pack_into=pack_into, range=range, len=len,
bytes=bytes, str_to_bytes=str_to_bytes, text_t=str):
"""Create closure that writes frames."""
write = transport.write
...
write(pack('>BHI%dsB' % framelen,
type_, channel, framelen, frame, 0xce))
...
示例选自py-amqp的代码,write 函数来自 transport 的同名函数,为了提高 frame_writer 的性能,在函数内部进行了本地化,重新定义了一个内部变量。这种提高性能的处理方式,我们可以在很多库中看到。关于一些框架的性能实测数据,github上的 FrameworkBenchmarks 项目里有一些展示,我自己也有一些对比实测数据,如下表:
| 框架 | QPS (plaintext/json) |
| --------------- | -------------------- |
| nginx | 85267 |
| go | 89354/87626 |
| node | 45212/40965 |
| node-thread | 31487/32903 |
| node-cluster(2) | 77450/71943 |
| pypy3+twisted | 14349/15921 |
| python3+aiohttp | 4959/4625 |
| c# | 59347/58829
最终的结果个人觉得,python目前的服务性能优化,就像你拿着手工刀在绣花,费老大劲;结果别人用3d打印,瞬间完成。
对我的实测详细数据感兴趣的请公众号留言,如果留言比较多,我会整理公开分享出来。
TypeScript
TypeScript是微软家推出的语言,是JavaScript的超集。TypeScript 发展至今,已经成为大型项目的标配,其提供的静态类型系统,大大增强了代码的可读性以及可维护性;同时,它提供最新和不断发展的 JavaScript 特性,能让我们建立更健壮的组件。TypeScript经过静态编译,生成JavaScript代码供各种环境执行。对后端服务来说,主要就是基于的NodeJS环境的运行时。
ts语法支持类型注解,解决了JavaScript的弱类型这一诟病;同时又是面向对象的语言,支持类,接口,继承,泛型等等;提供了很多先进的语法糖,比如装饰器,Mixin,预处理指令等。得益于Node.js 处理非阻塞 I/O 操作的事件循环机制运行效率也非常高效。主要缺点也是多进程和多线程使用起来复杂,而且不一定高效。比如上面示例,多线程方式反而比单线程效率低;多进程实现,又没法共享可变数据。
就语言的特定来说,其编译时和运行时两个状态加大了心理负担。比如下面的多线程实现, 这是node-js实现的主线程:
//add this code snippet to main.js
const { Worker } = require('worker_threads')
const runService = (WorkerData) => {
console.log(WorkerData)
return new Promise((resolve, reject) => {
// import workerExample.js script..
// 注意workerData是一个字典
const worker = new Worker('./workerExample.js', { workerData:WorkerData });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0)
reject(new Error(`stopped with ${code} exit code`));
})
})
}
const run = async () => {
const result = await runService('hello John Doe')
console.log(result);
}
run().catch(err => console.error(err))
主要就是启动主线程的时候创建了一个worker,也就是用户可控制的线程,然后主线程和这个子线程交互消息。线程的实现代码:
// add this to workerExample.js file.
const { workerData, parentPort } = require('worker_threads')
console.log("receive:", workerData)
parentPort.postMessage({ welcome: workerData })
如果换成使用ts实现,则worker大概需要提供两个文件,分别是js和ts,代表编译时候和运行时候的状态, 理解起来有点困难:
# worker.js
const path = require("path");
require("ts-node").register();
require(path.resolve(__dirname, "./worker.ts"));
# worker.ts
import worker from "worker_threads";
const wd = worker.workerData;
process.on('message', func(this));
还要一个缺点是,语言提供的库太少,导致项目依赖项有点太多了。比如下面是一个项目的依赖情况,虽然有一些是dev依赖,但是要想很好的工作也需要逐个去了解:
# package.json
{
"devDependencies": {
"@lerna/batch-packages": "^3.16.0",
"@lerna/filter-packages": "^4.0.0",
"@lerna/project": "^4.0.0",
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.2.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/debug": "^0.0.31",
"@types/express": "^4.16.1",
"@types/fossil-delta": "^1.0.0",
"@types/jest": "^26.0.24",
"@types/koa": "^2.0.49",
"@types/mocha": "^5.2.7",
"@types/node": "^16.3.2",
"@types/sinon": "^10.0.2",
"all-contributors-cli": "^5.4.0",
"assert": "^2.0.0",
"benchmark": "^2.1.1",
"c8": "^7.7.2",
"colyseus.js": "^0.14.13",
"cors": "^2.8.5",
"express": "^4.16.2",
"httpie": "^2.0.0-next.13",
"jest": "^27.0.6",
"koa": "^2.8.1",
"lerna": "^4.0.0",
"minimist": "^1.2.5",
"mocha": "^5.1.1",
"rimraf": "^2.7.1",
"rollup": "^2.47.0",
"rollup-plugin-node-externals": "^2.2.0",
"sinon": "^11.1.1",
"ts-jest": "^27.0.3",
"ts-node": "^7.0.1",
"ts-node-dev": "^1.1.6",
"tslint": "^5.11.0",
"typescript": "^4.3.5"
},
}
从Node.JS的趋势上看。07年大家耳熟的Atwood定律:凡是可以用 JavaScript 来写的应用,最终都会用 JavaScript 来写。经过十年后,在17年达到顶峰,然后逐渐下跌。
c#
c#在国内后端圈,感觉非常小众,可能是微软给人的深刻影响导致。比如下面juejin的主题关注和文章数据对比:
实际上可能大家一叶障目了,是刻板印象。c#的语法比java更优秀,传闻java的很多语法都借鉴自c#,.net也很早就支持跨平台运行。据2017年数据统计显示,微软是github上贡献最大的公司,而且现在github也是它家的。我们公司最近引入了一位大佬,极力推荐c#。大佬在c#上构建了部分游戏服务生态,比如一个叫做 鲁班(luban) 的配置工具,可以一键将游戏策划的excel转换成程序可以使用的配置数据,提供了一个配置文件的完整解决方案。
目前鲁班(luban)已经完全在github上开源,获得251个赞。欢迎大家点击文章左下角「阅读原文」前去围观点赞。送人玫瑰,手留余香。
c#使用下面简单的几行代码,就可以实现一个http服务, 全部的依赖仅仅使用系统包:
using System;
using System.IO;
using System.Text;
using System.Net;
using System.Threading.Tasks;
namespace HttpListenerExample
{
class HttpServer
{
public static HttpListener listener;
public static string url = "http://localhost:8000/";
public static async Task HandleIncomingConnections()
{
// While a user hasn't visited the `shutdown` url, keep on handling requests
while (true)
{
// Will wait here until we hear from a connection
HttpListenerContext ctx = await listener.GetContextAsync();
// Peel out the requests and response objects
HttpListenerRequest req = ctx.Request;
HttpListenerResponse resp = ctx.Response;
// Print out some info about the request
Console.WriteLine(req.Url.ToString());
Console.WriteLine(req.HttpMethod);
Console.WriteLine(req.UserHostName);
Console.WriteLine(req.UserAgent);
byte[] data = Encoding.UTF8.GetBytes("Hello, c#");
resp.ContentType = "text/html";
resp.ContentEncoding = Encoding.UTF8;
resp.ContentLength64 = data.LongLength;
// Write out to the response stream (asynchronously), then close it
await resp.OutputStream.WriteAsync(data, 0, data.Length);
resp.Close();
}
}
public static void Start()
{
// Create a Http server and start listening for incoming connections
listener = new HttpListener();
listener.Prefixes.Add(url);
listener.Start();
Console.WriteLine("Listening for connections on {0}", url);
// Handle requests
Task listenTask = HandleIncomingConnections();
listenTask.GetAwaiter().GetResult();
// Close the listener
listener.Close();
}
}
}
这样看c#的语法会和java很类似,同时dotnet还提供了mvc模式的框架,使用起来和spring-mvc很接近。对于java熟悉的同学上手应该非常迅速。
c#也有一些问题,比较不爽的地方就是不像go一样,可以方便的编译成单个bin文件,跨平台运行。还有一个问题是它歧视macOS,你想不到吧,它竟然歧视macOS。我们计划的架构中使用http2协议作为服务间通讯,经过一番实验发现,mac下竟然不支持,官方文档是这样说的 “HTTP/2 will be supported on macOS in a future release.” 。曾经的鄙视链底端windows,翻身农奴把歌唱了,鄙视起尊贵的mac。
好了,时间到。今天就先闲聊到这里吧,下篇我们继续讲讲go和java的故事,别忘了「阅读原文」去github点赞哦。
参考链接
鲁班 https://github.com/focus-creative-games/luban https://github.com/TechEmpower/FrameworkBenchmarks https://www.tslang.cn/index.html 微软开源软件列表 https://www.infoq.cn/article/2017/03/github-first-microsoft-open-sour https://docs.microsoft.com/en-us/dotnet/csharp/
欢迎进群一起进行技术交流
加群方式:公众号消息私信“加群”或加我好友再加群均可