Dosya ve İlişkili Verileri RESTful Web Hizmetine tercihen JSON olarak gönderme


757

Bu muhtemelen aptalca bir soru olacak ama o gecelerden birini yaşıyorum. Bir uygulamada RESTful API geliştiriyorum ve istemcinin verileri JSON olarak göndermesini istiyoruz. Bu uygulamanın bir kısmı, istemcinin bir dosya (genellikle bir resim) yanı sıra resim hakkındaki bilgileri yüklemesini gerektirir.

Bunun tek bir istekte nasıl olduğunu takip etmekte zorlanıyorum. Base64 dosya verisi bir JSON dizesi içine mümkün mü? Sunucuya 2 mesaj göndermem gerekecek mi? Bunun için JSON kullanmamalı mıyım?

Bir yan not olarak, arka uçta Grails kullanıyoruz ve bunlardan herhangi biri fark yaratıyorsa bu hizmetlere yerel mobil istemciler (iPhone, Android vb.) Tarafından erişiliyor.


1
Peki, bunu yapmanın en iyi yolu nedir?
James111

3
Meta verileri JSON yerine URL sorgu dizesinde gönderin.
jrc

Yanıtlar:


632

Burada benzer bir soru sordum:

REST web hizmeti kullanarak meta veri içeren bir dosyayı nasıl yüklerim?

Temel olarak üç seçeneğiniz var:

  1. Base64, veri boyutunu yaklaşık% 33 artırma pahasına dosyayı kodlar ve kodlama / kod çözme için hem sunucuda hem de istemcide işleme yükü ekler.
  2. Dosyayı önce multipart/form-dataPOST'ta gönderin ve istemciye bir kimlik döndürün. İstemci daha sonra meta verileri kimliğe gönderir ve sunucu dosyayı ve meta verileri yeniden ilişkilendirir.
  3. Önce meta verileri gönderin ve istemciye bir kimlik döndürün. İstemci daha sonra dosyayı kimliğe gönderir ve sunucu dosyayı ve meta verileri yeniden ilişkilendirir.

29
Seçenek 1'i seçersem, Base64 içeriğini yalnızca JSON dizesine ekler miyim? {file: '234JKFDS # $ @ # $ MFDDMS ....', adı: 'somename' ...} Yoksa başka bir şey mi var?
Gregg

15
Gregg, tam olarak söylediğin gibi, onu bir özellik olarak eklersin ve değer base64 kodlu dize olur. Bu muhtemelen en kolay yöntemdir, ancak dosya boyutuna bağlı olarak pratik olmayabilir. Örneğin, uygulamamız için, her biri 2-3 MB olan iPhone görüntüleri göndermemiz gerekiyor. % 33'lük bir artış kabul edilemez. Yalnızca küçük 20 KB resimler gönderiyorsanız, bu ek yük daha kabul edilebilir olabilir.
Daniel T.

19
Ayrıca base64 kodlama / kod çözme işlemlerini de biraz zaman alacaktır. Yapılması en kolay şey olabilir, ama kesinlikle en iyisi değil.
Daniel T.

8
base64 ile json? hmm .. Ben çok parçalı / form yapışmasını düşünüyorum
Omnipresent

12
Neden tek bir istekte çok parçalı / form verisi kullanılması reddediliyor?
1nstinct

107

Çok parçalı / form verisi içerik türünü kullanarak dosyayı ve verileri tek bir istekte gönderebilirsiniz :

Birçok uygulamada, bir kullanıcıya bir form sunulması mümkündür. Kullanıcı, yazılan, kullanıcı girdisi tarafından oluşturulan veya kullanıcının seçtiği dosyalardan gelen bilgiler dahil olmak üzere formu dolduracaktır. Form doldurulduğunda, formdaki veriler kullanıcıdan gelen uygulamaya gönderilir.

MultiPart / Form-Data tanımı, bu uygulamalardan birinden türetilir ...

Gönderen http://www.faqs.org/rfcs/rfc2388.html :

