Jak wcześniej zamknąć połączenie?


99

Próbuję wykonać wywołanie AJAX (przez JQuery), które zainicjuje dość długi proces. Chciałbym, aby skrypt po prostu wysłał odpowiedź wskazującą, że proces się rozpoczął, ale JQuery nie zwróci odpowiedzi, dopóki skrypt PHP nie zostanie uruchomiony.

Próbowałem tego z „zamkniętym” nagłówkiem (poniżej), a także z buforowaniem wyjścia; żaden nie wydaje się działać. Jakieś przypuszczenia? czy jest to coś, co muszę zrobić w JQuery?

<?php

echo( "We'll email you as soon as this is done." );

header( "Connection: Close" );

// do some stuff that will take a while

mail( 'dude@thatplace.com', "okay I'm done", 'Yup, all done.' );

?>

czy opróżniłeś swój bufor wyjściowy za pomocą ob_flush () i to nie zadziałało?
Vinko Vrsalovic

Odpowiedzi:


87

Poniższa strona podręcznika PHP (w tym notatki użytkownika) sugeruje wiele instrukcji dotyczących zamykania połączenia TCP z przeglądarką bez kończenia skryptu PHP:

Podobno wymaga to nieco więcej niż wysłania zamkniętego nagłówka.


Następnie OP potwierdza: tak , to załatwiło sprawę : wskazując na notatkę użytkownika nr 71172 (listopad 2006) skopiowaną tutaj:

Zamykanie połączenia z przeglądarką użytkowników przy jednoczesnym utrzymaniu uruchomionego skryptu php było problemem od czasu [PHP] 4.1, kiedy to zachowanie register_shutdown_function()zostało zmodyfikowane tak, aby nie zamykało automatycznie połączenia użytkowników.

sts at mail dot xubion dot hu Wysłano oryginalne rozwiązanie:

<?php
header("Connection: close");
ob_start();
phpinfo();
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush();
flush();
sleep(13);
error_log("do something in the background");
?>

Który działa dobrze, dopóki nie zastąpił phpinfo()na echo('text I want user to see');w takim przypadku nagłówki nie są wysyłane!

Rozwiązaniem jest jawne wyłączenie buforowania danych wyjściowych i wyczyszczenie bufora przed wysłaniem informacji z nagłówka. Przykład:

<?php
ob_end_clean();
header("Connection: close");
ignore_user_abort(true); // just to be safe
ob_start();
echo('Text the user will see');
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush(); // Strange behaviour, will not work
flush(); // Unless both are called !
// Do processing here 
sleep(30);
echo('Text user will never see');
?>

Właśnie spędziłem 3 godziny, próbując to rozgryźć, mam nadzieję, że to komuś pomoże :)

Przetestowano w:

  • IE 7.5730.11
  • Mozilla Firefox 1.81

Później, w lipcu 2010 r., W powiązanej odpowiedzi Arctic Fire połączył dwie dalsze uwagi użytkowników, które były następstwem powyższej:



1
Autor i @Timbo White, czy możliwe jest wcześniejsze zamknięcie połączenia bez znajomości rozmiaru treści? IE, bez konieczności przechwytywania treści przed zamknięciem.
skibulk

3
Hakerzy i kiepskie przeglądarki internetowe mogą nadal ignorować nagłówek HTTP zamykający połączenie i pobierać resztę danych wyjściowych. Upewnij się, że to, co będzie dalej, nie jest wrażliwe. być może ob_start (); tłumić wszystko: p
hanshenrik

3
Dodanie fastcgi_finish_request (); Mówi się, że pomyślnie zamyka połączenie, gdy powyższe nie działa. Jednak w moim przypadku uniemożliwiło to dalsze wykonywanie skryptu, więc używaj go ostrożnie.
Eric Dubé

@RichardSmith Ponieważ Connection: closenagłówek może zostać nadpisany przez inne oprogramowanie w stosie, na przykład odwrotne proxy w przypadku CGI (zauważyłem to zachowanie z nginx). Zobacz odpowiedź na ten temat od @hanshenrik. Generalnie Connection: closejest wykonywany po stronie klienta i nie powinien być traktowany jako odpowiedź na to pytanie. Połączenie powinno zostać zamknięte po stronie serwera .
7heo.tk

