工作中项目是物联网项目的,管理平台又是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. fastdfs单节点部署

    fastdfs单机版搭建 参考链接:https://blog.csdn.net/prcyang/article/details/89946190 搭建步骤 安装依赖 ​ yum -y install ...

  2. ZK(ZooKeeper)分布式锁实现

    点赞再看,养成习惯,微信搜索[牧小农]关注我获取更多资讯,风里雨里,小农等你. 本文中案例都会在上传到git上,请放心浏览 git地址:https://github.com/muxiaonong/Zo ...

  3. Beta阶段第十次会议

    Beta阶段第十次会议 时间:2020.5.26 完成工作 姓名 完成工作 难度 完成度 ltx 1.修正小程序新闻bug2.修正小程序认证bug 中 80% xyq 1.上传信息编辑部分代码到服务器 ...

  4. 内核驱动编译之Makefile shell pwd路径问题

    一般我们在写Makefile的时候为了获取到当前Makefile所在的文件夹路径,会使用TopDIR ?= $(shell pwd)来定义,后续的文件路径都是基于此TopDIR基础上使用. 今天在移植 ...

  5. 寻找下一个结点 牛客网 程序员面试金典 C++ java Python

    寻找下一个结点 牛客网 程序员面试金典 C++ java Python 题目描述 请设计一个算法,寻找二叉树中指定结点的下一个结点(即中序遍历的后继). 给定树的根结点指针TreeNode* root ...

  6. Python AttributeError: module 'sys' has no attribute 'setdefaultencoding'

    Python 3 与 Python 2 有很大的区别,其中Python 3 系统默认使用的就是utf-8编码. 所以,对于使用的是Python 3 的情况,就不需要sys.setdefaultenco ...

  7. 最近公共祖先(lca)与树上叉分

    lca的定义不在过多解释, 代码如下: inline void bfs() { queue<int>q; deep[s]=1;q.push(s); while(!q.empty()) { ...

  8. java实现微信分享

    之前项目中涉及到了微信分享的功能,然后总结下供有需要的朋友参考下. 在做之前可以先看下<微信JS-SDK说明文档>,大致了解下.我自己的工程目录是 1.HttpService和HttpSe ...

  9. (三)lamp环境搭建之编译安装php

    1,PRC (People's republic of China) timezone中设置的时间为中国时间. 2,php的官方镜像源,使用linux时可以直接下载的 http://cn2.php.n ...

  10. 【linux命令】 磁盘管理

    du du是查看硬盘的使用情况,统计文件或目录的空间大小. -a 显示所有目录或文件的大小 -b 以byte为单位,显示目录或文件的大小 -c 显示目录或文件的总和 -k 以KB为单位输出 -m 以M ...