工作中项目是物联网项目的,管理平台又是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. Golang通脉之包的管理

    在工程化的开发项目中,Go语言的源码复用是建立在包(package)基础之上的. 包(package)是多个Go源码的集合,是一种高级的代码复用方案,Go语言提供了很多内置包,如fmt.os.io等. ...

  2. Spark解决SQL和RDDjoin结果不一致问题(工作实录)

    问题描述:DataFrame的join结果不正确,dataframeA(6000无重复条数据) join dataframeB(220条无重复数据,由dataframeA转化而来,key值均源于dat ...

  3. vue3.x自定义组件双向数据绑定v-model

    vue2.x 语法 在 2.x 中,在组件上使用 v-model 相当于绑定 value prop 并触发 input 事件: <ChildComponent v-model="pag ...

  4. HttpClient.PatchAsJsonAsync - dotnet/runtime 项目贡献小记

    TL;DR 迫于 PatchAsJsonAsync 方法缺失,我给 dotnet/runtime 项目贡献了相关的 API,可惜要到 .NET7 才能用上. https://github.com/do ...

  5. Java:Set接口小记

    Java:Set接口小记 对 Java 中的 Set接口 与 其实现类,做一个微不足道的小小小小记 概述 public interface Set<E> extends Collectio ...

  6. UltraSoft - Beta - Scrum Meeting 3

    20200519会议纪要 Date: May 19th, 2020. Scrum 情况汇报 进度情况 组员 负责 今日进度 q2l PM.后端 暂无 Liuzh 前端 暂无 Kkkk 前端 完成了前端 ...

  7. Noip模拟68 2021.10.4

    T1 玩水 成功在考试的时候注释掉正解,换成了暴力,只因为不敢保证正解思路的正确 脑子瓦特了,不知道把暴力打成函数拼在一起,不知道当时咋想的.... 就是你找有没有一个点上面和左面的字符一样, 如果这 ...

  8. 计算机网络之传输层(传输层提供的服务及功能概述、端口、套接字--Socket、无连接UDP和面向连接TCP服务)

    文章转自:https://blog.csdn.net/weixin_43914604/article/details/105451022 学习课程:<2019王道考研计算机网络> 学习目的 ...

  9. linux exit 和 _exit的区别

    今天仔细看了一下exit和_exit这两个函数的区别,实际上exit也是调用了_exit退出函数的,只不过在调用_exit之前,exit还进行了一些多余的工作,也正是因为这样,相比起来exit就没有那 ...

  10. 【Go语言学习笔记】为什么要选择Go语言

    一门语言的兴起一定有他的原因,所谓天下苦Java久矣,Go的到来可以说很多后端开发的福音,尤其是在微服务.分布式这么火的今天,那么,他的优势到底是什么呢? 首先,我们需要现有后端语言的优势痛点: 其实 ...