Инструмент отладки приложения по одному порту Windows Azure
Разрабатывая приложение на платформе 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 что позволяет загружать файлы.