工作中项目是物联网项目的,管理平台又是bs架构。

如果用 Socket 的话,Web 端还需要转发,就全部统一采用了 WebSocket 。

DotNet 平台上的 WebSocket 实现有很多种,这里介绍一下用 DotNetty 来实现的方式。

只完成基本使用功能:

  管理连接、

  服务端接收消息、

  服务端主动向指定连接发送消息、

  服务端主动端口某连接、

  客户端连接断开响应。

本地环境 .net core 2.2

1.创建控制台应用

2.安装NuGet包

DotNetty.Buffers

DotNetty.Codecs

DotNetty.Codecs.Http

DotNetty.Common

DotNetty.Handlers

DotNetty.Transport

DotNetty.Transport.Libuv

3.创建辅助解析的工具类

新建类库 :Examples.Common

同步引用 NuGet 包。并安装以下几个。

Microsoft.Extensions.Configuration

Microsoft.Extensions.Configuration.FileExtensions

Microsoft.Extensions.Configuration.Json

Microsoft.Extensions.Logging.Console

Examples.Common.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup> <ItemGroup>
<PackageReference Include="DotNetty.Buffers" Version="0.6.0" />
<PackageReference Include="DotNetty.Codecs" Version="0.6.0" />
<PackageReference Include="DotNetty.Codecs.Http" Version="0.6.0" />
<PackageReference Include="DotNetty.Common" Version="0.6.0" />
<PackageReference Include="DotNetty.Handlers" Version="0.6.0" />
<PackageReference Include="DotNetty.Transport" Version="0.6.0" />
<PackageReference Include="DotNetty.Transport.Libuv" Version="0.6.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.1.1" />
</ItemGroup> </Project>

安装完了,记得在主控制台程序里面添加对该类库的引用。

4.添加解析辅助类

创建 ExampleHelper.cs

namespace Examples.Common
{
using System;
using DotNetty.Common.Internal.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Console; public static class ExampleHelper
{
static ExampleHelper()
{
Configuration = new ConfigurationBuilder()
.SetBasePath(ProcessDirectory)
.AddJsonFile("appsettings.json")
.Build();
} public static string ProcessDirectory
{
get
{
#if NETSTANDARD1_3
return AppContext.BaseDirectory;
#else
return AppDomain.CurrentDomain.BaseDirectory;
#endif
}
} public static IConfigurationRoot Configuration { get; } public static void SetConsoleLogger() => InternalLoggerFactory.DefaultFactory.AddProvider(new ConsoleLoggerProvider((s, level) => true, false));
}
}

创建 ServerSettings.cs

namespace Examples.Common
{
public static class ServerSettings
{
public static bool IsSsl
{
get
{
string ssl = ExampleHelper.Configuration["ssl"];
return !string.IsNullOrEmpty(ssl) && bool.Parse(ssl);
}
} public static int Port => int.Parse(ExampleHelper.Configuration["port"]); public static bool UseLibuv
{
get
{
string libuv = ExampleHelper.Configuration["libuv"];
return !string.IsNullOrEmpty(libuv) && bool.Parse(libuv);
}
}
}
}

创建 ClientSettings.cs

namespace Examples.Common
{
using System.Net; public class ClientSettings
{
public static bool IsSsl
{
get
{
string ssl = ExampleHelper.Configuration["ssl"];
return !string.IsNullOrEmpty(ssl) && bool.Parse(ssl);
}
} public static IPAddress Host => IPAddress.Parse(ExampleHelper.Configuration["host"]); public static int Port => int.Parse(ExampleHelper.Configuration["port"]); public static int Size => int.Parse(ExampleHelper.Configuration["size"]); public static bool UseLibuv
{
get
{
string libuv = ExampleHelper.Configuration["libuv"];
return !string.IsNullOrEmpty(libuv) && bool.Parse(libuv);
}
}
}
}

5.完成WebSocket的服务端代码

JSON 配置文件 appsettings.json

设置文件属性,始终复制。

{
"port": "8080",
"libuv": "true",
"ssl": "false"
}

程序启动 Program.cs

