話不多說,先上圖


背景:
微信聊天,經常會遇見視頻發不了,嗯,還有聊天不方便的問題,於是我就自己買了服務器,部署了一套可以直接在微信打開的網頁進行聊天,這樣只需要發送個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;
}
}
}
總體項目結構是這樣的

下期我將把前端代碼列出來,這個我只是為了實現功能,大神勿噴
