Làm thế nào để phục vụ làm việc? Khởi tạo, phiên, biến chia sẻ và đa luồng


1144

Giả sử, tôi có một máy chủ web chứa nhiều servlet. Để biết thông tin chuyển qua các servlet đó, tôi đang đặt các biến phiên và phiên.

Bây giờ, nếu 2 hoặc nhiều người dùng gửi yêu cầu đến máy chủ này thì điều gì xảy ra với các biến phiên?
Tất cả chúng sẽ phổ biến cho tất cả người dùng hay chúng sẽ khác nhau cho mỗi người dùng?
Nếu chúng khác nhau, thì máy chủ có thể phân biệt giữa những người dùng khác nhau như thế nào?

Một câu hỏi tương tự nữa, nếu có nngười dùng truy cập vào một servlet cụ thể, thì servlet này chỉ được khởi tạo ngay lần đầu tiên người dùng đầu tiên truy cập nó hoặc nó có được khởi tạo riêng cho tất cả người dùng không?
Nói cách khác, điều gì xảy ra với các biến thể hiện?

Câu trả lời:


1821

ServletContext

Khi bộ chứa servlet (như Apache Tomcat ) khởi động, nó sẽ triển khai và tải tất cả các ứng dụng web của nó. Khi một ứng dụng web được tải, thùng chứa servlet sẽ tạo ServletContextmột lần và giữ nó trong bộ nhớ của máy chủ. Các ứng dụng web của web.xmlvà tất cả bao gồm web-fragment.xmlcác file được phân tách, và mỗi <servlet>, <filter><listener>tìm thấy (hoặc mỗi lớp chú thích với @WebServlet, @WebFilter@WebListenertương ứng) được khởi tạo một lần và lưu giữ trong bộ nhớ của máy chủ là tốt. Đối với mỗi bộ lọc khởi tạo, init()phương thức của nó được gọi với một bộ lọc mới FilterConfig.

Khi Servletcó một <servlet><load-on-startup>hoặc @WebServlet(loadOnStartup)giá trị lớn hơn 0, sau đó nó init()phương pháp cũng được gọi trong quá trình startup với một mới ServletConfig. Các servlet đó được khởi tạo theo cùng thứ tự được chỉ định bởi giá trị đó ( 1là 1st, 2is 2nd, v.v.). Nếu cùng giá trị được quy định trong hơn một servlet, sau đó mỗi người trong số những servlets được nạp theo thứ tự như chúng xuất hiện trong web.xml, web-fragment.xmlhoặc @WebServletclassloading. Trong trường hợp không có giá trị "tải khi khởi động", init()phương thức sẽ được gọi bất cứ khi nào yêu cầu HTTP chạm vào servlet đó lần đầu tiên.

Khi bộ chứa servlet kết thúc với tất cả các bước khởi tạo được mô tả ở trên, thì nó ServletContextListener#contextInitialized()sẽ được gọi.

Khi tắt servlet container xuống, nó trút tất cả các ứng dụng web, gọi các destroy()phương pháp của tất cả các servlets nó khởi tạo và các bộ lọc, và tất cả ServletContext, Servlet, FilterListenercác trường hợp được cho vào thùng rác. Cuối cùng ServletContextListener#contextDestroyed()sẽ được viện dẫn.

HttpServletRequest và HttpServletResponse

Bộ chứa servlet được gắn vào một máy chủ web lắng nghe các yêu cầu HTTP trên một số cổng nhất định (cổng 8080 thường được sử dụng trong quá trình phát triển và cổng 80 trong sản xuất). Khi một máy khách (ví dụ: người dùng có trình duyệt web hoặc sử dụng theo chương trìnhURLConnection ) gửi yêu cầu HTTP, thùng chứa servlet sẽ tạo mới HttpServletRequestHttpServletResponsecác đối tượng và chuyển chúng qua bất kỳ định nghĩa nào Filtertrong chuỗi và cuối cùng là Servletthể hiện.

Trong trường hợp bộ lọc , doFilter()phương thức được gọi. Khi mã bộ chứa của servlet gọi chain.doFilter(request, response), yêu cầu và phản hồi tiếp tục đến bộ lọc tiếp theo hoặc nhấn vào servlet nếu không có bộ lọc còn lại.

