ET服务器框架学习笔记(十九)
前言
本篇将简单介绍下ET服务器框架中的HTTPServer。
提示:内容包含C#中的HTTP相关内容需要自行查阅相关API了
一、HttpComponent
ET服务器框架中对外提供HTTP服务的组件
1.初始化HTTP服务的配置
从StartConfigComponent获取配置
StartConfig startConfig = StartConfigComponent.Instance.StartConfig;
this.appType = startConfig.AppType;
this.HttpConfig = startConfig.GetComponent<HttpConfig>();
2.注册路径与对应处理方法实例类
- 从EventSystem中,拿到相关HttpHandlerAttribute特性标记的类,将路径与处理类的实例放入dispatcher进行管理。
调用LoadMethod方法。
public void Load()
{
this.dispatcher = new Dictionary<string, IHttpHandler>();
this.handlersMapping = new Dictionary<MethodInfo, IHttpHandler>();
this.getHandlers = new Dictionary<string, MethodInfo>();
this.postHandlers = new Dictionary<string, MethodInfo>();
List<Type> types = Game.EventSystem.GetTypes(typeof(HttpHandlerAttribute));
foreach (Type type in types)
{
object[] attrs = type.GetCustomAttributes(typeof(HttpHandlerAttribute), false);
if (attrs.Length == 0)
{
continue;
}
HttpHandlerAttribute httpHandlerAttribute = (HttpHandlerAttribute)attrs[0];
if (!httpHandlerAttribute.AppType.Is(this.appType))
{
continue;
}
object obj = Activator.CreateInstance(type);
IHttpHandler ihttpHandler = obj as IHttpHandler;
if (ihttpHandler == null)
{
throw new Exception($"HttpHandler handler not inherit IHttpHandler class: {obj.GetType().FullName}");
}
this.dispatcher.Add(httpHandlerAttribute.Path, ihttpHandler);
LoadMethod(type, httpHandlerAttribute, ihttpHandler);
}
}
- LoadMethod方法:
public void LoadMethod(Type type, HttpHandlerAttribute httpHandlerAttribute, IHttpHandler httpHandler)
{
// 扫描这个类里面的方法
MethodInfo[] methodInfos = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.InvokeMethod | BindingFlags.Instance);
foreach (MethodInfo method in methodInfos)
{
object[] getAttrs = method.GetCustomAttributes(typeof(GetAttribute), false);
if (getAttrs.Length != 0)
{
GetAttribute get = (GetAttribute)getAttrs[0];
string path = method.Name;
if (!string.IsNullOrEmpty(get.Path))
{
path = get.Path;
}
getHandlers.Add(httpHandlerAttribute.Path + path, method);
//Log.Debug($"add handler[{httpHandler}.{method.Name}] path {httpHandlerAttribute.Path + path}");
}
object[] postAttrs = method.GetCustomAttributes(typeof(PostAttribute), false);
if (postAttrs.Length != 0)
{
// Post处理方法
PostAttribute post = (PostAttribute)postAttrs[0];
string path = method.Name;
if (!string.IsNullOrEmpty(post.Path))
{
path = post.Path;
}
postHandlers.Add(httpHandlerAttribute.Path + path, method);
//Log.Debug($"add handler[{httpHandler}.{method.Name}] path {httpHandlerAttribute.Path + path}");
}
if (getAttrs.Length == 0 && postAttrs.Length == 0)
{
continue;
}
handlersMapping.Add(method, httpHandler);
}
}
通过类型信息,拿到处理类实例内的所有方法,1.获取Get特性标记的方法,拿到路径,与之前的处理实例的路径拼接起来,那么这个路径就对应了一个HTTP的Get访问路径。然后将这个路径,以及对应的方法信息(MethodInfo)注册到getHandlers中。2.同样的方式,将Post路径与方法注册到postHandlers中。3.最后将方法信息与处理类实例,都注册到handlersMapping中。
3.开启监听HTTP服务
- 初始化一个HttpListener,通过初始化的HttpConfig,开启监听的前缀:
this.listener.Prefixes.Add(s);
- 开始监听,并开启异步数据接收:
this.listener.Start();
this.Accept().Coroutine();
- 开启接收数据,while(true),异步获取context,当HttpListener有新的请求时,会唤醒await。调用InvokeHandler等待异步处理,处理完毕后调用context.Response.Close(),发送回处理结果。
public async ETVoid Accept()
{
long instanceId = this.InstanceId;
while (true)
{
if (this.InstanceId != instanceId)
{
return;
}
HttpListenerContext context = await this.listener.GetContextAsync();
await InvokeHandler(context);
context.Response.Close();
}
}
- InvokeHandler,调用处理,1.通过context.Request.HttpMethod,区分是Get还是Post请求。
2.通过context.Request.Url.AbsolutePath,获取对应的处理方法信息。3.再通过方法信息,拿到对应的处理类实例。
稍有不同的是Post需要从InputStream中读取数据出来,放入到postbody中。
context.Response.StatusCode = 404;
MethodInfo methodInfo = null;
IHttpHandler httpHandler = null;
string postbody = "";
switch (context.Request.HttpMethod)
{
case "GET":
this.getHandlers.TryGetValue(context.Request.Url.AbsolutePath, out methodInfo);
if (methodInfo != null)
{
this.handlersMapping.TryGetValue(methodInfo, out httpHandler);
}
break;
case "POST":
this.postHandlers.TryGetValue(context.Request.Url.AbsolutePath, out methodInfo);
if (methodInfo != null)
{
this.handlersMapping.TryGetValue(methodInfo, out httpHandler);
using (StreamReader sr = new StreamReader(context.Request.InputStream))
{
postbody = sr.ReadToEnd();
}
}
break;
default:
context.Response.StatusCode = 405;
break;
}
- InjectParameters方法,从获取的context,对应的方法信息,以及解析出来时PostBody数据,得到所有处理方法中的所有参数的实例对象。
具体步骤:1.遍历方法信息中的参数信息;2.根据参数信息,分别处理;3.如果是HttpListenerRequest,HttpListenerResponse,直接从context中拿到Request,Response,实例对象存入args数组对应的位置中;4.根据context.Request.HttpMethod方法获取是Get,还是Post;5.如果是Post,则根据上面解析好的PostBody数据,利用JSON反序列化为一个对象,存入args中;6.如果是Get,则从context.Request.QueryString中拿到对应的请求参数字符串,然后通过类型转换拿到实例对象;7.最终返回args数组。
private static object[] InjectParameters(HttpListenerContext context, MethodInfo methodInfo, string postbody)
{
context.Response.StatusCode = 200;
ParameterInfo[] parameterInfos = methodInfo.GetParameters();
object[] args = new object[parameterInfos.Length];
for (int i = 0; i < parameterInfos.Length; i++)
{
ParameterInfo item = parameterInfos[i];
if (item.ParameterType == typeof(HttpListenerRequest))
{
args[i] = context.Request;
continue;
}
if (item.ParameterType == typeof(HttpListenerResponse))
{
args[i] = context.Response;
continue;
}
try
{
switch (context.Request.HttpMethod)
{
case "POST":
if (item.Name == "postBody") // 约定参数名称为postBody,只传string类型。本来是byte[],有需求可以改。
{
args[i] = postbody;
}
else if (item.ParameterType.IsClass && item.ParameterType != typeof(string) && !string.IsNullOrEmpty(postbody))
{
object entity = JsonHelper.FromJson(item.ParameterType, postbody);
args[i] = entity;
}
break;
case "GET":
string query = context.Request.QueryString[item.Name];
if (query != null)
{
object value = Convert.ChangeType(query, item.ParameterType);
args[i] = value;
}
break;
default:
args[i] = null;
break;
}
}
catch (Exception e)
{
Log.Error(e);
args[i] = null;
}
}
return args;
}
- 通过InjectParameters与对应的处理方法信息,解析出所有处理方法需要的实例对象后,调用方法的Invoke方法,传入处理类实例,以及对应的参数数组,获取到恢复数据。特殊处理:如果回复类型是一个ETTASK,说明需要异步得到结果,那么就需要通过await,等待唤醒后,从结果中拿到真正的回复数据
object[] args = InjectParameters(context, methodInfo, postbody);
// 自动把返回值,以json方式响应。
object resp = methodInfo.Invoke(httpHandler, args);
object result = resp;
if (resp is ETTask<HttpResult> t)
{
await t;
result = t.GetType().GetProperty("Result").GetValue(t, null);
}
- 实例化一个流写入类,通过回复的数据类型,判断是字符串,还是一个对象,将结果通过ToString()方法,或者JsonHelper.ToJson方法转成字符串,然后写入到context.Response.OutputStream流中(格式为UTF8)
if (result != null)
{
using (StreamWriter sw = new StreamWriter(context.Response.OutputStream,Encoding.UTF8))
{
if (result.GetType() == typeof(string))
{
sw.Write(result.ToString());
}
else
{
sw.Write(JsonHelper.ToJson(result));
}
}
}
- 当处理结果已经存入到流中之后,调用context.Response.Close();发送结果给客户端,并结束这次访问。
总结
Http服务模块相对而言比较简单,就不做实例演示了,具体的使用方法,可以查看HttpTest类。
贴一下代码:
[HttpHandler(AppType.Gate, "/")]
public class HttpTest : AHttpHandler
{
[Get] // url-> /Login?name=11&age=1111
public string Login(string name, int age, HttpListenerRequest req, HttpListenerResponse resp)
{
Log.Info(name);
Log.Info($"{age}");
return "ok";
}
[Get("t")] // url-> /t
public int Test()
{
System.Console.WriteLine("");
return 1;
}
[Post] // url-> /Test1
public int Test1(HttpListenerRequest req)
{
return 1;
}
[Get] // url-> /Test2
public int Test2(HttpListenerResponse resp)
{
return 1;
}
[Get] // url-> /GetRechargeRecord
public async ETTask<HttpResult> GetRechargeRecord(long id)
{
// var db = Game.Scene.GetComponent<DBProxyComponent>();
// var info = await db.Query<RechargeRecord>(id);
await Task.Delay(1000); // 用于测试
object info = null;
if (info != null)
{
return Ok(data: info);
}
else
{
return Error("ID不存在!");
}
}
}
HttpTest类比较简单,但是基本上包含了所有的处理方式。实际的处理方式,需要在中间做一次先登录请求一个TOKEN,然后后续都拿这个TOKEN进行访问的处理。这个得自己实现了。
下一篇应该是ET服务器基础篇中的最后一篇了,梳理一下ET服务器框架中的压力测试模块。
不过在ET6.0中猫大改造了通信方式,压测模块应该都搞好了,所以下篇就简单介绍一下5.0中的压测方式吧。