http与websocket(基于SignalR)两种协议下的跨域基于ASP.NET MVC--竹子整理

时间:2022-07-17 19:44:35

这段时间,项目涉及到移动端,这就不可避免的涉及到了跨域的问题。这是本人第一次接触跨域,有些地方的配置是有点麻烦,导致一开始的不顺。

至于websocket具体是什么意义,用途如何:请百度。

简单说就是建立一个基于互联网的实时通信。

在这里整理下这些内容,方便日后回顾。

一:介绍了WebSocket下的基于SignalR的跨域与不跨域例子

二:简单介绍了Http下的跨域问题

Ⅰ.WebSocket下的跨域

如果使用原生的方法来开发WebSocket应用,还是比较复杂的,不过好在Asp.net给我们提供了一个框架:

SignalR:

  微软支持的运行在.NET平台上集客户端与服务器于一体的库。简单来说就是给我们提供了服务端的类库加上前端的JS库。

首先抛开跨域的问题,先写一个SignalR不跨域的例子:

1、NuGet来获得我们所需要的程序集:

SignalR:我们的框架

http与websocket(基于SignalR)两种协议下的跨域基于ASP.NET MVC--竹子整理

2、新建个继承于Hub的类

 1 /// <summary>
2 /// 对应不跨域的方法
3 /// </summary>
4 public class ChatHub : Hub
5 {
6 public void Send(string message)
7 {
8 //接收数据,再传输到指定或全部的客户端动态方法
9 Clients.All.addNewMessageToPage(message);
10 }
11 }

3、新建个Startup类:

只需添加一句话:app.MapSignalR();

具体的完整代码在下面的Startup里都有。

http与websocket(基于SignalR)两种协议下的跨域基于ASP.NET MVC--竹子整理

4、这样我们的服务端代码就完成了,接下来就是客户端代码:

 1 $(function() {
2
3 var chat = $.connection.chat;
5 $.connection.hub.start();
9 chat.addMessage = function(message){
11 alert(message);
13 }
14
15 $("#btn").click(function(){
16
17   chat.send("给服务端发送信息");
18
19 });
20
21 });

5、好了,现在来分析下客户端代码:

前面两行就是基本的猜也能猜到的获取实例,然后开启这个连接。(相当于启动线程)。第三行定义了一个函数,这个函数是与服务端的ChatHub类里的动态方法addNewMessageToPage对应的回调函数。服务端一执行动态方法,对应的客户端(这个由服务端发送时的选择有关,这里选择就是All,也就是所有的客户端)就执行对应的回调函数。

现在知道了动态方法跟回调函数是一一对应的,服务端触发动态方法,客户端执行回调函数。那么动态方法什么时候执行呢,显而易见,是客户端发送请求时,也就是这里“调用”服务端定义的Send方法时,触发了动态方法。(有点啰嗦了,为了保证人人看懂)。这样一来,最后这句 chat.send("给服务端发送信息");就很容易理解了。就是请求。

二、跨域(一)

先来从NuGet获取我们需要跨域所需的程序集:

http与websocket(基于SignalR)两种协议下的跨域基于ASP.NET MVC--竹子整理

有了前面的基础,对于SignalR也应该有了个基础的印象了,下面就来讲讲如何用SignalR实现跨域通信。

SignalR的实现机制与.NET WCF 或Remoting是相似的,都是使用远程代理来实现,在具体使用上,有两种不同目的的接口:PersistentConnection和Hub,其中PersistentConnection实现了长时间的轮询,Hub用来解决实时信息的交换问题。

先来了解一下基于PersistentConnection接口的跨域。

我们的跨域一:

1、注释掉我们Startup类中的唯一一行代码:app.MapSignalR();

然后换上我们跨域的代码:

1 //这个参数"/echo",是我们自己定义的一个路由,与客户端创建SignalR的实例时对应。
2 app.Map("/echo",
3 map =>
4 {
5 map.UseCors(CorsOptions.AllowAll);
6 map.RunSignalR<EchoConnection>();
7 }
8 );