"çok parçalı / form verisi" bir dizi parça içerir. Her bölümün, düzenleme türünün "form-data" olduğu ve düzenlemenin, "parametrenin" değerinin orijinal olduğu (ad) bir "ek" parametresi içerdiği bir içerik düzenleme başlığı [RFC 2183] içermesi beklenir. formdaki alan adı. Örneğin, bir parça bir başlık içerebilir:

İçerik-Elden Çıkarma: form verileri; name = "kullanıcı"

"kullanıcı" alanının girişine karşılık gelen değerle.

Sınırlar arasındaki her bölüme dosya bilgileri veya alan bilgileri ekleyebilirsiniz. Kullanıcının hem veri hem de form göndermesini gerektiren RESTful hizmetini başarıyla uyguladım ve çok bölümlü / form verileri mükemmel çalıştı. Hizmet Java / Spring kullanılarak oluşturuldu ve istemci C # kullanıyordu, bu yüzden ne yazık ki hizmetin nasıl kurulacağı konusunda size verecek herhangi bir Grails örneğim yok. Bu durumda JSON kullanmanıza gerek yoktur, çünkü her "form-veri" bölümü size parametrenin adını ve değerini belirtmek için bir yer sağlar.

Çok bölümlü / form verilerinin kullanılmasıyla ilgili iyi bir şey, HTTP tanımlı üstbilgiler kullanmanızdır, bu nedenle hizmetinizi oluşturmak için mevcut HTTP araçlarını kullanma REST felsefesine bağlı kalıyorsunuz.


1
Teşekkürler, ancak sorum istek için JSON'u kullanmak istemeye ve bunun mümkün olup olmadığına odaklandı. Önerdiğin gibi gönderebileceğimi zaten biliyorum.
Gregg

15
Evet, esasen "Bunun için JSON kullanmamalı mıyım?" İstemcinin JSON kullanmasını istediğiniz belirli bir neden var mı?
McStretch

3
Büyük olasılıkla bir iş gereksinimi veya tutarlılık. Elbette yapılacak ideal şey, Content-Type HTTP üstbilgisine dayalı olarak hem (form verisi hem de JSON yanıtı) kabul etmektir.
Daniel T.

2
JSON'un seçilmesi, hem istemci hem de sunucu tarafında çok daha zarif bir kodla sonuçlanır, bu da daha az potansiyel hataya yol açar. Form verileri dün çok.
superarts.org

5
Bazı .Net geliştiricisinin hissine zarar verirse söylediğim için özür dilerim. İngilizce benim ana dilim olmasa da, teknolojinin kendisi hakkında kaba bir şey söylemek benim için geçerli bir bahane değil. Form verilerini kullanmak harika ve kullanmaya devam ederseniz daha da harika olacaksınız!
superarts.org

53

Bu konu oldukça eski olduğunu biliyorum, ancak, burada bir seçenek eksik. Yüklenecek verilerle birlikte göndermek istediğiniz meta verileriniz (herhangi bir biçimde) varsa, tek bir multipart/relatedistekte bulunabilirsiniz.

Çok Parçalı / İlgili ortam tipi, birbiriyle ilişkili birkaç vücut parçasından oluşan bileşik nesneler için tasarlanmıştır.

Daha ayrıntılı bilgi için RFC 2387 teknik özelliklerini kontrol edebilirsiniz .

Temel olarak, böyle bir talebin her bir kısmı farklı türde içeriğe sahip olabilir ve tüm parçalar bir şekilde ilişkilidir (örneğin bir görüntü ve meta verileri). Parçalar bir sınır dizgisi ile tanımlanır ve son sınır dizgisini iki kısa çizgi izler.

Misal:

POST /upload HTTP/1.1
Host: www.hostname.com
Content-Type: multipart/related; boundary=xyz
Content-Length: [actual-content-length]

--xyz
Content-Type: application/json; charset=UTF-8

{
    "name": "Sample image",
    "desc": "...",
    ...
}

--xyz
Content-Type: image/jpeg