56

Konieczne jest wysłanie tych 2 nagłówków:

Connection: close
Content-Length: n (n = size of output in bytes )

Ponieważ potrzebujesz znać rozmiar pliku wyjściowego, musisz buforować dane wyjściowe, a następnie przesłać je do przeglądarki:

// buffer all upcoming output
ob_start();
echo "We'll email you as soon as this is done.";

// get the size of the output
$size = ob_get_length();

// send headers to tell the browser to close the connection
header("Content-Length: $size");
header('Connection: close');

// flush all output
ob_end_flush();
ob_flush();
flush();

// if you're using sessions, this prevents subsequent requests
// from hanging while the background process executes
if (session_id()) session_write_close();

/******** background process starts here ********/

Ponadto, jeśli twój serwer sieciowy używa automatycznej kompresji gzip na wyjściu (np. Apache z mod_deflate), to nie zadziała, ponieważ rzeczywisty rozmiar wyjścia jest zmieniony, a Content-Length nie jest już dokładne. Wyłącz kompresję gzip dla konkretnego skryptu.

Aby uzyskać więcej informacji, odwiedź http://www.zulius.com/how-to/close-browser-connection-continue-execution


16
Jeśli twój serwer kompresuje dane wyjściowe, możesz je wyłączyć za pomocą header("Content-Encoding: none\r\n");W ten sposób apache nie będzie ich kompresował.
GDmac,

1
@GDmac dziękuję! Nie mogłem sprawić, by to działało przez chwilę, ale wyłączenie kompresji załatwiło sprawę.
Reactgular

Nie ob_flush()jest to konieczne i faktycznie powoduje powiadomienie failed to flush buffer. Wyciągnąłem go i to działało świetnie.
Levi

2
Okazało się, że ob_flush()linia jest konieczna.
Deebster

21

Możesz użyć Fast-CGI z PHP-FPM, aby użyć tej fastcgi_end_request()funkcji . W ten sposób możesz kontynuować przetwarzanie, gdy odpowiedź została już wysłana do klienta.

Znajdziesz to w podręczniku PHP tutaj: FastCGI Process Manager (FPM) ; Jednak ta funkcja nie jest szczegółowo opisana w instrukcji. Tutaj fragment z PHP-FPM: PHP FastCGI Process Manager Wiki :


fastcgi_finish_request ()

Zakres: funkcja php

Kategoria: Optymalizacja

Ta funkcja pozwala przyspieszyć implementację niektórych zapytań php. Przyspieszenie jest możliwe, gdy w trakcie wykonywania skryptu występują akcje, które nie wpływają na odpowiedź serwera. Na przykład zapisanie sesji w memcached może nastąpić po utworzeniu strony i przekazaniu jej na serwer WWW. fastcgi_finish_request()to funkcja php, która zatrzymuje wyjście odpowiedzi. Serwer WWW natychmiast zaczyna przesyłać odpowiedź "powoli i niestety" do klienta, a php w tym samym czasie może zrobić wiele przydatnych rzeczy w kontekście zapytania, takich jak zapis sesji, konwersja pobranego wideo, obsługa wszelkiego rodzaju statystyk itp.

fastcgi_finish_request() może wywołać funkcję zamykającą.


Uwaga: fastcgi_finish_request() jest to dziwactwo , gdzie do rozmowy flush, printalbo echozakończy skrypt wcześnie.

Aby uniknąć tego problemu, możesz zadzwonić ignore_user_abort(true)bezpośrednio przed lub po fastcgi_finish_requestpołączeniu:

ignore_user_abort(true);
fastcgi_finish_request();

3
TO JEST RZECZYWISTA ODPOWIEDŹ!
Kirill Titov

2
jeśli używasz php-fpm - po prostu użyj tej funkcji - zapomnij o nagłówkach i wszystkim innym. Zaoszczędziłeś mi tyle czasu!
Ross

17

Pełna wersja:

ignore_user_abort(true);//avoid apache to kill the php running
ob_start();//start buffer output

echo "show something to user";
session_write_close();//close session file on server side to avoid blocking other requests

