传统桌面程序不能完全被web和移动端替代,但是需要改造。这里要说的是巧用webapi把以前用dll和com组件,ocx等方式做接口,做分布式开发的方式,改成restful 风格api的方式实现跨平台,多客户端(类型).并分享几则案例.
1、智能储物柜
项目背景:某智慧城市项目需要用到有智能锁的储物柜,用app扫码控制存取,并和智慧城市后台交互。智能锁系统是工业的塔式控制器,使用modbus ascii协议控制,端口使用串口。储物柜配备了工控电脑32寸竖屏,工控电脑控制塔式控制器(单片机),工控机上需要开发一套桌面程序,对外暴露储物柜的功能性(存取物品),对用户来说作为人机交互界面。话写的有点难懂还是上图吧:
规格有几种,这是不是实物忘记了。总之也没去过现场。
柜机人机界面
说明:
工作区是底部的1024*1080像素的区域,关键设计是把二维码的内容设计成了JSON,app扫描后获取到设备和意图,智慧城市后台对云主机上的中间件发起控制请求,中间件转发给柜机程序,柜机程序和塔式控制器通信,塔式控制器控制锁动作。
中间件程序界面
说明:中间使用winform+owin宿主webapi,对外暴露api,对柜机程序提供套接字连接。中间件是socket server端,柜机程序作为client。
还是晕了吧,没看懂么。简单来说柜机程序是个上位机程序,设备需要把控制锁的需求封装成api给外部调用。这里的解决方案是使用中间件,中间件对外暴露api外部发起控制请求,中间件对内(设备端程序)执行控制指令。
为了实现"网页和移动客户端控制工控设备"这个核心需求,这也是挤破了脑袋吧.呵呵呵,总算不枉费你进来围观了一回...
不留下点代码,算什么分享呢!哼!
好的,上代码:
这就是传说中的asp.net mvc webapi啊
winform宿主:
IPEndPoint ipeSender = new IPEndPoint(IPAddress.Any, 0); EndPoint epSender = (EndPoint)ipeSender; serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 首次探测时间5 秒, 间隔侦测时间2 秒 byte[] inValue = new byte[] { 1, 0, 0, 0, 0x88, 0x13, 0, 0, 0xd0, 0x07, 0, 0 }; serverSocket.IOControl(IOControlCode.KeepAliveValues, inValue, null); IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse(SocketBindingIP), int.Parse(config.AppSettings.Settings["Middleware_PORT"].Value)); try { serverSocket.Bind(ipEndPoint); serverSocket.Listen(1024); backgroundWorker.WorkerSupportsCancellation = true; backgroundWorker.RunWorkerAsync(); LogMessage(DateTime.Now + "->Socket启动成功,监听IP:" + ipEndPoint.Address.ToString() + ":" + config.AppSettings.Settings["Middleware_PORT"].Value); } catch (Exception ex) { Com.DataCool.DotNetExpand.LogHelper.Error("服务启动失败,原因:" + ex.Message); } btnServiceControl.Tag = 1; btnServiceControl.Text = "停止监听"; btnServiceControl.BackColor = Color.Green; pbxServiceStatus.BackgroundImage = Properties.Resources.online_status; lbWebApiBaseAddress.Text = SocketBindingIP; hostObject = WebApp.Start<RegisterRoutesStartup>("http://" + SocketBindingIP + ":5990");
public class RegisterRoutesStartup { public void Configuration(IAppBuilder appBuilder) { HttpConfiguration config = new HttpConfiguration(); //自定义路由 config.Routes.MapHttpRoute( name: "CustomApi", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional } ); //只响应Json请求 var jsonFormatter = new JsonMediaTypeFormatter(); config.Services.Replace(typeof(IContentNegotiator), new JsonContentNegotiator(jsonFormatter)); appBuilder.UseWebApi(config); } }
就是最后一句了。owin怎么宿主webapi去看看张善友等的文章吧。
public ApiActionResult BufferBox_API_Request(string StationNo, string CellNo, string Action) { var result = new ApiActionResult() { Success = false, Result = null, Message = "操作失败。" }; byte[] results = new byte[1024]; Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { clientSocket.Connect(new IPEndPoint(IPAddress.Parse(conf.AppSettings.Settings["Middleware_IP"].Value), Convert.ToInt32(conf.AppSettings.Settings["Middleware_PORT"].Value))); using (var db = new BufferBoxDBEntities()) { var stationEntity = db.station_signin_session.Where(st => st.SessionDict == StationNo).FirstOrDefault(); if (stationEntity == null) { result.Message = "设备不存在或者设备编号有误!"; result.Result = ""; return result; } var requestEntity = new API_Request_session { API_Request_IP = Request.GetClientIpAddress(), RequestID = Guid.NewGuid(), RequestData = CellNo + "|" + Action, RequestDataTime = DateTime.Now, ResultData = "", ExecuteFlag = false, StationNo = StationNo }; db.API_Request_session.AddObject(requestEntity); db.SaveChanges(); clientSocket.Send(Encoding.UTF8.GetBytes("api_request:" + JsonConvert.SerializeObject(requestEntity))); result.Success = true; result.Message = "设备已经受理请求。"; result.Result = requestEntity.RequestID.ToString(); } } catch (Exception ex) { result.Message = "中间件发生异常:" + ex.Message; } return result; }
这可是项目分析的关键之处啊。中间件是如何转发api请求并通知柜机客户端执行指令的呢。就是webapi里使用socket作为client去连接中间件的socket server的。
问题就是出在这里!webapi不能阻塞socket 直到柜机客户端响应之后回复了再返回给外部。
2、php页面js开POS触摸屏电脑外接的钱箱
这是昨天晚上接的一个小活。新年第一单,正是有了前面项目的经验,给提供了这个解决方案。
项目背景: php做的bs项目打包成桌面项目用内嵌浏览器访问php页面来代替POS触摸屏桌面程序。打印使用插件听说解决了,但是打开钱箱遇到麻烦了。由于发包方不知道网页如何控制本地设备,也不想用activex方式,所以提供了这个解决方案:
POS触摸屏上运行一windows服务程序对外提供api(控制钱箱)和php服务器端的中间件通信,中间件对外部暴露api。
这个项目图片不高大上,所以只有代码了:
using System; using System.Net; using System.Web.Http; using System.Net.Sockets; using System.Configuration; using System.Text; namespace MiddlewareServer { /// <summary> /// POS触摸屏收银机钱箱控制API控制器 /// </summary> public class MoneyBoxApiController : ApiController { public static readonly Configuration conf = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); [HttpGet] /// <summary> /// 打开POS钱箱,IP取发起请求的客户端的IP,中间件以此IP为依据通知该POS机执行开钱箱动作 /// 局域网环境IP最好是静态IP,不要使用DHIP,动态获取 /// </summary> /// <returns>{Success,Result=请求发起机器的IP地址,Message}</returns> public ApiActionResult OpenMoneyBox() { var result = new ApiActionResult() { Success = false, Result = null, Message = "操作失败。" }; byte[] results = new byte[1024]; Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { clientSocket.Connect(new IPEndPoint(IPAddress.Parse(conf.AppSettings.Settings["Middleware_IP"].Value), Convert.ToInt32(conf.AppSettings.Settings["Middleware_PORT"].Value))); string ip = Request.GetClientIpAddress(); clientSocket.Send(Encoding.UTF8.GetBytes("api_request:" + ip)); result.Result = ip; result.Success = true; result.Message = "请求成功。"; } catch (Exception ex) { result.Message = "中间件发生异常:" + ex.Message; } return result; } } }
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.ServiceProcess; using System.Text; using System.Runtime.InteropServices; using System.Configuration; using Microsoft.Win32.SafeHandles; using System.IO; using System.Net.Sockets; using System.Net; namespace MoneyBoxSvr { public partial class MoneyBoxService : ServiceBase { [DllImport("kernel32.dll", CharSet = CharSet.Auto)] private static extern IntPtr CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode, int lpSecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, int hTemplateFile); private Configuration config; /// <summary> /// 打印机端口名称 /// </summary> public string PrintPortName { get { return config.AppSettings.Settings["PortName"].Value; } } /// <summary> /// 中间件的IP地址 /// </summary> public string RemoteServerIP { get { return config.AppSettings.Settings["MiddlewareIP"].Value; } } /// <summary> /// 中间件监听的端口 /// </summary> public int MiddlewarePort { get { return Convert.ToInt32(config.AppSettings.Settings["MiddlewarePort"].Value); } } protected Socket clientSocket = null; /// <summary> /// 缓冲区 /// </summary> protected byte[] buffers = new byte[1024]; protected System.Threading.Thread socketThread; public MoneyBoxService() { InitializeComponent(); config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); } protected override void OnStart(string[] args) { StartSocketThread(); } protected override void OnStop() { base.OnStop(); } protected override void OnShutdown() { base.OnShutdown(); socketThread.Abort(); socketThread = null; } private void StartSocketThread() { socketThread = new System.Threading.Thread(ThreadWork); socketThread.Start(); } /// <summary> /// 异步接收到远程请求 /// </summary> /// <param name="ar"></param> private void OnReceive(IAsyncResult ar) { try { IPEndPoint ipeSender = new IPEndPoint(IPAddress.Any, 0); EndPoint epSender = (EndPoint)ipeSender; //结束挂起的,从特定终结点进行异步读取 if (clientSocket != null) { int len = clientSocket.EndReceiveFrom(ar, ref epSender); string requestCommand = System.Text.Encoding.UTF8.GetString(buffers); if (requestCommand.StartsWith("api_request")) { OpenMoneyBox(); } } } catch { } finally { try { IPEndPoint ipeSender = new IPEndPoint(IPAddress.Any, 0); EndPoint epSender = (EndPoint)ipeSender; buffers = new byte[1024]; clientSocket.BeginReceiveFrom(buffers, 0, buffers.Length, SocketFlags.None, ref epSender, new AsyncCallback(OnReceive), epSender); } catch { } } } private void ThreadWork() { while (true) { if (clientSocket == null) { #region 建立socket连接 IPAddress ip = IPAddress.Parse(RemoteServerIP); clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { // 首次探测时间5 秒, 间隔侦测时间2 秒 byte[] inValue = new byte[] { 1, 0, 0, 0, 0x88, 0x13, 0, 0, 0xd0, 0x07, 0, 0 }; clientSocket.IOControl(IOControlCode.KeepAliveValues, inValue, null); clientSocket.Connect(new IPEndPoint(IPAddress.Parse(RemoteServerIP), MiddlewarePort)); //配置服务器IP与端口 #region 签到 string request = "pos_sign_in:"; clientSocket.Send(Encoding.UTF8.GetBytes(request)); IPEndPoint ipeSender = new IPEndPoint(IPAddress.Any, 0); EndPoint epSender = (EndPoint)ipeSender; buffers = new byte[1024]; clientSocket.BeginReceiveFrom(buffers, 0, buffers.Length, SocketFlags.None, ref epSender, new AsyncCallback(OnReceive), epSender); #endregion } catch { if (clientSocket != null) { clientSocket.Close(); clientSocket = null; } } #endregion } if (clientSocket != null ) { #region 发0字节的包探测连接是否可用 bool blockingState = clientSocket.Blocking; try { byte[] tmp = new byte[1]; clientSocket.Blocking = false; clientSocket.Send(tmp, 0, 0); } catch { if (clientSocket != null) { clientSocket.Close(); clientSocket = null; } } finally { if (clientSocket != null) { clientSocket.Blocking = blockingState; } } #endregion } System.Threading.Thread.Sleep(5000); } } /// <summary> /// 开钱箱 /// </summary> public void OpenMoneyBox() { IntPtr iHandle = CreateFile(PrintPortName, 0x40000000, 0, 0, 3, 0, 0); if (iHandle.ToInt32() != -1) { SafeFileHandle handle = new SafeFileHandle(iHandle, true); FileStream fs = new FileStream(handle, FileAccess.ReadWrite); StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.Default); sw.Write(((char)27).ToString() + "p" + ((char)0).ToString() + ((char)60).ToString() + ((char)255).ToString()); sw.Close(); fs.Close(); } } } }
好久没写博客了。就这样吧,目的就是分享和总结。还有不说你也知道的,这文章怎么看怎么“软”。希望大家体谅一下,技术把代码变成钱本身就是困难的事情。适度广告一下吧,项目和私活就是这样找上门的。