C#搭建WebSocket服务实现通讯

news2024/11/25 19:23:11

在学习使用websocket之前我们先了解一下websocket:
WebSocket是一种在单个TCP连接上进行全双工通信的通信协议。与HTTP协议不同,它允许服务器主动向客户端发送数据,而不需要客户端明确地请求。这使得WebSocket非常适合需要实时或持续通信的应用程序,例如在线聊天、实时游戏、股票市场更新等。

websocket介绍

以下是WebSocket的一些关键特点
全双工通信:WebSocket允许客户端和服务器在同一时间内彼此发送数据,而不需要等待对方的响应。这种实时性使其成为许多实时应用程序的首选协议。
持久连接:与HTTP请求-响应模型不同,WebSocket连接在客户端和服务器之间保持打开状态,直到一方选择关闭连接。这消除了频繁地建立和终止连接的开销。
较低的开销:WebSocket在已建立连接的基础上传输数据,减少了与每个请求都要建立新连接的HTTP协议的开销。
轻量级标头:WebSocket协议的标头数据相对较小,这有助于减少数据传输时的开销。
跨域支持:WebSocket协议支持跨域通信,这意味着您可以在不同域之间建立WebSocket连接。
安全性:与HTTP一样,WebSocket可以通过使用TLS/SSL来加密通信,确保数据的安全性。
要建立WebSocket连接,客户端和服务器之间的初始握手是通过HTTP完成的。一旦握手成功,连接升级为WebSocket连接,允许双方直接交换数据帧。数据帧可以是文本数据或二进制数据,具体取决于应用程序的需求。

下面是一个示意性的WebSocket握手过程
客户端发起WebSocket握手请求,包含特定的HTTP头信息。
服务器响应握手请求,同样包含特定的HTTP头信息。
握手成功后,连接从HTTP协议升级到WebSocket协议。
客户端和服务器可以通过发送数据帧进行实时通信,直到其中一方关闭连接。

Ajax长轮询介绍

在websocket不被广泛使用之前Ajax的长轮询比较流行,本质上两者都是用于实现实时通信的技术,但是它们之间有以下区别:
连接方式:Ajax长轮询使用的是HTTP协议,而Websocket使用的是WebSocket协议。
性能:Ajax长轮询中客户端需要不断向服务器发送HTTP请求,服务器在收到请求后才能返回数据给客户端。这种方式会导致不必要的网络延迟和服务器压力,性能相对较差。Websocket采用双向通信方式,只需要建立一次连接,即可实现实时通信,性能较好。
兼容性:Ajax长轮询在大多数浏览器中都可以使用,但是在一些较老的浏览器中可能会有兼容性问题。Websocket需要浏览器支持HTML5才能使用。
实时性:Ajax长轮询的实时性较差,因为客户端需要不断向服务器发送请求,而服务器在收到请求后才能返回数据。Websocket的实时性较好,因为它采用双向通信方式。

完整代码https://download.csdn.net/download/qq_39569480/88264479

websocket用例

后端代码

1.注册服务

首先要在Startup文件中的ConfigureServices方法中注册HttpContextAccessor

// This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication3", Version = "v1" });
            });
            services.AddHttpContextAccessor();//注册http
        }

其次在Startup文件中的Configure方法中添加wesocket

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication2 v1"));
            }
            app.UseWebSockets();//添加websocket
            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

2.创建服务

首先定义一个Controller或者Service
注入IHttpContextAccessor以便获取socket请求
声明全局变量用于存储socket链接,这里没有将socket存储到类似redis或数据库中,因为socket链接不能被反序列化,如果有更好方法的小伙版可以留言。这里的socket链接池只要服务不停 链接就会一直存在。

[ApiController]
    public class WebSocketDemoController : ControllerBase
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private static Dictionary<string, WebSocket> _dic = new Dictionary<string, WebSocket>();
        public WebSocketDemoController(IHttpContextAccessor httpContextAccessor)
        {
            this._httpContextAccessor = httpContextAccessor;
        }

}

编写socket接口
[HttpGet(“/WsService”)]这里定义了socket的名字
http协议:访问时直接通过ws://Ip:Port/WsService去访问
https协议:访问时直接通过wss://Ip:Port/WsService去访问

[HttpGet("/WsService")]
        public async Task Get()
        {
            //Logger.LogInformation(CurrentUser.Id + CurrentUser.Name);
            if (_httpContextAccessor.HttpContext.WebSockets.IsWebSocketRequest)
            {
                //string token= _httpContextAccessor.HttpContext.Request.Headers["Sec-WebSocket-Protocol"];
                Console.WriteLine("有链接进入");
                //接受websocket客户端连接
                var webSocket = await _httpContextAccessor.HttpContext.WebSockets.AcceptWebSocketAsync();
                //await ReadWriteWebSocektAsync("Add",CurrentUser.Id.ToString(),webSocket); //如果用户的连接不存在加进缓存


                await Echo(webSocket);
            }
            else
            {
                //不是websocket客户端请求 ]
                _httpContextAccessor.HttpContext.Response.StatusCode = 400;
            }
        }