header("Content-Encoding: none");//send header to avoid the browser side to take content as gzip format
header("Content-Length: ".ob_get_length());//send length header
header("Connection: close");//or redirect to some url: header('Location: http://www.google.com');
ob_end_flush();flush();//really send content, can't change the order:1.ob buffer to normal buffer, 2.normal buffer to output

//continue do something on server side
ob_start();
sleep(5);//the user won't wait for the 5 seconds
echo 'for diyism';//user can't see this
file_put_contents('/tmp/process.log', ob_get_contents());
ob_end_clean();

kompletne w jakim sensie? Który problem wymagał wypełnienia zaakceptowanego skryptu odpowiedzi (który?) I które z twoich różnic w konfiguracji spowodowały, że było to konieczne?
hakre

4
ta linia: nagłówek ("Content-Encoding: none"); -> bardzo ważne.
Bobby Tables

2
Dzięki, to jedyne działające rozwiązanie na tej stronie. To powinno zostać zatwierdzone jako odpowiedź.

6

Lepszym rozwiązaniem jest rozwidlenie procesu w tle. Jest to dość proste na unix / linux:

<?php
echo "We'll email you as soon as this is done.";
system("php somestuff.php dude@thatplace.com >/dev/null &");
?>

Powinieneś spojrzeć na to pytanie, aby uzyskać lepsze przykłady:

PHP wykonuje proces w tle


4

Zakładając, że masz serwer Linux i dostęp do roota, spróbuj tego. To najprostsze rozwiązanie, jakie znalazłem.

Utwórz nowy katalog dla następujących plików i nadaj mu pełne uprawnienia. (Później możemy uczynić to bezpieczniejszym).

mkdir test
chmod -R 777 test
cd test

Umieść to w pliku o nazwie bgping.

echo starting bgping
ping -c 15 www.google.com > dump.txt &
echo ending bgping

Zwróć uwagę na &. Polecenie ping będzie działać w tle, podczas gdy bieżący proces przejdzie do polecenia echo. Wysyła ping do www.google.com 15 razy, co zajmie około 15 sekund.

Spraw, aby był wykonywalny.

chmod 777 bgping

Umieść to w pliku o nazwie bgtest.php.

<?php

echo "start bgtest.php\n";
exec('./bgping', $output, $result)."\n";
echo "output:".print_r($output,true)."\n";
echo "result:".print_r($result,true)."\n";
echo "end bgtest.php\n";

?>

Kiedy żądasz bgtest.php w przeglądarce, powinieneś szybko otrzymać następującą odpowiedź, bez czekania około 15 sekund na wykonanie polecenia ping.

start bgtest.php
output:Array
(
    [0] => starting bgping
    [1] => ending bgping
)

result:0
end bgtest.php

Na serwerze powinno być teraz uruchomione polecenie ping. Zamiast polecenia ping możesz uruchomić skrypt PHP:

php -n -f largejob.php > dump.txt &

Mam nadzieję że to pomoże!


4

Oto modyfikacja kodu Timbo, która działa z kompresją gzip.

// buffer all upcoming output
if(!ob_start("ob_gzhandler")){
    define('NO_GZ_BUFFER', true);
    ob_start();
}
echo "We'll email you as soon as this is done.";

//Flush here before getting content length if ob_gzhandler was used.
if(!defined('NO_GZ_BUFFER')){
    ob_end_flush();
}

// get the size of the output
$size = ob_get_length();

// send headers to tell the browser to close the connection
header("Content-Length: $size");
header('Connection: close');

// flush all output
ob_end_flush();
ob_flush();
flush();

// if you're using sessions, this prevents subsequent requests
// from hanging while the background process executes
if (session_id()) session_write_close();

/******** background process starts here ********/

JESTEŚ BOGIEM. Pracuję od 2 dni, żeby to rozgryźć. Działało na moim lokalnym deweloperze, ale nie na hoście. Byłem wężem. URATOWAŁEŚ MNIE. DZIĘKUJĘ CI!!!!
Chad Caldwell

3