2、新建一个类,继承于我们的接口PersistentConnection

通信的本质就是建立一个客户端与服务端的一个连接,SignalR就是我们帮助我们建立这个连接的“中间件”

这个类中就实现了接口中定义的一系列方法,如连接建立,断开连接,收到信息。这样一来,我们的chatHub类实际上就已经不起作用了,通信的方法都可以写在这个EchoConnection类中。

 1 public class EchoConnection:PersistentConnection
2 {
3 /// <summary>
4 /// 当前连接数
5 /// </summary>
6 private static int _connections = 0;
7 /// <summary>
8 /// 连接建立时执行
9 /// </summary>
10 /// <param name="request"></param>
11 /// <param name="connectionId"></param>
12 /// <returns></returns>
13 protected override async Task OnConnected(IRequest request, string connectionId)
14 {
15 Interlocked.Increment(ref _connections);
16 await Connection.Send(connectionId, "Hi, " + connectionId + "!");
17 await Connection.Broadcast("新连接 " + connectionId + "开启. 当前连接数: " + _connections);
18
19 }
20 /// <summary>
21 /// 连接关闭时执行
22 /// </summary>
23 /// <param name="request"></param>
24 /// <param name="connectionId"></param>
25 /// <returns></returns>
26 protected Task OnDisconnected(IRequest request, string connectionId)
27 {
28 Interlocked.Decrement(ref _connections);
29 return Connection.Broadcast(connectionId + " 连接关闭. 当前连接数: " + _connections);
30 }
31 /// <summary>
32 /// 服务器接收到前台发送的消息时执行 发送请求 connection.send("信息");
33 /// </summary>
34 /// <param name="request"></param>
35 /// <param name="connectionId"></param>
36 /// <param name="data"></param>
37 /// <returns></returns>
38 protected override Task OnReceived(IRequest request, string connectionId, string data)
39 {
40 var message = connectionId + ">> " + data;
41 return Connection.Broadcast(message);
42 }
43
44 }

3、接下来就是客户端代码:

同样,第一步,获取实例,建立连接

var connection = $.connection("http://localhost:23013/echo");//echo就是我们之前在Startup类中定义的路由。

connection.start();

建立连接后,就会触发服务端的OnConnected事件,然后这个事件中又有触发两个方法:Broadcast,Send。这个就相当于我们不跨域时自己定义的动态方法,so,我们就应该想到客户端应有对应的回调函数:Broadcast跟Send。但是!由于这不是我们自定义的,所以,名字就不是我们想的这样啦~~对于接收信息,我们直接调用connection.received(function (data){})来接收,对于发送信息到服务端也不需要我们手动来先在服务端定义方法如不跨域时在ChatHub中定义的send方法,直接connection(实例名).send()就可以发消息到服务端。而在服务端,是由OnReceived来处理。具体的代码同样在本文的最后。

跨域(一)小结:

  

 1  /// <summary>
2 /// 对应跨域的第一种方法
3 /// 1、前台直接用connection(实例).send()就可以发消息到服务端,用connection.received(function (data) {
4 /// $("body").append(data + "<br />");
5 /// });就能接收服务端放过来的消息
6 /// 2、后台Connection.Send(connectionId, "消息");可发送消息给指定的客户端
7 /// 用Connection.Broadcast("消息");可发送给所有的客户端
8 /// 用protected override Task OnReceived(IRequest request, string connectionId, string data)
9 /// {
10 /// var message = connectionId + ">> " + data;
11 /// return Connection.Broadcast(message);
12 /// }接收客户端发送来的消息,进行处理
13 /// </summary>

三、跨域(二)

相比PersistentConnection这种轮询机制,基于Hub利用js动态载入执行方法才是我们所说的websocket技术。

Let’s start

1、有的朋友应该知道我们的第一步都是为前端可以创建实例准备的了