namespace DotNettyWebSocket
{
using System;
using System.IO;
using System.Net;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using DotNetty.Codecs.Http;
using DotNetty.Common;
using DotNetty.Handlers.Tls;
using DotNetty.Transport.Bootstrapping;
using DotNetty.Transport.Channels;
using DotNetty.Transport.Channels.Sockets;
using DotNetty.Transport.Libuv;
using Examples.Common; class Program
{
static Program()
{
ResourceLeakDetector.Level = ResourceLeakDetector.DetectionLevel.Disabled;
} static async Task RunServerAsync()
{
Console.WriteLine(
$"\n{RuntimeInformation.OSArchitecture} {RuntimeInformation.OSDescription}"
+ $"\n{RuntimeInformation.ProcessArchitecture} {RuntimeInformation.FrameworkDescription}"
+ $"\nProcessor Count : {Environment.ProcessorCount}\n"); bool useLibuv = ServerSettings.UseLibuv;
Console.WriteLine("Transport type : " + (useLibuv ? "Libuv" : "Socket")); if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
} Console.WriteLine($"Server garbage collection : {(GCSettings.IsServerGC ? "Enabled" : "Disabled")}");
Console.WriteLine($"Current latency mode for garbage collection: {GCSettings.LatencyMode}");
Console.WriteLine("\n"); /*
Netty 提供了许多不同的 EventLoopGroup 的实现用来处理不同的传输。
在这个例子中我们实现了一个服务端的应用,因此会有2个 NioEventLoopGroup 会被使用。
第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。
如何知道多少个线程已经被使用,如何映射到已经创建的 Channel上都需要依赖于 IEventLoopGroup 的实现,并且可以通过构造函数来配置他们的关系。
*/ // 主工作线程组,设置为1个线程
// Boss线程:由这个线程池提供的线程是boss种类的,用于创建、连接、绑定socket, (有点像门卫)然后把这些socket传给worker线程池。
// 在服务器端每个监听的socket都有一个boss线程来处理。在客户端,只有一个boss线程来处理所有的socket。
IEventLoopGroup bossGroup; // 子工作线程组,----默认为内核数*2的线程数
// Worker线程:Worker线程执行所有的异步I/O,即处理操作
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 (ServerSettings.IsSsl)
{
tlsCertificate = new X509Certificate2(Path.Combine(ExampleHelper.ProcessDirectory, "dotnetty.com.pfx"), "password");
}
try
{
// 声明一个服务端Bootstrap,每个Netty服务端程序,都由ServerBootstrap控制,通过链式的方式组装需要的参数
// ServerBootstrap 启动NIO服务的辅助启动类,负责初始话netty服务器,并且开始监听端口的socket请求
var bootstrap = new ServerBootstrap(); // 设置主和工作线程组
bootstrap.Group(bossGroup, workGroup); if (useLibuv)
{
// 申明服务端通信通道为TcpServerChannel
// 设置非阻塞,用它来建立新accept的连接,用于构造serversocketchannel的工厂类
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>();
} // ChildChannelHandler 对出入的数据进行的业务操作,其继承ChannelInitializer
bootstrap
// 设置网络IO参数等
.Option(ChannelOption.SoBacklog, 8192)
/*
* ChannelInitializer 是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel。
* 也许你想通过增加一些处理类比如DiscardServerHandler 来配置一个新的 Channel 或者其对应的ChannelPipeline 来实现你的网络程序。
* 当你的程序变的复杂时,可能你会增加更多的处理类到 pipline 上,然后提取这些匿名类到最顶层的类上。
*/
// 设置工作线程参数
.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)); //业务handler ,这里是实际处理业务的Handler
//pipeline.AddLast(new WebSocketServerHandler());
//自己写的业务类
pipeline.AddLast(new SendFunction());
})); // bootstrap绑定到指定端口的行为 就是服务端启动服务,同样的Serverbootstrap可以bind到多个端口
int port = ServerSettings.Port;
IChannel bootstrapChannel = await bootstrap.BindAsync(IPAddress.Loopback, port);
// 似乎没有成功阻塞 而是连接服务端后 就马上执行下一句了 导致连接一次就关闭 (是成功进入 ChannelActive 判断的)也就是无法保持长连接
// 添加长连接即可,参考EchoClient Console.WriteLine("Open your web browser and navigate to "
+ $"{(ServerSettings.IsSsl ? "https" : "http")}"
+ $"://127.0.0.1:{port}/");
Console.WriteLine("Listening on "
+ $"{(ServerSettings.IsSsl ? "wss" : "ws")}"
+ $"://127.0.0.1:{port}/websocket");
Console.ReadLine(); // 关闭服务
await bootstrapChannel.CloseAsync();
}
finally
{
// 释放工作组线程
workGroup.ShutdownGracefullyAsync().Wait();
bossGroup.ShutdownGracefullyAsync().Wait();
}
} static void Main() => RunServerAsync().Wait();
}
}