Jestem na udostępnionym hoście i jestem fastcgi_finish_requestskonfigurowany do całkowitego zamykania skryptów. connection: closeRozwiązanie też mi się nie podoba . Użycie go wymusza oddzielne połączenie dla kolejnych żądań, co kosztuje dodatkowe zasoby serwera. CzytamTransfer-Encoding: cunked artykuł Wikipedii i dowiedziałem się, że 0\r\n\r\nkończy odpowiedź. Nie przetestowałem tego dokładnie we wszystkich wersjach przeglądarek i na różnych urządzeniach, ale działa to na wszystkich 4 moich obecnych przeglądarkach.

// Disable automatic compression
// @ini_set('zlib.output_compression', 'Off');
// @ini_set('output_buffering', 'Off');
// @ini_set('output_handler', '');
// @apache_setenv('no-gzip', 1);

// Chunked Transfer-Encoding & Gzip Content-Encoding
function ob_chunked_gzhandler($buffer, $phase) {
    if (!headers_sent()) header('Transfer-Encoding: chunked');
    $buffer = ob_gzhandler($buffer, $phase);
    return dechex(strlen($buffer))."\r\n$buffer\r\n";
}

ob_start('ob_chunked_gzhandler');

// First Chunk
echo "Hello World";
ob_flush();

// Second Chunk
echo ", Grand World";
ob_flush();

ob_end_clean();

// Terminating Chunk
echo "\x30\r\n\r\n";
ob_flush();
flush();

// Post Processing should not be displayed
for($i=0; $i<10; $i++) {
    print("Post-Processing");
    sleep(1);
}

Dzięki twojej dobrej odpowiedzi zdałem sobie sprawę, jak głupie (i niepotrzebne) jest używanie połączenia: blisko. Myślę, że niektórzy nie są zaznajomieni z nakrętkami i śrubami swojego serwera.
Justin

@Justin Napisałem to dawno temu. Patrząc na to ponownie, powinienem zauważyć, że może być konieczne wyregulowanie fragmentów do 4KB. Wydaje mi się, że pamiętam, że niektóre serwery nie opróżnią się, dopóki nie osiągną tego minimum.
skibulk

2

Możesz spróbować wielowątkowości.

możesz stworzyć skrypt, który wykonuje wywołanie systemowe (używając shell_exec ), które wywołuje plik binarny php ze skryptem, aby wykonać swoją pracę jako parametr. Ale nie sądzę, że jest to najbezpieczniejszy sposób. Może uda ci się to poprawić, chrootując proces php i inne rzeczy

Alternatywnie, w phpclasses jest klasa, która to robi http://www.phpclasses.org/browse/package/3953.html . Ale nie znam szczegółów implementacji


A jeśli nie chcesz czekać na zakończenie procesu, użyj &znaku, aby uruchomić proces w tle.
Liam

2

Odpowiedź TL; DR:

ignore_user_abort(true); //Safety measure so that the user doesn't stop the script too early.

$content = 'Hello World!'; //The content that will be sent to the browser.

header('Content-Length: ' . strlen($content)); //The browser will close the connection when the size of the content reaches "Content-Length", in this case, immediately.

ob_start(); //Content past this point...

echo $content;

//...will be sent to the browser (the output buffer gets flushed) when this code executes.
ob_end_flush();
ob_flush();
flush();

if(session_id())
{
    session_write_close(); //Closes writing to the output buffer.
}

//Anything past this point will be ran without involving the browser.

Odpowiedź funkcji:

ignore_user_abort(true);

function sendAndAbort($content)
{
    header('Content-Length: ' . strlen($content));

    ob_start();

    echo $content;

    ob_end_flush();
    ob_flush();
    flush();
}

sendAndAbort('Hello World!');

//Anything past this point will be ran without involving the browser.

1

Twój problem można rozwiązać, wykonując równoległe programowanie w php. Pytanie o to zadałem kilka tygodni temu tutaj: Jak używać wielowątkowości w aplikacjach PHP

I otrzymałem świetne odpowiedzi. Szczególnie jeden mi się podobał. Pisarz odniósł się do samouczka Easy Parallel Processing in PHP (wrzesień 2008; autor: johnlim), który może bardzo dobrze rozwiązać twój problem, ponieważ użyłem go już do rozwiązania podobnego problemu, który pojawił się kilka dni temu.


1