[image data]
[image data]
[image data]
...
--foo_bar_baz--

Çözümünüzü çok beğendim. Ne yazık ki, tarayıcıda mutlipart / ilgili istekler oluşturmanın bir yolu yok gibi görünüyor.
Petr Baudis

Bu şekilde api ile iletişim kurmak için (özellikle JS olanları) istemcilere alma konusunda deneyiminiz var mı
pvgoddijn 29:07

ne yazık ki, şu anda php (7.2.1) bu tür veriler için okuyucu yok ve kendi ayrıştırıcı oluşturmak zorunda kalacak
dewd

Sunucuların ve istemcilerin bunun için iyi destekleri olmaması üzücü.
Nader Ghanbari

14

Bu sorunun eski olduğunu biliyorum, ancak son günlerde aynı soruyu çözmek için tüm web'de arama yaptım. Resimler, başlık ve açıklama gönderen grails REST webservices ve iPhone Client var.

Yaklaşımımın en iyisi olup olmadığını bilmiyorum, ama çok kolay ve basit.

UIImagePickerController kullanarak bir resim çekmek ve resmin veri göndermek için istek başlık etiketleri kullanarak NSData sunucuya gönderirim.

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"myServerAddress"]];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:UIImageJPEGRepresentation(picture, 0.5)];
[request setValue:@"image/jpeg" forHTTPHeaderField:@"Content-Type"];
[request setValue:@"myPhotoTitle" forHTTPHeaderField:@"Photo-Title"];
[request setValue:@"myPhotoDescription" forHTTPHeaderField:@"Photo-Description"];

NSURLResponse *response;

NSError *error;

