Инструмент отладки приложения по одному порту Windows Azure

2 сентября 2010 г.

Разрабатывая приложение на платформе Windows Azure нам приходилось мириться с особенностью запуска debug-сборок на разных портах. Но когда мы начали активно отлаживать кросс-доменный AJAX эта проблема встала особенно остро. Потому как скрипты требовали использования абсолютного URL в тексте js-скрипта. Нам пришлось писать номер порта в URL: 127.0.0.1:81/bla-bla-bla. Когда при перезапуске приложения порт изменялся — приходилось перезапускать devfabric для того, чтобы сборка запускалась на 81-ом порту. Перезапуск отнимал драгоценное время, раздражение возрастало.
В один прекрасный момент терпение лопнуло и мы решили создать инструмент для отладки приложения на одном порту. Он представляет из себя ASPX-приложение, которое получает запросы и перенаправляет их на запущенную инстанцию Azure. Это позволяет нам не заботиться о том, на каком же порту запущен сейчас Azure.

Предварительная настройка

В процессе разработки родилась идея написать что-то вроде Reverse Proxy для того чтобы инстанция Azure была доступна по адресу azureproxy.com. Для этого мы используем IIS 7.5, который все равно крутится на машине.
В диспетчере служб IIS добавляем пул приложений AzureProxy

Версию среды .Net Framework указываем четвертую.
И добавляем веб-сайт

Здесь нужно указать имя сайта «AzureProxy», пул приложений «AzureProxy», физический путь — любой (здесь D:\AzureProxy\). Также нужно указать привязку: IP-адрес — 127.0.0.1, Порт — 80, Имя узла — azureproxy.com.
Также нам понадобится модуль Url Rewrite для IIS. Если он у вас ещё не установлен — необходимо посетить www.iis.net/download/URLRewrite и установить его.
Следующим шагом будет добавление azureproxy.com в файл hosts.
В редакторе открываем файл C:\Windows\System32\drivers\etc\hosts и добавляем в него строку
127.0.0.1 azureproxy.com
Предварительный этап закончен.

Программируем

Настало время открыть Visual Studio 2010.
Создаем новый проект

Тип проекта — ASP .NET Empty Web Application, остальное по желанию.
Правим файл Web.Config

<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
<pages enableViewStateMac="false" />
</system.web>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="AzureProxyModule" type="AzureProxy.AzureProxyModule, AzureProxy" />
</modules>
<rewrite>
<rules>
<rule name="All" stopProcessing="true">
<match url="^(.*)$" />
<action type="Rewrite" url="/Default.aspx?url={HtmlEncode:{R:0}}" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

Добавляем новую веб-форму

с именем Default.aspx
И оставляем в ней только
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="AzureProxy.Default" %>

Далее добавляем новый класс

c именем AzureProxyModule.cs следующего содержания

using System;
using System.Net;
using System.IO;
using System.Configuration;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;

