Đăng một tệp và dữ liệu được liên kết lên một dịch vụ web RESTful tốt nhất là JSON


757

Đây có lẽ sẽ là một câu hỏi ngu ngốc nhưng tôi đang có một trong những đêm đó. Trong một ứng dụng tôi đang phát triển API RESTful và chúng tôi muốn khách hàng gửi dữ liệu dưới dạng JSON. Một phần của ứng dụng này yêu cầu khách hàng tải lên một tệp (thường là hình ảnh) cũng như thông tin về hình ảnh.

Tôi đang gặp khó khăn trong việc theo dõi làm thế nào điều này xảy ra trong một yêu cầu. Có thể Base64 dữ liệu tệp thành một chuỗi JSON không? Tôi sẽ cần phải thực hiện 2 bài viết đến máy chủ? Tôi có nên sử dụng JSON cho việc này không?

Lưu ý phụ, chúng tôi đang sử dụng Grails trên phần phụ trợ và các dịch vụ này được truy cập bởi các máy khách di động gốc (iPhone, Android, v.v.), nếu bất kỳ điều nào tạo ra sự khác biệt.


1
Vì vậy, cách tốt nhất để làm điều này là gì?
James111

3
Gửi siêu dữ liệu trong chuỗi truy vấn URL, thay vì JSON.
ngày

Câu trả lời:


632

Tôi đã hỏi một câu hỏi tương tự ở đây:

Làm cách nào để tải lên tệp có siêu dữ liệu bằng dịch vụ web REST?

Về cơ bản, bạn có ba lựa chọn:

  1. Base64 mã hóa tệp, với chi phí tăng kích thước dữ liệu lên khoảng 33% và thêm chi phí xử lý trong cả máy chủ và máy khách để mã hóa / giải mã.
  2. Gửi tệp đầu tiên trong multipart/form-dataPOST và trả lại ID cho khách hàng. Sau đó, máy khách sẽ gửi siêu dữ liệu với ID và máy chủ sẽ liên kết lại tệp và siêu dữ liệu.
  3. Gửi siêu dữ liệu trước và trả lại ID cho khách hàng. Sau đó, khách hàng sẽ gửi tệp có ID và máy chủ liên kết lại tệp và siêu dữ liệu.

29
Nếu tôi chọn tùy chọn 1, tôi có chỉ bao gồm nội dung Base64 bên trong chuỗi JSON không? {file: '234JKFDS # $ @ # $ MFDDMS ....', tên: 'somename' ...} Hay còn điều gì nữa không?
Gregg

15
Gregg, chính xác như bạn đã nói, bạn sẽ chỉ đưa nó làm tài sản và giá trị sẽ là chuỗi được mã hóa base64. Đây có lẽ là phương pháp dễ nhất để đi cùng, nhưng có thể không thực tế tùy thuộc vào kích thước tệp. Ví dụ: đối với ứng dụng của chúng tôi, chúng tôi cần gửi hình ảnh iPhone mỗi tệp 2-3 MB. Mức tăng 33% là không thể chấp nhận được. Nếu bạn chỉ gửi hình ảnh 20KB nhỏ, chi phí đó có thể được chấp nhận hơn.
Daniel T.

19
Tôi cũng nên đề cập rằng mã hóa / giải mã cơ sở64 cũng sẽ mất một thời gian xử lý. Nó có thể là điều dễ nhất để làm, nhưng nó chắc chắn không phải là tốt nhất.
Daniel T.

8
json với cơ sở64? hmm .. Tôi đang suy nghĩ về việc gắn bó với nhiều hình thức / hình thức
khắp nơi vào

12
Tại sao từ chối sử dụng nhiều dữ liệu / biểu mẫu dữ liệu trong một yêu cầu?
1

107

Bạn có thể gửi tệp và dữ liệu trong một yêu cầu bằng cách sử dụng loại nội dung nhiều dữ liệu / biểu mẫu :