业务处理类 SendFunction.cs

using DotNetty.Buffers;
using DotNetty.Codecs.Http;
using DotNetty.Codecs.Http.WebSockets;
using DotNetty.Common.Utilities;
using DotNetty.Transport.Channels;
using DotNetty.Transport.Channels.Groups;
using Examples.Common;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using static DotNetty.Codecs.Http.HttpResponseStatus;
using static DotNetty.Codecs.Http.HttpVersion; namespace DotNettyWebSocket
{
public class SendFunction : SimpleChannelInboundHandler<object>
{
const string WebsocketPath = "/websocket"; WebSocketServerHandshaker handshaker; static volatile IChannelGroup groups; public override void ChannelReadComplete(IChannelHandlerContext context) => context.Flush(); //客户端连接异常
public override void ExceptionCaught(IChannelHandlerContext context, Exception exception)
{
WebSocketClose(context);
Console.WriteLine(" SendFunction Exception: " + exception);
context.CloseAsync();
} protected override void ChannelRead0(IChannelHandlerContext ctx, object msg)
{
if (msg is IFullHttpRequest request)
{
this.HandleHttpRequest(ctx, request);
}
else if (msg is WebSocketFrame frame)
{
this.HandleWebSocketFrame(ctx, frame);
}
} void HandleHttpRequest(IChannelHandlerContext ctx, IFullHttpRequest req)
{
if (!req.Result.IsSuccess)
{
SendHttpResponse(ctx, req, new DefaultFullHttpResponse(Http11, BadRequest));
return;
} if (!Equals(req.Method, HttpMethod.Get))
{
SendHttpResponse(ctx, req, new DefaultFullHttpResponse(Http11, Forbidden));
return;
} var wsFactory = new WebSocketServerHandshakerFactory(
GetWebSocketLocation(req), 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);
} base.HandlerAdded(ctx);
IChannelGroup g = groups;
if (g == null)
{
lock (this)
{
if (groups == null)
{
g = groups = new DefaultChannelGroup(ctx.Executor);
}
}
}
g.Add(ctx.Channel); //主动向当前连接的客户端发送信息
TextWebSocketFrame tst = new TextWebSocketFrame($"欢迎{ctx.Channel.RemoteAddress}加入111.");
TextWebSocketFrame tstId = new TextWebSocketFrame($"欢迎{ctx.Channel.Id}加入222.");
groups.WriteAndFlushAsync(tst);
groups.WriteAndFlushAsync(tstId); //保存连接对象
lock (ConnChannelList)
{
if (ConnChannelList.Count > 0)
{
if (ConnChannelList.ContainsKey(ctx.Channel.Id.ToString()))
{
ConnChannelList.Remove(ctx.Channel.Id.ToString());
}
}
ConnChannelList.Add(ctx.Channel.Id.ToString(), ctx.Channel.Id);
Console.WriteLine($"当前在线数:{ConnChannelList.Count}");
} Console.WriteLine("---------首次到达----------");
Console.WriteLine("连接成功");
Console.WriteLine($"欢迎{ctx.Channel.RemoteAddress}加入");
Console.WriteLine("---------首次到达----------");
} public static volatile Dictionary<string, IChannelId> ConnChannelList = new Dictionary<string, IChannelId>();
void HandleWebSocketFrame(IChannelHandlerContext ctx, WebSocketFrame frame)
{
//客户端关闭连接
if (frame is CloseWebSocketFrame)
{
WebSocketClose(ctx); Console.WriteLine($"连接关闭 {ctx.Channel.RemoteAddress}");
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 textFrame)
{
Console.WriteLine("---------消息到达----------");
Console.WriteLine("Received from client: " + frame.Content.ToString(Encoding.UTF8));
Console.WriteLine("---------消息到达----------"); //发送信息到指定连接
string[] strArg = textFrame.Text().Split(',');
if (strArg.Length > 1)
{
lock (ConnChannelList)
{
if (ConnChannelList.ContainsKey(strArg[0]))
{
var connChannel = groups.Find(ConnChannelList[strArg[0]]);//null if (connChannel != null)
{
//主动向当前连接的客户端发送信息
TextWebSocketFrame tst = new TextWebSocketFrame(strArg[1]);
connChannel.WriteAndFlushAsync(tst); //服务端断开指定客户端连接
if (strArg[1] == "close")
{
connChannel.CloseAsync();
}
}
}
}
} ctx.WriteAsync(frame.Retain()); return;
} if (frame is BinaryWebSocketFrame)
{
ctx.WriteAsync(frame.Retain());
}
} static void SendHttpResponse(IChannelHandlerContext ctx, IFullHttpRequest req, IFullHttpResponse res)
{
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);
} Task task = ctx.Channel.WriteAndFlushAsync(res);
if (!HttpUtil.IsKeepAlive(req) || res.Status.Code != 200)
{
task.ContinueWith((t, c) => ((IChannelHandlerContext)c).CloseAsync(),
ctx, TaskContinuationOptions.ExecuteSynchronously);
}
} 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 (ServerSettings.IsSsl)
{
return "wss://" + location;
}
else
{
return "ws://" + location;
}
} /// <summary>
/// 关闭ws连接
/// </summary>
/// <param name="ctx"></param>
static void WebSocketClose(IChannelHandlerContext ctx)
{
lock (ConnChannelList)
{
string channelId = ctx.Channel.Id.ToString();
if (ConnChannelList.ContainsKey(channelId))
{
ConnChannelList.Remove(channelId);
}
Console.WriteLine($"当前在线数:{ConnChannelList.Count}");
}
} }
}