Trong trường hợp của servlets , service()phương thức được gọi. Theo mặc định, phương thức này xác định một trong những doXxx()phương thức để gọi dựa trên request.getMethod(). Nếu phương thức xác định vắng mặt trong servlet, thì lỗi HTTP 405 được trả về trong phản hồi.

Đối tượng yêu cầu cung cấp quyền truy cập vào tất cả các thông tin về yêu cầu HTTP, chẳng hạn như URL, tiêu đề, chuỗi truy vấn và phần thân của nó. Ví dụ, đối tượng phản hồi cung cấp khả năng kiểm soát và gửi phản hồi HTTP theo cách bạn muốn, cho phép bạn đặt các tiêu đề và phần thân (thường là với nội dung HTML được tạo từ tệp JSP). Khi phản hồi HTTP được cam kết và kết thúc, cả đối tượng yêu cầu và phản hồi đều được tái chế và cung cấp để sử dụng lại.

Câu hỏi

Khi một khách hàng truy cập ứng dụng web lần đầu tiên và / hoặc HttpSessionlần đầu tiên có được thông qua request.getSession(), bộ chứa servlet tạo một HttpSessionđối tượng mới , tạo một ID dài và duy nhất (mà bạn có thể nhận được session.getId()) và lưu trữ nó trong máy chủ ký ức. Container servlet cũng đặt ra một Cookietrong các Set-Cookietiêu đề của HTTP response với JSESSIONIDnhư tên của nó và ID phiên duy nhất là giá trị của nó.

Theo đặc tả cookie HTTP (hợp đồng bất kỳ trình duyệt web và máy chủ web tốt nào phải tuân thủ), khách hàng (trình duyệt web) được yêu cầu gửi lại cookie này trong các yêu cầu tiếp theo trong Cookietiêu đề miễn là cookie hợp lệ ( tức là ID duy nhất phải tham chiếu đến một phiên chưa hết hạn và tên miền và đường dẫn là chính xác). Sử dụng trình giám sát lưu lượng HTTP tích hợp trong trình duyệt của bạn, bạn có thể xác minh rằng cookie hợp lệ (nhấn F12 trong Chrome / Firefox 23+ / IE9 + và kiểm tra tab Net / Network ). Bộ chứa servlet sẽ kiểm tra Cookietiêu đề của mọi yêu cầu HTTP đến cho sự hiện diện của cookie với tên JSESSIONIDvà sử dụng giá trị của nó (ID phiên) để lấy liên kết HttpSessiontừ bộ nhớ của máy chủ.

Việc HttpSessiontồn tại cho đến khi nó ở chế độ chờ (nghĩa là không được sử dụng trong yêu cầu) nhiều hơn giá trị thời gian chờ được chỉ định trong <session-timeout>, cài đặt trong web.xml. Giá trị thời gian chờ mặc định là 30 phút. Vì vậy, khi khách hàng không truy cập ứng dụng web lâu hơn thời gian được chỉ định, bộ chứa servlet sẽ bỏ qua phiên. Mọi yêu cầu tiếp theo, ngay cả với cookie được chỉ định, sẽ không có quyền truy cập vào cùng một phiên nữa; container servlet sẽ tạo ra một phiên mới.

Về phía khách hàng, cookie phiên vẫn tồn tại miễn là phiên bản trình duyệt đang chạy. Vì vậy, nếu máy khách đóng phiên bản trình duyệt (tất cả các tab / cửa sổ), thì phiên sẽ được chuyển sang phía máy khách. Trong phiên bản trình duyệt mới, cookie được liên kết với phiên sẽ không tồn tại, do đó, nó sẽ không còn được gửi. Điều này gây ra một cái hoàn toàn mới HttpSessionđược tạo ra, với một cookie phiên hoàn toàn mới được sử dụng.

