Node.js의 동기 요청


99

3 http API를 순차적으로 호출해야하는 경우 다음 코드에 대한 더 나은 대안은 무엇일까요?

http.get({ host: 'www.example.com', path: '/api_1.php' }, function(res) { 
  res.on('data', function(d) { 

    http.get({ host: 'www.example.com', path: '/api_2.php' }, function(res) { 
      res.on('data', function(d) { 

        http.get({ host: 'www.example.com', path: '/api_3.php' }, function(res) { 
          res.on('data', function(d) { 


          });
        });
        }
      });
    });
    }
  });
});
}

청소하는 것 외에는 그보다 더 잘할 수 있다고 생각하지 않습니다.
hvgotcodes

2
순서대로 정렬해야하는 이유는 무엇입니까?
Raynos

11
@Raynos 당신은 당신이 api_2에 보낼 것을 알기도 전에 api_1에서 일부 데이터를해야 할 수도 있습니다
andyortlieb

9
Futures는 상당히 사용되지 않는다는 점을 언급 할 가치가 있습니다. Bluebird 또는 Q와 같은 최신 라이브러리를 사용하는 것이
좋습니다

1
제목과 질문이 서로 모순됩니다. 질문에서 동기 요청을 설명하는 것이 아니라 일반적으로 각각 비동기 적으로 발생하는 일련의 요청을 설명합니다. 큰 차이점-동기식 호출 차단 및 일련의 비동기 작업이 차단되지 않습니다 (UI 차단, 서버의 다른 요청 처리 차단). sync-request이 질문의 제목에 대한 좋은 답변이지만 질문의 코드가 의미하는 바에 대한 답변이 아닌 라이브러리에 대한 답변이 아래에 있습니다 . 약속에 대한 아래 답변이 더 나은 답변입니다. 무슨 뜻입니까?
Jake

답변:


69

사용 deferreds 좋아 Futures.

var sequence = Futures.sequence();

sequence
  .then(function(next) {
     http.get({}, next);
  })
  .then(function(next, res) {
     res.on("data", next);
  })
  .then(function(next, d) {
     http.get({}, next);
  })
  .then(function(next, res) {
    ...
  })

범위를 전달해야하는 경우 다음과 같이하십시오.

  .then(function(next, d) {
    http.get({}, function(res) {
      next(res, d);
    });
  })
  .then(function(next, res, d) { })
    ...
  })

nodejs에 대한 대기 및 지연을 제공하는 IcedCoffeScript를 사용해보십시오.
Thanigainathan 2014

이것이 비 차단입니까? 내 말은 다음 기능을 차단하고 있지만 다른 비동기 기능의 실행을 차단하지는 않습니다.
Oktav 2014 년

1
예, 지연된 메서드는 비 차단 / 비동기입니다.
dvlsg 2014

4
ES6 약속 API 효과적으로 심지어 "선물"의 저자에 따르면,이를 교체해야합니다
알렉산더 밀스

Futures는 매우 오래되어 더 이상 사용되지 않습니다. 대신 q를 참조하십시오.
Jim Aho

53

나는 Raynos의 솔루션도 좋아하지만 다른 흐름 제어 라이브러리를 선호합니다.

https://github.com/caolan/async

각 후속 함수에서 결과가 필요한지 여부에 따라 직렬, 병렬 또는 폭포수를 사용합니다.

직렬로 실행해야하는 경우 시리즈 이지만 이후의 각 함수 호출에서 결과가 반드시 필요하지는 않습니다.

병렬 로 실행할 수있는 경우 병렬 로 실행할 수있는 경우 각 병렬 함수에서 각각의 결과가 필요하지 않으며 모두 완료되면 콜백이 필요합니다.

각 함수에서 결과를 모핑하고 다음으로 전달하려는 경우 폭포

endpoints = 
 [{ host: 'www.example.com', path: '/api_1.php' },
  { host: 'www.example.com', path: '/api_2.php' },
  { host: 'www.example.com', path: '/api_3.php' }];

async.mapSeries(endpoints, http.get, function(results){
    // Array of results
});

9
var http = require ( 'http');
Elle Mundy 2013

7
하. example.com은 실제로 이런 종류의 용도로 설계된 도메인입니다. 와.
meawoppl 2014 년

