因网站组(.net)与游戏服务端(c++)原来使用REST API通讯效率稍显低下,准备下期重构时改用rpc方式,经比较Thrift和gRPC两者的优劣(参照网上的对比结果),最终决定使用Thrift。
首先下载Thrift代码生成器,编写根据Thrift的语法规范(可参看https://www.cnblogs.com/tianhuilove/archive/2011/09/05/2167669.html)编写脚本文件OrderService.thrift,如下:
namespace csharp Qka.Contract service OrderService{ InvokeResult Create(1:Order order) Order Get(1:i32 orderId) list<Order> GetListByUserId(1:i32 userId,2:bool isPaid) InvokeResult Delete(1:i32 orderId) } enum ResponseCode { SUCCESS = 0, FAILED = 1, } struct Order { 1: required i32 OrderId; 2: required i32 SkuId; 3: required i32 Amount; 4: optional string Remark; } struct InvokeResult { 1: required ResponseCode code; 2: optional string Message; }
在Thrift代码生成器的目录下执行命令:./thrift.exe -gen csharp OrderService.thrift,发现同目录下多了一个gen-csharp文件夹,生成的代码放在这个文件夹里面。
新建一个.net core的解决方案,结构如下:
三个项目均添加apache-thrift-netcore的nuget包(这里服务端寄宿在asp.net core程序的原因是因为我们采用微服务的模式,每一块业务的UI、Rest API、RPC Server全部放在一块),将刚刚生成的代码文件拷至Qka.Contract项目里,其他两个项目添加Qka.Contract项目的引用。
在Qka.WebServer中实现服务接口:
public class OrderServiceImpl : Iface { public InvokeResult Create(Order order) { return new InvokeResult { Code = ResponseCode.SUCCESS, Message = $"订单{order.OrderId}创建成功!" }; } public InvokeResult Delete(int orderId) { return new InvokeResult { Code = ResponseCode.SUCCESS, Message = $"订单{orderId}删除成功成功!" }; } public Order Get(int orderId) { return new Order { OrderId = 1, SkuId = 1, Amount = 2, Remark = "黄金万两" }; } public List<Order> GetListByUserId(int userId, bool isPaid) { return new List<Order> { new Order { OrderId = 1, SkuId = 1, Amount = 10000, Remark = "黄金万两" }, new Order { OrderId = 2, SkuId = 2, Amount = 100, Remark = "白银百两" }, }; } }
编写ApplicationExtenssion,代码如下:
public static class ApplicationExtenssion { public static IApplicationBuilder UseThriftServer(this IApplicationBuilder appBuilder) { var orderService = new OrderServiceImpl(); Processor processor = new Processor(orderService); TServerTransport transport = new TServerSocket(8800); TServer server = new TThreadPoolServer(processor, transport); var services = appBuilder.ApplicationServices.CreateScope().ServiceProvider; var lifeTime = services.GetService<IApplicationLifetime>(); lifeTime.ApplicationStarted.Register(() => { server.Serve(); }); lifeTime.ApplicationStopped.Register(() => { server.Stop(); transport.Close(); }); return appBuilder; } }
上面的代码用的是TThreadPoolServer,网上的代码均采用TSimpleServer,通过反编译比较TSimpleServer、TThreadedServer、TThreadPoolServer,发现TSimpleServer只能同时响应一个客户端,TThreadedServer则维护了一个clientQueue,clientQueue最大值是100,TThreadPoolServer则用的是用线程池响应多个客户请求,生产环境绝不能用TSimpleServer。
在Startup.cs文件的Configure方法中添加:
app.UseThriftServer();
服务端代码大功告成,再来编写客户端调用代码:
class Program { static void Main(string[] args) { TTransport transport = new TSocket("localhost", 8800); TProtocol protocol = new TBinaryProtocol(transport); var client = new OrderService.Client(protocol); transport.Open(); var createResult = client.Create(new Order { OrderId = 100, SkuId = 2, Amount = 3, Remark = "测试创建订单" }); var order = client.Get(10); var list = client.GetListByUserId(10, true); var deleteResult = client.Delete(20); transport.Close(); Console.ReadKey(); } }
下面这段话引自https://www.cnblogs.com/cyfonly/p/6059374.html,解释上面代码中为什么采用TSocket和TBinaryProtocol:
Thrift 支持多种传输协议,用户可以根据实际需求选择合适的类型。Thrift 传输协议上总体可划分为文本 (text) 和二进制 (binary) 传输协议两大类,一般在生产环境中使用二进制类型的传输协议为多数(相对于文本和 JSON 具有更高的传输效率)。常用的协议包含:
TBinaryProtocol:是Thrift的默认协议,使用二进制编码格式进行数据传输,基本上直接发送原始数据 TCompactProtocol:压缩的、密集的数据传输协议,基于Variable-length quantity的zigzag 编码格式 TJSONProtocol:以JSON (JavaScript Object Notation)数据编码协议进行数据传输 TDebugProtocol:常常用以编码人员测试,以文本的形式展现方便阅读 关于以上几种类型的传输协议,如果想更深入更具体的了解其实现及工作原理,可以参考站外相关文章《thrift源码研究》。 传输方式 与传输协议一样,Thrift 也支持几种不同的传输方式。 1. TSocket:阻塞型 socket,用于客户端,采用系统函数 read 和 write 进行读写数据。 2. TServerSocket:非阻塞型 socket,用于服务器端,accecpt 到的 socket 类型都是 TSocket(即阻塞型 socket)。 3. TBufferedTransport 和 TFramedTransport 都是有缓存的,均继承TBufferBase,调用下一层 TTransport 类进行读写操作吗,结构极为相似。其中 TFramedTransport 以帧为传输单位,帧结构为:4个字节(int32_t)+传输字节串,头4个字节是存储后面字节串的长度,该字节串才是正确需要传输的数据,因此 TFramedTransport 每传一帧要比 TBufferedTransport 和 TSocket 多传4个字节。 4. TMemoryBuffer 继承 TBufferBase,用于程序内部通信用,不涉及任何网络I/O,可用于三种模式:(1)OBSERVE模式,不可写数据到缓存;(2)TAKE_OWNERSHIP模式,需负责释放缓存;(3)COPY模式,拷贝外面的内存块到TMemoryBuffer。 5. TFileTransport 直接继承 TTransport,用于写数据到文件。对事件的形式写数据,主线程负责将事件入列,写线程将事件入列,并将事件里的数据写入磁盘。这里面用到了两个队列,类型为 TFileTransportBuffer,一个用于主线程写事件,另一个用于写线程读事件,这就避免了线程竞争。在读完队列事件后,就会进行队列交换,由于由两个指针指向这两个队列,交换只要交换指针即可。它还支持以 chunk(块)的形式写数据到文件。 6. TFDTransport 是非常简单地写数据到文件和从文件读数据,它的 write 和 read 函数都是直接调用系统函数 write 和 read 进行写和读文件。 7. TSimpleFileTransport 直接继承 TFDTransport,没有添加任何成员函数和成员变量,不同的是构造函数的参数和在 TSimpleFileTransport 构造函数里对父类进行了初始化(打开指定文件并将fd传给父类和设置父类的close_policy为CLOSE_ON_DESTROY)。 8. TZlibTransport 跟 TBufferedTransport 和 TFramedTransport一样,调用下一层 TTransport 类进行读写操作。它采用<zlib.h>提供的 zlib 压缩和解压缩库函数来进行压解缩,写时先压缩再调用底层 TTransport 类发送数据,读时先调用 TTransport 类接收数据再进行解压,最后供上层处理。 9. TSSLSocket 继承 TSocket,阻塞型 socket,用于客户端。采用 openssl 的接口进行读写数据。checkHandshake()函数调用 SSL_set_fd 将 fd 和 ssl 绑定在一起,之后就可以通过 ssl 的 SSL_read和SSL_write 接口进行读写网络数据。 10. TSSLServerSocket 继承 TServerSocket,非阻塞型 socket, 用于服务器端。accecpt 到的 socket 类型都是 TSSLSocket 类型。 11. THttpClient 和 THttpServer 是基于 Http1.1 协议的继承 Transport 类型,均继承 THttpTransport,其中 THttpClient 用于客户端,THttpServer 用于服务器端。两者都调用下一层 TTransport 类进行读写操作,均用到TMemoryBuffer 作为读写缓存,只有调用 flush() 函数才会将真正调用网络 I/O 接口发送数据。