前言:最近接到一個需求,要實現客戶端對程序進行集中管理,相似於軟件管家。 一開始心想,他媽的這麼複雜。但咱們初步只是作簡化版,因此沒什麼技術難度。 但做爲一個常年作web開發的工程師來講,要作好一個客戶端,也不是一件容易的事情。恰好如今作的差很少了,總結一下這個過程。前端
1.準備工做,找到幾個開源源代碼,研究其代碼的核心實現方法。web
咱們找了三個源代碼,有兩個較爲複雜,沒有太深刻去研究,找了一個稍微簡單點的加以研究分析。 數據庫
同時也經過百度,查了一些資料,最爲基礎的是:如何下載文件。 json
有兩種方式,一種經過http數據流傳輸,另外一種是利用webclient的下載文件的方法。 windows
研究代碼發現,它用的是webclient下載文件的方式,這種方式顯而易見的好處是很是便於作進度條展現。api
webclient 示例僞代碼以下服務器
private WebClient client = null;網絡
client = new WebClient();
client.Credentials = CredentialCache.DefaultCredentials;
client.Headers.Add("Content-Type:application/octet-stream");
client.Encoding = Encoding.UTF8;多線程
client.DownloadProgressChanged += client_onDownloadChanged;
client.DownloadFileCompleted += client_onDownloadComplete;app
String uri = "http://xxx/test.rar";
String localfile = 本地文件路徑
String targetName = token對象,傳遞給完成下載時的回調。這裏我用的文件名(不帶路徑)
client.DownloadFileAsync(uri, localfile, targetName);
2.需求分析
核心的需求是這樣,指定客戶存放程序的路徑,若是該路徑下沒有相應的程序,則從服務器上下載,這裏咱們把這個過程叫作安裝,若是存在文件了,則比較版本信息,若是須要升級,則自動升到最新版本,不然直接啓動程序。
額外的需求有:1.版本控制,不一樣版本對應不一樣的程序,並且須要作版本發佈。
2.日誌記錄, 記錄程序運行期間出現的錯誤信息
3.軟件排行,軟件使用頻率的排行
4.其餘需求(程序安裝部署等)
3.具體實現
1.設計數據庫,針對每個環節設計相應的數據以及關聯。
2.客戶端實現(使用C# winform,因爲客戶機大多數是windows xp系統,爲了保證系統兼容性,沒有使用wpf技術)
客戶端分爲上、左、右三個部分,上爲標題欄,能夠控制窗體移動,左側爲不一樣的軟件類型,右側爲應用列表,用圖標加文字的方式展現。軟件類型、應用圖標,各自有通用的處理,因此將這兩個封裝成了用戶控件,主窗體只要遍歷輸出空間就能夠了。
數據,是經過http接口方式提供,返回的是json格式的數據。
在下載數據的時候,剛開始爲了快速實現,直接使用的同步方法,致使有時候該彈出對話框的地方,等待了好久纔出來。 後來,改用多線程,在線程中進行下載操做,同時傳入對象,更新UI,這樣就不會出現看上去卡死的狀況。
3.後臺功能實現
後臺的界面採用easyui,爲了快速開發,後臺是直接寫代碼,沒有采用什麼框架技術,用的ADO.NET訪問數據庫。包括接口的實現和各個管理功能的實現。功能模塊有:應用分類,應用管理,應用版本,文件上傳(針對指定版本的應用,上傳對應的文件,但此功能因爲實現較爲複雜,不夠穩定,暫時不用此功能),軟件排行,查看日誌。
4.功能調整小插曲
原本但願能經過後臺上傳文件,以達到上傳完成後,發佈版本,客戶端就能夠從上傳文件的目標路徑,下載到客戶端指定的程序存放路徑。 但通過屢次測試,發現上傳的文件不穩定,並且flash上傳控件能夠多選文件,但不能選文件夾,前端若是須要選文件夾,極可能存在不兼容的問題。所以,改變技術方案,後臺只發布版本,程序手動傳到服務器上,讓客戶端仍然從指定版本下載文件。這樣雖說,有了人工介入,但可以保證程序是正確可用的,投入生產環境時,出現問題的機率下降了。
因爲程序由不一樣的方式產生,因此在服務器配置上須要增長不少擴展配置,大多數mime配置爲application/octet-stream 。對於沒有後綴的文件,應該這樣配置: mime . application/octet-stream
5. 安裝部署
第一次使用installsheild,對其研究也花了些時間,第一步是配置一些安裝程序的基本信息,第二步是安裝以前所須要具有的環境,第三步是選文件,第四步是設置菜單欄和卸載程序,第五步是註冊表設置,第六步是安裝過程的一些提示信息的配置。
在項目下會有1-6共6個目錄,每一個目錄裏面有幾個項,每一項都是一些配置。特別注意Redistributables 這裏能夠配置安裝哪些基礎程序或依賴程序。勾選選項會下載相應程序安裝包,而且在生成時會帶上相應的安裝包。
6.一些winform開發的代碼細節
1.控件.SuspendLayout() 減小觸發layout事件,提升性能。
2.圖片設置居中的辦法(示例代碼):
this.pictureBox1.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
this.pictureBox1.Location = new System.Drawing.Point(10, 0);
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(60, 60);
this.pictureBox1.TabIndex = 0;
this.pictureBox1.TabStop = false;
this.pictureBox1.Click += new System.EventHandler(this.pictureBox1_Click);
3. 退出程序(關閉進程)
this.Close();
Environment.Exit(0);
4.窗口最大化最小化
最大化 : this.WindowState = FormWindowState.Maximized;
最小化 : this.WindowState = FormWindowState.Minimized;
5.開啓新線程,定時檢測網絡狀態代碼(兩種方法都在裏面,只是在使用的時候,只用了ping ip的方式,沒有調用 InternetGetConnectedState方法,這是由於我這裏須要測試內網網絡環境是否正常,而調用InternetGetConnectedState只能判斷是否連上外網)
//導入dll
[DllImport("wininet.dll", EntryPoint = "InternetGetConnectedState")]
//判斷網絡情況的方法,返回值true爲鏈接,false爲未鏈接
public extern static bool InternetGetConnectedState(out int conState, int reder);
public constructor(){
Thread netThread = new Thread(ListenNetWork);
netThread.Start();
}
private void ListenNetWork() {
//定時檢測網絡狀態
System.Timers.Timer timer = new System.Timers.Timer();
//時間頻率
timer.Interval = 1000;
//設置爲一直執行 false表示只執行一次
timer.AutoReset = true;
//開啓System.Timers.Timer.Elapsed事件
timer.Enabled = true;
//執行檢測網絡方法
timer.Elapsed += new System.Timers.ElapsedEventHandler(CheckNetState);
}
private void CheckNetState(object source, System.Timers.ElapsedEventArgs e)
{
int result = 0;
String message = "";
//調用api檢測網絡狀態
// bool flag = InternetGetConnectedState(out result, 0);
Ping ping = new Ping();
IPAddress address = IPAddress.Parse(ServerIp); //IPAddress.Loopback;
PingReply reply= ping.Send(address);
if (reply.Status==IPStatus.Success)
{
message = "網絡正常";
NetState.CurrentState = 1;
}
else
{
message = "網絡鏈接失敗";
NetState.CurrentState = -1;
}
this.status_label.Text = message;
}
6.窗體移動代碼
#region 鼠標選中移動窗體
[DllImport("user32.dll")]
public static extern bool ReleaseCapture();
[DllImport("user32.dll")]
public static extern bool SendMessage(IntPtr hwnd, int wMsg, int wParam, int lParam);
public const int WM_SYSCOMMAND = 0x0112;
public const int SC_MOVE = 0xF010;
public const int HTCAPTION = 0x0002;
Point mouseOff;//鼠標移動位置變量
bool leftFlag;//標籤是否爲左鍵
private void FrmMain_MouseDown(object sender, MouseEventArgs e)
{
ReleaseCapture();
SendMessage(this.Handle, WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
}
private void FrmMain_MouseMove(object sender, MouseEventArgs e)
{
if (leftFlag)
{
Point mouseSet = System.Windows.Forms.Control.MousePosition;
mouseSet.Offset(mouseOff.X, mouseOff.Y); //設置移動後的位置
Location = mouseSet;
}
}
private void FrmMain_MouseUp(object sender, MouseEventArgs e)
{
if (leftFlag)
{
leftFlag = false;//釋放鼠標後標註爲false;
}
}
#endregion
7.點擊菜單,切換菜單項代碼
System.Windows.Forms.Control.ControlCollection collection = this.panel3.Controls;
//重置樣式
for (int i = 0; i < collection.Count; i++)
{
Biz.Controls.MenuItem m_item = (Biz.Controls.MenuItem)collection[i];
m_item.BackColor = System.Drawing.SystemColors.InactiveCaption;
}
//選中當前項
Biz.Controls.MenuItem menuitem = (Biz.Controls.MenuItem)sender;
curMenuId = menuitem.Menuid;
menuitem.BackColor = System.Drawing.SystemColors.ActiveCaption;
8.給控件畫虛邊線
Pen pen = new Pen(SystemColors.ControlDark);
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
e.Graphics.DrawLine(pen, panel.Width - 1, panel.Height - 1, panel.Width - 1, 0);
9.多線程更新UI控件方法
private delegate void UpdateLabelCallback(String info);
private void FrmMain_Load(object sender, EventArgs e)
{
Thread subThread= new Thread(new ThreadStart(doSomeThing));
subThread.Start();
}
private void UpdateLabel(String info)
{
//this.lab_fileinfo.Text = info ;
if (this.lab_fileinfo.InvokeRequired)//若是調用控件的線程和建立建立控件的線程不是同一個則爲True
{
while (!this.lab_fileinfo.IsHandleCreated)
{
if (this.lab_fileinfo.Disposing || this.lab_fileinfo.IsDisposed)
{
return;
}
}
UpdateLabelCallback callback = new UpdateLabelCallback(UpdateLabel);
this.lab_fileinfo.Invoke(callback, new object[] { info });
}
else {
this.lab_fileinfo.Text = info;
}
}
private void doSomeThing(){
//省略邏輯代碼
UpdateLabel("doSomeThing");
}
10. 進程啓動程序方法
1.調用Process.Start方法
String exeFile = "D:\\QQ.exe";
System.Diagnostics.Process.Start(exeFile);
2.有些綠色版免安裝程序啓動exe文件時,必須制定啓動程序的工做路徑,不能直接用上述方法啓動,正確啓動方法以下:
String exeFile = "D:\\test\\QQ.exe";
String workDir = "D:\\test";
ProcessStartInfo process = new ProcessStartInfo();
process.FileName = exeFile;
process.UseShellExecute = false;
//關鍵在這裏,至關因而切換到該目錄下,打開exe 文件
process.WorkingDirectory = workDir;
process.CreateNoWindow = true;
Process.Start(process);
11.獲取機器名稱和mac地址
public class SysUtil { public static String machineName { get { return Dns.GetHostName(); } } public static String machineIp { get { IPAddress addr; addr = new IPAddress(Dns.GetHostByName(Dns.GetHostName()).AddressList[0].Address); return addr.ToString(); } } }
12.寫入日誌文件
public static void Write(string msg, bool isAppend) { try { string filename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "run.log"); if (!Directory.Exists(Path.GetDirectoryName(filename))) { Directory.CreateDirectory(Path.GetDirectoryName(filename)); } using (FileStream stream = new FileStream(filename, isAppend ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.None)) { StreamWriter writer = new StreamWriter(stream); writer.WriteLine(msg); writer.Close(); stream.Close(); } } catch { } }
13.http請求封裝
public String GetResponse(String uri) { HttpWebRequest request =(HttpWebRequest) HttpWebRequest.Create(uri); request.Method = "GET"; HttpWebResponse response=(HttpWebResponse)request.GetResponse(); Stream respStream = response.GetResponseStream(); StreamReader reader = new StreamReader(respStream); String info = reader.ReadToEnd(); return info; }
14.執行dos命令的封裝
/// <summary> /// DOS命令運行函數 /// </summary> /// <param name="commandText"></param> public void ExeCommand(string commandText) { Process p = new Process(); p.StartInfo.FileName = "cmd.exe"; p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardInput = true; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.RedirectStandardError = true; p.StartInfo.CreateNoWindow = true; try { p.Start(); p.StandardInput.WriteLine(commandText); p.StandardInput.WriteLine("exit"); } catch { } }
如下是部分截圖
本博客文章大可能是經驗積累總結,以避免從此忘卻,記錄下來。同時感謝您的閱讀,也但願能對您有所幫助。