در دو نوشته قبلی (اینجا و اینجا) به طور خیلی ساده نحوه کار کردن با OkHttp برای دریافت و ارسال اطلاعات از/به اینترنت گفتیم. در این مطلب کمی بیشتر با امکانات OkHttp کار میکنیم. امکاناتی که تقریباً همه جا به آنها نیاز خواهیم داشت.
آشنایی با HttpUrl:
مواقع زیادی پیش میآید که میخواهیم برای یک درخواست یک یا چند پارامتر هم ارسال کنیم. برای مثال میخواهیم لیست کالاهایی را ببینیم که تاریخ ثبت آنها در سرور بعد از یک تاریخ مشخص باشد. برای این کار باید در انتهای نشانی یا URL این پارامترها را اضافه کنیم. یک راه این است که این پارامترها را به انتهای URL اضافه کنیم و بعد مثل قبل درخواست (Request) را به سرور بفرستیم:
http://www.someurl.com?date=2016
اگر تعداد پارامترها بیشتر از یکی بود، آنها را با & از هم جدا میکنیم:
http://www.someurl.com?date=2016&brand=SONY
اگر تعداد پارامترها زیاد باشد، ساختن URL به این شکل سخت میشود و احتمال بروز خطا زیاد میشود. در این مواقع بهتر است از کلاس HttpUrl استفاده کنیم:
HttpUrl url = HttpUrl.parse("http://www.someurl.com"); HttpUrl.Builder urlBuilder = url.newBuilder(); urlBuilder.setQueryParameter("date", "2016"); urlBuilder.setQueryParameter("brand", "SONY"); Request.Builder requestBuilder = new Request.Builder(); requestBuilder.url(urlBuilder.build());
همانطور که میبینید استفاده از این کلاس بسیار سادهتر است و بسیار به ما در ساختن URL های پیچیده کمک میکند.
دسترسی به سرآیند یا header درخواست
سرآیند یا header در درخواستهای HTTP معمولاً مشابه Map<String,String> در جاوا است. هر فیلد سرآیند یا هدر فقط یک مقدار دارد و اگر مقدار جدیدی برای آن قرار داده شود، مقدار قبلی پاک میشود. برای دسترسی به سرآیند یا header به شکل زیر عمل میکنیم:
Request request = new Request.Builder() .url("https://api.github.com/repos/square/okhttp/issues") .header("User-Agent", "OkHttp Headers.java") .build();
با این حال بعضی وقتها پیش میآید که یه سرآیند یا هدر میتواند بیشتر از یک مقدار داشته باشد. این موضوع هم قانونی و مجاز و هم در بعضی مواقع ضروری است. در این مواقع به جای استفاده از متد header از متد addHeader استفاده میکنیم:
Request request = new Request.Builder() .url("https://api.github.com/repos/square/okhttp/issues") .header("User-Agent", "OkHttp Headers.java") .addHeader("Accept", "application/json; q=0.5") .addHeader("Accept", "application/vnd.github.v3+json") .build();
همانطور که در مثال بالا میبینید مقدار سرآیند یا هدر Accept دو مقدار متفاوت دارد.
پست کردن فایل
فرستادن یک فایل به سرور با استفاده OkHttp بسیار بسیار ساده است. فرض کنید میخواهیم فایل README.md را که فرمت آن متن مارک داون است به سرور بفرستیم. برای این کار از کلاس RequestBody استفاده میکنیم:
public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8"); private final OkHttpClient client = new OkHttpClient(); // ۱ File file = new File("README.md"); Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") // ۲ .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file)) .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
توضیح:
۱- ابتدا یک نمونه از کلاس File که نشانی و نام کامل فایل را به آن دادهایم میسازیم. البته بهتر است که وجود فایلی به آن اسم در آن نشانی را حتماً بررسی کنیم تا از وجود آن مطمئن شویم و اجرای کد به خطا نخورد. با این حال برای سادگی ما این قسمتها را حذف کردهایم و فرض میکنیم فایل README.md در مسیر جاری اجرای برنامه وجود دارد.
۲- با استفاده از RequestBody این فایل را به عنوان بدنه یا body درخواست ارسالی قرار میدهیم و با استفاده از تابع post فایل را به سرور میفرستیم.
توجه: دقت کنید که برای ارسال فایل نکات بسیار زیادی را باید بررسی کنید: آیا سرور آپلود فایل را میپذیرد؟ آیا حجم فایل از حجم فایل ارسالی تنظیم شده در سرور بیشتر نیست؟ با توجه به سرعت اینترنت، ارسال فایل چقدر زمان میبرد و آیا در این مدت کاربر میتواند فعالیت اصلی خود را دنبال کند؟ و سؤالهای زیادی از این دست.
پست کردن فرم
با استفاده از کلاس FormBody.Builder میتوان بدنه یا body درخواست را همانند تگ <form> در HTML ساخت. در این حالت نامها و مقادیر آنها به فرمت HTML ارسال میشوند.
private final OkHttpClient client = new OkHttpClient(); RequestBody formBody = new FormBody.Builder() .add("search", "Jurassic Park") .build(); Request request = new Request.Builder() .url("https://en.wikipedia.org/w/index.php") .post(formBody) .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
پست کردن فرم چندبخشی
کلاس MultipartBody.Builder میتواند بدنه یا body بسیار پیچیده برای درخواست بسازد که همسان با فرمهای آپلود فایل در HTML است. هر بخش از بدنه یک درخواست چندبخشی میتواند سرآیند یا هدر مخصوص به خودش را داشته باشد. سرآیندهای Content-Length و Content-Type اگر موجود باشند به صورت خودکار توسط OkHttp پر میشوند.
private static final String IMGUR_CLIENT_ID = "..."; private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png"); private final OkHttpClient client = new OkHttpClient(); RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("title", "Square Logo") .addFormDataPart("image", "logo-square.png", RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png"))) .build(); Request request = new Request.Builder() .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID) .url("https://api.imgur.com/3/image") .post(requestBody) .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
کش کردن پاسخ یا Response دریافتی
برای کش کردن پاسخ دریافتی باید یک شاخه فایل (File Directory) که هم میتوانید در آن بنویسید و هم میتوانید آن را بخوانید داشته باشید و هم اندازه یا حجم کش را. بهتر است شاخه کش یک شاخه خصوصی باشد تا برنامههایی که به آنها اطمینان نیست نتوانند آن را بخوانند.
اگر همزمان چند کش بخواهند به یک شاخه کش دسترسی داشته باشند خطا رخ خواهد داد. بیشتر برنامهها بهتر است فقط یک نمونه از OkHttpClient داشته باشند و کش آن را تنظیم کنند و بعد هر جا به آن نیاز داشتند فقط از همان نمونه موجود استفاده کنند. در غیر این صورت ممکن است دو کش همزمان با هم به کار بیفتند و یکدیگر یا کش پاسخ را خراب کنند و این احتمالاً باعث خرابی و از کارافتادگی برنامه میشود.
کش پاسخ برای همه تنظیمات از سرآیندهای HTTP استفاده میکند. میتوانید OkHttp را وادار کنید تا فقط از کش بخواند یا فقط از شبکه پاسخ را دریافت کند یا این که در صورت معتبر بودن کش از کش بخواند و در غیر این صورت از شبکه.
OkHttpClient client; // ۱ int cacheSize = 10 * 1024 * 1024; // 10 MiB // ۲ File cacheDirectory = new File("..."); // ۳ Cache cache = new Cache(cacheDirectory, cacheSize); client = new OkHttpClient.Builder() .cache(cache) .build(); // ۴ Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .build(); Response response1 = client.newCall(request).execute(); if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1); // ۵ String response1Body = response1.body().string(); System.out.println("Response 1 response: " + response1); System.out.println("Response 1 cache response:" + response1.cacheResponse()); System.out.println("Response 1 network response:" + response1.networkResponse()); Response response2 = client.newCall(request).execute(); if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2); // ۶ String response2Body = response2.body().string(); System.out.println("Response 2 response:" + response2); System.out.println("Response 2 cache response:" + response2.cacheResponse()); System.out.println("Response 2 network response:" + response2.networkResponse()); // ۷ System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
توضیح:
۱- اندازه کش را مشخص میکنیم. در این مثال اندازه کش ۱۰ مگابایت است.
۲- شاخه کش را مشخص میکنیم.
۳- یک نمونه از کلاس Cache میسازیم و شاخه کش و اندازه آن را به این نمونه میدهیم. سپس با استفاده از کلاس OkHttpClient.Builder یک نمونه از کلاس OkHttpClient میسازیم.
۴- حالا یک درخواست ایجاد میکنیم و آن را اجرا میکنیم.
۵- بررسی میکنیم تا ببینیم آیا در اولین اجرای این درخواست، آیا از کش استفاده شده است یا نه؟ قطعاً اولین درخواست به این فایل کش نشده و درخواست باید فایل را از اینترنت بگیرد و آن را کش کند.
۶- حالا یک بار دیگر همان درخواست را اجرا میکنیم. این بار چون اطلاعات قبلاً کش شده است، درخواست به اینترنت نمیرود و از کش اطلاعات گرفته میشود.
۷- پاسخی که از اینترنت گرفتهایم را با پاسخی که از کش گرفتهایم مقایسه میکنیم! قاعدتاً هر دو باید با هم برابر باشند!
اگر بخواهیم یک درخواست را مجبور کنیم که حتماً پاسخ را اینترنت بگیرد و کش را نادیده بگیرد، از CacheControl.FORCE_NETWORK استفاده میکنیم. اگر بخواهیم درخواست فقط از کش پاسخ را بگیرد، از CacheControl.FORCE_CACHE استفاده میکنیم. توجه داشته باشید که اگر از FORCE_CACHE استفاده کنید و اطلاعات درخواستی در کش موجود نباشد، OkHttp پاسخ ۵۰۴ Unsatisfiable Request را میدهد و به سراغ اینترنت نخواهد رفت:
// ۴ Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .cacheControl(CacheControl.FORCE_NETWORK) .build(); // OR Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .cacheControl(CacheControl.FORCE_CACHE) .build();
لغو کردن یک فراخوان یا Call
برای لغو کردن فوری یک فراخوان یا Call از متد Call.cancel() استفاده میکنیم. اگر یک ترد (Thread) همین حالا در حال نوشتن در درخواست یا خواندن از پاسخ باشد، یک خطای IOException میگیرد. برای صرفهجویی در شبکه وقتی که دیگر به این فراخوانی نیازی نیست، از این روش استفاده کنید. مثلاً وقتی که کاربر برنامه را ترک میکند. هم فراخوانهای همزمان و هم فراخوانهای ناهمزمان را میشود با این روش لغو یا کنسل کرد.
// ۱ OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build(); // ۲ final long startNanos = System.nanoTime(); // ۳ final Call call = client.newCall(request); // ۴ ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); executor.schedule(new Runnable() { @Override public void run() { System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f); // ۶ call.cancel(); System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f); } }, ۱, TimeUnit.SECONDS); try { System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f); // ۵ Response response = call.execute(); System.out.printf("%.2f Call was expected to fail, but completed: %s%n", (System.nanoTime() - startNanos) / 1e9f, response); } catch (IOException e) { // ۷ System.out.printf("%.2f Call failed as expected: %s%n", (System.nanoTime() - startNanos) / 1e9f, e); }
توضیح:
۱- یک نمونه از OkHttpClient و Request میسازیم. آدرسی که به درخواست یا Request میدهیم، دو ثانیه درخواست را معطل میکند و بعد پاسخ میدهد.
۲- زمان شروع فراخوان را ذخیره میکنیم. از این زمان برای اندازهگیری دقیق زمان شروع و لغو فراخوان استفاده میکنیم.
۳- یک نمونه از کلاس Call را با استفاده از Request آماده شده میسازیم.
۴- یک نمونه از کلاس ScheduledExecutorService میسازیم. کار این نمونه این است که یک ثانیه بعد از اجرای فراخوان اجرا شده و فراخوان را لغو کند.
۵- فراخوان را اجرا میکنیم.
۶- یک ثانیه پس از اجرای فراخوان، نمونه ScheduledExecutorService فعال شده و فراخوان را لغو میکند.
۷- اگر پیش از اتمام فراخوان، موفق به لغو فراخوان بشویم (همان چیزی که ما در این مثال انتظار داریم اتفاق بیافتد)، خطای IOException رخ میدهد.
تنظیم تایماوت برای درخواستها
بعضی وقتها اتصال اینترنتی برقرار نیست یا سرور در دسترس نیست. در چنین مواقعی میتوانید به کمک تایماوت، فراخوانهای به نتیجه نرسیده را لغو کنید. OkHttp از تایماوت اتصال، خواندن و نوشتن پشتیبانی میکند.
OkHttpClient client; client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); Request request = new Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build(); Response response = client.newCall(request).execute(); System.out.println("Response completed: " + response);
حالا خیلی چیزها از OkHttp میدانید ولی هنوز باید چیزهای بیشتری یاد بگیرید! مبحث اینترنت هنوز سه قسمت دیگر هم دارد! با ما باشید!
سلام مهندس امکانش هست که سورس کد این آموزش رو برامون بزارید؟خیلی لازممه
در ادامه یک پروژه با هم میسازیم و از همه این آموزشها در آن استفاده میکنیم
درود بر اساتید بزرگوار و سپاس برای نشر آموزش های خوبتان.
جناب مهندس قرار بود برای این مطالب بخش اینترنت، یک پروژه کار کنید و از این آموزش های خوبتون در این پروژه استفاده کنید! کی این پروژه رو در سایت قرار می دهید؟
چون من این مطالب را خواندم اما به صورت عملی نمی دونم که چطور باید از اینها استفاده کنم!!
خیلی سپاسگذارم.
عرض سلام و ادب،
با تشکر از آموزش های خوبتون، جناب بهزادیان این پروژه ای که فرمودید همه اموزش های بخش اینترنت رو در اون پیاده میکنید، کاش لطف میکردید و میذاشتید، عالی میشد جمع بندی و پیاده سازی عملی همه این مباحثی بود که زحمت کشیدید و خیلی خوب بیان کردید.
مدتها است به علت مشغله زیاد فرصت نوشتن نداشتم متاسفانه
بله کاملا درک میکنم. اما حیفه اسمارت لب هست که محتواش بروز نشه… من به لطف آموزش های شما و با کمک منابع دیگه تونستم اپ ام رو پیاده سازی کنم. اما معماری جدید اندروید jetpack مباحث جدید زیادی داره که جاش توی اسمارت لب خالیه. انشالله که فرصت پرداختن به این مباحث رو هم پیدا کنید. از زحماتتون ممنون.
با سلام. ممنون از آموزش خوبتون. تنها جایی که متوجه نشدم قسمت header ها هست. اینا اصلا چرا باید نوشته بشن و چه کاربردی دارند؟ میشه یه مقدار دربارش توضیح بدید
این مطلب با این فرض نوشته شده است که با مفاهیم پروتکل HTTP اشنا هستید. اگر آشنا نیستید مطالعه بیشتر در این زمینه را توصیه میکنم:
https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
سلام و خسته نباشید
این قسمت را خیلی بد توضیح دادین خیلی نامفهومه بعضی کد هارو اصلا نگفتین کجا نوشته شه
لطفا این رو اصلاحش کنین
ممنونم