消息处理方法

		/// <summary>
        /// 对客户端的处理,接受消息,发送消息,关闭连接
        /// </summary>
        /// <param name="webSocket"></param>
        /// <param name="dic"></param>
        /// <param name="userId"></param>
        /// <returns></returns>
        private async Task Echo(WebSocket webSocket)
        {
            try
            {
                var buffer = new byte[1024 * 4];

                //webSocket.SubProtocol = token;

                var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);


                while (!result.CloseStatus.HasValue)
                {
                    var message = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
                    WebSocektDto socketRequest = JsonConvert.DeserializeObject<WebSocektDto>(message);
                    buffer = new byte[1024 * 4];
                    //Logger.LogInformation($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}=接收到的信息=" + message);
                    if (socketRequest.type == "create_connect")
                    {//创建链接 
                        //var (sysUserId, sysUserName) = GetSystemUserId(socketRequest.auth);
                        /***************************************/
                        /*******重要****看一下下面两行注释******/
                        /***************************************/
                        //这里只是举了一个简单的示例,如果实际应用建议发起者传送token信息 比如上一行解析发起者传过来的token信息  把发起者的id和socket连接保存起来
                        //这里特别说明一下 为什么token信息要通过参数的形式传送而不是直接写到websocket请求的head中,是因为websocket不支持head传送信息
                        StoreWebSocekt("Add", socketRequest.sponsorUserId, webSocket);
                    }
                    else if (socketRequest.type == "dialing")
                    {//发起者打电话
                        //Logger.LogInformation($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}=发起通话");
                        string targetId = socketRequest.receiverUserId;//获取接收者id
                        string sponsorUserId = socketRequest.sponsorUserId;//发起者
                        string sponsorUserName = socketRequest.sponsorUserName;//发起者的姓名 
                        //var (sysUserId, sysUserName) = GetSystemUserId(socketRequest.auth);
                        //if (string.IsNullOrWhiteSpace(sysUserId)) throw new Exception("请认证");
                        //这一步是确保接收信息方的socket链接是否存在,如果不存在信息也找不到接收方
                        //首先在系统登录或者系统初始化时所有的用户都需要建立起链接  并且链接保存起来,以便此刻接收响应信息
                        if (_dic.ContainsKey(targetId))
                        {
                            //向客户端发送消息
                            if (_dic.TryGetValue(targetId, out WebSocket ReceiverSocket))
                            {//在我们自定义的链接池中如果找到了接收者的链接   那么我们给接收者发送信息
                                string sponsor = socketRequest.sponsorUserId;
                                //获取连接池中的接收者 连接    然后给接收者发送 通话信息
                                WebSocektDto PushMessageDto = new WebSocektDto()
                                {
                                    type = "called",//类型可以自己定义  为了方便知道自己的每个链接的作用
                                    ok = true,
                                    msg = "",
                                    data =
                                    new ResponseDetailsData { sponsorUserId = sponsorUserId, sponsorUserName = sponsorUserName, field1 = "", field2 = "", field3 = "" }//这里定义了一个实体是用于给接收者传递消息的,里边包含发起者的信息和发送的信息
                                };
                                byte[] serverMsg = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(PushMessageDto));
                                await ReceiverSocket.SendAsync(new ArraySegment<byte>(serverMsg, 0, serverMsg.Length), WebSocketMessageType.Text, result.EndOfMessage, CancellationToken.None);

                            }
                            else
                            {
                                //如果在我们自定义的链接池中没有找到接收者的链接 那么我们需要给发起者响应告诉他为什么
                                WebSocektDto rp = new WebSocektDto() { type = "dialing_response", ok = false, msg = "对方意外断开连接", data = new ResponseDetailsData { sponsorUserId = null } };
                                byte[] rpJson = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(rp));
                                await webSocket.SendAsync(new ArraySegment<byte>(rpJson, 0, rpJson.Length), WebSocketMessageType.Text, result.EndOfMessage, CancellationToken.None);
                            }
                        }
                        else
                        {//如果在我们自定义的链接池中没有找到接收者   那么我们需要给发起者响应告诉他为什么
                            WebSocektDto rp = new WebSocektDto() { type = "dialing_response", ok = false, msg = "对方未建立链接", data = new ResponseDetailsData { sponsorUserId = null } };
                            byte[] rpJson = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(rp));
                            await webSocket.SendAsync(new ArraySegment<byte>(rpJson, 0, rpJson.Length), WebSocketMessageType.Text, result.EndOfMessage, CancellationToken.None);
                        }
                    }
                    //继续接受客户端消息  
                    result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                }
                //关闭释放与客户端连接
                await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
                //await ReadWriteWebSocektAsync("Edit", CurrentUser.Id.ToString(), webSocket); //如果用户的连接不存在加进缓存

                string keys = _dic.Where(q => q.Value == webSocket).Select(q => q.Key).FirstOrDefault();//通过当前socket获取链接池中的key  然后删掉连接池中的链接
                if (!string.IsNullOrWhiteSpace(keys)) StoreWebSocekt("Edit", keys, webSocket);
            }
            catch (Exception e)
            {
            }
        }

