Merhaba, bu yazıda HttpClient kullanımını anlatıp Google Cloud Print servisine HTTP isteği göndermekle örnekleyeceğim.
Http Bilmek Lazım
HttpClient nesnesini bilebilmek için adından da anlaşılacağı üzerine HTTP mantığını iyi bilmek gerekiyor.
Bu yazıda hali hazırda HTTP hakkında bilgi sahibi olduğunuz varsayılmaktadır. Konuya uzak olanlar için biraz kaynaklardan bahsedeyim. Türkçe kaynaklarla başlayayım (sizin bildiğiniz başka değerli kaynaklar varsa paylaşırsanız sevinirim):
http://bahattincinic.com/post/89047254349/kim-k
http://www.denizirgin.com/post/2012/05/28/REST-RESTful-Web-Service.aspx/
Yabancı kaynaklar:
https://www.rfc-editor.org/rfc/pdfrfc/rfc2616.txt.pdf
https://www.w3.org/Protocols/rfc2616/rfc2616.html
HttpClient nedir?
Bilgi hazırlığı kısmını atlattığımıza göre gelelim konumuz olan HttpClient nesnesine. Asıl soru şu olmalı : “zaten WebRequest.CreateHttp ile bir HTTP isteği gerçekleştirebiliyorsam neden HttpClienta ihtiyaç duyayım?” Cevap işleri basitleştirmek. Eğer tek tük istekleriniz olacaksa WebRequest ile işinizi halledebiliyorsanız halledin. Böylece uygulamanıza framework dışı referanslar almamış olacaksınız. Fakat uygulamanız sürekli olarak bir servis ile iletişim halinde ise bu sefer WebRequest kullanımı işi içinden çıkılmaz hale getirebilir.
HttpClient framework ile baraber gelmediği için kendisini NuGet kullanarak indirmeniz gerekiyor.
Install-Package Microsoft.Net.Http
Not: NuGet’i yalnızca ortak projenize yüklemeniz yeterlidir. Android, IOS… projelerine yüklemeniz gerekmediği gibi zaten büyük olasılıkla hata alırsınız.
Tipik bir HttpClient kullanımı şu şekildedir :
using (var ht = new HttpClient())
{
using (var response = await ht.GetAsync("http://www.microsoft.com"))
{
_label.Text = await response.Content.ReadAsStringAsync();
}
}
Kısacık kodda görüldüğü gibi isteğin yapılması ve gelen sonucun işlenmesi hep asenkron olarak gerçekleştirilmekte. Bu sayede kullanıcı arayüzü donmadan sunucu ile iletişim kurmak mümkün olmakta.
HttpClientın IDisposable arayüzünü uyguladığını görüyorsunuz. Bu sizde her istek gerçekleştirildiğinde mevcut olanın dispose edilmesi ve yeni bir HttpClient nesnesi oluşturulması gerektiği düşüncesi oluşturabilir. Fakat eğer uygulamamız bu iletişimi uygulama açık olduğu sürece gerçekleştirecek ise yani tipik bir restAPI kullanımından bahsediyorsak her bir restService için bir HttpClient örneğini iyi olacaktır. Çünkü, bu kullanım öncelikle isim çözümlemesi, tcp tarafında port alınıp bırakılması, ssl tarafında doğrulama, cookie saklama gibi performans ve devamlılık konularında en iyi çözümdür. Uygulama veya modül sonlandığında dispose işlemini yapmanız daha doğru olacaktır.
Bir önceki paragraftaki durumu respone için konuşacak olursak. Burada ise tipik IDisposable mantığını yapmamızda fayda var. Eğer bize aracı olan nesne ile işimiz kalmamışsa kendisini dispose etmeliyiz. Burada response bir stream ve onun açık kalmasını istemeyiz.
Bir HttpClientın davranış biçimini yapıcı metoda verceğimiz HttpHandler türünden bir metotla belirleyebiliyoruz. Dilerseniz bu türden türeyen kendi davranışlarınızıda yapabilirsiniz. Biz tipik gelen-giden cookieleri otomatik halletmesi için şöyle bir tanımlama yapabiliriz:
var handler = new HttpClientHandler {UseCookies = true};
using (var ht = new HttpClient(handler))
Eğer bu handleri başka istemciler ile kullanmayacaksak, bu istemcinin işi bittiğinde dispose edilmesini isteyebiliriz. Bu durumda şu kullanım işimizi görecektir:
new HttpClient(new HttpClientHandler { UseCookies = true }, true)
Bize her zaman başarılı bir sonuç (HTTP 1XX veya 2XX) dönmeyecektir. Dönen sonucun olumlu mu olumsuz olduğuna hızlıca karar vermek için Response sınıfının IsSuccessStatusCode
özelliğine bakabiliriz.
using (var response = await ht.GetAsync("http://www.microsoft.com"))
{
if (response.IsSuccessStatusCode)
{
_label.Text = await response.Content.ReadAsStringAsync();
}
else
{
_label.Text = "HATA";
}
}
Sunucuya göndermek istediğimiz isteğimizin başlığında değişiklikler yapmak isteyebiliriz. Bir önceki örneğimizde Microsoft sunucusu bizim istemci uygulamamızı tanımadığını söylemişti. Biz de onu kandırarak sanki Chrome veya Mozilla olduğumuzu iletelim.
using (var ht = new HttpClient(new HttpClientHandler { UseCookies = true }, true))
{
ht.DefaultRequestHeaders.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("Mozilla", "5.0"));
ht.DefaultRequestHeaders.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("Chrome", "41.0.2228.0"));
using (var response = await ht.GetAsync("http://www.microsoft.com"))
{
if (response.IsSuccessStatusCode)
{
_label.Text = await response.Content.ReadAsStringAsync();
}
else
{
_label.Text = "HATA";
}
}
}
Ekran görüntüleri arasındaki farktan anlaşılacağı üzere Microsoft sunucusu artık bizim gerçek bir tarayıcıdan istekte bulunduğumuza göre sonuç gönderiyor.
POST, PUT, DELETE, HEAD gibi Http methodlarının kullanımlarını tek tek örneklemenin gerekmediğini düşünüyorum çünkü Get yerine Post yazmaktan çok farklı olmayacaklar. POST örneğini ve nasıl bir HTML forumunu POST ile gönderebileceğinizi zaten birazdan yapacağımız uygulamalardan görebilirsiniz. PATCH, TRACE gibi HttpClient ile hazır gelmeyen HTTP fiileri için SendAsync metodunu kullanabilirsiniz.
Örnek Yapalım
Hazırlık
Bir örnek ile uygulamayı pekiştirelim. MSDN forumlarında Google Cloud Print ile nasıl belge yazdırılabileceği hakkında bir soru gelmişti. Hazır bu konuyu yazıyorken o konuda bir çalışma yapalım istedim. Hali hazırda GCP için yazılımış wrapperlar olabilir. Biz, Googleın dokümanlarını okuyarak sıfırdan bir istemci yapma niyetindeyiz. Böylece ileride karşımıza çıkacak diğer senaryolar için de örnek teşkil etmiş olur.
Bitmiş örneğimiz şuna benzeyecek :
Hemen projemizi açalım. Proje şablonu olarak Xamarin APP 🡪 PCL 🡪 XAML seçiyorum.
NuGet kullanarak HttpClient ve Newtonsoft JSON paketlerini yukarıda anlattığım şekilde yüklüyorum.
Ne yapacağımı öğrenmek için, Google Servisleri konusunda ilgili dokümanları okumam lazım. Başlangıç noktam şu adres oluyor :
https://developers.google.com/cloud-print/docs/appInterfaces
Bu adres bana servisleri kullanabilmem için öncelikle “yetki” sahibi olmam gerektğini söylüyor.
Every API call you make must contain the following Authorization HTTP header:
Authorization: OAuth YOUR_ACCESS_TOKEN
, whereYOUR_ACCESS_TOKEN
refers to an OAuth2 access token. To fetch an access token, you will need to make use of the refresh token already stored with your application. For more information on using OAuth2 in this context, see here. Note also that the interfaces which may cause database writes (/submit and /deletejob) require a valid XSRF token.
“here” yazan kısayolu takip ediyorum. O adres de bana şöyle bilgi veriyor:
To begin, obtain OAuth 2.0 client credentials from the Google API Console. Then your client application requests an access token from the Google Authorization Server, extracts a token from the response, and sends the token to the Google API that you want to access. For an interactive demonstration of using OAuth 2.0 with Google (including the option to use your own client credentials), experiment with the OAuth 2.0 Playground.
Yani, Google API Consolea gitmeliyim ve orada bir uygulama oluşturmalıyım. İlgili kısayoldan ilerledim. Soldaki menüden credentials
kısmına geldim. create credentials
ile yeni bir uygulama oluşturdum.
Kullanıcı bilgileri ile https://developers.google.com/identity/protocols/OAuth2InstalledApp adresinde anlatıldığı şekilde almam gerekiyor. Yani burada kullanıcı mutlaka Googleın kendi sayfasına gitmeli. Güvenlik ve UX için böyle olması da gerekir. Yazının devamında bunu yapıyor olacağız.
Kodlar Gelsin
Artık ilk dokümana ve projeme geri dönebilirim. Öncelikle ben GCP işlemlerim için bir sınıf oluşturuyorum.
Sınıfımın boş hali şu şekilde :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Xamarin.Forms;
namespace CloudPrint.Clients
{
public sealed class GoogleCloudPrintClient
{
// kodlar buraya eklenecek
}
}
Artık sırayla kodlarımı ekleyebilirim :
private readonly HttpClient _httpClient;
private readonly string _clientId;
private readonly string _clientSecret;
public GoogleCloudPrintClient(string clientId, string clientSecret)
{
_clientId = clientId;
_clientSecret = clientSecret;
_httpClient = new HttpClient { BaseAddress = new Uri("https://www.google.com/cloudprint/") };
}
İlk önce yapıcı metodumu oluşturuyorum. Burada Google API sayfalarında uygulamam için verdiğim clientId ve clientSecret bilgilerimi tutacak değişkenlerimi hazırlıyorum. Ve tüm işleri yapacak HttpClient sınıfımın örneğini alıyorum.
Kullanıcının Google Hesabı ile giriş yapması gerekiyor. Bunun için yine ilgili dokümanlardaki adımları yapıyorum. Metot biraz uzun olduğu için açıklamaları metodun kendi içine yazdım.
public void GirisYap()
{
// Google ın login sayfasını gösterecek bir web view ayarlıyorum
var webSayfasi = new WebView
{
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand,
// scope : benim istediğim yetkileri belirtiyor.
// client_id : uygulamamı belirtiyor.
// response_type : geriye code dönmesini istediğimi belirtiyor.
// redirect_uri : giriş yapılınca yönlenmesini istediğim sayfayı belirtiyor.
Source =
$"https://accounts.google.com/o/oauth2/v2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloudprint&response_type=code&client_id={_clientId}&redirect_uri=http%3A%2F%2Flocalhost%3A80"
};
//Uygulamamın aktif sayfasını değiştiriyorum
var mevcutSayfa = Application.Current.MainPage;
Application.Current.MainPage = new ContentPage { Content = webSayfasi };
//web view adresi değiştiğinde
webSayfasi.Navigated += async (s, e) =>
{
// adres bu bilgiliyi içeriyorsa login başarılıdır, başarılı olmayan senaryoları size bırakıyorum
if (e.Url.Contains("?code="))
{
// ihtiyacım olan kod URL içinde, string i parçalayıp alıyorum
var kod = e.Url.Substring(e.Url.LastIndexOf("?code=", StringComparison.Ordinal) + 6);
// web view içinde yönlendirme hatası gözükmemesi için gizliyorum
webSayfasi.Source = new HtmlWebViewSource { Html = @"<html><body> <h1Bekleyiniz</h1> </body></html>" };
// tek kullanımlık bir http client açıyorum, tek kullanımlık olduğundan işim bittiği anda dispose ediyorum
using (var tokenClient = new HttpClient())
{
// elimdeki kod ve uygulamamın gizli anahtarı ile yetkilendirme işini yapıyorum
// bana token dönecek,
var requestContent = new FormUrlEncodedContent(new Dictionary<string, string>
{
{"code",kod },
{"client_id",_clientId },
{"client_secret", _clientSecret },
{"redirect_uri","http://localhost:80" },
{"grant_type","authorization_code" },
});
var yanit = await tokenClient.PostAsync("https://www.googleapis.com//oauth2/v4/token", requestContent);
var yanitIcerigi = await yanit.Content.ReadAsStringAsync();
// dönen bilgiyi json dan dictionary'e çevirip token kısmını alıyorum
var token = JsonConvert.DeserializeObject<IDictionary<string, string>>(yanitIcerigi)["access_token"];
// bundan sonraki tüm isteklerimde bu token ı kullanacağım
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
// Login ekranını göstermeden önceki sayfaya dönüyorum
Application.Current.MainPage = mevcutSayfa;
}
};
}
Yazıcı listesini alacak metodum :
public async Task<List<Printer>> YazicilariGetirAsync()
{
if (_httpClient.DefaultRequestHeaders.Authorization == null)
{
GirisYap();
return null;
}
var yanitIcerigi = await _httpClient.GetStringAsync("search");
return JsonConvert.DeserializeObject<AramaSonucu>(yanitIcerigi).Printers.ToList();
}
Metot olabildiğince basit. Fakat AramaSonucu
diye bir sınıf çıktı. Kendisine sonra değineceğim. Aynı işi yazdırma emri için de yapalım :
public async Task<bool?> YazdirAsync(Printer yazici, string yazi)
{
if (_httpClient.DefaultRequestHeaders.Authorization == null)
{
GirisYap();
return null;
}
var requestContent = new FormUrlEncodedContent(new Dictionary<string, string>
{
{"printerid",yazici.Id },
{"title", "Xamarin TR" },
{"ticket", JsonConvert.SerializeObject(new
{
version = "1.0",
print = new
{
color = new {type="STANDARD_MONOCHROME"},
copies = new {copies = 1}
},
},Newtonsoft.Json.Formatting.Indented)},
{"content", yazi },
{"contentType","text/plain" },
});
using (var yanit = await _httpClient.PostAsync("submit", requestContent))
{
return yanit.IsSuccessStatusCode;
}
}
Ticket nesnesi ile yazıcı ayarlarımızı belirleyebiliyoruz, ticket nesnesinin alabileceği tüm ayarlar için https://developers.google.com/cloud-print/docs/cdd#cjt adresine bakabilirsiniz.
Content olarak ben düz metin verdim. Siz isterseniz bir URL, bir PDF, bir JPG… yazdırılabilecek herhangi bir şey verebilirsiniz. Ne verdiyseniz Mime Typenı contentType
özelliğine vermeyi unutmayın.
Burada da Printer
adında bir sınıf var. Google bize yanıt olarak JSON dönüyor ve biz bunları kendi nesnelerimize dönüştürmemiz gerekiyor. Ben http://jsonutils.com adresini kullanarak bu sınıfların ihtiyacım kadarını dönen JSON lardan oluşturdum. Kendileri ve bu sınıfın tamamı için:
https://gist.github.com/cihanyakar/019840d8357bbaf4313051cdf59242ae
adresine bakabilirsiniz gereksiz kalabalık olmasın diye ayrı yere koydum.
Gelelim kullanıma, MainWindow.xaml
içeriğini şöyle değiştiriyorum :
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:CloudPrint"
x:Class="CloudPrint.MainPage">
<StackLayout>
<ListView x:Name="YazicilarListView" ItemTapped="YazicilarListView_ItemTapped" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding DisplayName}"></Label>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Clicked="Button_Clicked" Text="TIKLA"></Button>
</StackLayout>
</ContentPage>
Ve MainWindow.xaml.cs
şöyle olacak :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CloudPrint.Clients;
using Xamarin.Forms;
namespace CloudPrint
{
public partial class MainPage : ContentPage
{
private readonly GoogleCloudPrintClient _gcpc;
public MainPage()
{
_gcpc = new GoogleCloudPrintClient("CLIENT_ID", "CLIENT_SECRET");
InitializeComponent();
}
private async void Button_Clicked(object sender, EventArgs e)
{
var yazicilar = await _gcpc.YazicilariGetirAsync();
if (yazicilar != null)
{
YazicilarListView.ItemsSource = yazicilar;
}
}
private async void YazicilarListView_ItemTapped(object sender, ItemTappedEventArgs e)
{
var yazici = e.Item as Printer;
if (yazici == null) { return; }
YazicilarListView.IsEnabled = false;
await _gcpc.YazdirAsync(yazici, "Merhaba");
YazicilarListView.IsEnabled = true;
}
}
}
Görüşmek üzere.
Yorum Gönder