
傳統桌面程序不能完全被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();
}
}
}
}
好久沒寫博客了。就這樣吧,目的就是分享和總結。還有不說你也知道的,這文章怎么看怎么“軟”。希望大家體諒一下,技術把代碼變成錢本身就是困難的事情。適度廣告一下吧,項目和私活就是這樣找上門的。