async.series 코드는 적어도 async v0.2.10부터 작동하지 않습니다. series ()는 최대 2 개의 인수 만 취하고 첫 번째 인수의 요소를 함수로 실행하므로 async는 객체를 함수로 실행하려고하면 오류가 발생합니다.
뚜껑

1
forEachAsync ( github.com/FuturesJS/forEachAsync )를 사용하여이 코드에서 의도 한 것과 유사한 작업을 수행 할 수 있습니다 .
뚜껑

이것은 내가 원하는 것을 정확하게 수행합니다. 감사합니다!
aProperFox

33

공통 노드 라이브러리를 사용하여이 작업을 수행 할 수 있습니다 .

function get(url) {
  return new (require('httpclient').HttpClient)({
    method: 'GET',
      url: url
    }).finish().body.read().decodeToString();
}

var a = get('www.example.com/api_1.php'), 
    b = get('www.example.com/api_2.php'),
    c = get('www.example.com/api_3.php');

3
쓰레기, 나는 :( 작동 것 그것은 나던 생각 upvotedrequire(...).HttpClient is not a constructor
moeiscool

30

동기화 요청

지금까지 내가 찾아서 사용한 가장 쉬운 방법은 sync-request 이며 노드와 브라우저를 모두 지원합니다!

var request = require('sync-request');
var res = request('GET', 'http://google.com');
console.log(res.body.toString('utf-8'));

그것이 lib 폴 백이 있지만 미친 구성이나 복잡한 lib 설치가 아닙니다. 그냥 작동합니다. 여기에서 다른 예제를 시도해 보았고 할 일이 많거나 설치가 작동하지 않을 때 당황했습니다!

노트:

sync-request가 사용 하는 예제 는를 사용할 때 잘 작동하지 않으며 res.getBody()get body가 수행하는 모든 작업은 인코딩을 수락하고 응답 데이터를 변환하는 것입니다. 그냥 할 res.body.toString(encoding)대신.


동기화 요청이 매우 느리다는 것을 알았습니다 . 제 경우에는 10 배 더 빠른 github.com/dhruvbird/http-sync 다른 하나를 사용 하게되었습니다.
Filip Spiridonov 2015 년

나는 그것을 위해 어떤 느린 실행도하지 않았습니다. 이것은 자식 프로세스를 생성합니다. 시스템이 얼마나 많은 CPU를 사용하고 어떤 버전의 노드를 사용하고 있습니까? 전환이 필요한지 여부를 확인하고 싶습니다.
jemiloii

필립의 말에 동의합니다. 이것은 느립니다.
Rambo7

내가 플립을 요청했지만 응답이 없습니다. 시스템이 얼마나 많은 CPU를 사용하고 어떤 버전의 노드를 사용하고 있습니까?
jemiloii

이것은 상당한 양의 CPU를 사용하며 프로덕션 용도로는 권장되지 않습니다.
moeiscool

20

API 목록과 함께 재귀 함수를 사용합니다.

var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';

function callAPIs ( host, APIs ) {
  var API = APIs.shift();
  http.get({ host: host, path: API }, function(res) { 
    var body = '';
    res.on('data', function (d) {
      body += d; 
    });
    res.on('end', function () {
      if( APIs.length ) {
        callAPIs ( host, APIs );
      }
    });
  });
}

callAPIs( host, APIs );

편집 : 버전 요청

var request = require('request');
var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';
var APIs = APIs.map(function (api) {
  return 'http://' + host + api;
});

function callAPIs ( host, APIs ) {
  var API = APIs.shift();
  request(API, function(err, res, body) { 
    if( APIs.length ) {
      callAPIs ( host, APIs );
    }
  });
}

callAPIs( host, APIs );

편집 : 요청 / 비동기 버전

var request = require('request');
var async = require('async');
var APIs = [ '/api_1.php', '/api_2.php', '/api_3.php' ];
var host = 'www.example.com';
var APIs = APIs.map(function (api) {
  return 'http://' + host + api;
});

async.eachSeries(function (API, cb) {
  request(API, function (err, res, body) {
    cb(err);
  });
}, function (err) {
  //called when all done, or error occurs
});

이것이 제가 요청하는 가변 목록 (600 개 항목 및 증가)을 가지고 있기 때문에 제가 사용한 방법입니다. 즉, 코드에 문제가 있습니다. API 출력이 청크 크기보다 크면 '데이터'이벤트가 요청 당 여러 번 생성됩니다. 다음과 같이 데이터를 "버퍼링"하고 싶습니다. var body = ''; res.on ( 'data', function (data) {body + = data;}). on ( 'end', function () {callback (body); if (APIs.length) callAPIs (host, APIs);} );
Ankit Aggarwal

업데이트되었습니다. 재귀를 통해 문제를 더 간단하고 유연하게 만드는 방법을 보여주고 싶었습니다. 개인적으로 저는 여러 콜백을 쉽게 건너 뛸 수 있기 때문에 이런 종류의 요청 모듈을 항상 사용합니다.
generalhenry

@generalhenry, 요청 모듈을 사용하려면 어떻게해야합니까? 요청을 사용하여 위의 코드를 제공 할 수 있습니까?
Scotty

요청 버전과 요청 / 비동기 버전을 추가했습니다.
generalhenry

5

이 문제에 대한 해결책은 끝이없는 것 같습니다. 여기에 하나 더 있습니다. :)

