web管理系统中可以对业务数据执行新增和删除,现在需要当业务数据发生新增或删除操作后,尽可能实时的反应到WPF客户端上面。
web管理系统用VUE编写,后端服务为SpringBoot,WPF客户端基于.Netframework4.8编写。
WebSocketServer端采用SpringBoot框架实现,通过在springboot-web项目中集成 org.springframework.boot:spring-boot-starter-websocket
实现websocket的能力。
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
@EnableWebSocket
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/ws/label/{clientId}")
@Component
public class LabelWebSocket {
/**
* session list
*/
private static ConcurrentHashMap<String, Session> sessionList = new ConcurrentHashMap<>();
/**
* 当前 clientId
*/
private String currentClientId = "";
@OnOpen
public void open(Session session, @PathParam("clientId") String clientId) throws IOException {
if (sessionList.containsKey(clientId)) {
sessionList.remove(clientId);
}
sessionList.put(clientId, session);
currentClientId = clientId;
this.sendMsg(session, "connectok");
}
@OnClose
public void close(Session session) throws IOException {
sessionList.remove(currentClientId);
System.out.println("连接关闭,session=" + JSON.toJSONString(session.getId()));
}
@OnMessage
public void receiveMsg(Session session, String msg) throws IOException {
this.sendMsg(session, "接收到的消息为:" + msg);
// throw new RuntimeException("主动抛异常");
}
@OnError
public void error(Session session, Throwable e) throws IOException {
System.out.println("连接异常,session=" + JSON.toJSONString(session.getId()) + ";currentClientId=" + currentClientId);
this.sendMsg(session, "发生异常,e=" + e.getMessage());
e.printStackTrace();
}
/**
* @param clientId
* @param msg
*/
public boolean sendMsg(String clientId, String msg) throws IOException {
if (sessionList.containsKey(clientId)) {
Session session = sessionList.get(clientId);
this.sendMsg(session, msg);
return true;
} else {
return false;
}
}
private void sendMsg(Session session, String msg) throws IOException {
session.getBasicRemote().sendText(msg);
}
}
WebSocketClient端集成在WPF应用客户端中,通过前期调研,选中 WebSocketSharp 作为websocketclient工具,WebSocketSharp 是托管在Github的开源项目,MITLicense,目前4.9K的star。
//nuget
Install-Package WebSocketSharp -Pre
WebSocket ws = new WebSocket("ws://127.0.0.1:8083/ws/xx/clientId");
private void InitWebSocket()
{
ws.OnOpen += (sender, e) =>
{
Console.WriteLine("onOpen");
};
//允许ping
ws.EmitOnPing = true;
//接收到xiaoxi
ws.OnMessage += (sender, e) =>
{
ReceiveMessage(sender, e);
};
ws.Connect();
//发送消息
//ws.Send("BALUS")
;
}
private void ReceiveMessage(object sender, MessageEventArgs e)
{
if (e.IsText)
{
// Do something with e.Data.like jsonstring
Console.WriteLine(e.Data);
return;
}
if (e.IsBinary)
{
// Do something with e.RawData. like byte[]
return;
}
if (e.IsPing)
{
// Do something to notify that a ping has been received.
return;
}
}
由于 WebSocketSharp 会创建线程来处理 ReceiveMessage ,而WPF中子线程是无法更新UI的,所以需要引入 Dispatcher 来实现跨线程更新UI。
//当前线程
string name = Thread.CurrentThread.ManagedThreadId.ToString();
private void ReceiveMessage(object sender, MessageEventArgs e)
{
if (e.IsText)
{
// Do something with e.Data.like jsonstring
Console.WriteLine(e.Data);
//当前线程
string name = Thread.CurrentThread.ManagedThreadId.ToString();
App.Current.Dispatcher.Invoke((Action)(() =>
{
Image lab = new Image();
lab.Uid = "123456";
lab.Name = "labName";
lab.Width = 50; lab.Height = 50;
string url = "http://xxx:xxx/img/louyu.png";
BitmapImage bitmapImage = HttpUtil.getImage(url);
lab.Source = bitmapImage;
lab.AddHandler(MouseLeftButtonDownEvent, new MouseButtonEventHandler(LabelClick));
Canvas.SetTop(lab, 800);
Canvas.SetLeft(lab, 600);
this.cav.Children.Add(lab);
}));
return;
}
}
当websocket-server重启,此时client将丢失和server的连接,如果不引入重连机制,那么在手动重启client前,业务数据将无法实时更新,这在实际使用中肯定是不被允许的,所以需要引入重连机制。当捕捉到连接关闭之后,尝试重新连接。
重试次数5次,重试间隔5s,示例代码如下:
//连接关闭
wsLabel.OnClose += (sender, e) =>
{
OnWsLabelClose(sender, e);
};
......
int retry = 0;
/// <summary>
/// 连接关闭
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnWsLabelClose(object sender, CloseEventArgs e)
{
Log.LogInfo.Info("OnWsLabelClose,retry=" + retry);
if (retry < 5)
{
retry++;
Thread.Sleep(5000);
wsLabel.Connect();
}
else
{
wsLabel.Log.Error("The reconnecting has failed.");
Log.LogInfo.Info("重新连接失败,超过最大重试次数");
}
}
目前WebSocketServer和web后端服务是在同一个SpringBoot的工程中,所以只要将WebSocketServer托管到SpringContainer中,web后端服务可以通过 DI 的方式直接访问 WebSocketEndPoint。
如果考虑程序的低耦合,可以在WebSocketServer和web后端服务之间架设一个MQ。
@Autowired
private LabelWebSocket ws;
@GetMapping("/create")
public boolean createLabel() throws IOException {
String cameraId = "cml";
//todo
boolean result = ws.sendMsg(cameraId, "新增标签");
return result;
}
当前在 WebSocketServer 中,已经连接的client信息是记录在当前进程的cache中,如果服务做横向扩容,cache信息无法在多实例进程中传递,将导致无法正确的处理业务数据,并可能会发生意想不到的异常和bug,此问题在并发越高的情况下造成的影响越大
web后端服务为基于java语言的springboot程序,这种类型程序的特点是内存消耗特别严重。WebSocketServer服务在本项目中仅用作消息中间件,连通web后端服务和WPF客户端。
首先WebSocketServer没有太多的计算能力的消耗,内存消耗会随着连接客户端数量的增长而增长,网络将是最大的开销,一方面需要转发来自web后端服务的业务数据,并和WPF客户端保持长连接;另一方面WebSocketServer和WPF客户端的交互可能会走公网,而其和web后端服务必然是在局域网环境。
综上,将web后端服务和WebSocketServer分开部署对于硬件资源成本和利用率来说是最好的选择。
未引入重试机制,当某一个环节失败之后,将导致异常情况发生。
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数
我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c
我没有找到太多关于如何执行此操作的信息,尽管有很多关于如何使用像这样的redirect_to将参数传递给重定向的建议:action=>'something',:controller=>'something'在我的应用程序中,我在路由文件中有以下内容match'profile'=>'User#show'我的表演Action是这样的defshow@user=User.find(params[:user])@title=@user.first_nameend重定向发生在同一个用户Controller中,就像这样defregister@title="Registration"@user=Use
我正在使用RubyonRails3.0.9,我想生成一个传递一些自定义参数的link_toURL。也就是说,有一个articles_path(www.my_web_site_name.com/articles)我想生成如下内容:link_to'Samplelinktitle',...#HereIshouldimplementthecode#=>'http://www.my_web_site_name.com/articles?param1=value1¶m2=value2&...我如何编写link_to语句“alàRubyonRailsWay”以实现该目的?如果我想通过传递一些
我的工作要求我为某些测试自动生成电子邮件。我一直在四处寻找,但未能找到可以快速实现的合理解决方案。它需要在outlook而不是其他邮件服务器中,因为我们有一些奇怪的身份验证规则,我们需要保存草稿而不是仅仅发送邮件的选项。显然win32ole可以做到这一点,但我找不到任何相当简单的例子。 最佳答案 假设存储了Outlook凭据并且您设置为自动登录到Outlook,WIN32OLE可以很好地完成此操作:require'win32ole'outlook=WIN32OLE.new('Outlook.Application')message=
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
我正在使用Ruby,我正在与一个网络端点通信,该端点在发送消息本身之前需要格式化“header”。header中的第一个字段必须是消息长度,它被定义为网络字节顺序中的2二进制字节消息长度。比如我的消息长度是1024。如何将1024表示为二进制双字节? 最佳答案 Ruby(以及Perl和Python等)中字节整理的标准工具是pack和unpack。ruby的packisinArray.您的长度应该是两个字节长,并且按网络字节顺序排列,这听起来像是n格式说明符的工作:n|Integer|16-bitunsigned,network(bi
如何在Ruby中按名称传递函数?(我使用Ruby才几个小时,所以我还在想办法。)nums=[1,2,3,4]#Thisworks,butismoreverbosethanI'dlikenums.eachdo|i|putsiend#InJS,Icouldjustdosomethinglike:#nums.forEach(console.log)#InF#,itwouldbesomethinglike:#List.iternums(printf"%A")#InRuby,IwishIcoulddosomethinglike:nums.eachputs在Ruby中能不能做到类似的简洁?我可以只
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.