前两篇:
1.C# DotNetty (1) EchoServer
2.C# DotNetty (2) EchoClient
WebSocket本质还是Socket
在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
Ajax 轮询:在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
参考资料:https://www.liaoxuefeng.com/wiki/1022910821149312/1103303693824096
根据DotNetty官方给出的案例中,DoNetty分为以下两部分:
(github上下载的,参考前面的两篇)
第一类Http请求
第二类WebSocket帧
对于请求需要做的动作是:
1.判断请求是否成功
2.仅允许get方法:
3.根据url判断返回回应(一个demo界面和图表,实际使用中暂时用不上,但是可以作为参考)
4.一次握手,建立连接
根据不同版本获取握手的回应
第二类:WebSocket帧
案例中除了关闭帧,其他类型的帧原封不动返回
以官方给出的例子进行改写:
用于管理通道及两个事件循环组,以及具有广播事件,消息队列及定时广播的封装
public class ServerChannel
{
public IChannel Channel { get; set; }
public IEventLoopGroup Bossgroup { get; set; }
public IEventLoopGroup Workgroup { get; set; }
public WebSocketBroadCastEvent WebSocketBroadCastEvent { get; set; }
public ConcurrentQueue<String> MsgQueue { get; set; }
private Timer T { get; set; }
public void StartBroadCast(int dueTime, int period)
{
if (T == null)
{
T = new Timer((obj) =>
{
if (MsgQueue.TryPeek(out string res))//Debug
{
WebSocketBroadCastEvent.Active(res);
}
});
T.Change(dueTime, period);
}
}
public void Stop()
{
if (T != null)
{
T.Change(Timeout.Infinite, Timeout.Infinite);
T.Dispose();
}
}
}
广播事件类:
using System;
using System.Collections.Generic;
using System.Text;
namespace LocateAnalyzer.DotNetty
{
public delegate void BroadCastdelegate(String Msg);
public class WebSocketBroadCastEvent
{
public event BroadCastdelegate Myevent;
public void Active(String Msg)
{
Myevent?.Invoke(Msg);
}
}
}
WebSocket服务端开启,关闭,及广播消息
private static Dictionary<int, ServerChannel> ChannelDic = new Dictionary<int, ServerChannel>();//用于存储服务端通道
public static async Task RunServerAsync(Boolean UseLibuv, Boolean IsSsl, int Port)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
//https://docs.microsoft.com/zh-cn/dotnet/api/system.runtime.gclatencymode?redirectedfrom=MSDN&view=netframework-4.8
//调整垃圾收集器侵入应用程序的时间。
}
IEventLoopGroup bossGroup;
IEventLoopGroup workGroup;
if (UseLibuv)
{
var dispatcher = new DispatcherEventLoopGroup();
bossGroup = dispatcher;
workGroup = new WorkerEventLoopGroup(dispatcher);
}
else
{
bossGroup = new MultithreadEventLoopGroup(1);
workGroup = new MultithreadEventLoopGroup();
}
X509Certificate2 tlsCertificate = null;
if (IsSsl)
{
tlsCertificate = new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "dotnetty.com.pfx"), "password");
}
try
{
var bootstrap = new ServerBootstrap();
bootstrap.Group(bossGroup, workGroup);
if (UseLibuv)
{
bootstrap.Channel<TcpServerChannel>();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|| RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
bootstrap
.Option(ChannelOption.SoReuseport, true)
.ChildOption(ChannelOption.SoReuseaddr, true);
}
}
else
{
bootstrap.Channel<TcpServerSocketChannel>();
}
WebSocketBroadCastEvent webSocketBroadCastEvent = new WebSocketBroadCastEvent();
WebSocketServerHandler.SetSsl(IsSsl);
bootstrap
.Option(ChannelOption.SoBacklog, 8192)
.ChildHandler(new ActionChannelInitializer<IChannel>(channel =>
{
IChannelPipeline pipeline = channel.Pipeline;
if (tlsCertificate != null)
{
pipeline.AddLast(TlsHandler.Server(tlsCertificate));
}
pipeline.AddLast(new HttpServerCodec());
pipeline.AddLast(new HttpObjectAggregator(65536));
var Handler = new WebSocketServerHandler();
Handler.SetBroadCastEvent(webSocketBroadCastEvent);
pipeline.AddLast(Handler);
}));
IChannel bootstrapChannel = await bootstrap.BindAsync(IPAddress.Loopback, Port).ConfigureAwait(true);
if (ChannelDic.ContainsKey(Port))
throw new Exception("Port:" + Port + " is already Ocuppied");
ChannelDic[Port] = new ServerChannel()
{
Channel = bootstrapChannel,
Bossgroup = bossGroup,
Workgroup = workGroup,
MsgQueue = new ConcurrentQueue<string>(),
WebSocketBroadCastEvent = webSocketBroadCastEvent
};
BroadCast(Port, "123");//Debug
ChannelDic[Port].StartBroadCast(5000, 1000);//Debug
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
if (ChannelDic.ContainsKey(Port))
await ChannelDic[Port].Channel.CloseAsync().ConfigureAwait(true);
workGroup.ShutdownGracefullyAsync().Wait();
bossGroup.ShutdownGracefullyAsync().Wait();
}
}
public static void Stop(int Port)
{
if (!ChannelDic.ContainsKey(Port))
return;
var server = ChannelDic[Port];
server.Channel.CloseAsync().ConfigureAwait(true);
server.Workgroup.ShutdownGracefullyAsync().Wait();
server.Bossgroup.ShutdownGracefullyAsync().Wait();
ChannelDic.Remove(Port);
}
public static void BroadCast(int Port, String Msg)
{
if (!ChannelDic.ContainsKey(Port))
return;
ChannelDic[Port].MsgQueue.Enqueue(Msg);
}
WebSocketServerHandler:处理消息
using DotNetty.Codecs.Http;
using DotNetty.Codecs.Http.WebSockets;
using DotNetty.Transport.Channels;
using static DotNetty.Codecs.Http.HttpVersion;
using static DotNetty.Codecs.Http.HttpResponseStatus;
using DotNetty.Buffers;
using System.Text;
using System;
using System.Threading.Tasks;
using System.Diagnostics;
using DotNetty.Common.Utilities;
using System.Collections.Generic;
using System.Threading;
using System.Collections.Concurrent;
namespace LocateAnalyzer.DotNetty
{
public sealed class WebSocketServerHandler : SimpleChannelInboundHandler<object>
{
const string WebsocketPath = "/websocket";
private static Boolean IsSsl;
WebSocketServerHandshaker handshaker;
public static void SetSsl(bool isSsl)
{
IsSsl = isSsl;
}
protected override void ChannelRead0(IChannelHandlerContext ctx, object msg)
{
if (ctx == null)
return;
if (msg is IFullHttpRequest request)
{
this.HandleHttpRequest(ctx, request);
}
else if (msg is WebSocketFrame frame)
{
this.HandleWebSocketFrame(ctx, frame);
}
}
public override void ChannelReadComplete(IChannelHandlerContext context) => context.Flush();
void HandleHttpRequest(IChannelHandlerContext ctx, IFullHttpRequest req)
{
// Handle a bad request.
if (!req.Result.IsSuccess)
{
SendHttpResponse(ctx, req, new DefaultFullHttpResponse(Http11, BadRequest));
return;
}
// Allow only GET methods.
if (!Equals(req.Method, HttpMethod.Get))
{
SendHttpResponse(ctx, req, new DefaultFullHttpResponse(Http11, Forbidden));
return;
}
// Handshake
String url = GetWebSocketLocation(req);
var wsFactory = new WebSocketServerHandshakerFactory(url, null, true, 5 * 1024 * 1024);
this.handshaker = wsFactory.NewHandshaker(req);
if (this.handshaker == null)
{
WebSocketServerHandshakerFactory.SendUnsupportedVersionResponse(ctx.Channel);
}
else
{
this.handshaker.HandshakeAsync(ctx.Channel, req);
var clientAddress = ctx.Channel.RemoteAddress.ToString();
Console.WriteLine(url + ":" + clientAddress + " Connected!");
channel = ctx.Channel;
webSocketBroadCastEvent.Myevent += Send;
}
}
private IChannel channel;
private WebSocketBroadCastEvent webSocketBroadCastEvent;
public void SetBroadCastEvent(WebSocketBroadCastEvent webSocketBroadCastEvent)
{
this.webSocketBroadCastEvent = webSocketBroadCastEvent;
}
private void Send(String Msg)
{
WebSocketFrame frame = new TextWebSocketFrame(Msg);
channel.WriteAndFlushAsync(frame.Retain());
}
void HandleWebSocketFrame(IChannelHandlerContext ctx, WebSocketFrame frame)
{
// Check for closing frame
if (frame is CloseWebSocketFrame)
{
this.handshaker.CloseAsync(ctx.Channel, (CloseWebSocketFrame)frame.Retain());
return;
}
if (frame is PingWebSocketFrame)
{
ctx.WriteAsync(new PongWebSocketFrame((IByteBuffer)frame.Content.Retain()));
return;
}
if (frame is TextWebSocketFrame)
{
// Echo the frame
int readableBytes = frame.Content.ReadableBytes;
var msg = frame.Content.GetString(0, readableBytes, Encoding.UTF8);
Console.WriteLine("Recv from ws Client:" + msg);
WebSocketFrame replyFrame = frame;
replyFrame.Content.Clear();
replyFrame.Content.WriteBytes(Encoding.UTF8.GetBytes("reply:" + msg));
ctx.WriteAsync(replyFrame.Retain());
return;
}
if (frame is BinaryWebSocketFrame)
{
// Echo the frame
ctx.WriteAsync(frame.Retain());
}
}
static void SendHttpResponse(IChannelHandlerContext ctx, IFullHttpRequest req, IFullHttpResponse res)
{
// Generate an error page if response getStatus code is not OK (200).
if (res.Status.Code != 200)
{
IByteBuffer buf = Unpooled.CopiedBuffer(Encoding.UTF8.GetBytes(res.Status.ToString()));
res.Content.WriteBytes(buf);
buf.Release();
HttpUtil.SetContentLength(res, res.Content.ReadableBytes);
}
// Send the response and close the connection if necessary.
Task task = ctx.Channel.WriteAndFlushAsync(res);
if (!HttpUtil.IsKeepAlive(req) || res.Status.Code != 200)
{
task.ContinueWith((t, c) => ((IChannelHandlerContext)c).CloseAsync(),
ctx, TaskContinuationOptions.ExecuteSynchronously);
}
}
public override void ExceptionCaught(IChannelHandlerContext ctx, Exception e)
{
Console.WriteLine($"{nameof(WebSocketServerHandler)} {0}", e);
if (ctx != null)
ctx.CloseAsync();
webSocketBroadCastEvent.Myevent -= Send;
}
public override Task CloseAsync(IChannelHandlerContext context)
{
webSocketBroadCastEvent.Myevent -= Send;
Console.WriteLine(context.Channel.RemoteAddress.ToString() + " Close");
return base.CloseAsync(context);
}
static string GetWebSocketLocation(IFullHttpRequest req)
{
bool result = req.Headers.TryGet(HttpHeaderNames.Host, out ICharSequence value);
Debug.Assert(result, "Host header does not exist.");
string location = value.ToString() + WebsocketPath;
if (IsSsl)
{
return "wss://" + location;
}
else
{
return "ws://" + location;
}
}
}
}
目前用于调试:
每秒服务端会向客户端推送123字符串
然后客户端像服务端发送消息时,服务端会回:
reply:消息
实际使用中需要将这些代码删除
在建立连接后,客户端会将广播事件注册到服务端管道中,关闭会取消注册
然后服务端会开启一个Timer,定期从队列中取消息触发事件给客户端进行推送
效果:
服务端启动:
第一个客户端连接并收到推送:
第二个客户端连接并收到推送:
发送456,收到回复:
第二个客户端断开连接:
第一个仍然收到推送: