İPhone Core Verilerini web sunucusuyla nasıl senkronize edebilir ve sonra diğer cihazlara nasıl itebilirim? [kapalı]


293

Bir iPhone uygulamasında saklanan çekirdek verileri iPad veya Mac gibi birden fazla cihaz arasında senkronize etmek için bir yöntem üzerinde çalışıyorum. İOS'ta Temel Verilerle kullanmak için çok fazla (varsa) senkronizasyon çerçevesi yoktur. Ancak, aşağıdaki kavram hakkında düşünüyorum:

  1. Yerel çekirdek veri deposunda bir değişiklik yapılır ve değişiklik kaydedilir. (a) Aygıt çevrimiçi ise, değişiklik kümesini gönderen aygıtın aygıt kimliği de dahil olmak üzere, değişiklik kümesini sunucuya göndermeye çalışır. (b) Değişiklik kümesi sunucuya ulaşmazsa veya cihaz çevrimiçi değilse, uygulama çevrimiçi olduğunda gönderilecek değişikliği bir kuyruğa ekler.
  2. Bulutta oturan sunucu, aldığı ana değişiklik kümesini ana veritabanıyla birleştirir.
  3. Bulut sunucusunda bir değişiklik kümesi (veya bir değişiklik kümesi kuyruğu) birleştirildikten sonra, sunucu tüm bu değişiklik kümelerini bir tür yoklama sistemi kullanarak sunucuya kayıtlı diğer aygıtlara gönderir. (Apple'ın Push hizmetlerini kullanmayı düşündüm, ancak görünüşe göre bu yorumlara göre bu uygulanabilir bir sistem değil.)

Düşünmem gereken bir şey var mı? ObjectiveResource , Core Resource ve RestfulCoreData gibi REST çerçevelerine baktım . Tabii ki, bunların hepsi bağlı olmadığım Ruby on Rails ile çalışıyor, ama başlamak için bir yer. Çözümüm için sahip olduğum temel gereksinimler:

  1. Herhangi bir değişiklik ana iş parçacığı duraklatılmadan arka planda gönderilmelidir.
  2. Mümkün olduğunca az bant genişliği kullanmalıdır.

Bir takım zorlukları düşündüm:

  1. Farklı cihazlardaki farklı veri depoları için nesne kimliklerinin sunucuya bağlı olduğundan emin olun. Yani, veritabanında depolanan nesneye bir başvuru ile bağlı bir nesne kimlikleri ve aygıt kimlikleri tablosu olacak. Bir kayıt (DatabaseId [bu tabloya benzersiz]], ObjectId [tüm veritabanındaki öğeye benzersiz], Datafield1, Datafield2), ObjectId alanı başka bir tablo, AllObjects: (ObjectId, DeviceId, DeviceObjectId) başvurulacaktır. Ardından, aygıt bir değişiklik kümesini yukarı ittiğinde, yerel veri deposundaki çekirdek veri nesnesinden aygıt kimliği ve objectId boyunca geçer. Daha sonra bulut sunucum AllObjects tablosundaki objectId ve cihaz kimliğini kontrol eder ve ilk tabloda değiştirilecek kaydı bulur.
  2. Tüm değişikliklerin birleştirilebilmesi için zaman damgası uygulanmalıdır.
  3. Cihazın çok fazla pil kullanmadan sunucuyu yoklaması gerekir.
  4. Yerel aygıtların, sunucudan değişiklik alındığında / alındığında bellekte tutulan her şeyi güncellemesi gerekir.

Burada özlediğim başka bir şey var mı? Bunu mümkün kılmak için ne tür çerçevelere bakmalıyım?


5
Alınan Anlık Bildirimlere güvenemezsiniz. Kullanıcı sadece onlara dokunabilir ve ikinci bir bildirim geldiğinde, işletim sistemi ilkini atar. IMO anında iletme bildirimleri, kullanıcıyı kesintiye uğrattıkları için eşitleme güncellemelerini almanın kötü bir yoludur. Uygulama, her başlatıldığında senkronizasyonu başlatmalıdır.
Ole Begemann

TAMAM. Bilgi için teşekkürler - sunucuyu sürekli yoklamak ve başlatma sırasında güncellemeleri kontrol etmek dışında, cihazın güncelleme almasının bir yolu var mı? Uygulama aynı anda birden fazla cihazda açıksa, çalışmasını sağlamakla ilgileniyorum.
Jason

1
(Biraz geç biliyorum, ama herkes bununla karşılaşır ve aynı zamanda harikalar yaratır) aynı anda birden fazla cihazı senkronize tutmak için diğer cihaz veya sunucu ile açık bir bağlantı tutabilir ve diğer cihazlara bildirmek için mesaj gönderebilirsiniz ) bir güncelleme gerçekleştiğinde. (örneğin IRC / anlık mesajlaşma çalışma şekli)
Dan2552

1
@ Dan2552: Açıkladığınız şey [uzun yoklama] olarak bilinir [ en.wikipedia.org/wiki/… ve harika bir fikirdir, ancak açık bağlantılar bir mobil cihazda oldukça fazla pil ve bant genişliği tüketir.
johndodo

1
Ray Wenderlich'ten uygulamanızla web hizmeti arasında veri senkronizasyonu hakkında iyi bir eğitici: raywenderlich.com/15916/…
JRG-Developer

Yanıtlar:


144

Dan Grover tarafından iPhone 2009 konferansında tartışılan ve burada pdf belgesi olarak sunulan senkronizasyon stratejisini dikkatlice okumanızı ve uygulamayı öneririm .

Bu uygun bir çözümdür ve uygulanması o kadar da zor değildir (Dan bunu birkaç uygulamasında uyguladı), Chris tarafından tarif edilen çözümle çakışıyor. Senkronizasyon hakkında derinlemesine ve teorik bir tartışma için Russ Cox (MIT) ve William Josephson'dan (Princeton) makaleye bakınız:

Vektör Zaman Çiftleriyle Dosya Senkronizasyonu

bazı belirgin değişikliklerle temel verilere eşit derecede iyi uygulanır. Bu, genel olarak çok daha sağlam ve güvenilir bir senkronizasyon stratejisi sağlar, ancak doğru bir şekilde uygulanması için daha fazla çaba gerektirir.

DÜZENLE:

Görünüşe göre Grover'ın pdf dosyası artık mevcut değil (bozuk bağlantı, Mart 2015). GÜNCELLEME: bağlantıya Geri Dönüş Makinesi aracılığıyla buradan ulaşabilirsiniz

İSloud'un nihayet doğru çekirdek veri senkronizasyonunu desteklediği göz önüne alındığında, ZSync adı verilen ve Marcus Zarra tarafından geliştirilen Objective-C çerçevesi kullanımdan kaldırılmıştır.


Herkes ZSync videosu için güncellenmiş bir bağlantıya sahip mi? Ayrıca, ZSync hala korunuyor mu? En son 2010 yılında güncellendiğini görüyorum.
Jeremie Weldin

ZSync'in github'daki son taahhüdü Eylül 2010'da yapıldı ve bu da Marcus'un onu desteklemeyi bıraktığına inanmamı sağladı.
Bozulabilir Dave

1
Dan Grover tarafından açıklanan algoritma oldukça iyi. Ancak, çok iş parçacıklı bir sunucu koduyla çalışmaz (bu nedenle, bu hiç ölçeklenmez), çünkü yeni güncelleştirmeleri denetlemek için zaman kullanıldığında bir istemcinin güncelleştirmeyi kaçırmamasını sağlamak için bir yol yoktur. . Lütfen, yanılıyorsam beni düzeltin - bunun çalışan bir uygulamasını görmek için öldürürüm.
masi

1
@Patt, size istendiği gibi pdf dosyasını gönderdim. Şerefe Massimo Cafaro.
Massimo Cafaro

3
Dan Grover'ın eksik Platformlar Arası Veri Senkronizasyonu PDF slaytlarına Wayback Machine aracılığıyla erişilebilir.
Matthew Kairys

272

Yapmaya çalıştığın şeye benzer bir şey yaptım. Size ne öğrendiğimi ve nasıl yaptığımı anlatayım.

Temel Veri nesneniz ile sunucudaki model (veya db şeması) arasında bire bir ilişkiniz olduğunu varsayalım. Sadece sunucu içeriğini istemcilerle senkronize tutmak istersiniz, ancak istemciler veri değiştirebilir ve ekleyebilir. Eğer doğru anladıysam okumaya devam et.

Senkronizasyona yardımcı olmak için dört alan ekledim:

  1. sync_status - Bu alanı yalnızca temel veri modelinize ekleyin. Öğe üzerinde bekleyen bir değişiklik olup olmadığını belirlemek için uygulama tarafından kullanılır. Aşağıdaki kodları kullanıyorum: 0 değişiklik olmadığı anlamına gelir, 1 sunucu ile senkronize olmak için sıraya alınmış demektir ve 2 bu geçici bir nesne ve temizlenebilir anlamına gelir.
  2. is_deleted - Bunu sunucuya ve temel veri modeline ekleyin. Delete olayı, veritabanından veya istemci modelinizden bir satırı gerçekten silmemelidir, çünkü sizi senkronize etmek için hiçbir şey bırakmaz. Bu basit boole bayrağına sahip olarak, is_deleted değerini 1 olarak ayarlayabilir, senkronize edebilirsiniz ve herkes mutlu olacaktır. Silinmeyen öğeleri "is_deleted = 0" ile sorgulamak için sunucu ve istemcideki kodu da değiştirmeniz gerekir.
  3. last_modified - Bunu sunucuya ve temel veri modeline ekleyin. Bu alan, kayıtta herhangi bir değişiklik olduğunda sunucu tarafından geçerli tarih ve saatle otomatik olarak güncellenmelidir. Asla müşteri tarafından değiştirilmemelidir.
  4. guid - Sunucuya ve temel veri modeline global olarak benzersiz bir kimlik ekleyin (bkz. http://en.wikipedia.org/wiki/Globally_unique_identifier ). Bu alan birincil anahtar olur ve istemcide yeni kayıtlar oluşturulurken önem kazanır. Normalde birincil anahtarınız sunucuda artan bir tamsayıdır, ancak içeriğin çevrimdışı oluşturulabileceğini ve daha sonra senkronize edilebileceğini unutmayın. GUID, çevrimdışıyken bir anahtar oluşturmamıza izin verir.

İstemcide, bir şey değiştiğinde ve sunucu ile senkronize edilmesi gerektiğinde model nesnenizde sync_status değerini 1 olarak ayarlamak için kod ekleyin. Yeni model nesneleri bir GUID oluşturmalıdır.

Senkronizasyon tek bir istektir. İstek şunları içerir:

  • Model nesnelerinizin MAX last_modified zaman damgası. Bu, sunucuya yalnızca bu zaman damgasından sonra değişiklik istediğinizi bildirir.
  • Sync_status = 1 olan tüm öğeleri içeren bir JSON dizisi.

Sunucu isteği alır ve bunu yapar:

  • İçeriği JSON dizisinden alır ve içerdiği kayıtları değiştirir veya ekler. Son değiştirilen alan otomatik olarak güncellenir.
  • Sunucu, istekte gönderilen zaman damgasından daha büyük bir last_modified zaman damgası olan tüm nesneleri içeren bir JSON dizisi döndürür. Bu, yeni alınan ve kaydın sunucu ile başarıyla senkronize edildiğini kabul eden nesneleri içerir.

Uygulama yanıtı alır ve bunu yapar:

  • İçeriği JSON dizisinden alır ve içerdiği kayıtları değiştirir veya ekler. Her kayıt 0'lık bir sync_status alır.

Umarım bu yardımcı olur. Kayıt ve model kelimesini birbirinin yerine kullandım, ama sanırım bu fikri anlıyorsunuz. İyi şanslar.


2
Last_modified alanı yerel veritabanında da bulunur, ancak iPhone saati tarafından güncellenmez. Sunucu tarafından ayarlanır ve geri senkronize edilir. MAX (last_modified) tarihi, uygulamanın sunucuya o tarihten sonra değiştirilen her şeyi geri göndermesi için gönderdiği tarihtir.
chris

3
İstemcideki küresel bir değer onun yerini alabilir MAX(last_modified), ancak bu MAX(last_modified)yeterli olduğu için bu gereksiz olur . sync_statusBaşka bir role sahiptir. Daha önce yazdığım gibi, MAX(last_modified)neyin sunucudan senkronize edilmesi gerektiğini, sync_statusneyin sunucuya senkronize edilmesi gerektiğini belirler.
chris

2
@Flex_Addicted Teşekkürler. Evet, senkronize etmek istediğiniz her varlık için alanları çoğaltmanız gerekir. Bununla birlikte, bir modeli bir ilişkiyle senkronize ederken (örneğin, 1'den çoğa) daha fazla dikkat etmeniz gerekir.
chris

2
@BenPackard - Haklısın. Yaklaşım herhangi bir çakışma çözümlemesi yapmaz, böylece son müşteri kazanır. Kayıtlar tek bir kullanıcı tarafından düzenlendiğinden, uygulamalarımda bununla uğraşmak zorunda kalmadım. Bunu nasıl çözdüğünü bilmek isterdim.
chris

2
Merhaba @noilly, aşağıdaki durumu göz önünde bulundurun: Yerel bir nesne üzerinde değişiklik yapmak ve onu sunucuya eşitlemek gerekir. Senkronizasyon yalnızca saatler veya günler sonra gerçekleşebilir (bir süredir çevrimdışıysanız) ve bu süre içinde uygulama birkaç kez kapatılmış ve yeniden başlatılmış olabilir. Bu durumda NSManagedObjectContext üzerindeki yöntemler pek yardımcı olmaz.
chris

11

Hala bir yol arıyorsanız, Couchbase cep telefonuna bakın. Bu temelde istediğiniz her şeyi yapar. ( http://www.couchbase.com/nosql-databases/couchbase-mobile )


3
Bu, yalnızca verilerinizi ilişkisel verilerden ziyade belge olarak ifade edebiliyorsanız yapabilirsiniz. Etrafında iş var, ama her zaman güzel ya da buna değmezler.
Jeremie Weldin

belgeler küçük uygulamalar için yeterlidir
Hai Feng Kao

@radiospiel Bağlantınız koptu
Mick

Bu, arka ucun Couchbase DB'de yazılması gerektiğine de bağımlı olacaktır. Hatta senkronizasyon için NOSQL fikri ile başladım, ancak arka uçta MS SQL çalıştığından arka ucumu NOSQL olarak kısıtlayamıyorum.
thesummersign

@Mick: Tekrar çalışıyor gibi görünüyor (veya birisi bağlantıyı düzeltti mi? Teşekkür ederim)
radiospiel

7

@Cris'e benzer şekilde, istemci ve sunucu arasında senkronizasyon için sınıf uyguladım ve şu ana kadar bilinen tüm sorunları çözdüm (sunucuya / sunucudan veri gönderme / alma, zaman damgalarına dayalı çakışmaları birleştirme, güvenilir olmayan ağ koşullarında yinelenen girişleri kaldırma, iç içe verileri senkronize etme ve dosyalar vb.)

Sadece sınıfa hangi varlığın ve hangi sütunların senkronize edilmesi gerektiğini ve sunucunuzun nerede olduğunu söylersiniz.

M3Synchronization * syncEntity = [[M3Synchronization alloc] initForClass: @"Car"
                                                              andContext: context
                                                            andServerUrl: kWebsiteUrl
                                             andServerReceiverScriptName: kServerReceiverScript
                                              andServerFetcherScriptName: kServerFetcherScript
                                                    ansSyncedTableFields:@[@"licenceNumber", @"manufacturer", @"model"]
                                                    andUniqueTableFields:@[@"licenceNumber"]];


syncEntity.delegate = self; // delegate should implement onComplete and onError methods
syncEntity.additionalPostParamsDictionary = ... // add some POST params to authenticate current user

[syncEntity sync];

Kaynak, çalışan örnek ve daha fazla talimatı burada bulabilirsiniz: github.com/knagode/M3Synchronization .


Cihaz saatini anormal bir değere değiştirirsek sorun olmaz mı?
Altın

5

Push bildirimi ile verileri güncellemesi için kullanıcıya dikkat edin. Yerel verileri ve bulut sunucusundaki verileri kontrol etmek için uygulamada bir arka plan iş parçacığı kullanın, sunucuda değişiklik olurken yerel verileri değiştirin, bunun tersi de geçerlidir.

Bence en zor yanı, tarafın geçersiz olduğu verileri tahmin etmektir.

Umarım bu sana yardımcı olabilir


5

SynCloud olarak bilinen yeni Core Data Cloud Syncing API'mın ilk sürümünü yayınladım. Çok kullanıcılı senkronizasyon arayüzüne izin verdiği için SynCloud'un iCloud ile birçok farklılığı vardır. Çok tablolu, ilişkisel verilere izin verdiği için diğer senkronizasyon uygulamalarından da farklıdır.

Lütfen daha fazla bilgiyi http://www.syncloudapi.com adresinde bulabilirsiniz.

İOS 6 SDK ile derleyin, 27.09.2012 tarihinden itibaren çok güncel.


5
Stack Overflow'a hoş geldiniz! Cevabınızı gönderdiğiniz için teşekkürler! Lütfen Kendi Kendini Tanıtma ile ilgili SSS bölümünü dikkatle okuyun.
Andrew Barber

5

Ben GUID sorunu için iyi bir çözüm "dağıtılmış kimlik sistemi" olduğunu düşünüyorum. Doğru terimin ne olduğundan emin değilim, ama MS SQL sunucu belgelerinin onu çağırmak için kullandığını düşünüyorum (SQL / bu yöntemi dağıtılmış / senkronize veritabanları için kullanır). Oldukça basit:

Sunucu tüm kimlikleri atar. Her eşitleme yapıldığında, kontrol edilen ilk şey "Bu istemcide kaç tane kimliğim kalıyor?" İstemci azalıyorsa, sunucudan yeni bir kimlik bloğu ister. İstemci daha sonra yeni kayıtlar için bu aralıktaki kimlikleri kullanır. Bir sonraki senkronizasyondan önce "asla" bitmeyecek kadar büyük bir blok atayabilir, ancak sunucunun zaman içinde tükenecek kadar büyük olmamasına rağmen, bu çoğu ihtiyaç için harika çalışır. İstemci tükenirse, işlem oldukça basit olabilir, sadece kullanıcıya "eşzamanlayana kadar daha fazla öğe ekleyemeyeceğinizi söyleyin" ... çok fazla öğe eklerse, eski verileri önlemek için senkronize etmemeleri gerektiğini söyleyin yine de sorunlar?

Rastgele GUID'leri% 100 güvenli olmadığından ve genellikle standart bir ID'den (128 bit ve 32 bit) çok daha uzun olması gerektiğinden, bunun rastgele GUID'leri kullanmaktan daha üstün olduğunu düşünüyorum. Genellikle kimliğe göre dizinleriniz olur ve genellikle kimlik numaralarını bellekte tutar, bu nedenle onları küçük tutmak önemlidir.

Gerçekten cevap olarak yayınlamak istemedim, ama kimsenin bir yorum olarak göreceğini bilmiyorum ve bence bu konu için önemli ve diğer cevaplara dahil değil.


2

Öncelikle kaç veri, tablo ve ilişkiniz olacağını yeniden düşünmelisiniz. Benim çözümümde Dropbox dosyaları ile senkronizasyon uyguladım. Ana MOC değişiklikleri gözlemlemek ve bu verileri dosyalara kaydetmek (her satır gzipped json kaydedilir). Çalışan bir internet bağlantısı varsa, Dropbox'ta herhangi bir değişiklik olup olmadığını kontrol ederim (Dropbox bana delta değişiklikleri verir), onları indirin ve birleştirin (en son kazançlar) ve son olarak değiştirilen dosyaları koyun. Senkronizasyondan önce, diğer istemcilerin eksik verileri senkronize etmesini önlemek için Dropbox'a kilit dosyası koydum. Değişiklikleri indirirken yalnızca kısmi verilerin indirilmesi güvenlidir (örn. İnternet bağlantısı kesildi). İndirme işlemi tamamlandığında (tamamen veya kısmen) Temel Verilere dosya yüklemeye başlar. Çözümlenmemiş ilişkiler olduğunda (tüm dosyalar indirilmez) dosyaları yüklemeyi durdurur ve daha sonra indirmeyi bitirmeye çalışır. İlişkiler yalnızca GUID olarak saklanır, böylece hangi verilerin yükleneceğini tam veri bütünlüğüne sahip olacak şekilde kolayca kontrol edebilirim. Temel verilerde değişiklik yapıldıktan sonra senkronizasyon başlar. Herhangi bir değişiklik yoksa, Dropbox'ta birkaç dakikada bir ve uygulama başlangıcındaki değişiklikleri kontrol eder. Ek olarak, değişiklikler sunucuya gönderildiğinde, diğer cihazlara değişiklikler hakkında bilgi vermek için bir yayın gönderirim, böylece daha hızlı senkronize olurlar. Senkronize edilen her bir varlığın GUID özelliği vardır (guid, exchange dosyaları için dosya adı olarak da kullanılır). Ayrıca her dosyanın Dropbox revizyonunu sakladığım Sync veritabanı var (Dropbox delta durumu sıfırladığında karşılaştırabilirim). Dosyalar ayrıca varlık adı, durum (silinmiş / silinmemiş), kılavuz (dosya adıyla aynı), veritabanı revizyonunu (veri taşımalarını algılamak veya hiçbir zaman uygulama sürümleriyle senkronize etmemek için) ve elbette verileri (satır silinmemişse) içerir. böylece tam veri bütünlüğü sağlamak için hangi dosyaları yüklemek için kolayca kontrol edebilirsiniz. Temel verilerde değişiklik yapıldıktan sonra senkronizasyon başlar. Herhangi bir değişiklik yoksa, Dropbox'ta birkaç dakikada bir ve uygulama başlangıcındaki değişiklikleri kontrol eder. Ek olarak, değişiklikler sunucuya gönderildiğinde, diğer cihazlara değişiklikler hakkında bilgi vermek için bir yayın gönderirim, böylece daha hızlı senkronize olurlar. Senkronize edilen her bir varlığın GUID özelliği vardır (guid, exchange dosyaları için dosya adı olarak da kullanılır). Ayrıca her dosyanın Dropbox revizyonunu sakladığım Sync veritabanı var (Dropbox delta durumu sıfırladığında karşılaştırabilirim). Dosyalar ayrıca varlık adı, durum (silinmiş / silinmemiş), kılavuz (dosya adıyla aynı), veritabanı revizyonunu (veri taşımalarını algılamak veya hiçbir zaman uygulama sürümleriyle senkronize etmemek için) ve elbette verileri (satır silinmemişse) içerir. böylece tam veri bütünlüğü sağlamak için hangi dosyaları yüklemek için kolayca kontrol edebilirsiniz. Temel verilerde değişiklik yapıldıktan sonra senkronizasyon başlar. Herhangi bir değişiklik yoksa, Dropbox'ta birkaç dakikada bir ve uygulama başlangıcındaki değişiklikleri kontrol eder. Ek olarak, değişiklikler sunucuya gönderildiğinde, diğer cihazlara değişiklikler hakkında bilgi vermek için bir yayın gönderirim, böylece daha hızlı senkronize olurlar. Senkronize edilen her bir varlığın GUID özelliği vardır (guid, exchange dosyaları için dosya adı olarak da kullanılır). Ayrıca her dosyanın Dropbox revizyonunu sakladığım Sync veritabanı var (Dropbox delta durumu sıfırladığında karşılaştırabilirim). Dosyalar ayrıca varlık adı, durum (silinmiş / silinmemiş), kılavuz (dosya adıyla aynı), veritabanı revizyonunu (veri taşımalarını algılamak veya hiçbir zaman uygulama sürümleriyle senkronize etmemek için) ve elbette verileri (satır silinmemişse) içerir.

Bu çözüm binlerce dosya ve yaklaşık 30 varlık için çalışıyor. Dropbox yerine daha sonra yapmak istediğim REST web hizmeti olarak anahtar / değer deposu kullanabilirdim, ancak bunun için zamanım yok :) Şimdilik, benim çözümüm iCloud'dan daha güvenilir ve çok önemli, Nasıl çalıştığı üzerinde tam kontrole sahibim (esas olarak kendi kodum olduğu için).

Başka bir çözüm, MOC değişikliklerini işlem olarak kaydetmektir - sunucu ile değiştirilen çok daha az dosya olacaktır, ancak ilk yüklemeyi boş çekirdek verilere uygun sırayla yapmak daha zordur. iCloud bu şekilde çalışıyor ve diğer senkronizasyon çözümlerinin de benzer yaklaşımı var, örn. TICoreDataSync .

-- GÜNCELLEME

Bir süre sonra Ensembles'e taşındım - bu çözümü tekerleği yeniden icat etmeyi öneriyorum.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.