再一次修改我们的Startup类,之前的注释掉:

 1 app.Map("/signalr", map =>
2 {
3 map.UseCors(CorsOptions.AllowAll);
4 var hubConfiguration = new HubConfiguration
5 {
6 EnableJSONP = true//跨域的关键语句
7 };
8 map.RunSignalR(hubConfiguration);
9 });
10 app.MapSignalR();

上面的/signalR即我们这次设置的路由。一样,供客户端匹配

1、重新建立我们的MyHub类,跟最开始不跨域的步骤一致,新建类,继承于Hub。然后实现接口,里面也有连接、断开连接、重连事件。以及我们自定义的方法也都写在这里:

 1 /// <summary>
2 /// 对应跨域的第二种方法
3 /// public IGroupManager Groups { get; set; } hub类中还有个组属性,尚待开发,待看文档,再写demo
4 /// </summary>
5 public class MyHub : Hub
6 {
7 /// <summary>
8 /// 连接前执行
9 /// </summary>
10 /// <returns></returns>
11 public override System.Threading.Tasks.Task OnConnected()
12 {
13 //Clients.All.sayHello("连接成功");
14 Clients.Caller.sayHello("连接成功");//当前请求的用户
15 return base.OnConnected();
16 }
17
18 /// <summary>
19 /// 断开链接时执行
20 /// </summary>
21 /// <param name="stopCalled"></param>
22 /// <returns></returns>
23 public override System.Threading.Tasks.Task OnDisconnected(bool stopCalled)
24 {
25 return base.OnDisconnected(stopCalled);
26 }
27
28 /// <summary>
29 /// 重新建立连接 如服务器重启的时候,或者前台获取超时仍在等待,重连上时
30 /// </summary>
31 /// <returns></returns>
32 public override System.Threading.Tasks.Task OnReconnected()
33 {
34 return base.OnReconnected();
35 }
36
37 public void Hello(string name)//方法名称首字母大小写都与前台匹配上 前台首字母必须小写
38 {
39 //动态方法,与前台的回调函数名称一致
40 Clients.All.sayHello2("第二次");
41 Clients.All.sayHello3(Context.ConnectionId);//第三个回调函数,返回链接的ConnectionId
42 }
43 }

3、我们的客户端

第一步:获取实例,建立连接

var chat = $.connection.myHub;  //获取服务端实例  首字母小写,不是跟服务端一致的MyHub

chat.connection.url = "http://localhost:23013/signalr";测试项目的地址

$.connection.hub.start();

第二步、接下来就是跟服务端打个招呼

Chat.server.hello(“hello”);//对应的方法中有两个动态方法,sayHello2,sayHello3

第三步、接下来就是我们的回调函数,对应的回调函数就可以有两个,与上面对应。

   chat.client.sayHello2 = function(msg) {

                   alert(msg);

    };

chat.client.sayHello3 = function(msg) {

                   alert(msg);

    };

WebSocket小结:基于Hub接口的跨域方法的可扩展性还是很强的,这里有几个本人总结的注意点(针对Hub跨域):

1、回调函数 函数名要匹配,参数可以不匹配,后台传过来一个就执行一个,即若后台同时触发了两个同名的sayHello 依次执行

2、一定要有回调函数,不然不算连接成功,可以不调用服务器端的方法。若只是调用服务器端方法,没写回调函数,依然不算连接成功

3、在连接成功的情况下,后台先执行OnConnected事件,再执行前台调用的某个方法

4、用回调函数来判断是否真的连接成功,$.connection.hub.start().done里直接输出连接成功,是假成功。

5、所以同样,断开连接是否成功也应用回调函数来判断,这个回调函数对应后台代码应在OnDisconnected事件里

6、第五点失败,sayGoodBye是在执行完这个事件(OnDisconnected)后才传输到前台,而事件中执行完已经把链接断开了,前台是接收不到的。

Ⅱ、HTTP下的跨域

相对于websocket协议的跨域,http应该是简单的,网上也有很多资料。这里就当笔者自己的笔记吧。

http下的跨域指异步的传输,也就是说form表单提交这种非异步方式是不存在跨域问题的。网上最多的异步跨域是jsonp方式。