// do it once.
sync(fs, 'readFile')

// now use it anywhere in both sync or async ways.
var data = fs.readFile(__filename, 'utf8')

http://alexeypetrushin.github.com/synchronize


링크 한 라이브러리가 OP 문제에 대한 해결책을 제공하지만 귀하의 예에서 fs.readFile은 항상 동기화됩니다.
에릭

1
아니요, 콜백을 명시 적으로 제공하고 원하는 경우 비동기 버전으로 사용할 수 있습니다.
Alex Craft

1
예제는 파일 시스템 통신이 아닌 http 요청에 대한 것입니다.
Seth

5

또 다른 가능성은 완료된 작업을 추적하는 콜백을 설정하는 것입니다.

function onApiResults(requestId, response, results) {
    requestsCompleted |= requestId;

    switch(requestId) {
        case REQUEST_API1:
            ...
            [Call API2]
            break;
        case REQUEST_API2:
            ...
            [Call API3]
            break;
        case REQUEST_API3:
            ...
            break;
    }

    if(requestId == requestsNeeded)
        response.end();
}

그런 다음 각각에 ID를 할당하고 연결을 닫기 전에 완료해야하는 작업에 대한 요구 사항을 설정할 수 있습니다.

const var REQUEST_API1 = 0x01;
const var REQUEST_API2 = 0x02;
const var REQUEST_API3 = 0x03;
const var requestsNeeded = REQUEST_API1 | REQUEST_API2 | REQUEST_API3;

좋아요, 예쁘지 않습니다. 순차 호출을하는 또 다른 방법입니다. NodeJS가 가장 기본적인 동기 호출을 제공하지 않는 것은 유감입니다. 그러나 나는 비동기성에 대한 유혹이 무엇인지 이해합니다.


4

순차적 사용.

sudo npm install sequenty

또는

https://github.com/AndyShin/sequenty

아주 간단합니다.

var sequenty = require('sequenty'); 

function f1(cb) // cb: callback by sequenty
{
  console.log("I'm f1");
  cb(); // please call this after finshed
}

function f2(cb)
{
  console.log("I'm f2");
  cb();
}

sequenty.run([f1, f2]);

또한 다음과 같은 루프를 사용할 수 있습니다.

var f = [];
var queries = [ "select .. blah blah", "update blah blah", ...];

for (var i = 0; i < queries.length; i++)
{
  f[i] = function(cb, funcIndex) // sequenty gives you cb and funcIndex
  {
    db.query(queries[funcIndex], function(err, info)
    {
       cb(); // must be called
    });
  }
}

sequenty.run(f); // fire!

3

요청 라이브러리를 사용하면 문제 를 최소화 할 수 있습니다.

var request = require('request')

request({ uri: 'http://api.com/1' }, function(err, response, body){
    // use body
    request({ uri: 'http://api.com/2' }, function(err, response, body){
        // use body
        request({ uri: 'http://api.com/3' }, function(err, response, body){
            // use body
        })
    })
})