Tóm lại

  • Cuộc ServletContextsống miễn là ứng dụng web sống. Nó được chia sẻ giữa tất cả các yêu cầu trong tất cả các phiên.
  • Thời HttpSessiongian tồn tại của ứng dụng khách tương tác với ứng dụng web có cùng phiên bản trình duyệt và phiên không hết thời gian ở phía máy chủ. Nó được chia sẻ giữa tất cả các yêu cầu trong cùng một phiên.
  • Các HttpServletRequestHttpServletResponsetrực tiếp từ thời điểm đó servlet nhận được một yêu cầu HTTP từ các khách hàng, cho đến khi đáp ứng đầy đủ (trang web) đã đến. Nó không được chia sẻ ở nơi khác.
  • Tất cả Servlet, FilterListenertrường hợp sống lâu như các ứng dụng web sống. Chúng được chia sẻ giữa tất cả các yêu cầu trong tất cả các phiên.
  • Bất kỳ attributeđược xác định trong ServletContext, HttpServletRequestHttpSessionsẽ sống lâu như các đối tượng trong cuộc sống câu hỏi. Các đối tượng chính nó đại diện cho "phạm vi" trong khuôn khổ quản lý đậu như JSF, CDI, Spring, vv Những khung lưu trữ đậu scoped của họ như là một attributephạm vi phù hợp nhất của nó.

An toàn chủ đề

Điều đó nói rằng, mối quan tâm chính của bạn có thể là an toàn chủ đề . Bây giờ bạn nên biết rằng các servlet và bộ lọc được chia sẻ giữa tất cả các yêu cầu. Đó là điều hay về Java, đó là các luồng đa luồng và các luồng khác nhau (đọc: các yêu cầu HTTP) có thể sử dụng cùng một thể hiện. Nó sẽ quá tốn kém để tạo lại, init()destroy()chúng cho mỗi yêu cầu.

Bạn cũng nên nhận ra rằng bạn nên không bao giờ gán bất kỳ dữ liệu yêu cầu hoặc phiên scoped như một ví dụ biến của một servlet hoặc bộ lọc. Nó sẽ được chia sẻ giữa tất cả các yêu cầu khác trong các phiên khác. Đó không phải là chủ đề an toàn! Ví dụ dưới đây minh họa điều này:

public class ExampleServlet extends HttpServlet {

    private Object thisIsNOTThreadSafe;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Object thisIsThreadSafe;

        thisIsNOTThreadSafe = request.getParameter("foo"); // BAD!! Shared among all requests!
        thisIsThreadSafe = request.getParameter("foo"); // OK, this is thread safe.
    } 
}

Xem thêm:


25
Vì vậy, khi tôi bằng cách nào đó có thể tìm ra JSessionId được gửi cho khách hàng, tôi có thể đánh cắp phiên của anh ta không?
Toskan

54
@Toskan: đúng rồi. Nó được gọi là hack sửa lỗi phiên . Xin lưu ý rằng điều này không cụ thể đối với JSP / Servlet. Tất cả các ngôn ngữ phía máy chủ khác duy trì phiên bằng cookie cũng rất nhạy cảm, như PHP với PHPSESSIDcookie, ASP.NET với ASP.NET_SessionIDcookie, vân vân. Đó cũng là lý do tại sao việc viết lại URL với ;jsessionid=xxxmột số khung công tác MVC / Servlet MVC tự động được thực hiện. Chỉ cần đảm bảo rằng ID phiên không bao giờ bị lộ trong URL hoặc bằng các phương tiện khác trong các trang web để người dùng cuối không biết sẽ không bị tấn công.
BalusC

11
@Toskan: Ngoài ra, hãy đảm bảo rằng ứng dụng web của bạn không nhạy cảm với các cuộc tấn công XSS. Tức là không hiển thị lại bất kỳ đầu vào nào do người dùng kiểm soát ở dạng không thoát. XSS mở cửa để tìm cách thu thập ID phiên của tất cả các endusers. Xem thêm Khái niệm chung đằng sau XSS là gì?
BalusC

2
@BalusC, Xin lỗi vì sự ngu ngốc của tôi. Nó có nghĩa là tất cả người dùng truy cập cùng một ví dụ của thisIsNOTThreadSafe phải không?
làm lu mờ

4
@TwoThumbSticks 404 được trả về khi toàn bộ servlet vắng mặt. 405 được trả về khi có servlet nhưng phương thức doXxx () mong muốn không được thực hiện.
BalusC

428

