話不多說,先上圖 背景: 微信聊天,經常會遇見視頻發不了,嗯,還有聊天不方便的問題,於是我就自己買了伺服器,部署了一套可以直接在微信打開的網頁進行聊天,這樣只需要發送個url給朋友,就能聊天了! 由於自己無聊弄著玩的,代碼比較粗糙,各位多指正! 1、首先安裝SignalR,這步我就不做過多說明瞭 安 ...
話不多說,先上圖
背景:
微信聊天,經常會遇見視頻發不了,嗯,還有聊天不方便的問題,於是我就自己買了伺服器,部署了一套可以直接在微信打開的網頁進行聊天,這樣只需要發送個url給朋友,就能聊天了!
由於自己無聊弄著玩的,代碼比較粗糙,各位多指正!
1、首先安裝SignalR,這步我就不做過多說明瞭
安裝好以後在根目錄新建一個Hubs文件夾,做用戶的註冊和通知
MessageHub.cs 文件
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Web; namespace SignalR.Hubs { [HubName("MessageHub")] public class MessageHub : Hub { private readonly ChatTicker ticker; public MessageHub() { ticker = ChatTicker.Instance; } public void register(string username, string group = "default") { var list = (List<SiginalRModel>)HttpRuntime.Cache.Get("msg_hs"); if (list == null) { list = new List<SiginalRModel>(); } if (list.Any(x => x.connectionId == Context.ConnectionId)) { Clients.Client(Context.ConnectionId).broadcastMessage("已經註冊,無需再次註冊"); } else if (list.Any(x => x.name == username)) { var model = list.Where(x => x.name == username && x.group == group).FirstOrDefault(); if (model != null) { //註冊到全局 ticker.GlobalContext.Groups.Add(Context.ConnectionId, group); Clients.Client(model.connectionId).exit(); ticker.GlobalContext.Groups.Remove(model.connectionId, group); list.Remove(model); model.connectionId = Context.ConnectionId; list.Add(model); Clients.Group(group).removeUserList(model.connectionId); Thread.Sleep(200); var gourpList = list.Where(x => x.group == group).ToList(); Clients.Group(group).appendUserList(Context.ConnectionId, gourpList); HttpRuntime.Cache.Insert("msg_hs", list); // Clients.Client(model.connectionId).broadcastMessage("名稱重覆,只能註冊一個"); } //Clients.Client(Context.ConnectionId).broadcastMessage("名稱重覆,只能註冊一個"); } else { list.Add(new SiginalRModel() { name = username, group = group, connectionId = Context.ConnectionId }); //註冊到全局 ticker.GlobalContext.Groups.Add(Context.ConnectionId, group); Thread.Sleep(200); var gourpList = list.Where(x => x.group == group).ToList(); Clients.Group(group).appendUserList(Context.ConnectionId, gourpList); HttpRuntime.Cache.Insert("msg_hs", list); } } public void Say(string msg) { var list = (List<SiginalRModel>)HttpRuntime.Cache.Get("msg_hs"); if (list == null) { list = new List<SiginalRModel>(); } var userModel = list.Where(x => x.connectionId == Context.ConnectionId).FirstOrDefault(); if (userModel != null ) { Clients.Group(userModel.group).Say(userModel.name, msg); } } public void Exit() { OnDisconnected(true); } public override Task OnDisconnected(bool s) { var list = (List<SiginalRModel>)HttpRuntime.Cache.Get("msg_hs"); if (list == null) { list = new List<SiginalRModel>(); } var closeModel = list.Where(x => x.connectionId == Context.ConnectionId).FirstOrDefault(); if (closeModel != null) { list.Remove(closeModel); Clients.Group(closeModel.group).removeUserList(Context.ConnectionId); } HttpRuntime.Cache.Insert("msg_hs", list); return base.OnDisconnected(s); } } public class ChatTicker { #region 實現一個單例 private static readonly ChatTicker _instance = new ChatTicker(GlobalHost.ConnectionManager.GetHubContext<MessageHub>()); private readonly IHubContext m_context; private ChatTicker(IHubContext context) { m_context = context; //這裡不能直接調用Sender,因為Sender是一個不退出的“死迴圈”,否則這個構造函數將不會退出。 //其他的流程也將不會再執行下去了。所以要採用非同步的方式。 //Task.Run(() => Sender()); } public IHubContext GlobalContext { get { return m_context; } } public static ChatTicker Instance { get { return _instance; } } #endregion } public class SiginalRModel { public string connectionId { get; set; } public string group { get; set; } public string name { get; set; } } }
我把類和方法都寫到一塊了,大家最好是分開!
接下來是控制器
HomeController.cs
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Client; using SignalR.Hubs; using SignalR.ViewModels; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; using System.Web.Mvc; using Newtonsoft.Json; using System.Diagnostics; using System.Text.RegularExpressions; namespace SignalR.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } public ActionResult GetV(string v) { if (!string.IsNullOrEmpty(v)) { string url = RedisHelper.Get(v)?.ToString(); if (!string.IsNullOrEmpty(url)) { return Json(new { isOk = true, m = url }, JsonRequestBehavior.AllowGet); } return Json(new { isOk = false}, JsonRequestBehavior.AllowGet); } return Json(new { isOk = false }, JsonRequestBehavior.AllowGet); } public ActionResult getkey(string url) { if (!string.IsNullOrEmpty(url)) { var s = "v" + Util.GetRandomLetterAndNumberString(new Random(), 5).ToLower(); var dt = Convert.ToDateTime(DateTime.Now.AddDays(1).ToString("yyyy-MM-dd 04:00:00")); int min = Convert.ToInt16((dt - DateTime.Now).TotalMinutes); RedisHelper.Set(s, url, min); return Json(new { isOk = true, m = s }, JsonRequestBehavior.AllowGet); } return Json(new { isOk = false }, JsonRequestBehavior.AllowGet); } public ActionResult upfile() { try { if (Request.Files.Count > 0) { var file = Request.Files[0]; if (file != null) { var imgList = new List<string>() { ".gif", ".jpg", ".bmp", ".png" }; var videoList = new List<string>() { ".mp4" }; FileModel fmodel = new FileModel(); string name = Guid.NewGuid().ToString(); string fileExt = Path.GetExtension(file.FileName).ToLower();//上傳文件擴展名 string path = Server.MapPath("~/files/") + name + fileExt; file.SaveAs(path); string extension = new FileInfo(path).Extension; if (extension == ".mp4") { fmodel.t = 2; } else if (imgList.Contains(extension)) { fmodel.t = 1; } else { fmodel.t = 0; } string url = Guid.NewGuid().ToString(); fmodel.url = "http://" + Request.Url.Host; if (Request.Url.Port != 80) { fmodel.url += ":" + Request.Url.Port; } fmodel.url += "/files/" + name + fileExt; GetImageThumb(Server.MapPath("~") + "files\\" + name + fileExt, name); return Json(new { isOk = true, m = "file:" + JsonConvert.SerializeObject(fmodel) }, JsonRequestBehavior.AllowGet); } } } catch(Exception ex) { Log.Info(ex); } return Content(""); } public string GetImageThumb(string localVideo,string name) { string path = AppDomain.CurrentDomain.BaseDirectory; string ffmpegPath = path + "/ffmpeg.exe"; string oriVideoPath = localVideo; int frameIndex = 5; int _thubWidth; int _thubHeight; GetMovWidthAndHeight(localVideo, out _thubWidth, out _thubHeight); int thubWidth = 200; int thubHeight = _thubWidth == 0 ? 200 : (thubWidth * _thubHeight / _thubWidth ); string thubImagePath = path + "files\\" + name + ".jpg"; string command = string.Format("\"{0}\" -i \"{1}\" -ss {2} -vframes 1 -r 1 -ac 1 -ab 2 -s {3}*{4} -f image2 \"{5}\"", ffmpegPath, oriVideoPath, frameIndex, thubWidth, thubHeight, thubImagePath); Cmd.RunCmd(command); return name; } /// <summary> /// 獲取視頻的幀寬度和幀高度 /// </summary> /// <param name="videoFilePath">mov文件的路徑</param> /// <returns>null表示獲取寬度或高度失敗</returns> public static void GetMovWidthAndHeight(string videoFilePath, out int width, out int height) { try { //執行命令獲取該文件的一些信息 string ffmpegPath = AppDomain.CurrentDomain.BaseDirectory + "/ffmpeg.exe"; string output; string error; ExecuteCommand("\"" + ffmpegPath + "\"" + " -i " + "\"" + videoFilePath + "\"", out output, out error); if (string.IsNullOrEmpty(error)) { width = 0; height = 0; } //通過正則表達式獲取信息裡面的寬度信息 Regex regex = new Regex("(\\d{2,4})x(\\d{2,4})", RegexOptions.Compiled); Match m = regex.Match(error); if (m.Success) { width = int.Parse(m.Groups[1].Value); height = int.Parse(m.Groups[2].Value); } else { width = 0; height = 0; } } catch (Exception) { width = 0; height = 0; } } public static void ExecuteCommand(string command, out string output, out string error) { try { //創建一個進程 Process pc = new Process(); pc.StartInfo.FileName = command; pc.StartInfo.UseShellExecute = false; pc.StartInfo.RedirectStandardOutput = true; pc.StartInfo.RedirectStandardError = true; pc.StartInfo.CreateNoWindow = true; //啟動進程 pc.Start(); //準備讀出輸出流和錯誤流 string outputData = string.Empty; string errorData = string.Empty; pc.BeginOutputReadLine(); pc.BeginErrorReadLine(); pc.OutputDataReceived += (ss, ee) => { outputData += ee.Data; }; pc.ErrorDataReceived += (ss, ee) => { errorData += ee.Data; }; //等待退出 pc.WaitForExit(); //關閉進程 pc.Close(); //返迴流結果 output = outputData; error = errorData; } catch (Exception) { output = null; error = null; } } } public class Util { public static string GetRandomLetterAndNumberString(Random random, int length) { if (length < 0) { throw new ArgumentOutOfRangeException("length"); } char[] pattern = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' }; string result = ""; int n = pattern.Length; for (int i = 0; i < length; i++) { int rnd = random.Next(0, n); result += pattern[rnd]; } return result; } } class Cmd { private static string CmdPath = @"C:\Windows\System32\cmd.exe"; /// <summary> /// 執行cmd命令 返回cmd視窗顯示的信息 /// 多命令請使用批處理命令連接符: /// <![CDATA[ /// &:同時執行兩個命令 /// |:將上一個命令的輸出,作為下一個命令的輸入 /// &&:當&&前的命令成功時,才執行&&後的命令 /// ||:當||前的命令失敗時,才執行||後的命令]]> /// </summary> /// <param name="cmd">執行的命令</param> public static string RunCmd(string cmd) { cmd = cmd.Trim().TrimEnd('&') + "&exit";//說明:不管命令是否成功均執行exit命令,否則當調用ReadToEnd()方法時,會處於假死狀態 using (Process p = new Process()) { p.StartInfo.FileName = CmdPath; p.StartInfo.UseShellExecute = false; //是否使用操作系統shell啟動 p.StartInfo.RedirectStandardInput = true; //接受來自調用程式的輸入信息 p.StartInfo.RedirectStandardOutput = true; //由調用程式獲取輸出信息 p.StartInfo.RedirectStandardError = true; //重定向標準錯誤輸出 p.StartInfo.CreateNoWindow = true; //不顯示程式視窗 p.Start();//啟動程式 //向cmd視窗寫入命令 p.StandardInput.WriteLine(cmd); p.StandardInput.AutoFlush = true; //獲取cmd視窗的輸出信息 string output = p.StandardOutput.ReadToEnd(); p.WaitForExit();//等待程式執行完退出進程 p.Close(); return output; } } } }
我還是都寫到一塊了,大家記得分開!
SController.cs 這個是針對手機端單獨拎出來的,裡面不需要什麼內容
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SignalR.Controllers { public class SController : Controller { // GET: S public ActionResult Index() { return View(); } } }
根目錄新建一個ViewModels文件夾,裡面新建FileModel.cs文件
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace SignalR.ViewModels { public class FileModel { /// <summary> /// 1 : 圖片 2:視頻 /// </summary> public int t { get; set; } public string url { get; set; } } }
RedisHelper.cs
using Microsoft.AspNet.SignalR.Messaging; using StackExchange.Redis; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Runtime.Serialization.Formatters.Binary; using System.Threading.Tasks; using System.Web; namespace SignalR { public class RedisHelper { private static string Constr = "xxxx.cn:6379"; private static object _locker = new Object(); private static ConnectionMultiplexer _instance = null; /// <summary> /// 使用一個靜態屬性來返回已連接的實例,如下列中所示。這樣,一旦 ConnectionMultiplexer 斷開連接,便可以初始化新的連接實例。 /// </summary> public static ConnectionMultiplexer Instance { get { if (Constr.Length == 0) { throw new Exception("連接字元串未設置!"); } if (_instance == null) { lock (_locker) { if (_instance == null || !_instance.IsConnected) { _instance = ConnectionMultiplexer.Connect(Constr); } } } //註冊如下事件 _instance.ConnectionFailed += MuxerConnectionFailed; _instance.ConnectionRestored += MuxerConnectionRestored; _instance.ErrorMessage += MuxerErrorMessage; _instance.ConfigurationChanged += MuxerConfigurationChanged; _instance.HashSlotMoved += MuxerHashSlotMoved; _instance.InternalError += MuxerInternalError; return _instance; } } static RedisHelper() { } /// <summary> /// /// </summary> /// <returns></returns> public static IDatabase GetDatabase() { return Instance.GetDatabase(); } /// <summary> /// 這裡的 MergeKey 用來拼接 Key 的首碼,具體不同的業務模塊使用不同的首碼。 /// </summary> /// <param name="key"></param> /// <returns></returns> private static string MergeKey(string key) { return "SignalR:"+ key; //return BaseSystemInfo.SystemCode + key; } /// <summary> /// 根據key獲取緩存對象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public static T Get<T>(string key) { key = MergeKey(key); return Deserialize<T>(GetDatabase().StringGet(key)); } /// <summary> /// 根據key獲取緩存對象 /// </summary> /// <param name="key"></param> /// <returns></returns> public static object Get(string key) { key = MergeKey(key); return Deserialize<object>(GetDatabase().StringGet(key)); } /// <summary> /// 設置緩存 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="expireMinutes"></param> public static void Set(string key, object value, int expireMinutes = 0) { key = MergeKey(key); if (expireMinutes > 0) { GetDatabase().StringSet(key, Serialize(value), TimeSpan.FromMinutes(expireMinutes)); } else { GetDatabase().StringSet(key, Serialize(value)); } } /// <summary> /// 判斷在緩存中是否存在該key的緩存數據 /// </summary> /// <param name="key"></param> /// <returns></returns> public static bool Exists(string key) { key = MergeKey(key); return GetDatabase().KeyExists(key); //可直接調用 } /// <summary> /// 移除指定key的緩存 /// </summary> /// <param name="key"></param> /// <returns></returns> public static bool Remove(string key) { key = MergeKey(key); return GetDatabase().KeyDelete(key); } /// <summary> /// 非同步設置 /// </summary> /// <param name="key"></param> /// <param name="value"></param> public static async Task SetAsync(string key, object value) { key = MergeKey(key); await GetDatabase().StringSetAsync(key, Serialize(value)); } /// <summary> /// 根據key獲取緩存對象 /// </summary> /// <param name="key"></param> /// <returns></returns> public static async Task<object> GetAsync(string key) { key = MergeKey(key); object value = await GetDatabase().StringGetAsync(key); return value; } /// <summary> /// 實現遞增 /// </summary> /// <param name="key"></param> /// <returns></returns> public static long Increment(string key) { key = MergeKey(key); //三種命令模式 //Sync,同步模式會直接阻塞調用者,但是顯然不會阻塞其他線程。 //Async,非同步模式直接走的是Task模型。 //Fire - and - Forget,就是發送命令,然後完全不關心最終什麼時候完成命令操作。 //即發即棄:通過配置 CommandFlags 來實現即發即棄功能,在該實例中該方法會立即返回,如果是string則返回null 如果是int則返回0.這個操作將會繼續在後臺運行,一個典型的用法頁面計數器的實現: return GetDatabase().StringIncrement(key, flags: CommandFlags.FireAndForget); } /// <summary> /// 實現遞減 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <returns></returns> public static long Decrement(string key, string value) { key = MergeKey(key); return GetDatabase().HashDecrement(key, value, flags: CommandFlags.FireAndForget); } /// <summary> /// 序列化對象 /// </summary> /// <param name="o"></param> /// <returns></returns> private static byte[] Serialize(object o) { if (o == null) { return null; } BinaryFormatter binaryFormatter = new BinaryFormatter(); using (MemoryStream memoryStream = new MemoryStream()) { binaryFormatter.Serialize(memoryStream, o); byte[] objectDataAsStream = memoryStream.ToArray(); return objectDataAsStream; } } /// <summary> /// 反序列化對象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="stream"></param> /// <returns></returns> private static T Deserialize<T>(byte[] stream) { if (stream == null) { return default(T); } BinaryFormatter binaryFormatter = new BinaryFormatter(); using (MemoryStream memoryStream = new MemoryStream(stream)) { T result = (T)binaryFormatter.Deserialize(memoryStream); return result; } } /// <summary> /// 配置更改時 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void MuxerConfigurationChanged(object sender, EndPointEventArgs e) { //LogHelper.SafeLogMessage("Configuration changed: " + e.EndPoint); } /// <summary> /// 發生錯誤時 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void MuxerErrorMessage(object sender, RedisErrorEventArgs e) { //LogHelper.SafeLogMessage("ErrorMessage: " + e.Message); } /// <summary> /// 重新建立連接之前的錯誤 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void MuxerConnectionRestored(object sender, ConnectionFailedEventArgs e) { //LogHelper.SafeLogMessage("ConnectionRestored: " + e.EndPoint); } /// <summary> /// 連接失敗 , 如果重新連接成功你將不會收到這個通知 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void MuxerConnectionFailed(object sender, ConnectionFailedEventArgs e) { //LogHelper.SafeLogMessage("重新連接:Endpoint failed: " + e.EndPoint + ", " + e.FailureType +(e.Exception == null ? "" : (", " + e.Exception.Message))); } /// <summary> /// 更改集群 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void MuxerHashSlotMoved(object sender, HashSlotMovedEventArgs e) { //LogHelper.SafeLogMessage("HashSlotMoved:NewEndPoint" + e.NewEndPoint + ", OldEndPoint" + e.OldEndPoint); } /// <summary> /// redis類庫錯誤 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void MuxerInternalError(object sender, InternalErrorEventArgs e) { //LogHelper.SafeLogMessage("InternalError:Message" + e.Exception.Message); } //場景不一樣,選擇的模式便會不一樣,大家可以按照自己系統架構情況合理選擇長連接還是Lazy。 //建立連接後,通過調用ConnectionMultiplexer.GetDatabase 方法返回對 Redis Cache 資料庫的引用。從 GetDatabase 方法返回的對象是一個輕量級直通對象,不需要進行存儲。 /// <summary> /// 使用的是Lazy,在真正需要連接時創建連接。 /// 延遲載入技術 /// 微軟azure中的配置 連接模板 /// </summary> //private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() => //{ // //var options = ConfigurationOptions.Parse(constr); // ////options.ClientName = GetAppName(); // only known at runtime // //options.AllowAdmin = true; // //return ConnectionMultiplexer.Connect(options); // ConnectionMultiplexer muxer = ConnectionMultiplexer.Connect(Coonstr); // muxer.ConnectionFailed += MuxerConnectionFailed; // muxer.ConnectionRestored += MuxerConnectionRestored; // muxer.ErrorMessage += MuxerErrorMessage; // muxer.ConfigurationChanged += MuxerConfigurationChanged; // muxer.HashSlotMoved += MuxerHashSlotMoved; // muxer.InternalError += MuxerInternalError; // return muxer; //}); #region 當作消息代理中間件使用 一般使用更專業的消息隊列來處理這種業務場景 /// <summary> /// 當作消息代理中間件使用 /// 消息組建中,重要的概念便是生產者,消費者,消息中間件。 /// </summary> /// <param name="channel"></param> /// <param name="message"></param> /// <returns></returns> public static long Publish(string channel, string message) { StackExchange.Redis.ISubscriber sub = Instance.GetSubscriber(); //return sub.Publish("messages", "hello"); return sub.Publish(channel, message); } /// <summary> /// 在消費者端得到該消息並輸出 /// </summary> /// <param name="channelFrom"></param> /// <returns></returns> public static void Subscribe(string channelFrom) { StackExchange.Redis.ISubscriber sub = Instance.GetSubscriber(); sub.Subscribe(channelFrom, (channel, message) => { Console.WriteLine((string)message); }); } #endregion /// <summary> /// GetServer方法會接收一個EndPoint類或者一個唯一標識一臺伺服器的鍵值對 /// 有時候需要為單個伺服器指定特定的命令 /// 使用IServer可以使用所有的shell命令,比如: /// DateTime lastSave = server.LastSave(); /// ClientInfo[] clients = server.ClientList(); /// 如果報錯在連接字元串後加 ,allowAdmin=true; /// </summary> /// <returns></returns> public static IServer GetServer(string host, int port) { IServer server = Instance.GetServer(host, port); return server; } /// <summary> /// 獲取全部終結點 /// </summary> /// <returns></returns> public static EndPoint[] GetEndPoints() { EndPoint[] endpoints = Instance.GetEndPoints(); return endpoints; } } }
總體項目結構是這樣的
下期我將把前端代碼列出來,這個我只是為了實現功能,大神勿噴