Odpowiedź Joeri Sebrechts jest bliska, ale niszczy wszelkie istniejące treści, które mogą być buforowane, zanim zechcesz się rozłączyć. Nie wywołuje się ignore_user_abortpoprawnie, co pozwala na przedwczesne zakończenie działania skryptu. odpowiedź dizjizmu jest dobra, ale nie ma zastosowania. Np. Osoba może mieć większe lub mniejsze bufory wyjściowe, których ta odpowiedź nie obsługuje, więc może po prostu nie działać w twojej sytuacji i nie będziesz wiedział dlaczego.

Ta funkcja umożliwia odłączenie w dowolnym momencie (o ile nagłówki nie zostały jeszcze wysłane) i zachowuje treści, które do tej pory wygenerowałeś. Dodatkowy czas przetwarzania jest domyślnie nieograniczony.

function disconnect_continue_processing($time_limit = null) {
    ignore_user_abort(true);
    session_write_close();
    set_time_limit((int) $time_limit);//defaults to no limit
    while (ob_get_level() > 1) {//only keep the last buffer if nested
        ob_end_flush();
    }
    $last_buffer = ob_get_level();
    $length = $last_buffer ? ob_get_length() : 0;
    header("Content-Length: $length");
    header('Connection: close');
    if ($last_buffer) {
        ob_end_flush();
    }
    flush();
}

Jeśli potrzebujesz dodatkowej pamięci, przydziel ją przed wywołaniem tej funkcji.


1

Uwaga dla użytkowników mod_fcgid (używaj na własne ryzyko).

Szybkie rozwiązanie

Przyjęta odpowiedź Joeri Sebrechts jest rzeczywiście funkcjonalna. Jeśli jednak używasz mod_fcgid , może się okazać, że to rozwiązanie nie działa samodzielnie. Innymi słowy, gdy wywoływana jest funkcja flush, połączenie z klientem nie zostaje zamknięte.

FcgidOutputBufferSizeParametr konfiguracji mod_fcgid może być winien. Znalazłem tę wskazówkę w:

  1. ta odpowiedź Traversa Cartera i
  2. ten post na blogu Seumasa Mackinnona .

Po przeczytaniu powyższego możesz dojść do wniosku, że szybkim rozwiązaniem byłoby dodanie linii (patrz „Przykład wirtualnego hosta” na końcu):

FcgidOutputBufferSize 0

w pliku konfiguracyjnym Apache (np. httpd.conf), pliku konfiguracyjnym FCGI (np. fcgid.conf) lub w pliku hostów wirtualnych (np. httpd-vhosts.conf).

W (1) powyżej wspomniana jest zmienna o nazwie „OutputBufferSize”. To jest stara nazwa FcgidOutputBufferSizewspomnianego w (2) (patrz uwagi dotyczące aktualizacji na stronie internetowej Apache dla mod_fcgid ).

Szczegóły i drugie rozwiązanie

Powyższe rozwiązanie wyłącza buforowanie wykonywane przez mod_fcgid albo dla całego serwera, albo dla konkretnego hosta wirtualnego. Może to spowodować spadek wydajności Twojej witryny internetowej. Z drugiej strony może tak nie być, ponieważ PHP samodzielnie wykonuje buforowanie.

Jeśli nie chcesz wyłączać buforowania mod_fcgid , istnieje inne rozwiązanie ... możesz zmusić ten bufor do opróżnienia .

Poniższy kod właśnie to robi, bazując na rozwiązaniu zaproponowanym przez Joeri Sebrechts:

<?php
    ob_end_clean();
    header("Connection: close");
    ignore_user_abort(true); // just to be safe
    ob_start();
    echo('Text the user will see');

    echo(str_repeat(' ', 65537)); // [+] Line added: Fill up mod_fcgi's buffer.

    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush(); // Strange behaviour, will not work
    flush(); // Unless both are called !
    // Do processing here 
    sleep(30);
    echo('Text user will never see');
?>

To, co zasadniczo robi dodana linia kodu, to wypełnienie bufora mod_fcgi , zmuszając go w ten sposób do opróżnienia . Wybrano numer „65537”, ponieważ domyślną wartością FcgidOutputBufferSizezmiennej jest „65536”, jak wspomniano na stronie internetowej Apache dla odpowiedniej dyrektywy . W związku z tym może być konieczne odpowiednie dostosowanie tej wartości, jeśli w środowisku ustawiono inną wartość.

