OpenHarmony开发者论坛

标题: OpenHarmony_Http连接 [打印本页]

作者: Laval社区小助手    时间: 2023-12-26 15:21
标题: OpenHarmony_Http连接
[md]## 详情:[OpenHarmony_Http连接](https://laval.csdn.net/64801fed9787b754b2643f01.html)

## 概述

在OpenHarmony中,Http数据请求功能主要由ohos.net.http模块提供。使用该功能需要申请ohos.permission.INTERNET权限。

## Http接口简单介绍

| 模块/类                   | 函数/方法                                                                        | 功能说明                                                                                                                                                                                 |
| ------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ohos.net.http             | function createHttp(): HttpRequest                                               | 创建一个HttpRequest对象,该对象涵盖了发起/中断请求、订阅/取消Http响应头 事件等功能。每一个HttpRequest对象对应一个Http请求。如需发起多个Http请求,须为每个Http请求创建对应HttpRequest对象 |
| ohos.net.http.HttpRequest | on(type: "headersReceive", callback: AsyncCallback): void                        | 用于订阅http响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息。以callback方式进行异步回调                                                                                  |
| ohos.net.http.HttpRequest | once(type: "headersReceive", callback: Callback): void                           | 用于订阅http响应头,但是只触发一次。一旦触发之后,订阅就会被移除。以callback方式进行异步回调                                                                                             |
| ohos.net.http.HttpRequest | off(type: "headersReceive", callback: Callback): void                            | 取消对http响应头的监听。以callback方式进行异步回调                                                                                                                                       |
| ohos.net.http.HttpRequest | request(url: string, options: HttpRequestOptions, callback: AsyncCallback): void | 根据URL地址,发起一个GET请求。以callback方式进行异步回调                                                                                                                                 |
| ohos.net.http.HttpRequest | request(url: string, options: HttpRequestOptions, callback: AsyncCallback): void | 根据URL地址,采用不同方式(GET、POST等)发送请求,options中携带请求参数。以callback方式进行异步回调                                                                                      |
| ohos.net.http.HttpRequest | request(url: string, options?: HttpRequestOptions: Promise                       | 根据URL地址,采用不同方式(GET、POST等)发送请求,options中携带请求参数(可选)。以Promise方式进行异步回调                                                                                 |

## 开发步骤

* 导入@ohos.net.http模块。
* 创建HttpRequest对象。
* (可选)订阅HTTP响应头。
* 根据URL地址,设置请求参数,发起HTTP网络请求。
* (可选)处理HTTP响应头和HTTP网络请求的返回结果。

## Http连接示例

### 目标

使用http模块相关接口实现向服务端发送GET、POST、PUT、DELETE四种方式的请求

### 开发环境

示例开发环境:

```
IDE:DevEco Studio 3.0 Release(build:3.0.0.993)
SDK:Api Version9
开发模型:Stage
```

### 具体实现

#### 创建工程

打开DevEco Studio,创建SDK版本为API9、模型为Stage的OpenHarmony项目。

工程目录结构如下:

![](file:///C:/Users/kuangansheng/Desktop/%E8%AF%BE%E7%A8%8B%E6%A1%88%E4%BE%8BFAQ/%E8%AF%BE%E7%A8%8B%E6%A1%88%E4%BE%8BFAQ/%E5%85%AC%E5%BC%80%E8%AF%BE%E7%A8%8B/OpenHarmony_Http%E8%BF%9E%E6%8E%A5/image/%E5%B7%A5%E7%A8%8B%E7%9B%AE%E5%BD%95.jpg?lastModify=1686118352)

![](https://devpress.csdnimg.cn/d152170e86d94e2abf887baf2d79088e.jpg)

#### 配置权限

在工程中的module.json5文件中配置ohos.permission.INTERNET 权限。

```
 "abilities": [
     {
       ...
        ...
     }
   ],
"requestPermissions": [
     {
       "name": "ohos.permission.INTERNET"
     }
   ]
```

#### 包装Http接口

在ets/service 目录下创建HttpService.ets文件。为了方便在UI页面中调用http模块相关功能,在该文件中创建HttpService类对Http模块相关接口进行封装。

HttService.ets:

```
import http from '@ohos.net.http';
import Logger from '../util/Logger'

const TAG="HttpDemo"

export class HttpService{
 /**
  * HttpRequest对象,承担了发起/取消、订阅响应事件的职责
  */
 private httpClient:http.HttpRequest;


 private requestMethodMap={
    "GET":http.RequestMethod.GET,
    "POST":http.RequestMethod.POST,
    "PUT":http.RequestMethod.PUT,
    "DELETE":http.RequestMethod.DELETE
  }

 constructor(){
    this.createHttpRequest();
    this.onHeaderReceive();
  }

 /**
  * 创建HttpRequest对象,并赋值给httpClient
  */
 private  createHttpRequest(){
    Logger.info(TAG,"start create HttpRequest");
    this.httpClient=http.createHttp();
    Logger.info(TAG,"create HttpRequest sucess ");
  }


 /**
  * 销毁http连接,中断数据请求
  */
 public destroy(){
    this.httpClient.destroy();
    Logger.info(TAG,"HttpRequest destroyed")
  }

 /**
  * 用于订阅http响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
   * 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)
  */
 private onHeaderReceive(){
     this.httpClient.on("headersReceive",(data)=>{
         Logger.info(TAG,"ResponseHeader:"+JSON.stringify(data));
     })
  }


 /**
  * 根据url和请求参数,向服务端发送http请求
  * @param url
  * @param param
  */
 public requestHttp(url:string,param:RequestParam){
 
   Logger.info(TAG,"start request:"+JSON.stringify(param));

   let requestOption = {
     method: this.requestMethodMap[param.method],
     extraData: param.extraData,
     header: param.header,
     readTimeout: param.readTimeout,
     connectTimeout: param.connectTimeout
   }

   Logger.info(TAG, "the request param is:" + JSON.stringify(requestOption));
   this.httpClient.request(url,requestOption)
     .then((httpResponse) => {
       Logger.info(TAG, "send request sucess,the response is:" + JSON.stringify(httpResponse));
     })
     .catch((e) => {
       Logger.info(TAG, "send request fail,the err is:" + JSON.stringify(e));
     })
  }


}

/**
* 请求参数接口
*/
export interface RequestParam {

 method: string;

 extraData?: string | Object | ArrayBuffer;

 header?: Object; // default is 'content-type': 'application/json'

 readTimeout?: number; // default is 60s

 connectTimeout?: number; // default is 60s.
}

let httpService:HttpService=new HttpService();
export default httpService
```

#### 定义对话框组件

在ets/component目录下创建ReqParamDialog.ets文件,在该文件中定义用于设置请求参数的对话框。 完整代码如下:

```

@CustomDialog
@Component
export struct ReqParamDialog {
 @State
 list: number[] = [0];
 flag: number = 0;
 keys: string[] = [];
 values: string[] = [];
 setParamCallback: (keys: string[], values: string[]) => void;
 dialogController: CustomDialogController;

 build() {
   Column() {
     ForEach(this.list, (num) => {
       Row() {
         TextInput({ placeholder: "KEY:" })
           .width(120)
           .height(50)
           .onChange((key) => {
             this.keys[this.flag] = key;
           })
         Text(":")
           .width(10)
           .fontSize(16)
           .fontColor(Color.Blue)
           .textAlign(TextAlign.Center)
         TextInput({ placeholder: "VALUE:" })
           .height(50)
           .onChange((value) => {
             this.values[this.flag] = value;
           })
       }
     }, num => num.toString())
     Row() {
       Button("新增")
         .fontSize(25)
         .onClick(() => {
           this.flag++;
           this.list.push(this.flag);
         })
       Button("减少")
         .fontSize(25)
         .onClick(() => {
           if (this.list.length == 1) {
             return;
           }
           this.list.splice(this.list.length - 1, 1);
         })
       Button("确认")
         .fontSize(25)
         .onClick(() => {
           this.setParamCallback(this.keys, this.values);
           this.dialogController.close();
         })

       Button("取消")
         .fontSize(25)
         .onClick(() => {
           this.dialogController.close();
         })
     }

   }
  }
}
```

效果如下:

![](file:///C:/Users/kuangansheng/Desktop/%E8%AF%BE%E7%A8%8B%E6%A1%88%E4%BE%8BFAQ/%E8%AF%BE%E7%A8%8B%E6%A1%88%E4%BE%8BFAQ/%E5%85%AC%E5%BC%80%E8%AF%BE%E7%A8%8B/OpenHarmony_Http%E8%BF%9E%E6%8E%A5/image/%E5%AF%B9%E8%AF%9D%E6%A1%86.jpg?lastModify=1686118352)

![](https://devpress.csdnimg.cn/9c054f14cd9f415ab42392a71e3d69c9.jpg)

#### 定义Http请求组件

在ets/component目录下创建HttpRequest.ets文件,在该文件中绘制http请求的UI界面。

* 定义选择http请求方法的菜单组件

```
...
...
@Component
export struct HttpRequest{
 methods:string[]=["GET","POST","PUT","DELETE"];
 ...
 ...
 @Builder
 requestMethod() {
   Flex({direction:FlexDirection.Column}) {
     ForEach(this.methods,(method)=>{
       Column(){
         Text(method)
           .fontSize(20)
           .margin({bottom:1})
           .onClick(()=>{
             this.selectedMethod=method;
           })
       }
     },method=>method)
   }
  }
 ...
 ...
}
```

* 组合菜单组件和对话框

将菜单组件绑定到显示请求方法的Text组件上。

```
build() {
    ...
       Text(this.selectedMethod)//显示选择的Http请求方法
         ...
         .bindMenu(this.requestMethod())
    ...
  }

```

在HttpRequest组件中创建CustomDialogController对象,并将自定义对话框作为参数传入。

```
import httpService,{RequestParam} from '../service/HttpService'
import {ReqParamDialog} from '../component/ReqParamDialog'

@Component
export struct HttpRequest{
 ...
 ...
dialogController: CustomDialogController = new CustomDialogController({builder: ReqParamDialog({setParamCallback:this.setParamCallback.bind(this)}), autoCancel: true, alignment: DialogAlignment.Center })
...
...
  }
 build() {
    ...
     ...
  }
...
}
```

HttpRequest.ets文件完整代码:

```
import httpService,{RequestParam} from '../service/HttpService'
import {ReqParamDialog} from '../component/ReqParamDialog'

@Component
export struct HttpRequest{
 /**
  * http请求方法的一个数组,用于生成选择菜单
  */
 methods:string[]=["GET","POST","PUT","DELETE"];
 /**
  * 请求方法,默认为GET
  */
 @State
 selectedMethod:string="GET";
 /**
  * 请求地址,默认为 http://reqres.in/api/users?page=2
  */
 url:string="http://reqres.in/api/users?page=2";
 /**
  * 请求参数
  */
 requestParam:RequestParam=null;
 /**
  * 创建CustomDialogController,传入自定义对话框,控制对话框的显示和关闭
  */
 dialogController: CustomDialogController = new CustomDialogController({builder: ReqParamDialog({setParamCallback:this.setParamCallback.bind(this)}), autoCancel: true, alignment: DialogAlignment.Center })

 @Builder
 requestMethod() {
   Flex({direction:FlexDirection.Column}) {
     ForEach(this.methods,(method)=>{
       Column(){
         Text(method)
           .fontSize(20)
           .margin({bottom:1})
           .onClick(()=>{
             this.selectedMethod=method;
           })
       }
     },method=>method)
   }
   
  }
 build() {
     Column() {
       Row(){
         Text(this.selectedMethod)
           .fontSize(25)
           .align(Alignment.Center)
           .border({width:1})
           .bindMenu(this.requestMethod())
           .width(100)
         TextInput({placeholder:"http:"})
           .onChange((url)=>{
             this.url=url;
           })
       }
       Row(){
         Button("设置参数")
           .fontSize(25)
           .onClick(()=>{
              this.dialogController.open();
           })
       }
       .margin({top:5,bottom:10})
         
       Button("发送请求")
         .fontSize(25)
         .onClick(()=>{
             this.setParamIfNecessary();
             httpService.requestHttp(this.url,this.requestParam);
             this.resetRequestParam();
         })
     }
     .width('100%')
     .height('100%')
  }

 /**
  * 请求发送完之后,重置参数
  */
 resetRequestParam(){
    this.requestParam=null;
  }

 /**
  * 若没有给请求设置参数,则使用默认的参数
  */
 setParamIfNecessary(){
   if(this.requestParam==null){
     this.requestParam={
       method: this.selectedMethod,
       header: {
         'content-type': 'application/json'
       },
       readTimeout: 60000, // default is 60s
       connectTimeout: 60000 // default is 60s.
     }
   }
  }

 /**
  *传递给对话框的回调函数,将对话框输入的参数封装为对象
  */
 setParamCallback(keys:string[],values:string[]){
   let extraData=this.buildExtraData(keys,values);
   this.requestParam = {
     header: {
       "content-type": "application/json"
     },
     method: this.selectedMethod,
     extraData: extraData,
     readTimeout: 60000, // default is 60s
     connectTimeout: 60000 // default is 60s.
   }
  }

 /**
  * 封装请求参数为一个对象
  */
 buildExtraData(keys:string[],values:string[]){
   let extraData={};
   for(let i=0;i<keys.length;i++){
     extraData[keys]=values;
   }
   return extraData;
  }
}
```

#### 创建入口页面

在ets/pages目录下创建index.ets文件。在该文件中导入HttpRequest.ets组件创建程序的入口页面。

```
import {HttpRequest} from '../component/HttpRequest'

@Entry
@Component
struct Index {
 build() {
   Column(){
     HttpRequest()
   }
  }
}


```

页面最终效果预览:

![](file:///C:/Users/kuangansheng/Desktop/%E8%AF%BE%E7%A8%8B%E6%A1%88%E4%BE%8BFAQ/%E8%AF%BE%E7%A8%8B%E6%A1%88%E4%BE%8BFAQ/%E5%85%AC%E5%BC%80%E8%AF%BE%E7%A8%8B/OpenHarmony_Http%E8%BF%9E%E6%8E%A5/image/%E8%AF%B7%E6%B1%82%E7%95%8C%E9%9D%A2%E9%A2%84%E8%A7%88.jpg?lastModify=1686118352)

![](https://devpress.csdnimg.cn/ba97f79f37b141959eabf3bb38727e56.jpg)

#### 测试结果

开发板型号:WAGNER

OH版本:3.2.5.5

**发送GET请求**

测试的url: [https://reqres.in/api/users?page=2](https://reqres.in/api/users?page=2&login=from_csdn)

日志打印:

```
//响应头信息:
08-03 16:52:33.702  6439  6439 I 02200/JsApp: HttpDemo: ResponseHeader:{"access-control-allow-origin":"*","age":"1998","cache-control":"max-age=14400","cf-cache-status":"HIT","cf-ray":"734dd15b999e7da
c-LAX","connection":"keep-alive","content-encoding":"gzip","content-type":"application/json; charset=utf-8","date":"Wed, 03 Aug 2022 08:52:33 GMT","etag":"W/\"406-ut0vzoCuidvyMf8arZpMpJ6ZRDw\"","expec
t-ct":"max-age=604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\"","expires":"Wed, 03 Aug 2022 09:52:32 GMT","location":"https://reqres.in/api/users?page=2","nel":"{\"su
ccess_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}","report-to":"{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=65aGmR0ar0fCc%2BGz5YrBCPBBUwmzeA%2BDUDIEiePrK2
XvwICV9KBBBuj0vIxq4RP0G7%2F%2BKwhOuR8d8PnT1IEIqpStt2taYJ%2F%2B3fj4JjxjU2Sddgys%2FCMVtQSJW7qtbfT8MnZq3asH4A%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}","server":"cloudflare","transfer-encoding"
:"chunked","vary":"Accept-Encoding","via":"1.1 vegur","x-powered-by":


08-03 16:52:33.702  6439  6439 I 02200/JsApp: HttpDemo: send request sucess

//响应内容
08-03 16:52:33.702  6439  6439 I 02200/JsApp: HttpDemo: the result is:{code:200,data:{"page":2,"per_page":6,"total":12,"total_pages":2,"data":[{"id":7,"email":"michael.lawson@reqres.in","first_name":"
Michael","last_name":"Lawson","avatar":"https://reqres.in/img/faces/7-image.jpg"},{"id":8,"email":"lindsay.ferguson@reqres.in","first_name":"Lindsay","last_name":"Ferguson","avatar":"https://reqres.in
/img/faces/8-image.jpg"},{"id":9,"email":"tobias.funke@reqres.in","first_name":"Tobias","last_name":"Funke","avatar":"https://reqres.in/img/faces/9-image.jpg"},{"id":10,"email":"byron.fields@reqres.in
","first_name":"Byron","last_name":"Fields","avatar":"https://reqres.in/img/faces/10-image.jpg"},{"id":11,"email":"george.edwards@reqres.in","first_name":"George","last_name":"Edwards","avatar":"https
://reqres.in/img/faces/11-image.jpg"},{"id":12,"email":"rachel.howell@reqres.in","first_name":"Rachel","last_name":"Howell","avatar":"https://reqres.in/img/faces/12-image.jpg"}],"support":{"url":"http
s://reqres.in/#support-heading","text":"To keep ReqRes free, contribu

```

**发送POST请求**

测试的url: [https://reqres.in/api/users](https://reqres.in/api/users?login=from_csdn) 参数:

```
{
 "name":"morpheus",
 "job":"leader"
}
```

日志打印:

```
//响应头信息:
08-03 17:00:17.796  6439  6439 I 02200/JsApp: HttpDemo: ResponseHeader:{"access-control-allow-origin":"*","cache-control":"max-age=3600","cf-cache-status":"DYNAMIC","cf-ray":"734ddcb00acf5281-LAX","co
nnection":"keep-alive","content-length":"51","content-type":"application/json; charset=utf-8","date":"Wed, 03 Aug 2022 09:00:17 GMT","etag":"W/\"33-wOvBK8Nue+Ck3OlpSqu8QL5bQlI\"","expect-ct":"max-age=
604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\"","expires":"Wed, 03 Aug 2022 10:00:16 GMT","location":"https://reqres.in/api/users","nel":"{\"success_fraction\":0,\"r
eport_to\":\"cf-nel\",\"max_age\":604800}","report-to":"{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=096ZxRLklCMfXbATfkCIKKHVMpSiEnPqDsg%2FH6qZzSkV9SsgEZGbr%2BiPJEqvIjbK
q0vUdmSf66q2gl8Tb14tgNp6b1TEbf%2Fg1IJVOZNNYi5m5Wb%2FaXUjCc4wXFVdXnLg8Uojxk7DPg%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}","server":"cloudflare","transfer-encoding":"chunked","vary":"Accept-En
coding","via":"1.1 vegur","x-powered-by":"Express"}


08-03 17:00:17.796  6439  6439 I 02200/JsApp: HttpDemo: send request sucess

//响应内容
08-03 17:00:17.796  6439  6439 I 02200/JsApp: HttpDemo: the result is:{code:201,data:{"id":"846","createdAt":"2022-08-03T09:00:17.537Z"}}


```

**发送PUT请求**

测试的url: [https://reqres.in/api/users/2](https://reqres.in/api/users/2?login=from_csdn)

参数:

```
{
 "name":"morpheus",
 "job":"zion resident"
}
```

日志打印:

```
//响应头信息:
08-03 17:02:00.411  6439  6439 I 02200/JsApp: HttpDemo: ResponseHeader:{"access-control-allow-origin":"*","cache-control":"max-age=3600","cf-cache-status":"DYNAMIC","cf-ray":"734ddf30ed467add-LAX","co
nnection":"keep-alive","content-length":"40","content-type":"application/json; charset=utf-8","date":"Wed, 03 Aug 2022 09:02:00 GMT","etag":"W/\"28-3PvShaMs/ugdjKvpR1lS0+UyqjQ\"","expect-ct":"max-age=
604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\"","expires":"Wed, 03 Aug 2022 10:01:58 GMT","location":"https://reqres.in/api/users/2","nel":"{\"success_fraction\":0,\
"report_to\":\"cf-nel\",\"max_age\":604800}","report-to":"{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=409PQEWa3s3L1hO7EGdMvoi19ChvQfyhXJWmwspO79yOYiLOA665haY9mse4apGcNj
uLo8JHCEsRDPrNk0UUqiIYOn4d5t7AF4ewaet5QVxCsB%2Fx%2BG6fzQgRtU0A0ZdkNvqZU2QDrA%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}","server":"cloudflare","transfer-encoding":"chunked","vary":"Accept-Enco
ding","via":"1.1 vegur","x-powered-by":"Express"}
08-03 17:02:00.412  6439  6439 I 02200/JsApp: HttpDemo: send request sucess


//响应内容
08-03 17:02:00.412  6439  6439 I 02200/JsApp: HttpDemo: the result is:{code:200,data:{"updatedAt":"2022-08-03T09:02:00.068Z"}}

```

**发送DELETE请求**

测试的url: [https://reqres.in/api/users/2](https://reqres.in/api/users/2?login=from_csdn)

日志打印:

```
//响应头信息:
08-03 17:02:55.093  6439  6439 I 02200/JsApp: HttpDemo: ResponseHeader:{"access-control-allow-origin":"*","cache-control":"max-age=3600","cf-cache-status":"DYNAMIC","cf-ray":"734de0865f4e7e1c-LAX","co
nnection":"keep-alive","date":"Wed, 03 Aug 2022 09:02:54 GMT","etag":"W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\"","expect-ct":"max-age=604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/ex
pect-ct\"","expires":"Wed, 03 Aug 2022 10:02:53 GMT","location":"https://reqres.in/api/users/2","nel":"{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}","report-to":"{\"endpoints\"
:[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=N66IndbYWWvUovc%2Bp4irrFXzr%2Bf3cV5xwgz9MPbv49eVZpF9tPWeKBFr4UbgMNmGQ1kRb8DoiqpcG81GD%2FrzhzH818MBCVT1BrZ1iYXyocny1lVyBBN5CuI5%2Bzc5vwCGHp
m0HqnvLw%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}","server":"cloudflare","transfer-encoding":"chunked","vary":"Accept-Encoding","via":"1.1 vegur","x-powered-by":"Express"}


08-03 17:02:55.093  6439  6439 I 02200/JsApp: HttpDemo: send request sucess

//响应内容
08-03 17:02:55.093  6439  6439 I 02200/JsApp: HttpDemo: the result is:{code:204,data:}


```

## 参考文献

[1] 应用权限列表. [https://gitee.com/openharmony/do ... /permission-list.md](https://gitee.com/openharmony/do ... .md?login=from_csdn)

[2] 使用ArkTS语言开发(Stage模型). [https://gitee.com/openharmony/do ... t-with-ets-stage.md](https://gitee.com/openharmony/do ... .md?login=from_csdn)

[3] Http数据请求. [https://gitee.com/openharmony/do ... ity/http-request.md](https://gitee.com/openharmony/do ... .md?login=from_csdn)

[4] Http接口介绍. [https://gitee.com/openharmony/do ... pis/js-apis-http.md](https://gitee.com/openharmony/do ... .md?login=from_csdn)
[/md]




欢迎光临 OpenHarmony开发者论坛 (https://forums.openharmony.cn/) Powered by Discuz! X3.5