因网站组(.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 = ,
SkuId = ,
Amount = ,
Remark = "黄金万两"
};
} public List<Order> GetListByUserId(int userId, bool isPaid)
{
return new List<Order>
{
new Order
{
OrderId = ,
SkuId = ,
Amount = ,
Remark = "黄金万两"
},
new Order
{
OrderId = ,
SkuId = ,
Amount = ,
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();
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", );
TProtocol protocol = new TBinaryProtocol(transport);
var client = new OrderService.Client(protocol);
transport.Open(); var createResult = client.Create(new Order
{
OrderId = ,
SkuId = ,
Amount = ,
Remark = "测试创建订单"
});
var order = client.Get();
var list = client.GetListByUserId(, true);
var deleteResult = client.Delete(); 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 接口发送数据。