Trong nhiều ứng dụng, người dùng có thể được trình bày với một biểu mẫu. Người dùng sẽ điền vào biểu mẫu, bao gồm thông tin được nhập, được tạo bởi đầu vào của người dùng hoặc được bao gồm từ các tệp mà người dùng đã chọn. Khi biểu mẫu được điền, dữ liệu từ biểu mẫu được gửi từ người dùng đến ứng dụng nhận.

Định nghĩa của MultiPart / Form-Data được lấy từ một trong những ứng dụng đó ...

Từ http://www.faqs.org/rfcs/rfc2388.html :

"Đa dữ liệu / biểu mẫu dữ liệu" chứa một loạt các phần. Mỗi phần được dự kiến ​​chứa tiêu đề xử lý nội dung [RFC 2183] trong đó loại sắp xếp là "dữ liệu biểu mẫu" và trong đó bố trí chứa tham số (bổ sung) của "tên", trong đó giá trị của tham số đó là gốc tên trường trong mẫu. Ví dụ: một phần có thể chứa một tiêu đề:

Nội dung-Bố trí: hình thức-dữ liệu; tên = "người dùng"

với giá trị tương ứng với mục nhập của trường "người dùng".

Bạn có thể bao gồm thông tin tệp hoặc thông tin trường trong mỗi phần giữa các ranh giới. Tôi đã triển khai thành công dịch vụ RESTful yêu cầu người dùng gửi cả dữ liệu và biểu mẫu và dữ liệu đa dữ liệu / biểu mẫu hoạt động hoàn hảo. Dịch vụ được xây dựng bằng Java / Spring và ứng dụng khách đang sử dụng C #, vì vậy thật không may, tôi không có bất kỳ ví dụ Grails nào để cung cấp cho bạn về cách thiết lập dịch vụ. Bạn không cần sử dụng JSON trong trường hợp này vì mỗi phần "dữ liệu biểu mẫu" cung cấp cho bạn một nơi để chỉ định tên của tham số và giá trị của nó.

Điểm hay của việc sử dụng dữ liệu đa dữ liệu / biểu mẫu là bạn đang sử dụng các tiêu đề do HTTP xác định, do đó, bạn đang gắn bó với triết lý REST sử dụng các công cụ HTTP hiện có để tạo dịch vụ của bạn.


1
Cảm ơn, nhưng câu hỏi của tôi tập trung vào việc muốn sử dụng JSON cho yêu cầu và nếu điều đó là có thể. Tôi đã biết rằng tôi có thể gửi nó theo cách bạn đề xuất.
Gregg

15
Vâng, đó thực chất là câu trả lời của tôi cho "Tôi có nên không sử dụng JSON cho việc này không?" Có một lý do cụ thể tại sao bạn muốn khách hàng sử dụng JSON không?
McStretch

3
Nhiều khả năng là một yêu cầu kinh doanh hoặc giữ sự nhất quán. Tất nhiên, điều lý tưởng cần làm là chấp nhận cả hai (dữ liệu biểu mẫu và phản hồi JSON) dựa trên tiêu đề HTTP Kiểu nội dung.
Daniel T.

2
Việc chọn JSON dẫn đến mã thanh lịch hơn nhiều ở cả phía máy khách và máy chủ, điều này dẫn đến các lỗi ít tiềm năng hơn. Dữ liệu mẫu là như vậy ngày hôm qua.
superarts.org

5
Tôi xin lỗi vì những gì tôi đã nói nếu điều đó làm tổn thương cảm giác của một số nhà phát triển .Net. Mặc dù tiếng Anh không phải là ngôn ngữ mẹ đẻ của tôi, nhưng đó không phải là lý do hợp lệ để tôi nói điều gì đó thô lỗ về chính công nghệ. Sử dụng dữ liệu biểu mẫu là tuyệt vời và nếu bạn tiếp tục sử dụng nó, bạn sẽ còn tuyệt vời hơn nữa!
superarts.org

53

Tôi biết rằng chủ đề này khá cũ, tuy nhiên, tôi thiếu ở đây một tùy chọn. Nếu bạn có siêu dữ liệu (ở bất kỳ định dạng nào) mà bạn muốn gửi cùng với dữ liệu để tải lên, bạn có thể thực hiện một multipart/relatedyêu cầu.