Phiên

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Nói tóm lại: máy chủ web cấp một mã định danh duy nhất cho mỗi khách truy cập trong lần truy cập đầu tiên . Khách truy cập phải mang lại ID đó để anh ta được nhận ra lần sau. Mã định danh này cũng cho phép máy chủ phân tách chính xác các đối tượng thuộc sở hữu của một phiên so với phiên khác.

Khởi tạo Servlet

Nếu tải khi khởi độngsai :

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Nếu tải khi khởi độngđúng :

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Khi anh ta ở chế độ dịch vụ và trên rãnh, cùng một servlet sẽ hoạt động theo yêu cầu từ tất cả các khách hàng khác.

nhập mô tả hình ảnh ở đây

Tại sao không phải là một ý tưởng tốt cho mỗi khách hàng? Hãy nghĩ về điều này: Bạn sẽ thuê một anh chàng pizza cho mỗi đơn hàng đã đến? Làm điều đó và bạn sẽ ngừng kinh doanh ngay lập tức.

Nó đi kèm với một rủi ro nhỏ mặc dù. Hãy nhớ rằng: anh chàng độc thân này giữ tất cả thông tin đặt hàng trong túi của mình: vì vậy nếu bạn không thận trọng về an toàn luồng trên các máy chủ , anh ta có thể sẽ đưa ra thứ tự sai cho một khách hàng nhất định.


26
Hình ảnh của bạn là rất tốt cho sự hiểu biết của tôi. Tôi có một câu hỏi, nhà hàng pizza này sẽ làm gì khi có quá nhiều đơn đặt hàng pizza đến, chỉ chờ một anh chàng pizza hoặc thuê thêm anh chàng pizza? Cảm ơn .
zh18

6
Anh ta sẽ trả lại một tin nhắn vớito many requests at this moment. try again later
Please_Dont_Bully_Me_SO_Lords

3
Servlets, không giống như người giao hàng Pizza, có thể thực hiện nhiều hơn một lần giao hàng cùng một lúc. Họ chỉ cần chăm sóc đặc biệt về nơi họ ghi lại địa chỉ của khách hàng, hương vị của pizza ...
bruno

42

Phiên trong các máy chủ Java cũng giống như phiên trong các ngôn ngữ khác như PHP. Nó là duy nhất cho người dùng. Máy chủ có thể theo dõi nó theo các cách khác nhau như cookie, viết lại url, v.v. Bài viết tài liệu Java này giải thích nó trong ngữ cảnh của các máy chủ Java và chỉ ra rằng chính xác cách duy trì phiên là một chi tiết triển khai dành cho các nhà thiết kế của máy chủ. Thông số kỹ thuật chỉ quy định rằng nó phải được duy trì là duy nhất cho người dùng trên nhiều kết nối đến máy chủ. Kiểm tra bài viết này từ Oracle để biết thêm thông tin về cả hai câu hỏi của bạn.

Chỉnh sửa Có một hướng dẫn tuyệt vời ở đây về cách làm việc với phiên bên trong các servlet. Và đây là một chương từ Sun về Java Servlets, chúng là gì và cách sử dụng chúng. Giữa hai bài viết, bạn sẽ có thể trả lời tất cả các câu hỏi của bạn.


Điều này đưa ra một câu hỏi khác cho tôi, vì chỉ có một bối cảnh servlet cho toàn bộ ứng dụng và chúng tôi có quyền truy cập vào các biến phiên thông qua dịch vụ này, vì vậy làm thế nào các biến phiên có thể là duy nhất cho mỗi người dùng? Cảm ơn ..
Ku Jon

1
Làm thế nào bạn truy cập phiên từ servletContext? Bạn không đề cập đến servletContext.setAttribution () phải không?
matt b

4
@KuJon Mỗi ứng dụng web có một ServletContextđối tượng. Đối tượng đó có không, một hoặc nhiều đối tượng phiên - một tập hợp các đối tượng phiên. Mỗi phiên được xác định bởi một số loại chuỗi định danh, như được thấy trong phim hoạt hình về câu trả lời khác. Mã định danh đó được theo dõi trên máy khách bằng cách viết lại cookie hoặc URL. Mỗi đối tượng phiên có các biến riêng.
Basil Bourque

33

