产品及架构介绍
定义
命名空间:TouchSocket.WebApi
程序集:TouchSocket.WebApi.dll
一、说明
WebApi是通用的Rpc调用,与编程语言无关,与操作系统无关。其路由机制模仿AspNetCore,可实现很多路由机制。但是因为http兼容性错综复杂,所以目前TouchSocket的WebApi仅支持GET、POST函数。使用体验接近于AspNetCore。
二、特点
- 高性能,100个客户端,10w次调用,仅用时17s。
- 全异常反馈 。
- 支持大部分路由规则。
- 支持js、Android等调用。
三、定义服务
在服务器端中新建一个类,继承于RpcServer类(或实现IRpcServer),然后在该类中写公共方法,并用WebApi属性标签标记。
public partial class ApiServer : RpcServer
{
private readonly ILog m_logger;
public ApiServer(ILog logger)
{
this.m_logger = logger;
}
[WebApi(Method = HttpMethodType.Get)]
public int Sum(int a, int b)
{
return a + b;
}
}
四、启动服务器
更多注册Rpc的方法请看注册Rpc服务
var service = new HttpService();
await service.SetupAsync(new TouchSocketConfig()
.SetListenIPHosts(7789)
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
a.AddRpcStore(store =>
{
store.RegisterServer<ApiServer>();//注册服务
});
})
.ConfigurePlugins(a =>
{
a.UseCheckClear();
a.UseWebApi();
//此插件是http的兜底插件,应该最后添加。作用是当所有路由不匹配时返回404.且内部也会处理Option请求。可以更好的处理来自浏览器的跨域探测。
a.UseDefaultHttpServicePlugin();
}));
await service.StartAsync();
Console.WriteLine("以下连接用于测试webApi");
Console.WriteLine($"使用:http://127.0.0.1:7789/ApiServer/Sum?a=10&b=20");
五、参数规则
5.1 Get规则
使用Get
进行请求时,服务方法可以声明多个参数,但是每个参数都必须是简单类型(例如:int、string、DateTime等)。
[WebApi(Method = HttpMethodType.Get)]
public int Get(int a)
{
return a;
}
[WebApi(Method = HttpMethodType.Get)]
public int Sum(int a, int b)
{
return a + b;
}
5.2 Post规则
使用Post
进行请求时,服务方法可以声明多个参数,但是当参数是基础类型或者字符串类型时,它也会来源于Query
参数。
同时,有且只有当最后一个参数为其他类型时,才会从Body
解析。
例如:
以下参数依然来自Query
,Body
为空也可以。
[WebApi(Method = HttpMethodType.Post)]
public int Sum(int a, int b)
{
return a + b;
}
当最后一个参数为其他类型时,它将会从Body
解析。
例如:
以下参数,前两个来自Query
,MyClass
将从Body
解析。
[WebApi(Method = HttpMethodType.Post)]
public int Sum(int a, int b, MyClass myClass)
{
return a + b;
}
5.3 特性规则
WebApi
支持使用特性来定制参数规则。目前支持[FromQuery]
、[FromHeader]
、[FromForm]
、[FromBody]
等。
例如:
[FromQuery]
[WebApi(Method = HttpMethodType.Get)]
public int SumFromQuery([FromQuery] int a, [FromQuery] int b)
{
return a + b;
}
[FromHeader]
[WebApi(Method = HttpMethodType.Get)]
public int SumFromHeader([FromHeader] int a, [FromHeader] int b)
{
return a + b;
}
[FromForm]
[WebApi(Method = HttpMethodType.Post)]
public int SumFromForm([FromForm] int a, [FromForm] int b)
{
return a + b;
}
[FromBody]
[WebApi(Method = HttpMethodType.Post)]
public int SumFromBody([FromBody] MyClass myClass)
{
return myClass.A + myClass.B;
}
当使用特性时,可以重新指定参数名,例如:
[WebApi(Method = HttpMethodType.Get)]
public int SumFromQuery([FromQuery(Name ="aa")] int a, [FromQuery] int b)
{
return a + b;
}
六、路由规则
框架的路由规则比较简单,默认情况下,以服务的名称+方法名称作为路由。
例如下列:
将会以/ApiServer/Sum
为请求url(不区分大小写)。
public class ApiServer : RpcServer
{
[WebApi(Method = HttpMethodType.Get)]
public int Sum(int a, int b)
{
return a + b;
}
}
当需要定制路由消息时,可用[api]
替代服务名,[action]
替代方法名。
例如下列:
将会以user/ApiServer/test/Sum
为请求url(不区分大小写)。
[Router("/user/[api]/test/[action]")]
public class ApiServer : RpcServer
{
[WebApi(Method = HttpMethodType.Get)]
public int Sum(int a, int b)
{
return a + b;
}
}
Router
特性不仅可以用于服务,也可以用于方法。而且可以多个使用。
七、调用上下文
框架中,每个请求都会产生一个调用上下文(ICallContext),这个上下文可以获取到当前请求的客户端、服务、方法、参数等。
通用调用上下文可以参阅通用Rpc调用上下文。
下列将介绍WebApi
的专用调用上下文。
首先,在服务方法中,只要有参数类型为IWebApiCallContext
(或者ICallContext
),框架会自动注入当前调用上下文。
例如:
[WebApi(Method = HttpMethodType.Get)]
public int SumCallContext(IWebApiCallContext callContext, int a, int b)
{
return a + b;
}
调用上下文的位置没有限制,但是建议在方法参数的最前面,这样即使对Post
,也不会有额外的判断压力。
7.1 获取当前客户端信息
一般来说,调用上下文的Caller
就是实际通信的客户端,也就是HttpSessionClient
。
if (callContext.Caller is IHttpSessionClient httpSessionClient)
{
Console.WriteLine($"IP:{httpSessionClient.IP}");
Console.WriteLine($"Port:{httpSessionClient.Port}");
Console.WriteLine($"Id:{httpSessionClient.Id}");
}
7.2 获取HttpContext
WebApi
的调用上下文,除了Caller
,还有HttpContext
。
//http内容
var httpContext = callContext.HttpContext;
//http请求
var request = httpContext.Request;
//http响应
var response = httpContext.Response;
当获取到HttpContext
后,就可以做HttpService
的所有功能。例如:读取请求头,修改响应头,修改响应内容,修改响应状态码、响应WebSocket连接等。具体可以参考创建HttpService和WebSocketService。
八、调用服务
8.1 直接调用
直接调用,则是不使用任何代理,直接Call Rpc,使用比较简单,浏览器也能直接调用实现。
【Url请求】
http://127.0.0.1:7789/ApiServer/Sum?a=10&b=20
8.2 框架HttpClient调用
框架内置的HttpClient
,可以参考HttpClient的使用,此处做一个简单使用示例。
var client = new HttpClient();
await client.ConnectAsync("127.0.0.1:7789");
string responseString = await client.GetStringAsync("/ApiServer/Sum?a=10&b=20");
8.3 内置WebApiClient调用
内置WebApi
的客户端和HttpClient
基本一致,但是封装了一些Rpc
的调用接口,可以更加方便的执行一些操作。
【创建WebApi客户端】
var client = new WebApiClient();
await client.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("127.0.0.1:7789")
.ConfigurePlugins(a =>
{
a.Add<MyWebApiPlugin>();
}));
await client.ConnectAsync();
【GET调用】
在使用GET
进行调用时,其InvokeKey
直接使用Url即可,然后还需要构建一个WebApiRequest
,然后使用Invoke
(或者InvokeT
)进行调用。
例如:
var request = new WebApiRequest();
request.Method = HttpMethodType.Get;
request.Querys = new KeyValuePair<string, string>[] { new KeyValuePair<string, string>("a", "10"), new KeyValuePair<string, string>("b", "20") };
var sum1 = client.InvokeT<int>("/ApiServer/Sum", invokeOption_30s, request);
Console.WriteLine($"Get调用成功,结果:{sum1}");
【POST调用】
例如:
var requestForPost = new WebApiRequest();
requestForPost.Method = HttpMethodType.Post;
requestForPost.Body = new MyClass() { A = 10, B = 20 };
var sum2 = client.InvokeT<int>("/ApiServer/TestPost", invokeOption_30s, requestForPost);
Console.WriteLine($"Post调用成功,结果:{sum2}");
8.4 Dotnet自带HttpClient调用
Dotnet
自带HttpClient
则是通过连接池的方式访问。详情看HttpClient
【创建客户端】
var client = new WebApiClientSlim(new System.Net.Http.HttpClient());
await client.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("http://127.0.0.1:7789"));
调用方法同上。
按照微软建议,HttpClient
应该保证整个程序中单实例使用,所以可以在WebApiClientSlim
构造函数中传入已存在的对象。
WebApiClientSlim
仅在net6.0+,net481可用。
8.5 生成代理调用
使用WebApi
客户端进行调用时,其使用规则相较比较复杂的。但是在实际使用时,基本上是不需要手动书写调用代码的。下面将介绍代理生成调用。
在服务器端,注册完服务后,就可以生成客户端调用代码了。详细的操作可以查看服务端代理生成
a.UseWebApi()
.ConfigureRpcStore(store =>
{
store.RegisterServer<ApiServer>();//注册服务
#if DEBUG
//下列代码,会生成客户端的调用代码。
var codeString = store.GetProxyCodes("WebApiProxy", typeof(WebApiAttribute));
File.WriteAllText("../../../WebApiProxy.cs", codeString);
#endif
});
然后把生成的.cs文件复制(或链接)到客户端项目。然后客户端直接使用同名扩展方法
即可调用。
var sum3 =await client.SumAsync(10,20);
8.6 使用DispatchProxy代理调用
使用DispatchProxy代理调用,可以实现动态代理,详情请看DispatchProxy代理生成
首先,需要声明一个基类,用于通讯基础。
/// <summary>
/// 新建一个类,继承WebApiDispatchProxy,亦或者RpcDispatchProxy基类。
/// 然后实现抽象方法,主要是能获取到调用的IRpcClient派生接口。
/// </summary>
class MyWebApiDispatchProxy : WebApiDispatchProxy
{
private readonly WebApiClient m_client;
public MyWebApiDispatchProxy()
{
this.m_client = CreateWebApiClient();
}
private static WebApiClient CreateWebApiClient()
{
var client = new WebApiClient();
await client.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("127.0.0.1:7789")
.ConfigurePlugins(a =>
{
a.UseReconnection();
}));
await client.ConnectAsync();
Console.WriteLine("连接成功");
return client;
}
public override IWebApiClientBase GetClient()
{
return m_client;
}
}
然后按照服务,定义一个相同的代理接口。
interface IApiServer
{
[Router("ApiServer/[action]")]
[WebApi(Method = HttpMethodType.Get)]
int Sum(int a, int b);
}
路由规则和服务端相同。
最后生成代理,并按照接口调用。
IApiServer api = MyWebApiDispatchProxy.Create<IApiServer, MyWebApiDispatchProxy>();
while (true)
{
Console.WriteLine("请输入两个数,中间用空格隔开,回车确认");
string str = Console.ReadLine();
var strs = str.Split(' ');
int a = int.Parse(strs[0]);
int b = int.Parse(strs[1]);
var sum = api.Sum(a, b);
Console.WriteLine(sum);
}
WebApi
的调用,除了上述,还支持源生成调用,更多请看源生成调用。
九、数据格式化
数据格式化,就是对WebApi
执行前后的数据进行序列化和反序列化。一般来说,常用的格式化类型有两种,一种是JSON
,另一种是XML
。具体的,应该根据Accept
请求头中的数据格式来选择。默认情况下:
- 如果Accept请求头中包含
application/json
、text/json
,则使用JSON格式化。 - 如果Accept请求头中包含
application/xml
、text/xml
,则使用XML格式化。 - 如果Accept请求头中包含
text/plain
,则使用文本格式化(如果是复杂类型,则依然会按照Json或则Xml)。
9.1 格式化配置
在添加WebApi
插件时,可以通过ConfigureConverter
方法来配置数据格式化。
.ConfigurePlugins(a =>
{
a.UseWebApi()
.ConfigureConverter(converter =>
{
//配置转换器
//converter.Clear();//可以选择性的清空现有所有格式化器
//添加Json格式化器,可以自定义Json的一些设置
converter.AddJsonSerializerFormatter(new Newtonsoft.Json.JsonSerializerSettings() {Formatting= Newtonsoft.Json.Formatting.None } );
//添加Xml格式化器
converter.AddXmlSerializerFormatter();
});
})
9.2 自定义格式化器
TouchSocket的WebApi
插件支持自定义格式化器。
新建一个类,实现ISerializerFormatter
接口,并实现相关方法。
class MySerializerFormatter : ISerializerFormatter<string, HttpContext>
{
public int Order { get; set; }
public bool TryDeserialize(HttpContext state, in string source, Type targetType, out object target)
{
//反序列化
throw new NotImplementedException();
}
public bool TrySerialize(HttpContext state, in object target, out string source)
{
//序列化
throw new NotImplementedException();
}
}
添加格式化器
.ConfigurePlugins(a =>
{
a.UseWebApi()
.ConfigureConverter(converter =>
{
converter.Add(new MySerializerFormatter());
});
})
十、鉴权、授权
10.1 请求插件实现
在AspNetCore中,鉴权与授权是通过中间件实现的。而TouchSocket的WebApi(HttpService)在设计时也可以使用类似方式实现该功能。下列就以伪代码jwt鉴权示例。
首先声明一个鉴权插件。用于判断当前请求header中是否包含授权header。
/// <summary>
/// 鉴权插件
/// </summary>
class AuthenticationPlugin : PluginBase, IHttpPlugin
{
public async Task OnHttpRequest(IHttpSessionClient client, HttpContextEventArgs e)
{
string aut = e.Context.Request.Headers["Authorization"];
if (aut.IsNullOrEmpty())//授权header为空
{
await e.Context.Response
.SetStatus(401, "授权失败")
.AnswerAsync();
return;
}
//伪代码,假设使用jwt解码成功。那就执行下一个插件。
//if (jwt.Encode(aut))
//{
// 此处可以做一些授权相关的。
//}
await e.InvokeNext();
}
}
然后添加使用插件即可。
.ConfigurePlugins(a =>
{
a.UseCheckClear();
a.Add<AuthenticationPlugin>();
a.UseWebApi();
...
})
鉴权插件的添加,应该在UseWebApi之前。这样才能保证api的安全性。
10.2 Rpc Aop实现
WebApi也属于Rpc的行列,所以在执行时,也可以在Rpc的Aop中实现鉴权。具体请看Rpc服务AOP
十一、跨域
在WebApi
中的跨域,除了Cors跨域全局设置之外,还支持特性设置,进行更细粒度的控制。
所以,首先添加跨域服务是必须的。
.ConfigureContainer(a =>
{
//添加跨域服务
a.AddCors(corsOption =>
{
//添加跨域策略,后续使用policyName即可应用跨域策略。
corsOption.Add("cors", corsBuilder =>
{
corsBuilder.AllowAnyMethod()
.AllowAnyOrigin();
});
});
})
然后,在WebApi中使用特性进行跨域设置。
public partial class ApiServer : RpcServer
{
[EnableCors("cors")]//使用跨域
[WebApi(Method = HttpMethodType.Get)]
public int Sum(int a, int b)
{
return a + b;
}
}
EnableCors
特性,不仅可以用于方法,还支持服务类,接口直接使用。