그러나 최대한의 굉장함을 얻으려면 Step과 같은 제어 흐름 라이브러리를 시도해야합니다. 허용되는 경우 요청을 병렬화 할 수도 있습니다.

var request = require('request')
var Step    = require('step')

// request returns body as 3rd argument
// we have to move it so it works with Step :(
request.getBody = function(o, cb){
    request(o, function(err, resp, body){
        cb(err, body)
    })
}

Step(
    function getData(){
        request.getBody({ uri: 'http://api.com/?method=1' }, this.parallel())
        request.getBody({ uri: 'http://api.com/?method=2' }, this.parallel())
        request.getBody({ uri: 'http://api.com/?method=3' }, this.parallel())
    },
    function doStuff(err, r1, r2, r3){
        console.log(r1,r2,r3)
    }
)

3

2018 년부터 ES6 모듈과 약속을 사용하여 다음과 같은 함수를 작성할 수 있습니다.

import { get } from 'http';

export const fetch = (url) => new Promise((resolve, reject) => {
  get(url, (res) => {
    let data = '';
    res.on('end', () => resolve(data));
    res.on('data', (buf) => data += buf.toString());
  })
    .on('error', e => reject(e));
});

그리고 다른 모듈에서

let data;
data = await fetch('http://www.example.com/api_1.php');
// do something with data...
data = await fetch('http://www.example.com/api_2.php');
// do something with data
data = await fetch('http://www.example.com/api_3.php');
// do something with data

코드는 비동기 컨텍스트 ( async키워드 사용 ) 에서 실행되어야합니다.


2

제어 흐름 라이브러리가 많이 있습니다. 저는 conseq를 좋아 합니다 (... 제가 작성했기 때문입니다.) 또한 on('data')여러 번 실행할 수 있으므로 restler 와 같은 REST 래퍼 라이브러리를 사용합니다 .

Seq()
  .seq(function () {
    rest.get('http://www.example.com/api_1.php').on('complete', this.next);
  })
  .seq(function (d1) {
    this.d1 = d1;
    rest.get('http://www.example.com/api_2.php').on('complete', this.next);
  })
  .seq(function (d2) {
    this.d2 = d2;
    rest.get('http://www.example.com/api_3.php').on('complete', this.next);
  })
  .seq(function (d3) {
    // use this.d1, this.d2, d3
  })

2

이것은 Raynos에 의해 잘 대답되었습니다. 그러나 답변이 게시 된 이후로 시퀀스 라이브러리가 변경되었습니다.

시퀀스가 작동하려면 https://github.com/FuturesJS/sequence/tree/9daf0000289954b85c0925119821752fbfb3521e 링크를 따르십시오 .

이것은 당신이 그것을 작동시킬 수있는 방법입니다 npm install sequence:

var seq = require('sequence').Sequence;
var sequence = seq.create();

seq.then(function call 1).then(function call 2);

1

다음은 인덱스 대신 배열의 인수를 사용하는 @ andy-shin의 내 버전입니다.

function run(funcs, args) {
    var i = 0;
    var recursive = function() {
        funcs[i](function() {
            i++;
            if (i < funcs.length)
                recursive();
        }, args[i]);
    };
    recursive();
}

1

... 4 년 후 ...

다음은 프레임 워크 Danf를 사용한 원래 솔루션입니다 (이러한 종류의 코드는 필요하지 않고 일부 구성 만 필요).

// config/common/config/sequences.js

'use strict';

module.exports = {
    executeMySyncQueries: {
        operations: [
            {
                order: 0,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_1.php',
                    'GET'
                ],
                scope: 'response1'
            },
            {
                order: 1,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_2.php',
                    'GET'
                ],
                scope: 'response2'
            },
            {
                order: 2,
                service: 'danf:http.router',
                method: 'follow',
                arguments: [
                    'www.example.com/api_3.php',
                    'GET'
                ],
                scope: 'response3'
            }
        ]
    }
};

order병렬로 실행하려는 작업에 동일한 값을 사용하십시오 .

더 짧게 만들고 싶다면 수집 프로세스를 사용할 수 있습니다.

// config/common/config/sequences.js

'use strict';