namespace AzureProxy
{
public class AzureProxyModule : IHttpModule
{

const string Domain = "127.0.0.1";
const int MaxPort = 110;

public static string Port = "";
public Queue<int> ports = new Queue<int>();
public List threads = new List();
public bool ThreadsLive = false;

public AzureProxyModule()
{
}

public String ModuleName
{
get { return "AzureProxyModule"; }
}

public void Init(HttpApplication httpApp)
{

httpApp.BeginRequest +=
new EventHandler(this.OnBeginRequest);

httpApp.EndRequest +=
new EventHandler(this.OnEndRequest);
}

public void OnBeginRequest(object o, EventArgs ea)
{
}

public void OnEndRequest(object o, EventArgs ea)
{
HttpApplication httpApp = (HttpApplication)o;
if (Port.Length == 0)
{
SearchPort();
}

DoRequest();

}

public void DoRequest()
{

byte[] buffer = new byte[4096];
HttpContext ctx;
ctx = HttpContext.Current;
if (ctx.Request.QueryString["url"] == null) return;

HttpWebRequest myHttp = (HttpWebRequest)HttpWebRequest.Create("http://" + Domain + ":" + Port + ctx.Request.RawUrl.ToString());
myHttp.AllowAutoRedirect = false;
myHttp.KeepAlive = true;
myHttp.CookieContainer = new CookieContainer();
myHttp.UserAgent = ctx.Request.UserAgent;
foreach (string CookieName in ctx.Request.Cookies.AllKeys)
{
if (ctx.Request.Cookies[CookieName].Domain != null)
{
myHttp.CookieContainer.Add(new Cookie(ctx.Request.Cookies[CookieName].Name, ctx.Request.Cookies[CookieName].Value, ctx.Request.Cookies[CookieName].Path, ctx.Request.Cookies[CookieName].Domain));
}
else
{
myHttp.CookieContainer.Add(new Cookie(ctx.Request.Cookies[CookieName].Name, ctx.Request.Cookies[CookieName].Value, ctx.Request.Cookies[CookieName].Path, Domain));
}
}
myHttp.ContentType = ctx.Request.ContentType;

if (ctx.Request.HttpMethod == "POST")
{
myHttp.Method = "POST";
myHttp.AllowWriteStreamBuffering = true;
myHttp.ContentLength = ctx.Request.InputStream.Length;
Stream requestStream = myHttp.GetRequestStream();
int length = 0;
while ((length = ctx.Request.InputStream.Read(buffer, 0, buffer.Length)) > 0)
{
requestStream.Write(buffer, 0, length);
}
}

try
{
WebResponse response = myHttp.GetResponse();

if (response.Headers["Location"] != null)
{
ctx.Response.Redirect(response.Headers["Location"]);
}

using (Stream stream = response.GetResponseStream())
{
using (MemoryStream memoryStream = new MemoryStream())
{
int count = 0;
do
{
count = stream.Read(buffer, 0, buffer.Length);
memoryStream.Write(buffer, 0, count);

} while (count != 0);

ctx.Response.ContentType = response.ContentType;
foreach (string HeaderKey in response.Headers.Keys)
{
ctx.Response.AddHeader(HeaderKey, response.Headers[HeaderKey]);
}

ctx.Response.BinaryWrite(memoryStream.ToArray());

}
}
}
catch (WebException code)
{
switch (code.Message)
{
case "Unable to connect to the remote server":
SearchPort();
DoRequest();
break;
case "The remote server returned an error: (404) Not Found.":
ctx.Response.Status = "404 File Not Found";
break;
}
return;
}
}

public void SearchPort()
{
Port = "";
for (int port = 81; port < MaxPort; port++)
{
ports.Enqueue(port);
threads.Add(new Thread(new ThreadStart(PortTest)));
}
threads.ForEach(new Action(ThreadStart));

while (TreadsIsLive() && (Port.Length == 0))
{
//ждем ответа от трэдов
}

//завершаем все трэды
threads.ForEach(new Action(ThreadAbort));
}

public void Dispose() { }

public bool TreadsIsLive()
{
ThreadsLive = false;
threads.ForEach(new Action(ThreadTest));
return true;
}

public void ThreadTest(Thread t)
{
ThreadsLive = ThreadsLive || t.IsAlive;
}

protected void ThreadStart(Thread t)
{
try
{
ThreadsLive = true;
if (!t.IsAlive) t.Start();
}
catch { }
}

protected void ThreadAbort(Thread t)
{
t.Abort();
}

protected void PortTest()
{
int port;
try
{
port = ports.Dequeue();

HttpWebRequest myHttpWebRequest =
(HttpWebRequest)HttpWebRequest.Create("http://" + Domain + ":" + port.ToString());
try
{
HttpWebResponse myHttpWebResponse =
(HttpWebResponse)myHttpWebRequest.GetResponse();
Port = port.ToString();
return;
}
catch { }
}
catch { }
}
}
}

Собираем приложение, и публикуем его в IIS (Build > Publish AzureProxy)


Теперь инстанция Azure доступна по адресу azureproxy.com.

Заключение

Использованная техника позволяет проксировать запросы к запущенным инстанциям Azure с одного адреса. Поиск доступного порта осуществляется в дочерних трэдах параллельно, что позволяет быстро находить порт. Проксируются как GET так и POST запросы, входящий поток полностью передается инстанции Azure что позволяет загружать файлы.

Теги:
рубрика C#, Windows