Jquery跨域请求 

在JavaScript中,有一个很重要的安全性限制,被称为“Same- Origin Policy”(同源策略)。这一策略对于JavaScript代码能够访问的页面内容做了很重要的限制,
即JavaScript只能访问与包含它的文档或脚本 在同一域名下的内容。不同域名下的脚本不能互相访问,即便是子域也不行。关于同源策略,读者可百度更详细的解释,这里不再赘述。
但是有时候又不可避免地需要进行跨域操作,这时候“同源策略”就是一个限制了,怎么办呢?采用JSONP跨域GET请求是一个常用的解决方案,下面我们来看一下JSONP跨域是如何实现的,
并探讨下JSONP跨域的原理。 这里提到了JSONP,那有人就问了,它同JSON有什么区别不同和区别呢,接下我们就来看看,百度百科有以下说明: JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它基于JavaScript(Standard ECMA-262 3rd Edition - December 1999)的一个子集。
JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。这些特性使JSON成为理想的数据交换语言。
易于人阅读和编写,同时也易于机器解析和生成(网络传输速度快)。
JSONP(JSON with Padding)是JSON的 一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。由于同源策略,一般来说位于 server1.example.com 的网页无法与
不是 server1.example.com的服务器沟通,而 HTML 的<script> 元素是一个例外。利用 <script> 元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 资料,
而这种使用模式就是所谓的 JSONP。用 JSONP 抓到的资料并不是 JSON,而是任意的JavaScript,用 JavaScript 直译器执行而不是用 JSON 解析器解析。 到这里,应该明白了,JSON是一种轻量级的数据交换格式,像xml一样,是用来描述数据间的。JSONP是一种使用JSON数据的方式,返回的不是JSON对象,是包含JSON对象的javaScript脚本。
那JSONP是如何工作的呢,我们知道,由于同源策略的限制,XmlHttpRequest只允许请求当前源(域名、协议、端口)的资源。若要跨域请求出于安全性考虑是不行的,但是我们发现,
Web页面上调用js文件时则不受是否跨域的影响,而且拥有”src”这个属性的标签都拥有跨域的能力,比如<script>、<img>、<iframe>,这时候,聪明的程序猿就想到了变通的方法,
如果要进行跨域请求, 通过使用html的script标记来进行跨域请求,并在响应中返回要执行的script代码,其中可以直接使用JSON传递 javascript对象。即在跨域的服务端生成JSON数据,
然后包装成script脚本回传,着不就突破同源策略的限制,解决了跨域访问的问题了么。 下面我们就看下怎么实现: 前端代码: function CallWebServiceByJsonp() { $("#SubEquipmentDetails").html(''); $.ajax({ type: "GET", cache: false, url: "http://servername/webservice/webservice.asmx/GetSingleInfo", data: { strCparent: $("#Equipment_ID").val() }, dataType: "jsonp", //jsonp: "callback", jsonpCallback: "OnGetMemberSuccessByjsonp" }); } function OnGetMemberSuccessByjsonp(data) { //处理data alert(data); } 后端的WebService代码: [WebMethod] [ScriptMethod(ResponseFormat = ResponseFormat.Json, UseHttpGet = true)] public void GetSingleInfo(string strCparent) { string ret = string.Empty; HttpContext.Current.Response.ContentType = "application/json;charset=utf-8"; string jsonCallBackFunName = HttpContext.Current.Request.Params["callback"].ToString(); //string jsonCallBackFunName1 = HttpContext.Current.Request.QueryString["callback"].Trim(); //上面代码必须 //中间代码执行自己的业务操作,可返回自己的任意信息(多数据类型) BLL.equipment eq_bll = new BLL.equipment(); List<Model.equipment> equipmentList = new List<Model.equipment>(); equipmentList = eq_bll.GetModelEquimentList(strCparent); ret = JsonConvert.SerializeObject(equipmentList); //下面代码必须 HttpContext.Current.Response.Write(string.Format("{0}({1})", jsonCallBackFunName, ret)); HttpContext.Current.Response.End(); } 如上所示,前端的CallWebServiceByJsonp方法采用jQuery的ajax方法调用后端的Web服务GetSingleInfo方法,后台的GetSingleInfo方法,
使用前端的回调方法OnGetMemberSuccessByjsonp包装后台的业务操作的JSON对象,返回给前端一段javascript片段执行。巧妙的解决了跨域访问问题。 JSONP的缺点: JSONP不提供错误处理。如果动态插入的代码正常运行,你可以得到返回,但是如果失败了,那么什么都不会发生。 链接:http://www.cnblogs.com/JerryTian/p/4194900.html

  

我比较喜欢下面这种简洁的方式:

$.ajax({
type:"GET",
url:"http://localhost:6874/Admin/Login/IsLegal", //跨域URL
dataType:"json",
data:{
par1:”参数1”,
par2:”参数2”
},
success:function (result){
if(result.id == "123"){
$("#div1").html("验证成功,可进行跳转");
}else{
$("#div1").html(result.name);
}
},
error:function (XMLHttpRequest, textStatus,errorThrown) { alert(errorThrown); // 调用本次AJAX请求时传递的options参数 } });

服务端代码:

1 public ActionResult IsLegal(string par1,string par2)
2 {
3 var str = new { id = "123", name = "joey" };
4
5 Httpcontext.Response.Appendheader("access-control-allow-origin", "*");
6
7 return json(str, jsonrequestbehavior.allowget);
8 }

或者定义一个特性标签,这样我们在需要跨域的action上打上特性标签即可。

public class CrossSiteAttribute : ActionFilterAttribute
{
private const string Origin = "Origin";
private const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
private const string originHeaderdefault = "*";
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
actionExecutedContext.Response.Headers.Add(AccessControlAllowOrigin, originHeaderdefault);
}
}

这样就定义了一个特性标签,[CrossSite]

使用:

[CrossSite]
public ActionResult Test()
{
return Content("跨域OK");
}

上面均是针对MVC或web api的,即对Access-Control-Allow-Origin进行了一个封装,本质都是对配置文件进行修改。所以最直接的就是配置文件修改:

webconfig中添加:

http与websocket(基于SignalR)两种协议下的跨域基于ASP.NET MVC--竹子整理

简单实用,同时可以进行post传输。

其他的跨域策略

一是通过Flash插件发送HTTP请求,这种方式可以绕过浏览器的安全限制,但必须安装Flash,并且跟Flash交互。不过Flash用起来麻烦,而且现在用得也越来越少了。

二是通过在同源域名下架设一个代理服务器来转发,JavaScript负责把请求发送到代理服务器:

'/proxy?url=http://www.sina.com.cn'

代理服务器再把结果返回,这样就遵守了浏览器的同源策略。这种方式麻烦之处在于需要服务器端额外做开发。

第三种方式称为JSONP,它有个限制,只能用GET请求,并且要求返回JavaScript。这种方式跨域实际上是利用了浏览器允许跨域引用JavaScript资源:

JSONP通常以函数调用的形式返回,例如,返回JavaScript内容如下:

Foo('data')

这样一来,我们如果在页面中先准备好foo()函数,然后给页面动态加一个<script>节点,相当于动态读取外域的JavaScript资源,最后就等着接收回调了。

最后是之前上文一直提到的“本文最后”

  1 #region 不跨域的方法
2 //app.MapSignalR();
3 #endregion
4
5 #region 跨域的第一种方法
6 //这个参数"/echo",是我们自己定义的一个路由,与客户端创建SignalR的实例时对应。
7 app.Map("/echo",
8 map =>
9 {
10 map.UseCors(CorsOptions.AllowAll);
11 map.RunSignalR<EchoConnection>();
12 }
13 );
14 #endregion
15
16 #region 第一种方法前端调用代码
17 // <script src="js/jquery-1.8.2.js"></script>
18 // <script src="js/jquery.signalR-2.2.0.min.js"></script>
19 // <script type="text/javascript">
20 // $(function () {
21 // var connection = $.connection("http://localhost:23013/echo");//对应的服务器端地址
22 // //var connection = $.connection("http://192.168.137.1/q");
23 // connection.logging = true;
24 // connection.received(function (data) {
25 // $("body").append(data + "<br />");
26 // });
27 // connection.error(function (err) {
28 // alert("存在一个错误. \n" +
29 // "Error: " + err.message);
30 // });
31 // connection.start().done(function () {
32 // $("#send").click(function () {
33 // connection.send($("#text").val());
34 // $("#text").val("").focus();
35 // });
36 // });
37 // });
38 //</script>
39 #endregion
40
41 #region 跨域的第二种方法
42 //app.Map("/signalr", map =>
43 //{
44 // map.UseCors(CorsOptions.AllowAll);
45 // var hubConfiguration = new HubConfiguration
46 // {
47 // EnableJSONP = true//跨域的关键语句
48 // };
49 // map.RunSignalR(hubConfiguration);
50 //});
51 //app.MapSignalR();
52
53 #endregion
54
55 #region 第二种方法前台代码
56 // <script src="js/jquery-1.8.2.js"></script>
57 // <script src="js/jquery.signalR-2.2.0.min.js"></script>
58 // <script src="http://localhost:23013/signalr/js" type="text/javascript" charset="utf-8"></script>地址是服务器端地址加上/设置的路由/js
59 // <script type="text/javascript">
60 // var chat = $.connection.myHub; //获取服务端实例 首字母小写,不是跟服务端一致的MyHub
61 //console.log(chat);
62 //chat.connection.url = "http://localhost:23013/signalr";测试项目的地址
63 // chat.connection.url = "http://localhost:6874/joey";
64 //1、回调函数 函数名要匹配,参数可以不匹配,后台传过来一个就执行一个,即若后台同时触发了两个同名的sayHello
65 //依次执行
66 //2、一定要有回调函数,不然不算连接成功,可以不调用服务器端的方法。若只是调用服务器端方法,没写回调函数,
67 //依然不算连接成功
68 //3、在连接成功的情况下,后台先执行OnConnected事件,再执行前台调用的某个方法
69 //4、用回调函数来判断是否真的连接成功,$.connection.hub.start().done里直接输出连接成功,是假成功。
70 //5、所以同样,断开连接是否成功也应用回调函数来判断,这个回调函数对应后台代码应在OnDisconnected事件里
71 //6、第五点失败,sayGoodBye是在执行完这个事件(OnDisconnected)后才传输到前台,而事件中执行完已经把链接断开了,前台是接收不到的
72 //chat.client.sayHello = function(connectionCode) {
73 // if(connectionCode == 1)
74 // {
75 // $("#IsConnSuc").val("1");//给隐藏字段赋值,1表示连接成功
76 // alert("连接成功");
77 // }
78 // else
79 // {
80 // alert("连接失败");
81 // }
82 // };
83 //-----------注释掉的---------------------------------------------------------------------
84 // chat.client.sayGoodBye = function(connectionCode) {
85 // if(connectionCode == 1)
86 // {
87 // alert("断开连接成功");
88 // }
89 // else
90 // {
91 // alert("断开连接失败");
92 // }
93 // };
94 // //建立连接
95 // $.connection.hub.start().done(function() {
96 // // Call the Send method on the hub.
97 // chat.server.hello("fd"); //调用服务器端定义的方法 方法名首字母小写,后台对应的方法首字母大小写都能匹配上
98 // });
99 //-----------注释掉的---------------------------------------------------------------------
100 // $(function(){
101 // $("#start").click(function(){
102 // //建立连接
103 // $.connection.hub.start();
104 // });
105
106 // $("#stop").click(function() {
107 // if($("#IsConnSuc").val() == "1"){
108 // //有连接时,才执行断开连接操作
109 // $.connection.hub.stop();
110 // $("#IsConnSuc").val("0");
111 // alert("断开连接成功");
112 // }
113
114 // });
115
116 // });
117
118 //</script>
119 #endregion