Khi bộ chứa servlet (như Apache Tomcat) khởi động, nó sẽ đọc từ tệp web.xml (chỉ một cho mỗi ứng dụng) nếu có lỗi xảy ra hoặc xuất hiện lỗi tại bảng điều khiển bên container, nếu không, nó sẽ triển khai và tải tất cả web các ứng dụng bằng cách sử dụng web.xml (được đặt tên là mô tả triển khai).

Trong giai đoạn khởi tạo của servlet, cá thể servlet đã sẵn sàng nhưng nó không thể phục vụ yêu cầu của máy khách vì nó bị thiếu hai thông tin:
1: thông tin ngữ cảnh
2: thông tin cấu hình ban đầu

Công cụ Servlet tạo đối tượng giao diện servletConfig đóng gói thông tin còn thiếu ở trên vào công cụ servlet gọi init () của servlet bằng cách cung cấp các tham chiếu đối tượng servletConfig làm đối số. Khi init () được thực thi hoàn toàn, servlet sẵn sàng phục vụ yêu cầu của máy khách.

Q) Trong vòng đời của servlet, có bao nhiêu lần khởi tạo và khởi tạo xảy ra ??

A) chỉ một lần (đối với mỗi yêu cầu máy khách, một luồng mới được tạo) chỉ một phiên bản của servlet phục vụ bất kỳ số lượng yêu cầu máy khách nào, tức là sau khi phục vụ một máy chủ yêu cầu máy khách không chết. Nó chờ đợi các yêu cầu máy khách khác, tức là những gì CGI (đối với mọi yêu cầu của máy khách được tạo ra một quy trình mới) đã khắc phục được giới hạn với servlet (công cụ servlet bên trong tạo luồng).

Q) Khái niệm phiên làm việc như thế nào?

A) bất cứ khi nào getSession () được gọi trên đối tượng HttpServletRequest

Bước 1 : đối tượng yêu cầu được ước tính cho ID phiên đến.

Bước 2 : nếu ID không có sẵn, một đối tượng HttpSession hoàn toàn mới được tạo và ID phiên tương ứng của nó được tạo (ví dụ của HashTable) ID phiên được lưu trữ vào đối tượng phản hồi httpservlet và tham chiếu của đối tượng HttpSession được trả về servlet (doGet / doPost) .

Bước 3 : nếu ID phiên đối tượng hoàn toàn mới không được tạo thì phiên ID được chọn từ tìm kiếm đối tượng yêu cầu được thực hiện trong bộ sưu tập các phiên bằng cách sử dụng ID phiên làm khóa.

Khi tìm kiếm thành công, phiên ID được lưu trữ vào HttpServletResponse và các tham chiếu đối tượng phiên hiện có được trả về doGet () hoặc doPost () của UserDefineservlet.

Ghi chú:

1) khi điều khiển rời khỏi mã servlet đến máy khách, đừng quên rằng đối tượng phiên đang được giữ bởi thùng chứa servlet, tức là công cụ servlet

2) đa luồng được dành cho những người phát triển servlet để thực hiện nghĩa là., Xử lý nhiều yêu cầu của khách hàng không có gì phải bận tâm về mã đa luồng

Hình thức inshort:

Một servlet được tạo khi ứng dụng khởi động (nó được triển khai trên thùng chứa servlet) hoặc khi nó được truy cập lần đầu (tùy thuộc vào cài đặt khởi động tải) khi khởi tạo servlet, phương thức init () của servlet được gọi sau đó servlet (ví dụ một và duy nhất) xử lý tất cả các yêu cầu (phương thức service () của nó được gọi bởi nhiều luồng). Đó là lý do tại sao không nên có bất kỳ đồng bộ hóa nào trong đó và bạn nên tránh các biến thể hiện của servlet khi ứng dụng không được triển khai (thùng chứa servlet dừng), phương thức kill () được gọi.


20

Phiên - những gì Chris Thompson nói.

Khởi tạo - một servlet được khởi tạo khi container nhận yêu cầu đầu tiên được ánh xạ tới servlet (trừ khi servlet được cấu hình để tải khi khởi động với <load-on-startup>phần tử trong web.xml). Ví dụ tương tự được sử dụng để phục vụ các yêu cầu tiếp theo.


