我比較好奇的是webapi伺服器怎麼處理http請求和websocket請求。有了上一篇番外的研究,這裡就可以試著自己寫個非常簡易的webapi伺服器來接收這兩種請求。
效果
-
http請求
訊息列印
響應解析
-
websocket請求
訊息列印
使用聊天室測試
其實兩種請求差不多,就只是一些頭部欄位有差別
-
http訊息
//客戶端傳送的訊息 string clientMsg = @"Get /httppath?msg=你好 HTTP/1.1 CustomField:f1 CustomField2:f2 "; //服務端傳送的訊息 string serverMsg = @"HTTP/1.1 200 CustomField2:f2 資料以收到";
-
websocket訊息
//客戶端傳送的訊息 string clientMsg = @"Get /httppath HTTP/1.1 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: xxxxx "; //服務端傳送的訊息 string serverMsg = @"HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: xxxxx ";
虛擬碼分析
http頭部是ASCII編碼。body部分預設也是,除非指定了content-type
http因為是無連線的,所以請求處理過程應該是這樣
- 客戶端解析
clientMsg
獲取要連線的伺服器 - 客戶端根據請求先建立tcp連線
- 客戶端傳送
ASCII.GetBytes("Get /httppath....")
- 服務端接收後
GetString(clientMsg)
- 服務端根據請求路徑執行對應方法
Action()
- 服務端傳送
ASCII.GetBytes("HTTP/1.1 200....")
- 服務端關閉連線
websocket傳送的第一條訊息也是採用http格式,流程相似,但是要保持連線,所以請求處理過程有所差異
- 客戶端解析
clientMsg
獲取要連線的伺服器 - 客戶端根據請求先建立tcp連線
- 客戶端傳送
GetBytes("Get /httppath....")
,然後,呼叫等待訊息發方法阻塞執行緒awite ReciveAsync()
- 服務端接收後
GetString(clientMsg)
- 服務端看到訊息頭部包含三個欄位
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: xxx
,開一個接收訊息的執行緒 - 服務端傳送
GetBytes("HTTP/1.1 101...")
- 服務端在接收訊息的執行緒中寫個while迴圈,判斷監聽客戶端發來的訊息,並呼叫對應方法處理
- 客戶端收到訊息後判斷是101訊息,開一個接收訊息的執行緒
- 客戶端在接收訊息的執行緒中寫個while迴圈,判斷監聽服務端發來的訊息,並呼叫對應方法處理
寫一個 HTTP & WebSocket 伺服器和客戶端
- 首先是解析訊息
var buffer = new byte[1024*4]; int msgLength = await client.Client.ReceiveAsync(new ArraySegment<byte>(buffer)); string str=UTF8Encoding.UTF8.GetString(buffer,0,msgLength); Console.WriteLine(str); HttpRequet request = new HttpRequet(str);
- 核心思想是判斷訊息是不是符合websocket連線請求格式,而這非常容易
public bool IsWebsocket() { if (this.headers.ContainsKey("Connection") && this.headers["Connection"] == "Upgrade" && this.headers.ContainsKey("Upgrade") && this.headers["Upgrade"] == "websocket") return true; else return false; }
- 然後是根據訊息判斷如何處理
//轉websocket的訊息 if (request.IsWebsocket()) { //用tcp連線構造一個WebSocket物件 WebSocket webSocket =await request.AcceptWebsocket(client, request.headers["Sec-WebSocket-Key"]); } //其他HTTP訊息 else { string header = @$"HTTP/1.1 200 CustomField2: f2 content-type: text/html; charset=utf-8 "; string body = "資料以收到"; }
完整程式碼
TCP與Socket埠測試.cs
internal class Program
{
static void Main(string[] args)
{
//伺服器
if (args.Length == 1) {
StartServer(args[0]);
}
}
private static void StartServer(string args)
{
int serverPort = Convert.ToInt32(args);
var server = new TcpListener(IPAddress.Parse("127.0.0.1"), serverPort);
Console.WriteLine($"TCP伺服器 127.0.0.1:{serverPort}");
server.Start();
int cnt = 0;
Task.Run(async () =>
{
List<TcpClient> clients = new List<TcpClient>();
while (true)
{
TcpClient client = await server.AcceptTcpClientAsync();
clients.Add(client);
cnt++;
var ep = client.Client.RemoteEndPoint as IPEndPoint;
Console.WriteLine($"TCP客戶端_{cnt} {ep.Address}:{ep.Port}");
//給這個客戶端開一個聊天執行緒
//作業系統將會根據遊客埠對應表將控制權交給對應遊客執行緒
StartChat(client);
}
}).Wait();
}
public static async Task StartChat(TcpClient client)
{
var buffer = new byte[1024*4];
int msgLength = await client.Client.ReceiveAsync(new ArraySegment<byte>(buffer));
string str=UTF8Encoding.UTF8.GetString(buffer,0,msgLength);
Console.WriteLine(str);
HttpRequet request = new HttpRequet(str);
//轉websocket的訊息
if (request.IsWebsocket())
{
WebSocket webSocket =await request.AcceptWebsocket(client, request.headers["Sec-WebSocket-Key"]);
//傳送一條websocket格式的打招呼訊息
var msg = new byte[] {
0x15,
0xe6,0x98,0x9f,0xe7,0xa9,0xb9,0xe9,0x93,0x81,0xe9,0x81,0x93,0xe5,0xa4,0xa7,0xe5,0xae,0xb6,0xe5,0xba,0xad,
0x00,
0x15,0x00,0x00,0x00,
0xe6,0xac,0xa2,0xe8,0xbf,0x8e,0xe8,0xbf,0x9b,0xe5,0x85,0xa5,0xe8,0x81,0x8a,0xe5,0xa4,0xa9,0xe5,0xae,0xa4
};
await webSocket.SendAsync(msg, WebSocketMessageType.Binary, true, CancellationToken.None);
//之後採用websocket規定的格式傳輸訊息
while (!webSocket.CloseStatus.HasValue)
{
await webSocket.ReceiveAsync(buffer,CancellationToken.None);
}
}
//其他HTTP訊息
else
{
using (MemoryStream memoryStream = new MemoryStream())
{
string header = @$"HTTP/1.1 200
CustomField2: f2
content-type: text/html; charset=utf-8
";
string body = "資料以收到";
//響應請求
memoryStream.Write(new ArraySegment<byte>(ASCIIEncoding.ASCII.GetBytes(header)));
memoryStream.Write(new ArraySegment<byte>(UTF8Encoding.UTF8.GetBytes(body)));
await client.Client.SendAsync(new ArraySegment<byte>(memoryStream.ToArray()));
Console.WriteLine(header+body);
//關閉連線
client.Close();
}
}
}
}
public class HttpRequet
{
/// <summary>
/// 解析HTTP訊息
/// </summary>
public HttpRequet(string str)
{
Str = str;
//開始行
var startLine = str.Split("\r\n")[0];
var lines= startLine.Split("\r\n");
httpMethod = lines[0].Split(' ')[0];
path = lines[0].Split(' ')[1];
//頭部
var headerslines= str.Split("\r\n\r\n")[0].Split("\r\n");
headers = new Dictionary<string, string>();
for (int i = 1; i < headerslines.Length; i++)
{
var header = headerslines[i].Split(": ");
headers.Add(header[0], header[1]);
}
}
/// <summary>
/// 請求原始訊息
/// </summary>
public string Str { get; }
/// <summary>
/// 請求方法
/// </summary>
public string httpMethod { get; internal set; }
/// <summary>
/// 請求路徑
/// </summary>
public string path { get; set; }
/// <summary>
/// 頭部欄位
/// </summary>
public Dictionary<string,string> headers { get; set; }
/// <summary>
/// 判斷是否是轉協議的請求
/// </summary>
/// <returns></returns>
public bool IsWebsocket()
{
if (this.headers.ContainsKey("Connection") && this.headers["Connection"] == "Upgrade" && this.headers.ContainsKey("Upgrade") && this.headers["Upgrade"] == "websocket")
return true;
else
return false;
}
/// <summary>
/// 響應轉協議請求並未用當前連線建立一個WebSocket物件
/// </summary>
/// <param name="client"></param>
/// <returns></returns>
public async Task<WebSocket> AcceptWebsocket(TcpClient client,string Sec_WebSocket_Key)
{
using (MemoryStream memoryStream = new MemoryStream())
{
string header = @$"HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: {GenerateResponseKey(Sec_WebSocket_Key)}
";
memoryStream.Write(new ArraySegment<byte>(ASCIIEncoding.ASCII.GetBytes(header)));
await client.Client.SendAsync(new ArraySegment<byte>(memoryStream.ToArray()));
Console.WriteLine(header);
return WebSocket.CreateFromStream(client.GetStream(), true, null, TimeSpan.FromSeconds(10));
}
}
public static string GenerateResponseKey(string requestKey)
{
const string guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
string concatenated = requestKey + guid;
byte[] hashed = System.Security.Cryptography.SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(concatenated));
return Convert.ToBase64String(hashed);
}
}