C# 實現包裝tcp/ip 爲websocket 服務器傳輸圖片
介紹
要求:web客戶端,c#服務器
實現服務器單向給客戶端傳輸圖片。
網上找了一下,可以用websocketSharp實現,但是引入第三方庫需要架構組審批之類的,總之不太好。
我想想試試用tcp/ip 包裝成websocket 進行傳輸圖片,結果還是踩了不少坑的。
包裝
websocket 是建立在tcp/ip上的,
那麼,只要我知道這兩者之間有哪些區別,就好辦了!
here we go~
- step 1 升級協議
客戶端先打開一個TCP連接,隨後再發起升級協商,升級爲websocket。
客戶端發送的升級協議爲:
GET /socket HTTP/1.1 // 請求的方法必須是GET,HTTP版本必須至少是1.1
Host: thirdparty.com
Origin: Example Domain
Connection: Upgrade
Upgrade: websocket // 請求升級到WebSocket 協議
Sec-WebSocket-Version: 13 // 客戶端使用的WebSocket 協議版本
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 自動生成的鍵,以驗證服務器對協議的支持,其值必須是nonce組成的隨機選擇的16字節的被base64編碼後的值
Sec-WebSocket-Protocol: appProtocol, appProtocol-v2 // 可選的應用指定的子協議列表
Sec-WebSocket-Extensions: x-webkit-deflate-message, x-custom-extension // 可選的客戶端支持的協議擴展列表,指示了客戶端希望使用的協議級別的擴展
其中 Sec-WebSocket-Key: 客戶端用來驗證服務器支持請求的鍵,服務器必須加密該值後返回。
服務器迴應客戶端的升級請求:
HTTP/1.1 101 Switching Protocols // 101 響應碼確認升級到WebSocket 協議
Upgrade: websocket
Connection: Upgrade
Access-Control-Allow-Origin: Example Domain // CORS 首部表示選擇同意跨源連接
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // 簽名的鍵值驗證協議支持
Sec-WebSocket-Protocol: appProtocol-v2 // 服務器選擇的應用子協議
Sec-WebSocket-Extensions: x-custom-extension // 服務器選擇的WebSocket 擴展
其中Sec-WebSocket-Accept 是將 客戶端中Sec-WebSocket-Key加密哈希的新值。
加密方法如下:
- 將Sec-WebSocket-Key 拼接 ”258EAFA5-E914-47DA-95CA-C5AB0DC85B11” 爲新字符串
- 對新字符串進行哈希
- 再進行base64-encode
ok
我的思路:
當tcp服務器監聽到客戶端升級websocket請求後,發送一個包裝成websocket應答格式的包給客戶端,建立連接!
這一步,服務器包裝應答的C#代碼爲:
private static byte[] PackHandShakeData(string secKeyAccept)
{
var responseBuilder = new StringBuilder();
responseBuilder.Append("HTTP/1.1 101 Switching Protocols" + Environment.NewLine);
responseBuilder.Append("Upgrade: websocket" + Environment.NewLine);
responseBuilder.Append("Connection: Upgrade" + Environment.NewLine);
responseBuilder.Append("Sec-WebSocket-Accept: " + secKeyAccept + Environment.NewLine + Environment.NewLine);
return Encoding.UTF8.GetBytes(responseBuilder.ToString());
}
加密key:
private static string GetSecKeyAccetp(byte[] handShakeBytes, int bytesLength)
{
string handShakeText = Encoding.UTF8.GetString(handShakeBytes, 0, bytesLength);
string key = string.Empty;
Regex r = new Regex(@"Sec\-WebSocket\-Key:(.*?)\r\n");
Match m = r.Match(handShakeText);
if (m.Groups.Count != 0)
{
key = Regex.Replace(m.Value, @"Sec\-WebSocket\-Key:(.*?)\r\n", "$1").Trim();
}
byte[] encryptionString = SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
return Convert.ToBase64String(encryptionString);
}
2.數據包裝
建立連接後就開始傳輸數據了!
websocket數據格式:
WebSocket 數據二進制格式:
- FIN: 1 bit 。表示此幀是否是消息的最後幀,第一幀也可能是最後幀。
- RSV1,RSV2,RSV3: 各1 bit 。必須是0,除非協商了擴展定義了非0的意義。
- opcode:4 bit。表示被傳輸幀的類型:x0 表示一個後續幀;x1 表示一個文本幀;x2 表示一個二進制幀;x3-7 爲以後的非控制幀保留;x8 表示一個連接關閉;x9 表示一個ping;xA 表示一個pong;xB-F 爲以後的控制幀保留。
- Mask: 1 bit。表示淨荷是否有掩碼(只適用於客戶端發送給服務器的消息)。
- Payload length: 7 bit, 7 + 16 bit, 7 + 64 bit。 淨荷長度由可變長度字段表示: 如果是 0~125,就是淨荷長度;如果是 126,則接下來 2 字節表示的 16 位無符號整數纔是這一幀的長度; 如果是 127,則接下來 8 字節表示的 64 位無符號整數纔是這一幀的長度。
關鍵的來了!
opcode!
要實現的是傳輸圖片 ,也就是二進制幀,所以是x2!
(之前傳圖片一直不成功就是在這裏)
聰明的你應該知道了 我們數據包(假設叫它)byte data[]
第一行應該是:
date[0] = 0x82
0x82二進制爲1000 0010
1表示最後幀
10 爲 x2 表示二進制
對應的C#代碼爲:
private static byte[] PackDataBytes(byte[] bytes)
{
byte[] contentBytes = null;
byte[] temp = bytes;
//byte[] temp =message;
if (temp.Length < 126)
{
contentBytes = new byte[temp.Length + 2];
contentBytes[0] = 0x82;
contentBytes[1] = (byte)temp.Length;
Array.Copy(temp, 0, contentBytes, 2, temp.Length);
}
else if (temp.Length < 0xFFFF)
{
contentBytes = new byte[temp.Length + 4];
contentBytes[0] = 0x82;
contentBytes[1] = 126;
contentBytes[2] = (byte)(temp.Length & 0xFF);
contentBytes[3] = (byte)(temp.Length >> 8 & 0xFF);
Array.Copy(temp, 0, contentBytes, 4, temp.Length);
}
else
{
contentBytes = new byte[temp.Length + 10];
contentBytes[0] = 0x82;
contentBytes[1] = 127;
contentBytes[2] = 0;
contentBytes[3] = 0;
contentBytes[4] = 0;
contentBytes[5] = 0;
contentBytes[6] = (byte)(temp.Length >> 24);
contentBytes[7] = (byte)(temp.Length >> 16);
contentBytes[8] = (byte)(temp.Length >> 8);
contentBytes[9] = (byte)(temp.Length & 0xFF);
Array.Copy(temp, 0, contentBytes, 10, temp.Length);
}
return contentBytes;
想要傳字符串的話0x81就行
ok,搞定!
實現C#服務器代碼
class WebsocketImageWrapper
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
Thread threadWatch = null;
Socket socketWatch = null;
int countClient = 0;
Dictionary<int, Socket> ClientConnectionItems = new Dictionary<int , Socket> { };
/// <summary>
/// start a tcp/ip server
/// </summary>
public WebsocketImageWrapper(IPEndPoint endpoint)
{
//define a socket using tcp/ip
socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//IPAddress ipaddress = IPAddress.Parse("127.0.0.1");
//IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 1234);
try
{
//define the ip address as localhost and the endpoint as 1234
socketWatch.Bind(endpoint);
//limit the length of the socket's listening queue to 20
socketWatch.Listen(20);
}catch (Exception ex)
{
Logger.Error(ex, "websoket start failed");
}
threadWatch = new Thread(WatchConnecting);
threadWatch.IsBackground = true;
threadWatch.Start();
Logger.Debug("websocket start ok");
}
/// <summary>
/// watch the websocket connection and then do the handshake
/// </summary>
private void WatchConnecting()
{
//Socket socConnection = null;
while (true)
{
Socket socConnection = socketWatch.Accept();
countClient++;
ClientConnectionItems.Add(countClient, socConnection);
byte[] buffer = new byte[1024];
int length = socConnection.Receive(buffer);
//to connection with the websocket client, server must do the handshake
socConnection.Send(PackHandShakeData(GetSecKeyAccetp(buffer, length)));
//create a parameterized thread
var paramerStart = new ParameterizedThreadStart(RecMsg);
Thread thr = new Thread(paramerStart);
thr.IsBackground = true;
thr.Start(countClient);
}
}
/// <summary>
/// send blob image to client
/// </summary>
/// <param name="socConnection"></param>
private void SendImage(Socket socConnection)
{}