[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

Sunucu tarafında, kodu kullanarak fotoğrafı alıyorum:

InputStream is = request.inputStream

def receivedPhotoFile = (IOUtils.toByteArray(is))

def photo = new Photo()
photo.photoFile = receivedPhotoFile //photoFile is a transient attribute
photo.title = request.getHeader("Photo-Title")
photo.description = request.getHeader("Photo-Description")
photo.imageURL = "temp"    

if (photo.save()) {    

    File saveLocation = grailsAttributes.getApplicationContext().getResource(File.separator + "images").getFile()
    saveLocation.mkdirs()

    File tempFile = File.createTempFile("photo", ".jpg", saveLocation)

    photo.imageURL = saveLocation.getName() + "/" + tempFile.getName()

    tempFile.append(photo.photoFile);

} else {

    println("Error")

}

Gelecekte sorun yaşayıp yaşamadığımı bilmiyorum ama şimdi üretim ortamında iyi çalışıyor.


1
Http başlıklarını kullanma seçeneğini seviyorum. Bu, özellikle meta veriler ve standart http başlıkları arasında bir simetri olduğunda iyi çalışır, ancak açıkça kendi başınızı icat edebilirsiniz.
EJ Campbell

14

İşte benim yaklaşım API (örnek kullanıyorum) - gördüğünüz gibi API ( file_idsunucuya yüklenen dosya tanımlayıcısı) kullanmıyorum :

  1. photoSunucuda nesne oluştur :

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. Dosya yükle ( filefotoğraf başına yalnızca bir tane olduğu için tekil formda olduğunu unutmayın ):

    POST: /projects/{project_id}/photos/{photo_id}/file
    body: file to upload
    response: -

Ve sonra örneğin:

  1. Fotoğraf listesini oku

    GET: /projects/{project_id}/photos
    response: [ photo, photo, photo, ... ] (array of objects)
  2. Bazı fotoğraf ayrıntılarını okuyun

    GET: /projects/{project_id}/photos/{photo_id}
    response: { id: 666, name: 'some_schema.jpg', comment:'blah'} (photo object)
  3. Fotoğraf dosyasını okuyun

    GET: /projects/{project_id}/photos/{photo_id}/file
    response: file content

Sonuç olarak, POST ile önce bir nesne (fotoğraf) oluşturursunuz ve daha sonra dosya ile ikinci bir istek gönderirsiniz (yine POST).


3
Bu, bunu başarmanın daha 'RESTFUL' yolu gibi görünüyor.
James Webster

Yeni oluşturulan kaynaklar için POST işlemi, nesnenin basit sürüm detaylarında konum kimliği döndürmelidir
Ivan Proskuryakov

@ivanproskuryakov neden "zorunluluk"? Yukarıdaki örnekte (2. maddede POST) dosya kimliği işe yaramaz. İkinci argüman (2. noktadaki POST için) tekil form '/ file' ('/ files' değil) kullanıyorum, bu nedenle kimlik gerekli değil çünkü yol: / projects / 2 / photos / 3 / file kimlik fotoğraf dosyasına FULL bilgi veriyor.
Kamil Kiełczewski

HTTP protokol belirtiminden. w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Oluşturuldu "Yeni oluşturulan kaynağa, yanıtın varlığında döndürülen URI (ler) tarafından başvuruda bulunulan kaynak için en spesifik URI tarafından başvurulabilir bir Konum başlığı alanı. " @ KamilKiełczewski (bir) ve (iki) bir POST işlemi olarak birleştirilebilir POST: / projects / {project_id} / photos Tek bir fotoğraf (kaynak *) işlemi için kullanılabilecek konum başlığını döndürür GET: tüm detayları içeren tek bir fotoğraf
CGET:

1
Meta veriler ve yükleme ayrı işlemlerse, uç noktalarda şu sorunlar vardır: Dosya yükleme için POST işlemi kullanılır - POST, idempotent değildir. Yeni bir kaynak oluşturmadan kaynağı değiştirdiğiniz için PUT (idempotent) kullanılmalıdır. REST, kaynak adı verilen nesnelerle çalışır . POST: “../photos/“ PUT: “../photos/{photo_id}” GET: “../photos/“ GET: “../photos/{photo_id}” PS. Yüklemeyi ayrı bir son noktaya ayırmak, öngörülemeyen davranışlara yol açabilir. restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resources.html
Ivan Proskuryakov

6

FormData Nesneleri: Ajax Kullanarak Dosya Yükleme

XMLHttpRequest Düzey 2, yeni FormData arabirimi için destek ekler. FormData nesneleri, form alanlarını ve değerlerini temsil eden bir dizi anahtar / değer çiftini kolayca oluşturmanın bir yolunu sağlar; bunlar daha sonra XMLHttpRequest send () yöntemi kullanılarak kolayca gönderilebilir.

function AjaxFileUpload() {
    var file = document.getElementById("files");
    //var file = fileInput;
    var fd = new FormData();
    fd.append("imageFileData", file);
    var xhr = new XMLHttpRequest();
    xhr.open("POST", '/ws/fileUpload.do');
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
             alert('success');
        }
        else if (uploadResult == 'success')
             alert('error');
    };
    xhr.send(fd);
}

https://developer.mozilla.org/en-US/docs/Web/API/FormData


6

Tek eksik örnek ANDROID örneği olduğu için ekleyeceğim. Bu teknik, Activity sınıfınızda bildirilmesi gereken özel bir AsyncTask kullanır.

private class UploadFile extends AsyncTask<Void, Integer, String> {
    @Override
    protected void onPreExecute() {
        // set a status bar or show a dialog to the user here
        super.onPreExecute();
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        // progress[0] is the current status (e.g. 10%)
        // here you can update the user interface with the current status
    }

    @Override
    protected String doInBackground(Void... params) {
        return uploadFile();
    }