3
Chính xác. Suy nghĩ bổ sung: Mỗi yêu cầu có một luồng mới (hoặc được tái chế) để chạy trên cá thể Servlet duy nhất đó. Mỗi Servlet có một thể hiện và có thể có nhiều luồng (nếu có nhiều yêu cầu đồng thời).
Basil Bourque

13

Đặc tả Servlet JSR-315 xác định rõ hành vi bộ chứa web trong các phương thức dịch vụ (và doGet, doPost, doPut, v.v.) (2.3.3.1 Các vấn đề đa luồng, Trang 9):

Một thùng chứa servlet có thể gửi các yêu cầu đồng thời thông qua phương thức dịch vụ của servlet. Để xử lý các yêu cầu, Servlet Developer phải cung cấp đầy đủ các quy định để xử lý đồng thời với nhiều luồng trong phương thức dịch vụ.

Mặc dù không được khuyến nghị, một giải pháp thay thế cho Nhà phát triển là triển khai giao diện SingleThreadModel yêu cầu bộ chứa đảm bảo rằng chỉ có một luồng yêu cầu tại một thời điểm trong phương thức dịch vụ. Một thùng chứa servlet có thể đáp ứng yêu cầu này bằng cách tuần tự hóa các yêu cầu trên một servlet hoặc bằng cách duy trì một nhóm các trường hợp servlet. Nếu servlet là một phần của ứng dụng Web đã được đánh dấu là có thể phân phối, thì container có thể duy trì một nhóm các cá thể servlet trong mỗi JVM mà ứng dụng được phân phối.

Đối với các servlet không triển khai giao diện SingleThreadModel, nếu phương thức dịch vụ (hoặc các phương thức như doGet hoặc doPost được gửi đến phương thức dịch vụ của lớp trừu tượng HttpServlet) đã được xác định với từ khóa được đồng bộ hóa, thì thùng chứa servlet không thể sử dụng phương thức nhóm đối tượng , nhưng phải tuần tự hóa các yêu cầu thông qua nó. Chúng tôi khuyến nghị các Nhà phát triển không đồng bộ hóa phương thức dịch vụ (hoặc các phương thức được gửi đến nó) trong những trường hợp này vì ảnh hưởng có hại đến hiệu suất


2
FYI, thông số Servlet hiện tại (2015-01) là 3,1, được xác định bởi JSR 340 .
Basil Bourque


1
Câu trả lời rất gọn gàng! @tharindu_DG
Tom Taylor

0

Như đã giải thích rõ ràng từ các giải thích ở trên, bằng cách triển khai SingleThreadModel , một servlet có thể được đảm bảo an toàn luồng bởi bộ chứa servlet. Việc thực hiện container có thể làm điều này theo 2 cách:

1) Sắp xếp các yêu cầu (xếp hàng) vào một thể hiện - điều này tương tự như một servlet KHÔNG triển khai SingleThreadModel NHƯNG đồng bộ hóa các phương thức dịch vụ / doXXX; HOẶC LÀ

2) Tạo một nhóm các trường hợp - đó là một tùy chọn tốt hơn và đánh đổi giữa nỗ lực khởi động / khởi tạo / thời gian của servlet so với các tham số hạn chế (thời gian bộ nhớ / CPU) của môi trường lưu trữ servlet.


-1

Số Servlets không phảichủ đề an toàn

Điều này cho phép truy cập nhiều hơn một chủ đề cùng một lúc

nếu bạn muốn làm cho nó Servlet như Thread an toàn., bạn có thể đi

Implement SingleThreadInterface(i) Giao diện trống không có

phương pháp

hoặc chúng ta có thể đi cho các phương thức đồng bộ hóa

chúng ta có thể làm cho toàn bộ phương thức dịch vụ được đồng bộ hóa bằng cách sử dụng đồng bộ hóa

từ khóa trước phương thức

Thí dụ::

public Synchronized class service(ServletRequest request,ServletResponse response)throws ServletException,IOException

hoặc chúng ta có thể đặt khối mã trong khối Đồng bộ hóa

Thí dụ::

Synchronized(Object)

{

----Instructions-----

}

Tôi cảm thấy khối Đồng bộ hóa tốt hơn so với tạo toàn bộ phương thức

Đã đồng bộ hóa

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.