在通讯应用中很多时候需要和已有标准的应用协议进行通讯,针对这情况就要针对相应协议的实现;标准协议上考虑的情况比较多,所以协议的复杂度也相对高些,对比之前的Protobuf通讯的简单协议来说则会复杂。接下来用组件去实现一个简单的HTTP协议服务,让浏览器可以去访问它。
HTTP协议
对于HTTP协议的介绍相信也不用过多描述,毕竟这个协议已经应用了N年了,网上针对这一协议的介绍也非常多。这协议的版本有1.0,1.1和2.0,接下来实现的是HTTP1.1。其实更符合多场景应用是2.0,不过2.0的协议复杂度就比较高了,所以就不在这里实现介绍了。
HTTP 1.1协议只允许同一时间处理一个请求,就是当服务端接收到请求后直到响应完成才会处理下一下请求。为了满足这需要针对通讯协议制定了Request和Response对象。
Request对象
该对象主要用于收集HTTP的请求信息,定义如下:
class HttpRequest{//当前HTTP版本信息public string HttpVersion { get; set; }//请求的方法public string Method { get; set; }//基础的urlpublic string BaseUrl { get; set; }//客户端IPpublic string ClientIP { get; set; }//请求路径public string Path { get; set; }//Url参数public string QueryString { get; set; }//完整URLpublic string Url { get; set; }//头部信息public Dictionary<string, string> Headers { get; private set; } = new Dictionary<string, string>();//HTTP内容public byte[] Body { get; set; }//HTTP内容长度public int ContentLength { get; set; }//请求对象状态public RequestStatus Status { get; set; } = RequestStatus.None;}
以上是一个HTTP请求的简单描述对象,服务会根据网络数据根据HTTP协议转换成相应的对象消息。
HttpResponse对象
该对象用于设置请求响应内容,定义如下:
class HttpResponse : IWriteHandler{public HttpResponse(){Headers["Content-Type"] = "text/html";}public string HttpVersion { get; set; } = "HTTP/1.1";public int Status { get; set; }public string StatusMessage { get; set; } = "OK";public string Body { get; set; }public Dictionary<string, string> Headers = new Dictionary<string, string>();public void Write(Stream stream){var pipeStream = stream.ToPipeStream();//写入响应状态pipeStream.WriteLine($"{HttpVersion} {Status} {StatusMessage}");//写入头部信息foreach (var item in Headers)pipeStream.WriteLine($"{item.Key}: {item.Value}");byte[] bodyData = null;if (!string.IsNullOrEmpty(Body)){bodyData = Encoding.UTF8.GetBytes(Body);}if (bodyData != null){pipeStream.WriteLine($"Content-Length: {bodyData.Length}");}pipeStream.WriteLine("");//写入响应消息体if (bodyData != null){pipeStream.Write(bodyData, 0, bodyData.Length);}Completed?.Invoke(this);}public Action<IWriteHandler> Completed { get; set; }}
对象实现了IWriteHandler接口,用于告诉组件提供自定义流输出实现。
协议实现
在这个示例中协议分析并没有实现IPacket,而是直接接管SessionReceive方法来对流进行HTTP协议分析,具体实现代码如下:
public override void SessionReceive(IServer server, SessionReceiveEventArgs e)
{var request = GetRequest(e.Session);var pipeStream = e.Stream.ToPipeStream();if (LoadRequest(request, pipeStream) == RequestStatus.Completed){OnCompleted(request, e.Session);}
}private RequestStatus LoadRequest(HttpRequest request, PipeStream stream)
{//分析HTTP请求信息LoadRequestLine(request, stream);//分析头信息LoadRequestHeader(request, stream);//加载BodyLoadRequestBody(request, stream);return request.Status;
}private void LoadRequestLine(HttpRequest request, PipeStream stream)
{if (request.Status == RequestStatus.None){if (stream.TryReadLine(out string line)){var subItem = line.SubLeftWith(' ', out string value);request.Method = value;subItem = subItem.SubLeftWith(' ', out value);request.Url = value;request.HttpVersion = subItem;subItem = request.Url.SubRightWith('?', out value);request.QueryString = value;request.BaseUrl = subItem;request.Path = subItem.SubRightWith('/', out value);if (request.Path != "/")request.Path += "/";request.Status = RequestStatus.LoadingHeader;}}
}private void LoadRequestHeader(HttpRequest request, PipeStream stream)
{if (request.Status == RequestStatus.LoadingHeader){while (stream.TryReadLine(out string line)){if (string.IsNullOrEmpty(line)){if (request.ContentLength == 0){request.Status = RequestStatus.Completed;}else{request.Status = RequestStatus.LoadingBody;}return;}var name = line.SubRightWith(':', out string value);if (String.Compare(name, "Content-Length", true) == 0){request.ContentLength = int.Parse(value);}request.Headers[name] = value.Trim();}}
}
private void LoadRequestBody(HttpRequest request, PipeStream stream)
{if (request.Status == RequestStatus.LoadingBody){if (stream.Length >= request.ContentLength){var data = new byte[request.ContentLength]; ;stream.Read(data, 0, data.Length);request.Body = data;request.Status = RequestStatus.Completed;}}
}
在分析过程中最常用的方法是TryReadLine,主要原因HTTP的头信息数据都是以换行的方式来描述,直到读取一个空行表明头部已结。如果存在Content-Length头信息描述说明存在消息体(HTTP有两种描述消息体的情况,这里就不多作介绍了)。
服务处理
协议处理好后就可以集成在服务中,相对于协议分析来说集成就更简单了。
public static void Main(string[] args)
{mServer = SocketFactory.CreateTcpServer<Program>();mServer.Options.DefaultListen.Port = 80;mServer.Options.AddListenSSL("ssl.pfx", "123456");mServer.Open();System.Threading.Thread.Sleep(-1);
}
private void OnCompleted(HttpRequest request, ISession session)
{HttpResponse response = new HttpResponse();StringBuilder sb = new StringBuilder();sb.AppendLine("<html>");sb.AppendLine("<body>");sb.AppendLine($"<p>Method:{request.Method}</p>");sb.AppendLine($"<p>Url:{request.Url}</p>");sb.AppendLine($"<p>Path:{request.Path}</p>");sb.AppendLine($"<p>QueryString:{request.QueryString}</p>");sb.AppendLine($"<p>ClientIP:{request.ClientIP}</p>");sb.AppendLine($"<p>Content-Length:{request.ContentLength}</p>");foreach (var item in request.Headers){sb.AppendLine($"<p>{item.Key}:{item.Value}</p>");}sb.AppendLine("</body>");sb.AppendLine("</html>");response.Body = sb.ToString();ClearRequest(session);session.Send(response);
}
在服务中把默认监听的端口改成80,然后添加一个SSL监听用于支持HTTPS访问;示例中通过OnCompleted方法响应请求内容,主要返回的内容是把当前请求的请求详细信息输出。
访问结果
启动服务后可以通过浏览器访问相关地址:
HTTP
HTTPS
由于证书是自己创建的,所以会被浏览器标记为不安全。
下载
链接:https://pan.baidu.com/s/1P7QTSjJH4Q1ftHiRoQT8MQ
提取码:7kig
【BeetleX通讯框架代码详解】
BeetleX
开源跨平台通讯框架(支持TLS)
轻松实现高性能:tcp、http、websocket、redis、rpc和网关等服务应用
https://beetlex.io