    private String uploadFile() {

        String responseString = null;
        HttpClient httpClient = new DefaultHttpClient();
        HttpPost httpPost = new HttpPost("http://example.com/upload-file");

        try {
            AndroidMultiPartEntity ampEntity = new AndroidMultiPartEntity(
                new ProgressListener() {
                    @Override
                        public void transferred(long num) {
                            // this trigger the progressUpdate event
                            publishProgress((int) ((num / (float) totalSize) * 100));
                        }
            });

            File myFile = new File("/my/image/path/example.jpg");

            ampEntity.addPart("fileFieldName", new FileBody(myFile));

            totalSize = ampEntity.getContentLength();
            httpPost.setEntity(ampEntity);

            // Making server call
            HttpResponse httpResponse = httpClient.execute(httpPost);
            HttpEntity httpEntity = httpResponse.getEntity();

            int statusCode = httpResponse.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                responseString = EntityUtils.toString(httpEntity);
            } else {
                responseString = "Error, http status: "
                        + statusCode;
            }

        } catch (Exception e) {
            responseString = e.getMessage();
        }
        return responseString;
    }

    @Override
    protected void onPostExecute(String result) {
        // if you want update the user interface with upload result
        super.onPostExecute(result);
    }

}

Bu nedenle, dosyanızı yüklemek istediğinizde şunu arayın:

new UploadFile().execute();

Merhaba, AndroidMultiPartEntity nedir lütfen açıklayın ... ve ne yapmam gerektiğini pdf, word veya xls dosyası yüklemek istiyorsanız, lütfen biraz rehberlik verin ... bu konuda yeniyim.
amit pandya

1
@amitpandya Kodu genel bir dosya yüklemesi olarak değiştirdim, bu yüzden okuyan herkes için daha açık
lifeisfoo

2

Arka uç sunucusuna bazı dizeler göndermek istedim. Json'u multipart ile kullanmadım, istek parametrelerini kullandım.

@RequestMapping(value = "/upload", method = RequestMethod.POST)
public void uploadFile(HttpServletRequest request,
        HttpServletResponse response, @RequestParam("uuid") String uuid,
        @RequestParam("type") DocType type,
        @RequestParam("file") MultipartFile uploadfile)

URL şöyle görünür

http://localhost:8080/file/upload?uuid=46f073d0&type=PASSPORT

Dosya yükleme ile birlikte iki params (uuid ve tip) geçiyorum. Umarım bu, gönderilecek karmaşık json verilerine sahip olmayanlara yardımcı olacaktır.


1

Https://square.github.io/okhttp/ kütüphanesini kullanmayı deneyebilirsiniz . İstek gövdesini çok bölümlü olarak ayarlayabilir ve ardından dosya ve json nesnelerini aşağıdaki gibi ayrı ayrı ekleyebilirsiniz:

MultipartBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("uploadFile", uploadFile.getName(), okhttp3.RequestBody.create(uploadFile, MediaType.parse("image/png")))
                .addFormDataPart("file metadata", json)
                .build();

        Request request = new Request.Builder()
                .url("https://uploadurl.com/uploadFile")
                .post(requestBody)
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

            logger.info(response.body().string());

0
@RequestMapping(value = "/uploadImageJson", method = RequestMethod.POST)
    public @ResponseBody Object jsongStrImage(@RequestParam(value="image") MultipartFile image, @RequestParam String jsonStr) {
-- use  com.fasterxml.jackson.databind.ObjectMapper convert Json String to Object
}

-5

Lütfen aşağıdaki içe aktarma işlemine sahip olduğunuzdan emin olun. Tabii diğer standart ithalatlar

import org.springframework.core.io.FileSystemResource


    void uploadzipFiles(String token) {

        RestBuilder rest = new RestBuilder(connectTimeout:10000, readTimeout:20000)

        def zipFile = new File("testdata.zip")
        def Id = "001G00000"
        MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>()
        form.add("id", id)
        form.add('file',new FileSystemResource(zipFile))
        def urld ='''http://URL''';
        def resp = rest.post(urld) {
            header('X-Auth-Token', clientSecret)
            contentType "multipart/form-data"
            body(form)
        }
        println "resp::"+resp
        println "resp::"+resp.text
        println "resp::"+resp.headers
        println "resp::"+resp.body
        println "resp::"+resp.status
    }

1
Bu getjava.lang.ClassCastException: org.springframework.core.io.FileSystemResource cannot be cast to java.lang.String
Mariano Ruiz
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.