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)
{}