3.完成以上步骤我们进行测试

启动服务
在这里插入图片描述

1.创建链接
此步骤为建立socket的连接 并不是实际的工作,因为我们需要确保与服务器的socket通信所以先建立连接。
在这里插入图片描述
此步骤对应前端的代码
ws = new WebSocket(“ws://192.168.0.107:8088”);
在创建ws服务后会自动创建监听
ws.onopen = function() {
};

2.让后台保存用户的socket链接
在系统登录时保存起用户的链接,方便以后互相发送消息
这里我模拟两个用户登录后台保存其socket链接
在这里插入图片描述
在这里插入图片描述
3.1122用户给2211用户发送信息
1122用户发送信息
在这里插入图片描述
我们回到2211的窗口查看,接收到了1122用户发送的信息
在这里插入图片描述
当然我们2211也可以给1122回复信息
在这里插入图片描述

再回到1122的窗口
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/941265.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

go gin 参数绑定常用验证器

https://pkg.go.dev/github.com/go-playground/validator/v10#readme-baked-in-validations min 最小max 最大len 长度限制gt 大于eq 等于ne 不等于eqfield 与某个字段值一样nefield 与某个字段值不一样 package mainimport ("net/http""github.com/gin-gonic…

无锁并发:探秘CAS机制的魔力

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; 无锁并发&#xff1a;探秘CAS机制的魔力 ⏱️ 创作时间&#xff1a; 2…

React 生命周期新旧对比

前言 React16.4版本之后使用了新的生命周期&#xff0c;它使用了一些新的生命周期钩子&#xff08;getDerivedStateFromProps、getSnapshotBeforeUpdate&#xff09;&#xff0c;并且即将废弃老版的3个生命周期钩子&#xff08;componentWillMount、componentWillReceiveProps…

中国社科院与美国杜兰大学金融管理硕士——创造职业生涯的转折点

作为金融领域从业人员&#xff0c;时刻都在关注行业最新资讯是非常有必要的&#xff0c;只有金融业的前沿讯息&#xff0c;才能防范于未然&#xff0c;紧跟时代脚步。针对在职的你&#xff0c;如何利用业余时间让自己实现质的飞跃你&#xff1f;中国社科院与美国杜兰大学金融管…

门禁系统忘记登入密码,现在更换电脑如何迁移旧电脑门禁系统的数据

环境&#xff1a; ivms-4200 v3.10.0.6_c 问题描述&#xff1a; 门禁系统忘记登入密码,现在更换电脑如何迁移旧电脑门禁系统的数据&#xff0c;旧电脑记住密码&#xff0c;忘了密码和密保了 解决方案&#xff1a; 1.前往海康官网下载4200客户端&#xff0c;在新电脑上安装 …

树的介绍(C语言版)

前言 在数据结构中树是一种很重要的数据结构&#xff0c;很多其他的数据结构和算法都是通过树衍生出来的&#xff0c;比如&#xff1a;堆&#xff0c;AVL树&#xff0c;红黑色等本质上都是一棵树&#xff0c;他们只是树的一种特殊结构&#xff0c;还有其他比如linux系统的文件系…

building and deploying a single-Master RocketMQ cluster

building and deploying a single-Master RocketMQ cluster 1 、下载RocketMQ安装包(这里是通过源码安装)2、安装3、启动nameserver4、启动borkerStart the broker serviceVerify that the broker service is started successfully, for example, the brokers ip is 192.168.1.…

使用ChatGPT一键生成思维导图

指令1&#xff1a;接下来你回复的所有内容&#xff0c;都放到Markdown代码框中。 指令2&#xff1a;作为一个Docker专家&#xff0c;为我编写一个详细全面的Docker学习大纲&#xff0c;包括基础知识、进阶知识、项目实践案例&#xff0c;学习书籍推荐、学习网站推荐等&#xf…

13、Vue3 大事件管理系统

一、大事件项目介绍 和 创建 1.1 Vue3 大事件管理系统 在线演示&#xff1a; https://fe-bigevent-web.itheima.net/login 接口文档: https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835 基地址&#xff1a; http://big-event-vue-api-t.i…

技术面:ts是如何编译成js,js又如何在浏览器运行的?

文章目录 ts是如何编译成jsjs又如何在浏览器运行 TypeScript 代码需要通过【编译器】将其编译成 JavaScript 代码&#xff0c;而 JavaScript 代码则需要在浏览器中下载、解析和执行&#xff0c;才能最终呈现出页面内容&#xff0c;并与用户进行交互 ts是如何编译成js 摘抄&…

Tuxera NTFS for Mac2023苹果电脑Mac硬盘读写工具

Tuxera NTFS for Mac是一款高效稳定的NTFS读写工具&#xff0c;可以让你在Mac上完整地读写兼容NTFS格式驱动器&#xff0c;对磁盘进行访问、编辑、存储和传输文件等操作。Tuxera NTFS for Mac软件是一款高效稳定的NTFS读写工具&#xff0c;可以让你在Mac上完整地读写兼容NTFS格…

QT 消息对话框按钮显示

前言 搞QT嘛&#xff0c;大多数都是军工。都要国产化&#xff0c;而且消息对话框的按钮的英文也不是很得劲&#xff0c;所以需要汉化。使用静态函数的按钮就是显示英文&#xff0c;汉化的代码如下。 void Widget::on_pushButton_clicked() {QMessageBox box(QMessageBox::Inf…

腾讯云coding平台平台inda目录遍历漏洞复现

前言 其实就是一个python的库可以遍历到&#xff0c;并不能遍历到别的路径下&#xff0c;后续可利用性不大&#xff0c;并且目前这个平台私有部署量不多&#xff0c;大多都是用腾讯云在线部署的。 CODING DevOps 是面向软件研发团队的一站式研发协作管理平台&#xff0c;提供…

Matlab(基本操作与矩阵输入)

目录 1.Matlab视窗详读 2.基本操作与矩阵输入 2.1 运算符的优先级 2.2 初等数学函数 2.3 嵌入函数 2.4 特殊变量和常量 2.5 Matlab的优先级调用 2.6 数字显示格式长 2.7 命令行中端 2.8 部分函数 2.9 向量和矩阵 2.10 数组索引 2.11 串联矩阵 2.12 生成数值序列 …

合宙Air724UG LuatOS-Air LVGL API控件--进度条 (Bar)

进度条 (Bar) Bar 是进度条&#xff0c;可以用来显示数值&#xff0c;加载进度。 示例代码 – 创建进度条 bar lvgl.bar_create(lvgl.scr_act(), nil) – 设置尺寸 lvgl.obj_set_size(bar, 200, 20); – 设置位置居中 lvgl.obj_align(bar, NULL, lvgl.ALIGN_CENTER, 0, 0) …

9.4 集成功率放大电路

OTL、OCL 和 BTL 电路均有各种不同输出功率和不同电压增益的集成电路。应当注意&#xff0c;在使用 OTL 电路时&#xff0c;需外接输出电容。为了改善频率特性&#xff0c;减小非线性失真&#xff0c;很多电路内部还引入深度负反馈。这里以低频功放为例。 一、集成功率放大电路…

vue项目,如何修改Element-Plus等UI组件库的样式,三种方式搞定!!!

前言 我们在学习和使用组件库构建页面的时候&#xff0c;时常会遇到这样的问题。 即&#xff0c;尽管组件库已经提供了较多的功能&#xff0c;来帮助我们构建自定义的效果&#xff0c;但有时仍不能使我们满意。 这个时候我们就不得不修改UI库的样式&#xff0c;来达到想要的状…

修改类属性后出现错误 Filtered request failed. 和 unable to find class for code 253

问题描述&#xff1a; 类增加字段后项目无法启动。 javax.servlet.ServletException: Filtered request failed. Caused by: java.lang.RuntimeException: unable to find class for code 253 原因分析&#xff1a; 由于修改的User类是设置了序列化的&#xff0c;在未修改…

RISC-V 中国峰会 | OpenMPL引人注目,RISC-V Summit China 2023圆满落幕

RISC-V中国峰会圆满落幕 2023年8月25日&#xff0c;为期三天的RISC-V中国峰会&#xff08;RISC-V Summit China 2023&#xff09;圆满落幕。本届峰会以“RISC-V生态共建”为主题&#xff0c;结合当下全球新形势&#xff0c;把握全球新时机&#xff0c;呈现RISC-V全球新观点、新…

【C语言进阶(8)】自定义数据类型1:结构体

文章目录 前言Ⅰ 结构体的声明和定义⒈结构体声明⒉结构体定义⒊特殊的声明 Ⅱ 结构体的自引用Ⅲ 结构体初始化Ⅳ 访问结构体成员Ⅴ 结构体内存对齐⒈结构体内存对齐规则⒉分析结构体大小⒊嵌套结构体内存大小⒋内存对齐存在的原因 Ⅵ 修改默认对齐数Ⅶ 结构体传参 前言 C 语言…