module.exports = {
    executeMySyncQueries: {
        operations: [
            {
                service: 'danf:http.router',
                method: 'follow',
                // Process the operation on each item
                // of the following collection.
                collection: {
                    // Define the input collection.
                    input: [
                        'www.example.com/api_1.php',
                        'www.example.com/api_2.php',
                        'www.example.com/api_3.php'
                    ],
                    // Define the async method used.
                    // You can specify any collection method
                    // of the async lib.
                    // '--' is a shorcut for 'forEachOfSeries'
                    // which is an execution in series.
                    method: '--'
                },
                arguments: [
                    // Resolve reference '@@.@@' in the context
                    // of the input item.
                    '@@.@@',
                    'GET'
                ],
                // Set the responses in the property 'responses'
                // of the stream.
                scope: 'responses'
            }
        ]
    }
};

상기 살펴보세요 개요 더 많은 정보를위한 프레임 워크를.


1

http.request (분석 보고서를 작성하기 위해 탄력적 검색에 대한 ~ 10,000 집계 쿼리) 속도를 제한해야했기 때문에 여기에 왔습니다. 다음은 내 컴퓨터를 질식 시켰습니다.

for (item in set) {
    http.request(... + item + ...);
}

내 URL은 매우 간단하므로 이것은 원래 질문에 사소하게 적용되지 않을 수 있지만 잠재적으로 적용 가능하고 여기에 나와 비슷한 문제로 여기에 도착하고 사소한 JavaScript 무 라이브러리 솔루션을 원하는 독자에게 쓸 가치가 있다고 생각합니다.

내 직업은 주문 의존적이지 않았고 이것을 막는 첫 번째 접근 방식은 그것을 청크하기 위해 쉘 스크립트로 래핑하는 것입니다 (JavaScript를 처음 사용하기 때문입니다). 그것은 기능적이지만 만족스럽지 않았습니다. 결국 내 JavaScript 해결 방법은 다음과 같습니다.

var stack=[];
stack.push('BOTTOM');

function get_top() {
  var top = stack.pop();
  if (top != 'BOTTOM')
    collect(top);
}

function collect(item) {
    http.request( ... + item + ...
    result.on('end', function() {
      ...
      get_top();
    });
    );
}

for (item in set) {
   stack.push(item);
}

get_top();