6.测试HTML脚本

<!DOCTYPE html>
<html> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head> <body> <input type="text" id="message">
<div id="msgBox"></div> <input type="button" onclick="sendText()" value="发送信息"> <script> var ws = ''; window.onload = function () {
connect();
} function connect() {
var address = "ws://127.0.0.1:8080/websocket";
// address = "wss://127.0.0.1:8080/websocket";
ws = new WebSocket(address); ws.onopen = function (e) { }; //收到信息时
ws.onmessage = function (e) {
var Html = '<p>' + e.data + '</p>';
document.getElementById("msgBox").innerHTML += Html;
}; //发生错误时
ws.onerror = function (e) { }; //连接关闭时
ws.onclose = function (e) {
document.getElementById("msgBox").innerHTML += "<p>与服务器的连接已断开。</p>";
}; } function sendText() {
ws.send(document.getElementById("message").value);
} </script> </body> </html>

7.运行测试

运行第一个HTML页面

运行第二个

发送消息

给第二个HTML发送消息,要拿一些特征。

关闭第二个页面

基础功能都已经完成。

在 Ubuntu 上测试也 OK。

从 SuperWebSocket 换到 DotNetty 主要原因就是想上 Linux 。

DotNetty实现WebSocket的简单使用的更多相关文章

  1. nodejs与websocket模拟简单的聊天室

    nodejs与websocket模拟简单的聊天室 server.js const http = require('http') const fs = require('fs') var userip ...

  2. DotNetty关键概念及简单示例(基于NET5)

    DotNetty关键概念及简单示例(基于NET5) 目录 DotNetty关键概念及简单示例(基于NET5) 1.DotNetty 设计的关键 1.1 核心组件 1.1.1 Channel 1.1.2 ...

  3. websocket实现简单聊天程序

    程序的流程图: 主要代码: 服务端 app.js 先加载所需要的通信模块: var express = require('express'); var app = express(); var htt ...

  4. 用swoole和websocket开发简单聊天室

    首先,我想说下写代码的一些习惯,第一,任何可配置的参数或变量都要写到一个config文件中.第二,代码中一定要有日志记录和完善的报错并记录报错.言归正传,swoole应该是每个phper必须要了解的, ...

  5. websocket(二) websocket的简单实现,识别用户属性的群聊

    没什么好说的,websocket实现非常简单,我们直接看代码. 运行环境:jdk8 tomcat8 无须其他jar包. 具体环境支持自己百度 package com.reach.socketContr ...

  6. [tornado]websocket 最简单demo

    想法 前两天想看看django 长轮询或者是websocket的方案,发现都不太好使. tornado很适合做这个工作,于是找了些资料,参照了做了个最简单demo,以便备用. 具体的概念就不说了,to ...

  7. websocket(二)--简单实现网页版群聊

    websocket可以实现服务端的消息推送,而不必在客户端轮询,大大的节省的资源,对于实时通讯来说简直是个大喜讯. 在上一篇文章中介绍了协议握手,这篇文章将通过实现简单的群聊来帮助进一步了解webso ...

  8. websocket的简单使用

    一 轮询 什么是轮询:设置每一段时间去访问一次服务器,然后服务器返回最新的数据.这样服务器的压力会非常的大,并且还会有延迟.适用于小型程序. 实现:再客户端的页面设置一个定时发送请求的任务,每个这段时 ...

  9. WebSocket的简单认识&SpringBoot整合websocket

    1. 什么是WebSocket?菜鸟对websocket的解释如下 WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议. WebSocket 使得客户端和服务 ...