Moje środowisko

  • WampServer 2.5
  • Apache 2.4.9
  • PHP 5.5.19 VC11, x86, Non Thread Safe
  • mod_fcgid / 2.3.9
  • Windows 7 Professional x64

Przykładowy host wirtualny

<VirtualHost *:80>
    DocumentRoot "d:/wamp/www/example"
    ServerName example.local

    FcgidOutputBufferSize 0

    <Directory "d:/wamp/www/example">
        Require all granted
    </Directory>
</VirtualHost>

Próbowałem wielu rozwiązań. I to jest jedyne rozwiązanie działające dla mnie z mod_fcgid.
Tsounabe

1

to działało dla mnie

//avoid apache to kill the php running
ignore_user_abort(true);
//start buffer output
ob_start();

echo "show something to user1";
//close session file on server side to avoid blocking other requests
session_write_close();

//send length header
header("Content-Length: ".ob_get_length());
header("Connection: close");
//really send content, can't change the order:
//1.ob buffer to normal buffer,
//2.normal buffer to output
ob_end_flush();
flush();
//continue do something on server side
ob_start();
//replace it with the background task
sleep(20);

0

Ok, więc zasadniczo tak, jak jQuery wykonuje żądanie XHR, nawet metoda ob_flush nie będzie działać, ponieważ nie możesz uruchomić funkcji na każdej zmianie onreadystatechange. jQuery sprawdza stan, a następnie wybiera odpowiednie działania do podjęcia (ukończenie, błąd, sukces, przekroczenie limitu czasu). I chociaż nie mogłem znaleźć odniesienia, pamiętam, że nie działa to ze wszystkimi implementacjami XHR. Metoda, która moim zdaniem powinna zadziałać, to skrzyżowanie odpytywania ob_flush i forever-frame.

<?php
 function wrap($str)
 {
  return "<script>{$str}</script>";
 };

 ob_start(); // begin buffering output
 echo wrap("console.log('test1');");
 ob_flush(); // push current buffer
 flush(); // this flush actually pushed to the browser
 $t = time();
 while($t > (time() - 3)) {} // wait 3 seconds
 echo wrap("console.log('test2');");
?>

<html>
 <body>
  <iframe src="ob.php"></iframe>
 </body>
</html>

A ponieważ skrypty są wykonywane w tekście, gdy bufory są opróżniane, otrzymasz wykonanie. Aby było to przydatne, zmień plik console.log na metodę wywołania zwrotnego zdefiniowaną w głównym ustawieniach skryptu, aby otrzymywać dane i wykonywać na nich działania. Mam nadzieję że to pomoże. Pozdrawiam, Morgan.


0

Alternatywnym rozwiązaniem jest dodanie zadania do kolejki i utworzenie skryptu cron, który sprawdza dostępność nowych zadań i uruchamia je.

Ostatnio musiałem to zrobić, aby ominąć ograniczenia narzucone przez współdzielonego hosta - exec () et al został wyłączony dla PHP uruchamianego przez serwer WWW, ale mógł działać w skrypcie powłoki.


0

Jeśli flush()funkcja nie działa. Musisz ustawić następne opcje w php.ini, takie jak:

output_buffering = Off  
zlib.output_compression = Off  

0

Najnowsze rozwiązanie robocze

    // client can see outputs if any
    ignore_user_abort(true);
    ob_start();
    echo "success";
    $buffer_size = ob_get_length();
    session_write_close();
    header("Content-Encoding: none");
    header("Content-Length: $buffer_size");
    header("Connection: close");
    ob_end_flush();
    ob_flush();
    flush();

    sleep(2);
    ob_start();
    // client cannot see the result of code below

0

Po wypróbowaniu wielu różnych rozwiązań z tego wątku (po tym, jak żadne z nich nie działało dla mnie), znalazłem rozwiązanie na oficjalnej stronie PHP.net:

function sendResponse($response) {
    ob_end_clean();
    header("Connection: close\r\n");
    header("Content-Encoding: none\r\n");
    ignore_user_abort(true);
    ob_start();

    echo $response; // Actual response that will be sent to the user

    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush();
    flush();
    if (ob_get_contents()) {
        ob_end_clean();
    }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.