collectget_top 간의 상호 재귀처럼 보입니다 . 시스템이 비동기식이고 수집 함수 가 on. ( 'end' 에서 이벤트에 대해 숨겨진 콜백으로 완료 되기 때문에 효과가 있는지 잘 모르겠습니다 .

원래 질문에 적용하기에 충분히 일반적이라고 생각합니다. 내 시나리오와 같이 시퀀스 / 세트가 알려진 경우 모든 URL / 키를 한 번에 스택에 푸시 할 수 있습니다. 이동하면서 계산되면 on ( 'end' 함수는 get_top () 직전에 스택의 다음 URL을 푸시 할 수 있습니다. . 결과가 중첩되지 않고 API를 호출 할 때 리팩토링하기가 더 쉬울 수 있습니다. 변화.

나는 이것이 위의 @generalhenry의 간단한 재귀 버전과 효과적으로 동일하다는 것을 알고 있습니다 (그래서 나는 그것을 찬성했습니다!)


0

슈퍼 요청

이것은 요청을 기반으로하고 promise를 사용하는 또 다른 동기 모듈입니다. 사용하기 매우 간단하고 모카 테스트와 잘 작동합니다.

npm install super-request

request("http://domain.com")
    .post("/login")
    .form({username: "username", password: "password"})
    .expect(200)
    .expect({loggedIn: true})
    .end() //this request is done 
    //now start a new one in the same session 
    .get("/some/protected/route")
    .expect(200, {hello: "world"})
    .end(function(err){
        if(err){
            throw err;
        }
    });

0

이 코드는 프라 미스 배열을 동기 및 순차적으로 실행하는 데 사용할 수 있으며 그 후에 .then()호출 에서 최종 코드를 실행할 수 있습니다 .

const allTasks = [() => promise1, () => promise2, () => promise3];

function executePromisesSync(tasks) {
  return tasks.reduce((task, nextTask) => task.then(nextTask), Promise.resolve());
}

executePromisesSync(allTasks).then(
  result => console.log(result),
  error => console.error(error)
);

0

await, Promises를 사용하지 않고 (우리 자신을 제외하고) 어떤 (외부) 라이브러리를 포함하지 않고도 당신 (그리고 나)이 원하는 것을 실제로 얻었습니다.

방법은 다음과 같습니다.

node.js와 함께 사용할 C ++ 모듈을 만들고 해당 C ++ 모듈 함수가 HTTP 요청을 만들고 데이터를 문자열로 반환하며 다음을 수행하여 직접 사용할 수 있습니다.

var myData = newModule.get(url);

시작할 준비되셨 습니까?

1 단계 : 컴퓨터의 다른 곳에 새 폴더를 만듭니다.이 폴더를 사용하여 module.node 파일 (C ++에서 컴파일 됨)을 빌드하고 나중에 이동할 수 있습니다.

새 폴더에서 (정리를 위해 mynewFolder / src에 넣었습니다) :

npm init

그때

npm install node-gyp -g

이제 2 개의 새 파일을 만듭니다 : 1, something.cpp라고 부르고이 코드를 그 안에 넣습니다 (또는 원하는 경우 수정).

#pragma comment(lib, "urlmon.lib")
#include <sstream>
#include <WTypes.h>  
#include <node.h>
#include <urlmon.h> 
#include <iostream>
using namespace std;
using namespace v8;

Local<Value> S(const char* inp, Isolate* is) {
    return String::NewFromUtf8(
        is,
        inp,
        NewStringType::kNormal
    ).ToLocalChecked();
}

Local<Value> N(double inp, Isolate* is) {
    return Number::New(
        is,
        inp
    );
}

const char* stdStr(Local<Value> str, Isolate* is) {
    String::Utf8Value val(is, str);
    return *val;
}

double num(Local<Value> inp) {
    return inp.As<Number>()->Value();
}

Local<Value> str(Local<Value> inp) {
    return inp.As<String>();
}

Local<Value> get(const char* url, Isolate* is) {
    IStream* stream;
    HRESULT res = URLOpenBlockingStream(0, url, &stream, 0, 0);

    char buffer[100];
    unsigned long bytesReadSoFar;
    stringstream ss;
    stream->Read(buffer, 100, &bytesReadSoFar);
    while(bytesReadSoFar > 0U) {
        ss.write(buffer, (long long) bytesReadSoFar);
        stream->Read(buffer, 100, &bytesReadSoFar);
    }
    stream->Release();
    const string tmp = ss.str();
    const char* cstr = tmp.c_str();
    return S(cstr, is);
}

void Hello(const FunctionCallbackInfo<Value>& arguments) {
    cout << "Yo there!!" << endl;

    Isolate* is = arguments.GetIsolate();
    Local<Context> ctx = is->GetCurrentContext();

    const char* url = stdStr(arguments[0], is);
    Local<Value> pg = get(url,is);

    Local<Object> obj = Object::New(is);
    obj->Set(ctx,
        S("result",is),
        pg
    );
    arguments.GetReturnValue().Set(
       obj
    );

}

void Init(Local<Object> exports) {
    NODE_SET_METHOD(exports, "get", Hello);
}

NODE_MODULE(cobypp, Init);

이제라는 동일한 디렉토리에 새 파일을 만들고 something.gyp다음과 같은 내용을 넣습니다.

{
   "targets": [
       {
           "target_name": "cobypp",
           "sources": [ "src/cobypp.cpp" ]
       }
   ]
}

이제 package.json 파일에 다음을 추가하십시오. "gypfile": true,

이제 : 콘솔에서 node-gyp rebuild

전체 명령을 통과하고 오류없이 끝에 "ok"라고 말하면 (거의) 괜찮은 것입니다. 그렇지 않은 경우에는 주석을 남기십시오.

그러나 그것이 작동한다면 build / Release / cobypp.node (또는 당신을 위해 호출 된 것)로 이동하여 주 node.js 폴더에 복사 한 다음 node.js에 복사하십시오.

var myCPP = require("./cobypp")
var myData = myCPP.get("http://google.com").result;
console.log(myData);

..

response.end(myData);//or whatever
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.