Loại đa phương tiện / Phương tiện liên quan được dành cho các đối tượng hỗn hợp bao gồm một số bộ phận cơ thể liên quan đến nhau.

Bạn có thể kiểm tra thông số kỹ thuật RFC 2387 để biết thêm chi tiết chuyên sâu.

Về cơ bản mỗi phần của một yêu cầu như vậy có thể có nội dung với loại khác nhau và tất cả các phần có liên quan bằng cách nào đó (ví dụ: hình ảnh và siêu dữ liệu). Các phần được xác định bởi một chuỗi ranh giới và chuỗi ranh giới cuối cùng được theo sau bởi hai dấu gạch nối.

Thí dụ:

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--

Tôi thích giải pháp của bạn tốt nhất cho đến nay. Thật không may, dường như không có cách nào để tạo các yêu cầu liên quan / liên quan đến trình duyệt.
Petr Baudis

Bạn có bất kỳ kinh nghiệm nào trong việc đưa khách hàng đến (đặc biệt là những người JS) để giao tiếp với api theo cách này không
pvgoddijn

không may, hiện nay không có người đọc cho loại dữ liệu này trên php (7.2.1) và bạn sẽ phải xây dựng phân tích cú pháp của riêng bạn
dewd

Thật đáng buồn khi các máy chủ và máy khách không có sự hỗ trợ tốt cho việc này.
Nader Ghanbari

14

Tôi biết câu hỏi này đã cũ, nhưng trong những ngày qua tôi đã tìm kiếm toàn bộ trang web để giải quyết câu hỏi tương tự. Tôi có grails REST webservice và iPhone Client gửi hình ảnh, tiêu đề và mô tả.

Tôi không biết cách tiếp cận của tôi là tốt nhất, nhưng rất dễ dàng và đơn giản.

Tôi chụp ảnh bằng UIImagePickerControll và gửi đến máy chủ NSData bằng các thẻ tiêu đề yêu cầu để gửi dữ liệu của ảnh.

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];

Ở phía máy chủ, tôi nhận được ảnh bằng mã:

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")

}

Tôi không biết mình có vấn đề gì trong tương lai không, nhưng hiện tại đang hoạt động tốt trong môi trường sản xuất.


1
Tôi thích tùy chọn này sử dụng tiêu đề http. Điều này đặc biệt hiệu quả khi có một số đối xứng giữa siêu dữ liệu và tiêu đề http tiêu chuẩn, nhưng rõ ràng bạn có thể phát minh ra của riêng bạn.
EJ Campbell

14

Đây là API tiếp cận của tôi (tôi sử dụng ví dụ) - như bạn có thể thấy, bạn không sử dụng bất kỳ file_id(định danh tệp đã tải lên máy chủ) trong API:

  1. Tạo photođối tượng trên máy chủ:

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. Tải lên tệp (lưu ý fileở dạng số ít vì chỉ có một cho mỗi ảnh):

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

Và sau đó, ví dụ:

  1. Đọc danh sách ảnh

    GET: /projects/{project_id}/photos
    response: [ photo, photo, photo, ... ] (array of objects)
  2. Đọc một số chi tiết ảnh

    GET: /projects/{project_id}/photos/{photo_id}
    response: { id: 666, name: 'some_schema.jpg', comment:'blah'} (photo object)
  3. Đọc tập tin ảnh

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

Vì vậy, kết luận là, đầu tiên bạn tạo một đối tượng (ảnh) bằng POST, và sau đó bạn gửi yêu cầu thứ hai với tệp (lại POST).


3
Điều này có vẻ giống như cách 'RESTFUL' hơn để đạt được điều này.
James Webster

Thao tác POST cho các tài nguyên mới được tạo, phải trả về id vị trí, trong chi tiết phiên bản đơn giản của đối tượng
Ivan Proskuryakov

