本系列服务端双工通信包括两种实现方式:一、使用Socket构建;二、使用WCF构建。本文为使用Socket构建服务端的双工通信,客户端同样使用Html5的WebSocket技术进行调用。
一、网页客户端:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<script src="scripts/jquery-1.10.2.min.js"></script>
<script>
var socket;
//url必须使用ws或者wss(加密)作为头,这个url设定好后,在javascript脚本中可以通过访问websocket对象的url来重新获取
//通信建立连接后,就可以双向通信了,使用websocket对象的send方法加json数据便可将任何形式数据传往服务器 //通过onmessage事件来接收服务器传送过来数据:
//通过onopern事件监听socket打开事件
//通过onclose监听socket关闭事件
//通过webSocket.close()方法关闭socket连接;
//通过readyState属性获取websocket对象状态:
//CONNECTION 0 正在连接
//OPEN 1 已经连接
//CLOSING 2 正在关闭
//CLOSE 3 已经关闭
$(function () {
$('#conn').click(function () {
//ws = new WebSocket('ws://' + window.location.hostname + ':' + '4649/Echo/');
socket = new WebSocket('ws://localhost:8021/');
$('#tips').text('正在连接'); socket.addEventListener("open", function (e) {
$('#tips').html(
'<div>Connected. Waiting for messages...</div>');
//window.setInterval(function () {
// socket.send("the time is " + new Date());
//}, 1000);
}, false) socket.addEventListener("message", function (evt) {
$('#tips').text(evt.data);
}); socket.onerror = function (evt) {
$('#tips').text(JSON.stringify(evt));
}
socket.onclose = function () {
$('#tips').text('已经关闭');
}
}); $('#close').click(function () {
socket.close();
}); $('#send').click(function () {
if (socket.readyState == WebSocket.OPEN) {
socket.send($('#content').val());
}
else {
$('#tips').text('连接已经关闭');
}
});
});
</script>
</head>
<body>
<form id="form1">
<div>
<input id="conn" type="button" value="连接" />
<input id="close" type="button" value="关闭" />
<span id="tips"></span>
<input id="content" type="text" />
<input id="send" type="button" value="发送" />
</div>
</form>
</body>
</html>
二、服务端:
创建控制台应用程序,代码如下:
class Program
{
//客户端集合
static List<Socket> clients = new List<Socket>();
static byte[] buffer = new byte[];
//static bool IsWebSocketClient = false; //客户端连接是否为websocket
static void Main(string[] args)
{
//创建一个新的Socket,这里我们使用最常用的基于TCP的Stream Socket(流式套接字)
Socket SeverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//将该socket绑定到主机上面的某个端口
SeverSocket.Bind(new IPEndPoint(IPAddress.Any, ));
//设置Socket为监听状态并设置允许的最大队列数为4
SeverSocket.Listen(); //服务端开始异步接受客户端的连接请求
SeverSocket.BeginAccept(AsyncAcceptCallback, SeverSocket); Console.WriteLine("Sever is ready"); //创建一个时钟,每隔1分钟发送一个心跳包给客户端
SendHeartPackToClients(); Console.Read();
}
#region 创建一个时钟,每隔10秒发送一个心跳包给客户端 private static void SendHeartPackToClients()
{
System.Timers.Timer time = new System.Timers.Timer();
time.Interval = * ;
time.Enabled = true;
time.Elapsed += time_Elapsed;
time.Start();
} static void time_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
SendMsgToAllClients("hi," + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
}
/// <summary>
/// 发送消息给所有连接的客户端
/// </summary>
private static void SendMsgToAllClients(string msg)
{
try
{
foreach (var client in clients)
{
if (client.Connected)
{
client.Send(PackageServerData(msg));
}
}
}
catch (Exception)
{
//TODO
} } #endregion
/// <summary>
/// 服务端异步接受连接的回调处理方法
/// </summary>
/// <param name="ar"></param>
private static void AsyncAcceptCallback(IAsyncResult ar)
{
var ServerSocket = ar.AsyncState as Socket;
//异步接受传入的连接,并创建客户端Socket
var ClientSocket = ServerSocket.EndAccept(ar); //将客户端加入集合中
clients.Add(ClientSocket); //开始异步接收该客户端发送的消息
ClientSocket.BeginReceive(buffer, , buffer.Length, SocketFlags.None, AsyncReceiveCallback, ClientSocket); //服务端开始异步接受下一个客户端的连接请求
ServerSocket.BeginAccept(AsyncAcceptCallback, ServerSocket);
}
/// <summary>
/// 异步接收消息的回调处理方法
/// </summary>
/// <param name="ar"></param>
private static void AsyncReceiveCallback(IAsyncResult ar)
{
try
{
var ClientSocket = ar.AsyncState as Socket;
int RevLength = ClientSocket.EndReceive(ar); string RevMsg = Encoding.UTF8.GetString(buffer, , RevLength); #region WebSocket处理代码
//判断是否为浏览器websocket发过来的请求,若是,则打包服务器握手数据,实现第4次握手
if (RevMsg.Contains("Sec-WebSocket-Key"))
{
//IsWebSocketClient = true;
Console.WriteLine(RevMsg);
ClientSocket.Send(PackageHandShakeData(buffer, RevLength));
}
else {
string AnalyzeMsg = AnalyzeClientData(buffer, RevLength);
Console.WriteLine(AnalyzeMsg);
ClientSocket.Send(PackageServerData("收到您的信息!"));
}
#endregion //继续接收该客户端下一条发送的消息
ClientSocket.BeginReceive(buffer, , buffer.Length, SocketFlags.None, AsyncReceiveCallback, ClientSocket);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
} }
#region 客户端和服务端的响应
/*
* 客户端向服务器发送请求
*
* GET / HTTP/1.1
* Origin: http://localhost:1416
* Sec-WebSocket-Key: vDyPp55hT1PphRU5OAe2Wg==
* Connection: Upgrade
* Upgrade: Websocket
*Sec-WebSocket-Version: 13
* User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
* Host: localhost:8064
* DNT: 1
* Cache-Control: no-cache
* Cookie: DTRememberName=admin
*
* 服务器给出响应
*
* HTTP/1.1 101 Switching Protocols
* Upgrade: websocket
* Connection: Upgrade
* Sec-WebSocket-Accept: xsOSgr30aKL2GNZKNHKmeT1qYjA=
*
* 在请求中的“Sec-WebSocket-Key”是随机的,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个魔幻字符串
* “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用 SHA-1 加密,之后进行 BASE-64编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端
*/
#endregion
/// <summary>
/// 打包服务器握手数据
/// </summary>
/// <returns>The hand shake data.</returns>
/// <param name="handShakeBytes">Hand shake bytes.</param>
/// <param name="length">Length.</param>
private static byte[] PackageHandShakeData(byte[] handShakeBytes, int length)
{
string handShakeText = Encoding.UTF8.GetString(handShakeBytes, , length);
string key = string.Empty;
Regex reg = new Regex(@"Sec\-WebSocket\-Key:(.*?)\r\n");
Match m = reg.Match(handShakeText);
if (m.Value != "")
{
key = Regex.Replace(m.Value, @"Sec\-WebSocket\-Key:(.*?)\r\n", "$1").Trim();
} byte[] secKeyBytes = SHA1.Create().ComputeHash(
Encoding.ASCII.GetBytes(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
string secKey = Convert.ToBase64String(secKeyBytes); var responseBuilder = new StringBuilder();
responseBuilder.Append("HTTP/1.1 101 Switching Protocols" + "\r\n");
responseBuilder.Append("Upgrade: websocket" + "\r\n");
responseBuilder.Append("Connection: Upgrade" + "\r\n");
responseBuilder.Append("Sec-WebSocket-Accept: " + secKey + "\r\n\r\n"); //如果把上一行换成下面两行,才是thewebsocketprotocol-17协议,但居然握手不成功,目前仍没弄明白!
//responseBuilder.Append("Sec-WebSocket-Accept: " + secKeyAccept + Environment.NewLine);
//responseBuilder.Append("Sec-WebSocket-Protocol: chat" + Environment.NewLine); return Encoding.UTF8.GetBytes(responseBuilder.ToString());
} #region 处理接收的数据
/// <summary>
/// 处理接收的数据
/// 参考 http://www.cnblogs.com/smark/archive/2012/11/26/2789812.html
/// </summary>
/// <param name="recBytes"></param>
/// <param name="length"></param>
/// <returns></returns>
private static string AnalyzeClientData(byte[] recBytes, int length)
{
int start = ;
// 如果有数据则至少包括3位
if (length < ) return "";
// 判断是否为结束针
bool IsEof = (recBytes[start] >> ) > ;
// 暂不处理超过一帧的数据
if (!IsEof) return "";
start++;
// 是否包含掩码
bool hasMask = (recBytes[start] >> ) > ;
// 不包含掩码的暂不处理
if (!hasMask) return "";
// 获取数据长度
UInt64 mPackageLength = (UInt64)recBytes[start] & 0x7F;
start++;
// 存储4位掩码值
byte[] Masking_key = new byte[];
// 存储数据
byte[] mDataPackage;
if (mPackageLength == )
{
// 等于126 随后的两个字节16位表示数据长度
mPackageLength = (UInt64)(recBytes[start] << | recBytes[start + ]);
start += ;
}
if (mPackageLength == )
{
// 等于127 随后的八个字节64位表示数据长度
mPackageLength = (UInt64)(recBytes[start] << ( * ) | recBytes[start] << ( * ) | recBytes[start] << ( * ) | recBytes[start] << ( * ) | recBytes[start] << ( * ) | recBytes[start] << ( * ) | recBytes[start] << | recBytes[start + ]);
start += ;
}
mDataPackage = new byte[mPackageLength];
for (UInt64 i = ; i < mPackageLength; i++)
{
mDataPackage[i] = recBytes[i + (UInt64)start + ];
}
Buffer.BlockCopy(recBytes, start, Masking_key, , );
for (UInt64 i = ; i < mPackageLength; i++)
{
mDataPackage[i] = (byte)(mDataPackage[i] ^ Masking_key[i % ]);
}
return Encoding.UTF8.GetString(mDataPackage);
}
#endregion #region 发送数据
/// <summary>
/// 把发送给客户端消息打包处理(拼接上谁什么时候发的什么消息)
/// </summary>
/// <returns>The data.</returns>
/// <param name="message">Message.</param>
private static byte[] PackageServerData(string msg)
{
byte[] content = null;
byte[] temp = Encoding.UTF8.GetBytes(msg);
if (temp.Length < )
{
content = new byte[temp.Length + ];
content[] = 0x81;
content[] = (byte)temp.Length;
Buffer.BlockCopy(temp, , content, , temp.Length);
}
else if (temp.Length < 0xFFFF)
{
content = new byte[temp.Length + ];
content[] = 0x81;
content[] = ;
content[] = (byte)(temp.Length & 0xFF);
content[] = (byte)(temp.Length >> & 0xFF);
Buffer.BlockCopy(temp, , content, , temp.Length);
}
return content;
}
#endregion
}