完整项目托管地址:https://github.com/hooow-does-it-work/http
前面文章实现了HTTP请求头的解析:C#实现HTTP服务器:(1)解析HTTP请求头
现在我们尝试向浏览器发送一些内容。
本文使用Content-Length发送固定长度内容的响应数据,后面会写文使用Chunked方式发送数据。
修改前面文章NewClient重载的内容如下。
public class HttpServer : TcpIocpServer
{
protected override void NewClient(Socket client)
{
Stream stream = new NetworkStream(client, true);
//捕获一个HttpRequest
HttpRequest request = HttpRequest.Capture(stream);
//实例化HttpResponse,方便管理HTTP响应头
HttpResponse response = new HttpResponse(200);
//需要发送到客户端的内容
string responseText = "<p>hello world!</p>";
//内容对应的二进制数据
byte[] responseBody = Encoding.ASCII.GetBytes(responseText);
//设置Content-Length请求头,大小为响应内容的二进制数据长度
//由于HTTP/1.1协议的限制,客户端必须有明确的界定条件,来从传输中读取正确的内容
//一种方法就是,设置Content-Length头,客户端只要读取指定长度的数据即可。
//再一种方法就是,使用Chunked,进行分块传输,用一个结束快告诉客户端,数据传输完毕了
//还有就是,服务器发送完数据后,可以直接关闭连接,客户端读完全部数据。
//
//对于一些不需要响应内容的code,可以不设置。例如204 No Content
response.SetHeader("Content-Length", responseBody.Length.ToString());
//设置响应内容类型,这里设置为text/html
response.SetHeader("Content-Type", "text/html");
//告诉浏览器,本次连接为非长链,服务器会关闭连接。
//HTTP/1.1允许长链接,可以在一个连接上发送多次请求和响应。
response.SetHeader("Connection", "close");
//获取整个需要发送给客户端的响应头
string responseHeaders = response.GetAllResponseHeaders();
byte[] responseHeaderBuffer = Encoding.ASCII.GetBytes(responseHeaders);
//发送响应头
stream.Write(responseHeaderBuffer, 0, responseHeaderBuffer.Length);
//发送响应内容
stream.Write(responseBody, 0, responseBody.Length);
stream.Close();
}
}
服务器启动成功后,浏览器访问:http://127.0.0.1:4189/
我们可以将浏览器请求的信息,发回给浏览器,看看我们解析的数据和浏览器发送的是否一致。
将responseText部分代码修改为下面内容。
//需要发送到客户端的内容
string responseText = $"<p>hello world!</p><pre>{request.GetAllRequestHeaders()}</pre>";
浏览器访问:http://127.0.0.1:4189/,响应如下图。
可以看到,我们解析的内容,跟浏览器发送的Request Headers完全一致。
Response Headers也是我们在代码中设置的响应头。
这里我们实现了使用Content-Length向浏览器发送数据。
下篇文章,我们将使用Chunked方式,发送数据到浏览器。
HttpResponse类源码
public class HttpResponse
{
private int _statusCode = 200;
private string _statusText = "OK";
private string _httpProtocol = null;
private NameValueCollection _headers = new NameValueCollection();
public NameValueCollection Headers => _headers;
public int StatusCode {
get => _statusCode;
set {
string statusText = HttpStatus.GetStatus(value);
if (string.IsNullOrEmpty(statusText))
throw new Exception($"未知状态码:{value}");
_statusCode = value;
_statusText = statusText;
}
}
internal HttpResponse() { }
/// <summary>
/// 使用状态码和协议实例化一个HttpResponse类
/// </summary>
/// <param name="statusCode">状态码,200、400等</param>
/// <param name="httpProtocol">协议,默认用HTTP/1.1</param>
public HttpResponse(int statusCode, string httpProtocol = "HTTP/1.1")
{
StatusCode = statusCode;
_httpProtocol = httpProtocol;
}
/// <summary>
/// 添加响应头
/// </summary>
/// <param name="name">名称</param>
/// <param name="value">值</param>
public void AddHeader(string name, string value)
{
_headers.Add(name, value);
}
/// <summary>
/// 设置响应头
/// </summary>
/// <param name="name">名称</param>
/// <param name="value">值</param>
public void SetHeader(string name, string value)
{
_headers.Set(name, value);
}
/// <summary>
/// 删除响应头
/// </summary>
/// <param name="name">名称</param>
public void RemoveHeader(string name)
{
_headers.Remove(name);
}
/// <summary>
/// 获获取完整的响应头
/// </summary>
/// <returns>完整响应头</returns>
public string GetAllResponseHeaders()
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("{0} {1} {2}\r\n", _httpProtocol, _statusCode, _statusText);
foreach (string name in _headers.Keys)
{
string[] values = _headers.GetValues(name);
if (values == null || values.Length == 0) continue;
foreach (string value in values)
{
if (value == null) continue;
sb.AppendFormat("{0}: {1}\r\n", name, value);
}
}
sb.Append("\r\n");
return sb.ToString();
}
}
HttpStatus类源码
可以从statusCode获取statusText。
public class HttpStatus
{
private static readonly string[][] s_HTTPStatusDescriptions;
private static Dictionary<int, string> m_Cached = new Dictionary<int, string>();
static HttpStatus()
{
s_HTTPStatusDescriptions = new string[6][];
s_HTTPStatusDescriptions[1] = new string[] {
"Continue", "Switching Protocols", "Processing"
};
s_HTTPStatusDescriptions[2] = new string[] {
"OK", "Created", "Accepted", "Non-Authoritative Information", "No Content", "Reset Content", "Partial Content", "Multi-Status"
};
s_HTTPStatusDescriptions[3] = new string[] {
"Multiple Choices", "Moved Permanently", "Found", "See Other", "Not Modified", "Use Proxy", string.Empty, "Temporary Redirect"
};
s_HTTPStatusDescriptions[4] = new string[] {
"Bad Request", "Unauthorized", "Payment Required", "Forbidden", "Not Found", "Method Not Allowed", "Not Acceptable",
"Proxy Authentication Required", "Request Timeout", "Conflict", "Gone", "Length Required", "Precondition Failed",
"Request Entity Too Large", "Request-Uri Too Long", "Unsupported Media Type",
"Requested Range Not Satisfiable", "Expectation Failed", string.Empty, string.Empty, string.Empty, string.Empty,
"Unprocessable Entity", "Locked", "Failed Dependency"
};
s_HTTPStatusDescriptions[5] = new string[] { "Internal Server Error", "Not Implemented", "Bad Gateway", "Service Unavailable",
"Gateway Timeout", "Http Version Not Supported", string.Empty, "Insufficient Storage"
};
}
public static string GetStatus(int code)
{
if (m_Cached.ContainsKey(code)) return m_Cached[code];
int main = code / 100;
int sub = code % 100;
if (main <= 0 || main > 5)
{
m_Cached[code] = "";
return "";
}
string[] texts = s_HTTPStatusDescriptions[main];
if (sub < 0 || sub >= texts.Length)
{
m_Cached[code] = "";
return "";
}
m_Cached[code] = texts[sub];
return texts[sub];
}
}