@ivanproskuryakov tại sao "phải"? Trong ví dụ trên (POST ở điểm 2) id tệp là vô dụng. Đối số thứ hai (đối với POST ở điểm 2) tôi sử dụng dạng số ít '/ tệp' (không phải '/ tệp') vì vậy không cần ID vì đường dẫn: / dự án / 2 / ảnh / 3 / tệp cung cấp thông tin ĐẦY ĐỦ cho tệp ảnh nhận dạng.
Kamil Kiełczewski

Từ đặc tả giao thức HTTP. w3.org/Prot Protocol / rfc2616 / rfc2616-sec10.html 10.2.2 201 được tạo "Tài nguyên mới được tạo có thể được tham chiếu bởi (các) URI được trả về trong thực thể của phản hồi, với URI cụ thể nhất cho tài nguyên được cung cấp bởi trường tiêu đề Vị trí. " @ KamilKiełczewski (một) và (hai) có thể được kết hợp thành một thao tác POST POST: / dự án / {project_id} / photos Sẽ trả về cho bạn tiêu đề vị trí, có thể được sử dụng cho hoạt động GET ảnh (tài nguyên *) ảnh duy nhất với tất cả các chi tiết CGET: để có được tất cả bộ sưu tập các bức ảnh
Ivan Proskuryakov

1
Nếu siêu dữ liệu và tải lên là các hoạt động riêng biệt, thì các điểm cuối có các vấn đề sau: Đối với tải lên tệp, thao tác POST được sử dụng - POST không phải là idempotent. PUT (idempotent) phải được sử dụng vì bạn đang thay đổi tài nguyên mà không tạo tài nguyên mới. REST hoạt động với các đối tượng được gọi là tài nguyên . BÀI VIẾT: Từ ../photos/, PUT: Hồi ../photos/ đũaphoto_id} BẮT ĐẦU: Mạnh ../photos/, GET GET: xông ../photos/ đũaphoto_id} Chuyện PS. Tách tải lên vào điểm cuối riêng biệt có thể dẫn đến hành vi không dự đoán được. restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resource.html
Ivan Proskuryakov

6

Đối tượng FormData: Tải tệp lên bằng Ajax

XMLHttpRequest Cấp 2 bổ sung hỗ trợ cho giao diện FormData mới. Các đối tượng FormData cung cấp một cách để dễ dàng xây dựng một tập hợp các cặp khóa / giá trị đại diện cho các trường biểu mẫu và các giá trị của chúng, sau đó có thể dễ dàng gửi bằng cách sử dụng phương thức send () XMLHttpRequest.

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

Vì ví dụ duy nhất còn thiếu là ví dụ ANDROID , tôi sẽ thêm nó. Kỹ thuật này sử dụng AsyncTask tùy chỉnh nên được khai báo bên trong lớp Activity của bạn.

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);
    }

}

Vì vậy, khi bạn muốn tải lên tệp của mình, chỉ cần gọi:

new UploadFile().execute();

Xin chào, AndroidMultiPartEntity là gì, vui lòng giải thích ... và nếu tôi muốn tải lên tệp pdf, word hoặc xls những gì tôi phải làm, vui lòng cung cấp một số hướng dẫn ... tôi mới biết điều này.
amit pandya

1
@amitpandya Tôi đã thay đổi mã thành tải lên tệp chung chung để mọi người đọc nó rõ ràng hơn
lifeisfoo

2

Tôi muốn gửi một số chuỗi đến máy chủ phụ trợ. Tôi không sử dụng json với nhiều phần, tôi đã sử dụng params yêu cầu.

@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 sẽ trông như thế nào

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

Tôi đang vượt qua hai thông số (uuid và loại) cùng với tải lên tệp. Hy vọng điều này sẽ giúp những người không có dữ liệu json phức tạp để gửi.


1

Bạn có thể thử sử dụng https://sapes.github.io/okhttp/ thư viện. Bạn có thể đặt phần thân yêu cầu thành nhiều phần, sau đó thêm các tệp và các đối tượng json riêng như vậy:

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

Hãy đảm bảo rằng bạn đã nhập sau. Nhập khẩu tiêu chuẩn khác

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
Nhận nàyjava.lang.ClassCastException: org.springframework.core.io.FileSystemResource cannot be cast to java.lang.String
Mariano Ruiz
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.