随机推荐

  1. Windows10使用技巧

    Windows10配置技巧 新机配置 "我的电脑"图标设置 在桌面右击鼠标=>个性化=>点击左侧"主题"=>点击相关的设置中的"桌面 ...

  2. python streamlit 速成web页面,深度学习模型展示.

    #  点我查看 参考文献 py中一个web应用,Streamlit 是一个开源 Python 库,可让您轻松创建和共享用于机器学习和数据科学的精美自定义 Web 应用程序.只需几分钟,您就可以构建和部 ...

  3. 如何再一台电脑上配置多个tomcat同时运行

    1.配置运行tomcat 首先要配置java的jdk环境,这个就不在谢了  不懂去网上查查,这里主要介绍再jdk环境没配置好的情况下 如何配置运行多个tomcat 2.第一个tomcat: 找到&qu ...

  4. 洛谷 P3232 [HNOI2013]游走

    链接: P3232 题意: 和上次考试 T4 的简化且无修改一样,经典图上高斯消元求期望. 分析: 要求出每个点的期望出发次数 \(f_i\),每个点度数为 \(d_i\),有 \[f1=\sum\d ...

  5. linked-list-cycle leetcode C++

    Given a linked list, determine if it has a cycle in it. Follow up: Can you solve it without using ex ...

  6. 一步一步学ROP之gadgets和2free篇(蒸米spark)

    目录 一步一步学ROP之gadgets和2free篇(蒸米spark) 0x00序 0x01 通用 gadgets part2 0x02 利用mmap执行任意shellcode 0x03 堆漏洞利用之 ...

  7. x64 InlineHook 黑魔法

    目录 x64 InlineHook 黑魔法 为什么不能用X86 的HOOK方式? 原理:jmp + rip 进行寻址6字节方式跳转 手动InlineHook 临时地址x(找一块空内存) 计算偏移 源地 ...

  8. 黑客是如何利用DNS域传送漏洞进行渗透与攻击的?

    一.DNS域传送 DNS :Domain Name System 一个保存IP地址和域名相互映射关系的分布式数据库,重要的互联网基础设施,默认使用的TCP/UDP端口号是53 常见DNS记录类型: 1 ...

  9. ONVIF客户端中预置位设置代码实现过程

    simpleOnvif的功能:提供支持Windows.Linux.arm.Android.iOS等各种平台的SDK库,方便集成,二次开发 之前跟大家分享了我们安徽思蔷信息科技的simpleOnvif的 ...

  10. kubernetes常见命令

    kubernetes命令 kubectl get pod --all-namespaces查看pod节点 kubectl delete -n service/pods/deplay 删除指定内容 ku ...