仿微信 即时聊天工具 - SignalR (一)
程序员文章站
2022-06-16 21:03:48
话不多说,先上图 背景: 微信聊天,经常会遇见视频发不了,嗯,还有聊天不方便的问题,于是我就自己买了服务器,部署了一套可以直接在微信打开的网页进行聊天,这样只需要发送个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;
}
}
}
总体项目结构是这样的
下期我将把前端代码列出来,这个我只是为了实现功能,大神勿喷