Kotlin 语言 BindingAdapter 与 LiveData双向绑定

Kotlin 语言 BindingAdapter 与 LiveData双向绑定

环境编译配置

AndroidStudio 2021.1.1

Gradle:gradle-7.1.2-bin.zip

创建一个项目

项目的build.gradle配置:

1
2
3
4
5
dependencies {
//重要配置
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
}

App module build.gradle配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}

android {
//...... 重要配置
dataBinding {
enabled true
}
}

dependencies {
//...... 重要配置
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
}

普通双向绑定

普通的组件,例如TextView、EditText,官方平台提供对双向数据绑定的内置支持

image-20220312163737020

所以常用的组件,可以通过@={}符号,实现数据的双向绑定,以下取个例子:

创建 MainViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/***
* UI View Model
*/
class MainViewModel : ViewModel() {
/***
* 用户名,监听EditText的输入,以及修改EditText的值,达到双向数据通信
*/
val mUserNameMD = MutableLiveData("")

/***
* 密码
*/
val mUserPasswdMD = MutableLiveData("")
}

MainActivity 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class MainActivity : AppCompatActivity() {

private val mState: MainViewModel by lazy {
ViewModelProvider(this).get(MainViewModel::class.java)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main
)
binding.vm = mState //设置ViewModeL
binding.click = OnCLickProxy() //点击按钮监听
binding.lifecycleOwner = this //一定要加上这句话,否则UI不会刷新
observe()
}

private fun observe() {

//监听输入用户名时,是否会同步更新值至 ViewModel
mState.mUserNameMD.observe(this) {
Log.i(MainActivity::class.java.simpleName, "UserName: $it")
}

//监听输入密码时,是否会同步更新值至 ViewModel
mState.mUserPasswdMD.observe(this) {
Log.i(MainActivity::class.java.simpleName, "UserPasswd: $it")
}

}

inner class OnCLickProxy {

fun onSubmit() {
//点击按钮后,获取输入框的值
Log.i(MainActivity::class.java.simpleName, "取值, UserName: ${mState.mUserNameMD.value}, UserPasswd: ${mState.mUserPasswdMD.value}")

//点击按钮后,修改输入框的值,观察输入框是否有变化
mState.mUserNameMD.postValue("张三")
mState.mUserPasswdMD.postValue("123")
}

}
}

activity_main代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

<variable
name="vm"
type="com.example.databinding.MainViewModel" />

<variable
name="click"
type="com.example.databinding.MainActivity.OnCLickProxy" />

</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="40dip"
tools:context=".MainActivity">

<TextView
android:id="@+id/tvDesc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="数据双向绑定"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tvUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="用户名:"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvDesc" />

<EditText
android:id="@+id/etUserName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={vm.mUserNameMD}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvUserName" />

<TextView
android:id="@+id/tvUserPasswd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="密码:"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/etUserName" />

<EditText
android:id="@+id/etUserPasswd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={vm.mUserPasswdMD}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvUserPasswd" />


<Button
android:id="@+id/btnSubmit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:onClick="@{()->click.onSubmit()}"
android:text="提交"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/etUserPasswd" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

image-20220315101439671

效果:

在输入框里输入任意值,点击提交按钮,能够取到输入框里的数据,同时可以改变输入框的值

自定义View双向绑定

我们创建一个自定义组件MyLoginView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyLoginView : LinearLayout {

lateinit var mEtUserName: EditText
lateinit var mEtUserPasswd: EditText

constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context)
}

constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}

private fun init(context: Context) {
//加载布局文件到此自定义组件
//注意:第二个参数需填this,表示加载layout_login.xml到此自定义组件中。如果填null,则不加载,即不会显示text_layout.xml中的内容
val view: View = LayoutInflater.from(context).inflate(R.layout.layout_login, this)

mEtUserName = view.findViewById(R.id.etUserName)
mEtUserPasswd = view.findViewById(R.id.etUserPasswd)

}
}

layout_login.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
tools:context=".MainActivity">

<TextView
android:id="@+id/tvDesc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="数据双向绑定(自定义组件)"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tvUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="用户名:"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvDesc" />

<EditText
android:id="@+id/etUserName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvUserName" />

<TextView
android:id="@+id/tvUserPasswd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="密码:"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/etUserName" />

<EditText
android:id="@+id/etUserPasswd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvUserPasswd" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

自定义DatabindingAdapter(注意是kotlin文件,不是类。定义成object类也可以):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 双向数据通信,设置UI的值
*/
@BindingAdapter(value = ["userName", "userPasswd"], requireAll = false)
fun setUserInfo(view: MyLoginView?, userName: String?, userPasswd: String?) {
view?.mEtUserName?.setText(userName ?: "")
view?.mEtUserPasswd?.setText(userPasswd ?: "")
}

/**
* 当 `userNameAttrChanged` 的 InverseBindingListener onChange()被调用时,此方法会被调用
*/
@InverseBindingAdapter(attribute = "userName")
fun getUserName(view: MyLoginView?): String {
return view?.mEtUserName?.text.toString()
}

/***
* 这个 Binding Adapter不需要定在xml中,但必须要指定后缀 "AttrChanged"
* 也就是: 更新属性+AttrChanged
* 这里是 监听userName 的值
*/
@BindingAdapter("userNameAttrChanged")
fun setUserNameListener(view: MyLoginView, listener: InverseBindingListener?) {
//监听文本输入框
view.mEtUserName?.onFocusChangeListener = View.OnFocusChangeListener { focusedView, hasFocus ->
if (!hasFocus) {
// If the focus left, update the listener
listener?.onChange()
}
}
}


@InverseBindingAdapter(attribute = "userPasswd")
fun getUserPasswd(view: MyLoginView?): String {
return view?.mEtUserPasswd?.text.toString()
}


@BindingAdapter("userPasswdAttrChanged")
fun setUserPasswdListener(view: MyLoginView, listener: InverseBindingListener?) {
//监听文本输入框
view.mEtUserPasswd?.onFocusChangeListener = View.OnFocusChangeListener { focusedView, hasFocus ->
if (!hasFocus) {
listener?.onChange()
}
}
}

MainActivity2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class MainActivity2 : AppCompatActivity() {

private val mState: MainView2Model by lazy {
ViewModelProvider(this).get(MainView2Model::class.java)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMain2Binding = DataBindingUtil.setContentView(
this, R.layout.activity_main2
)
binding.vm = mState //设置ViewModeL
binding.click = OnCLickProxy() //点击按钮监听
binding.lifecycleOwner = this //一定要加上这句话,否则UI不会刷新
observe()
}

private fun observe() {

//监听输入用户名时,是否会同步更新值至 ViewModel(失去焦点才更新)
mState.mUserNameMD.observe(this) {
Log.i(MainActivity2::class.java.simpleName, "UserName: $it")
}
//监听输入密码时,是否会同步更新值至 ViewModel(失去焦点才更新)
mState.mUserPasswdMD.observe(this) {
Log.i(MainActivity2::class.java.simpleName, "UserPasswd: $it")
}


}

inner class OnCLickProxy {

fun onSubmit(view: View) {
//输入框失去焦点,让值更新
view.isFocusableInTouchMode = true
view.isFocusable = true
view.requestFocus()

//点击按钮后,获取输入框的值
Log.i(MainActivity2::class.java.simpleName, "取值, UserName: ${mState.mUserNameMD.value},")
Log.i(MainActivity2::class.java.simpleName, "取值, UserPasswd: ${mState.mUserPasswdMD.value},")


//点击按钮后,修改输入框的值,观察输入框是否有变化
mState.mUserNameMD.postValue("张三")
mState.mUserPasswdMD.postValue("123")
}

}
}

activity_main2.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

<variable
name="vm"
type="com.example.databinding.customview.MainView2Model" />

<variable
name="click"
type="com.example.databinding.customview.MainActivity2.OnCLickProxy" />

</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="40dip"
tools:context=".MainActivity">

<com.example.databinding.customview.MyLoginView
android:id="@+id/loginView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:userName="@={vm.mUserNameMD}"
app:userPasswd="@={vm.mUserPasswdMD}" />


<Button
android:id="@+id/btnSubmit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:onClick="@{(view)->click.onSubmit(view)}"
android:text="提交"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginView" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

MainView2Model

1
2
3
4
5
6
7
8
class MainView2Model : ViewModel() {
/***
* 用户名,监听EditText的输入,以及修改EditText的值,达到双向数据通信
*/
val mUserNameMD = MutableLiveData("")
val mUserPasswdMD = MutableLiveData("")

}

以上源码:https://github.com/myc185/TwowaySample.git

相关问题

编译时遇到 Caused by: javax.net.ssl.SSLException: SSL peer shut down incorrectly

在项目build.gradle中增加

1
2
3
4
maven {
url = "http://maven.aliyun.com/nexus/content/groups/public/"
allowInsecureProtocol = true //gradle build tools 7.0 以上的要增加
}

然后 点击 File –> Sync Project with Gradle FIles 重新编译一次即可

全部如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
buildscript {

//.....省略
repositories {
maven {
url = "http://maven.aliyun.com/nexus/content/groups/public/"
allowInsecureProtocol = true
}
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:$gradleVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}

allprojects {
repositories {
maven {
url = "http://maven.aliyun.com/nexus/content/groups/public/"
allowInsecureProtocol = true
}
google()
mavenCentral()
}
}
//.....省略

相关资料

参考文档

https://developer.android.com/topic/libraries/data-binding/two-way?hl=zh-cn

参考SampleDemo:

https://github.com/android/databinding-samples

新换电脑中重新安装部署Hexo

需求

如果自己更换了新的电脑,或者重装了系统,原有的hexo博客目录如何快速用起来?

安装 Node.js环境

https://nodejs.org/zh-cn/download/ 官方下载安装推荐的长期维护版本

image-20220304111010763

安装成功后,在任意地方邮件鼠标,选择Git Bash Here

image-20220304111855445

执行如下命令j检查Node.js 安装情况:

1
node -v

image-20220304112000807

安装node.js 回自动安装 npm

1
npm -v

image-20220304112102720

安装 Hexo

由于前面安装了 npm,因此我们可以使用 npm 来安装 Hexo。在任意地方右键点击 Git Bash Here,会打开一个窗口,然后在终端中复制运行以下代码:

1
npm install -g hexo-cli

因为是更新的原来的blog文件夹,里面的配置_config.yml已经存在了,不再进行修改

如果需要更新服务器,请在这个配置文件中修改

注意:如果hexo版本更新较大,建议重新生成blog文件夹,然后重新修改_config.yml文件

1
2
3
hexo init blog
cd blog
npm install

在本地计算机安装插件: hexo-deployer-githexo-server。在 blog 根目录下右键打开 GitBash Here,输入:

1
2
npm install hexo-deployer-git --save
npm install hexo-server

配置 Git 全局变量:

1
2
git config --global user.email "xxx@163.com"
git config --global user.name "Lucky Mo"

现在在根目录下右键打开终端, 输入

1
2
3
hexo server

hexo s

这时终端会显示本地的链接:

发布文章

将需要发布 的文章 .md 文件放在 \blog\source\_posts

然后执行以下命令

1
2
3
4
5
6
7
8
9
hexo clean
hexo generate
hexo deploy

hexo cl
hexo g
hexo d

hexo cl && hexo g && hexo d

常见问题

1. Hexo 4000 端口 无法打网页

修改端口即可

1
hexo s -p 4001

2. The “mode” argument must be integer

image-20220304134617332

解决方案:

1
2
npm update
npm audit fix --force

在 blog根目录中找到package.json,修改hexo-renderer-stylus 为2.0.0

1
"hexo-renderer-stylus": "^2.0.0",

3. Hexo 部署失败 Please make sure you have the correct access rights

这种情况是服务器 git下的公钥校验失败了

方案一:

重新上传本地的公钥id_rsa.pub到git的authorized_keys文件中即可

先上传本地 id_rsa.pub 到 私有服务器 git 用户目录下

1
cat id_rsa.pub >> ~/.ssh/authorized_keys

Hexo端口被占用问题解决

问题

更换了新的电脑,通过Dropbox重新更新下来blog文件夹

hexo s 命令启动本地服务器,但是localhost:4000无法访问

image-20220304133106506

http://localhost:4000 无法访问

解决方案

  1. 更换端口重新尝试

    1
    hexo s -p 4001
  2. 在_config.yml文件中加入配置

    1
    2
    3
    4
    server:
    port: 5000
    compress: true
    header: true
  3. 如果修改端口无法解决,则:

​ 打开控制面板 — 程序和功能 — 打开或关闭Windows功能 —- 勾选上Internet Informent Services —–点击确定即可

image-20220304141941178

关于无线路由器不得不说的一些事

一、路由器的工作原理

1、首先来简单地解释一下这几个概念:软路由、硬路由、NAT

软路由:可以看做是带CPU、内存、硬盘的小电脑,并刷入了LEAD或OPENWRT的系统。特点是由第三方软件支持,功能强大,体积小,功耗低,内存和CPU处理能力比硬理由要强大得多。如果不考虑成本,普通的台式机、笔记本电脑都是可以当做软路由使用的。

image-20191213181536170

硬路由:我们平时所见到的无线路由器,都能当做是硬路由。

image-20191213181951668

**NAT:**Network Address Translation,网络地址转换。内网中的机器(内网,大多都是192.168.x.x网段)想要访问外网,必须通过路由器进行NAT进行地址转换(检查数据包的头部,而且对其进行修改,从而实现同一内网中不同主机共用更少的公网IP),才能连接互联网上的机器。这个NAT的转换,不同的路由极限性能不一样。

2、了解Mbit和MB之间的关系

我们常听到这么说:“我家拉了100兆的宽带,但是下载最大的下载速度也就10来兆,这是为什么?”

“拉了100兆的宽带” 指的是 100Mbit,“下载速度也就10来兆”指的是10MB

划重点:

100Mbit / 8 = 12.5 MB

也就是100兆带宽,理论最大下载速度只能达到12.5MB,时间单位是秒。即下载1GB(1024MB)的电影,耗时约 1024/12.5=81.92秒,一分半钟不到。

我们接下来讲的都是Mbit(除非指定),也就是百兆路口由器、千兆口路由器、互联网访问带宽等都是指速率是Mbit/s(简称Mbps)

3、路由器的芯片组成架构

我们现在使用的路由器,一般都是由主芯片组合无线芯片组构成,路由器的运作原理大概如下图:

image-20191213224443694

无线连接速度取决于信号强弱,芯片协议,天线聚合等;信号强弱取决于芯片,功率设置,天线,阻隔层度等;总体来说因素较多。

如果不考虑有线连接,电脑(或手机等设备)通过连接路由器的无线芯片组进行上网,数据发送接受的过程大概是这样:

image-20191213233856281

因此,如果不考虑路由器总线等这些微小的损耗,我们可以说,电脑无线上的带宽,取决于无线连接带宽NAT带宽互联网带宽之间最慢的那一个。响应延迟则为NAT延迟无线延迟互联网延迟。

取个例子:

假设访问互联网的带宽是100Mbps,路由器的NAT性能是50Mbps,无线带宽连接是866.7Mbps(5GHz),那你的上网速率是50Mbps

4、影响上网带宽因素分析

下面我们对这三个大个环节进行逐一分析

4.1 互联网带宽

这里的互联网带宽,就是你每年或者每月需要缴费的上网带宽了。从几年前的4M、8M、12M这种常见的ADSL拨号上网,到现在的50M、100M、200M、500M、1000M都司空见惯光线入户。

光线用户一般都配有一台光猫,需要注意的地方是:

  • 光猫它本身也是一个路由器,不要自作聪明拿它来进行拨号上网,使用它自身默认的桥接模式即可,因为它的性能不好,使用你自己购买的路由器来拨号即可

  • 如果光猫带有两个网口,且你申请入户带宽超过100M,请看准网线接入的口是1000M的。

  • 确保网线是超五类或六类网线

这样,互联网带宽这块基本不会出什么问题,你申请多少兆带宽入户,那就是有多少带宽了。

4.2 NAT带宽

每一台路由器都会有一个NAT极限性能,如果这个极限性能低于互联网的速度,就会影响我们的网速。同样的NAT性能,小包越多,连接数越多,总的带宽越小,产生的延迟就越大

同一性能下NAT实际吞吐量(即每秒传输的速度)因连接数和数据包多少不同而不同。大概理解为每一个连接和每个数据包都要占用一定的CPU和RAM(硬件NAT则有最大连接数一说)。所以说小数据包越多,连接数量越多,NAT处理越占资源,性能不变下每秒能处理的数据越少(即速度越慢)

BT使用多来源病和的方式获取更大的带宽。迅雷则通过多连接的方式从每个来源获取更大的带宽,本质上都会增加连接数量。在连接数量不超限的情况下,速度增加,当连接数增多到NAT无法处理的时候(即每秒处理能力低于网速的时候),就会影响网速。这就是我们平时多人上网,都喊着要把下载关掉的原因,但实际上很大程度是路由器NAT能力不够影响的,而不是真的带宽不够造成的。

现在大部分的路由器对百兆的WAN to LAN(即NAT)处理,已经不存在问题了,100元的路由器NAT硬件都能做得很好了。但是如果连接数量太多,依然会影响使用体验(实际上更多情况是无线模块造成的影响,同样并发和小包是无线模块的杀手,而且更脆弱,后面我们会详细将)

实际上很多我们能买到的全千兆端口的路由器,NAT性能可能在300Mbps~700Mbps之间,并不能跑满千兆的WAN to LAN,且随着连接数增加(使用压力增大)的情况下,NAT吞吐量会有所下降,可能甚至无法跑满200M甚至100M的带宽,所以NAT性能建议在Iperf3测试中至少跑满甚至超过实际网速。

其实,一般家用的路由器比大多数的软路由NAT能力要强大得多。它芯片组内除了CPU外,有专门的NAT处理模块,由于是专用芯片,因此可以提供相更强的NAT性能。联发科的MT7261(小米路由器PRO),博通BCM47189(腾达AC9博通版)等都可以提供千兆NAT的性能。

总结:

  • 如果是多人使用,想有好的上网体验,路由器不要买太差(几十块或一百出头的百兆路由器,就不要购买了),有能力的可以先辨别芯片的NAT处理能力再购买(NAT的处理速率一定要大于带宽),这样NAT带宽这块就不必太过担忧

  • 如果可以,尽量关闭后台不使用的程序,特别是网盘、腾讯视频、迅雷等

  • 使用人数少,连数据不大,小包数据量不多的情况下,NAT并不是路由器无线上网的瓶颈。

4.3 无线带宽

路由器的无线使用体验,是最让我们困扰、头痛的部分,因此将它拉出来单独一节讲解

二、影响路由器无线带宽的因素

每一个无线路由器的关键指标就是SoC(系统级芯片),它囊括路由器的NAT、CPU、RAM、无线模块等搭配的详细信息

SoC 的定义多种多样,由于其内涵丰富、应用广泛,很难给出准确定义。一般来说,SoC称为系统级芯片,也有称片上系统,意指它是一个产品,是一个有专用目标的集成电路,其中包含完整系统并嵌入软件的全部内容。同时它又是一种技术,用以实现从确定系统成功开始,到软/硬件划分,并完成设计的整个过程。

如何查看一个路由器的SoC?这里拿测试小组使用的华硕 AC66U B1来举例:

小插曲:本来想看一下测试小组用的最多的华为荣耀HIROUTER-CD20路由器举例的,但是网上搜了一遍没能找到芯片的相关信息,JD貌似也下架了,可能这款路由器用户量很小,根本没能入了测评大师们的法眼。后来我想起那天特意看它盒子说明,标有一个“边缘路由器”字样(我当时很纳闷这是什么意思),然后重新搜了一下这个设备的组网信息,找到了这款的相关说明。它们是华为套装分布式路由器(一套三个卖),每个都可以当做主体/分身路由使用,适用于家庭楼层间的无线组网。我认为,它不合适测试组较为高强度和连接数较大的工作,虽然说它们都是千兆级别;或者说我们使用的姿势不对,埋没了它组网的才能。

在这个神奇的Wiki中搜索AC66UB1,就能找到相关信息

image-20191214223236269

如果你在这里搜不到路由器型号,可以先查找SoC型号,然后再回到神奇的网站去搜索

百度搜索 ASUS AC66U B1 SoC 就能立马知道使用的是博通的芯片BCM4708C0

image-20191214221621834

通过查看详情信息,我们抽取ASUS AC66U B1这台路由器的SoC关键数据如下(其他我也看不懂):

  • CPU 双核 1GHz(不是软路由,不用太过关注)

  • RAM 256M(相对比较大的了)

  • 一个USB3.0,一个USB2.0

  • 4口1000M LAN

  • IEEE 802.11协议:a/b/g/n + ac

  • 无线MIMO技术:三根天线,2.4GHz 3x3 MIMO 最高450Mbps 5GHz 3x3 MIMO 最高 1300Mbps

  • 一个1000M WAN 口

  • 第三方固件支持梅林(老司机会懂的)

image-20191214224808002

image-20191214225002160

image-20191214234100442

很遗憾,暂时没能找到这块SoC的NAT(WAN-LAN Throughput)性能,只是在胡乱点开的国外论坛别人的对话里说是1Gbps,但不知实际性能如何。找机会测一测。

小插曲: 路由器的时候,商家标题都会加上 1900M、2100M无线这种数字,这里其实是2.4GHz和5GHz速率的总和,如果硬件配置跟得上,数值越大当然越好。这么宣传有点误导的嫌疑,但人家说的并没有错。

拿AC66U-B1来说,2.4GHz 是450Mbps,5GHz是1300Mbps,总和是1700Mbps。这里值得一提的是,这里的速度是协议速度,由于距离等各种因素,实际工作时的速率大概是协议速度的60%~70%不等,不同的路由器,不同的无线功率,会有不同的表现

另外,JD上很便宜销量巨大的TP-LINK TL-WDR5620 无线速率1200M,但是WAN和LAN口都是百兆口,因此实际工作时,无线速率根本跑不到这么大。这个‘取巧’的宣传不知骗过了多少帅哥美女,包括我在内,承认当初是冲着这个1200M去的,悔恨不迭啊….

image-20191214235504608

1、无线连接协议

无线连接协议 IEEE 802.11

IEEE 802.11是美国电气和电子工程师协会IEEE在1997年6月颁布的无线网络标准。它是第一代无线局域网标准之一。IEEE 802.11规定了无线局域网在2.4GHz波段进行操作,这一波段被全球无线电法规组织定义为扩频使用波段。标准的802.11主要用户解决办公室局域网和园区网中用户与用户终端的无线接入,速率最高只能达到2Mbit/s

协议经过不断地更新,无线协议已经有 a/b/g/n/ac/ax这几个迭代了,其中ac/ax属于5GHz频段。而ax属于WiFi6的协议了,目前设备还没有普及。

无线连接频段

我们常用的路由器,使用的是 2.4GHz5GHz两个频段

我们Wi-Fi常用的协议是 802.11 n 和 802.11 ac两种,其中n协议同时支持2.4GHz和5GHz两个频段的连接。

  • 802.11 b 极速是 11Mbps

  • 802.11 g 极速是 54Mbps

  • 802.11 n 极速是 150Mbps(1x1 MIMO 40MHz频宽)

  • 802.11 ac 极速是 433Mbps(1x1 MIMO 80MHz频宽)

更多信息可以在 IEEE 802.11维基百科 中查阅

2、无线MIMO技术

好了,下面我们来重点关注无线的MIMO技术。通过之前收集的信息,我们了解到AC66U支持 3x3 MIMO,可以简单理解为 三根天线可以同时传输数据,也就是路由器具备三条Streams

image-20191215034342359

我们以华硕AC66U B1为例,5GHz频段,三根天线,不考虑连接距离的情况,每根天线的理论极限速率是 433Mbps。

1. 2x2 MIMO的手机连接

假设支持 2x2 MIMO手机连接路由器的时候,只会用到其中两根天线,那手机连接路由器的极限速率就是 866Mbps

同理,如果是 1x1 MIMO的手机,速率就是433Mbps

2. 3x3 MIMO的电脑连接

假设支持 3x3 MIMO电脑连接路由器的时候,所有天线都会用到,它们之间的传输速率就是路由器5GHz的极限速率 1300Mbps

无线MIMO需要特别关注的地方

  1. MIMO技术并不是单方面的技术,除了路由器,也需要连接路由器的设备支持才可以建立高速的连接体验
  2. 不支持MU-MIMO技术的路由器,一块无线芯片,同时间内只能向一个设备发送数据,也就是说,如果手机跟电脑同时连接路由器,当路由器向手机发送数据的时候(即使当前的天线并没有全都使用),电脑也会处于等待状态,等到向手机发送数据完毕,才会向电脑发送,以此轮流交替传输。因此,当发送大量数据,特别是一个个小数据包(或者小文件)的时候,所有连接到了无线路由器的设备并不会有等候的感觉,指挥觉得传输速度减慢。
  3. 如果有一台设备距离路由器很远,那它的速率会变得很低,因此发数据给它会变慢甚至超时,从而会拉低其他设备的速度

无线连接总结:

  • MIMO技术并不是单方面的技术,除了路由器,也需要连接路由器的设备支持才可以建立高速的连接体验
  • 购买路由器,尽量选购千兆路由器,NAT和无线速率稳定的路由器。路由器测评网站:ACWIFI
  • 连接设备数量越多,传输速度越容易受影响。
  • 避免远距离的设备连接路由器,它会拉低其他所有设备的速度,影响整体的体验
  • 2.4GHz无线连接,我们只需要考虑信号和稳定性就好了,因为它的连接速度比较慢。我们要养成连接5GHz无线SSID的习惯来提高我们的用网体验。
  • 无线连接是非常玄学的东西,影响它的因素非常多,距离、使用人数、电视电磁炉干扰、墙壁阻隔、不同良心厂商生产出来的无线功率等等。不要相信什么穿墙王的描述。不要想象有一台超级路由器,可以无数人连接使用,当达到路由器瓶颈时,我们应该考虑通过有线+新的路由器的方式将压力分担出去。

三、关于公司整改无线网络的想法

网络需要整改的缘由是:WiFi设备数量越来也多,互相干扰导致终端设备出现断开连接、数据丢包、上网速率下降等问题,影响正常工作。

这里的相互干扰,表现的最明显的就是2.4GHz频段了,我们来看看无线路由多了之后,为什么会相互干扰。

1、WiFi 干扰问题的分析

无线WiFi数据传输是依赖于电磁波的,2.4GHz频段拢共有14个频道(信道)来承载数据发送,每个频道带宽20MHz(有些路由器可以动态调整为40MHz),带宽大的,速率也就越大。

image-20191215112400990

IEEE 802.11 2.4GHz的频率范围是2412MHz~2484MHz,其中容纳了14个频道,就会发送重叠,进而产生干扰。百度维基中对信道列表的干扰问题描述是这样的:

image-20191215113609654

总结起来就是:有多个WiFi同时使用时,为了避免发生信号干扰,两两路由器之间的距离要小于-50dBr(这个是信号强度),或者两两路由器之间的信道至少要有22MHz的间距。

同时,很人性化地给出来互不干扰的信道推荐(注意看使用的协议和带宽):

image-20191215114508797

2、我臆想的无线网络整改方案

2.1 方案一:重新布线组网,统一管理

1.AC + AP组网

AP 可以理解成没有WAN口(或只使用LAN口)并关闭了DHCP的路由器

这种方式是使用一个软路由进行DHCP分配,其他无线路由器当做AP

image-20191215130836843

这个方案的优点:

  • 全程布线,带宽的瓶颈仅仅在于无线部分

  • 能有效分担多人连接同一个无线产生的压力

  • 成本相对较低,现有的路由器都能当做一个AP使用

这个方案的缺点:

  • 即使每个AP的SSID名称一样,从一个AP走进另一个AP,并不能自动无缝切换网络,可能会出现你在很强的AP范围,却连上了很弱的那个AP
  • 在有特殊需求的开发人员或者测试人员区域,AP热点可能比较密集,问题又回到 了原点
  • 无法满足模拟指定SSID的特殊需求(当然每个AP的SSID可以自定义名称,这个就不是问题了)

这个方案,投入成本相对较少,也对整体无线网络有了一定优化。但我个人觉得这个方案合适办公隔离教性强(即多个房间隔离),有没有特殊开发测试需求,仅仅需要能够上网的环境

2.MESH组网

支持Mesh组网的无线路由器,可以看做是两台路由器之间进行了无线桥接,从而在不需要布线的情况下,扩大信号覆盖的范围。

我个人不太推荐这种方案,理由是:

  • 投入较大,市面上的套装一般较贵。并不是贫穷限制了想象,毕竟没有尝试过,如果花了大成本,达不到想要的效果,心痛是必然的。
  • 这种组网是无线组网。而无线又是很玄学的东西(受影响干扰的因素实在太多)
  • 我认为这个合适大家庭,不合适企业用,特别是有开发和测试需求的我们。
3.KVR无线漫游组网

先来看一下苹果官方的KVR详解

image-20191215134121742

简单地说

K:无线路由器告诉我们设备,周围可用的接入点

R:快速认证,节省接入点的认证时间

V:监控设备与无线路由器负载判断最接接入点以及控制切换时机

这个组网方案类布线跟AC+AP方式一样,但能解决AC+AP组网无缝漫游切换的问题。

但是对路由器有一定要求:

  • 需要支持KVR协议

  • 需要第三方固件支持

  • 部署入门门槛比较高,合适技术控折腾

这里不再展开讨论。

2.2 方案二:局部优化,整体协调

公司除了研发和测试组,能够感觉到Wi-Fi信号的干扰外,我相信大多数人并没有察觉,即使他们处于信号严重覆盖的区域。因为他们仅仅是有上网的需求,偶尔的慢速可能并不影响他们的满意度,或者他们会归咎于电脑的卡顿问题。

因此我这里先把人群进行归类:

  • 测试小组人员 重灾区,也是亟待优化解决的那部分人。他们可能需要人手一台路由器,可能一台路由器挂在十几个设备。特别是摄像头的P2P是占用连接数、上传带宽的,非常容易遇到塞车的情况。
  • 开发调试人员 次灾区,可能不需要挂在多台设备,但需要路由器就在近前
  • 其他员工人员 能有不错的上网体验就会心满意足。

法则:我们以测试人员为核心,进行局部优化,其他人员避让协调

我们来解决测试人员的问题:

1
2
3
4
5
6
7
1. 使用时,尽量不要超出路由器5M的范围
2. ASUS-AC66U-B1保留,重新购买新的两台合适的路由器。4/5人共用这三台。(因为合适的2.4GHz g/n协议信道不干扰的只有1/6/11 三道,所以先暂时购买三台试水)。平时测试时请自行合理分配连接的路由器,比如:平均分担连接设备的数量到各个路由器;当遇到问题需要排查网络原因时,适时地清空出一台路由器单独连接测试;对路由器连接控制进行归类,长期连接的归一台,经常测试的归类一台等等。
3. 可以给测试小组搭建 1AC+3AP组网,使他们的无线路由器都处于同一个网段,提高平时测试的上网体验
4. 测试时,手机仅连接5G,2.4G留给摄像头,不使用的设备就关掉不要连接。
5. 进行老化测试或者其他测试的设备,可以拿之前不用的路由器去屏蔽房、研发室搭建2.4GHz b协议的 4信道组网,也要尽可能不要再在一台路由器连接过多的设备。这种测试比较贴切用户的使用情况,就是路由器品牌不一,连接数较少的情况
6. 每一台路由器测试人员都应该有专人管理,实时把控连接数,不允许其他相关的人连接。

我们来解决开发人员的问题:

1
2
3
4
5
6
1. 在测试小组周围的,请避开测试小组使用的信道,并如果可以,放置位置远离测试人员的路由器
2. 3~4人共用一台设备,特殊情况可以2人共用一台,例如远离测试小组的地方。原则是先多人共用,如果不满足开发需求,再增加路由器,避免资源浪费和路由器的信号干扰
3. 平时使用时,手机或者笔记本电脑连接5G网络,摄像头使用2.4GHz频段。不使用的设备就关掉不要连接。能连接网线的优先使用网线进行连接。
4. 不需要2.4GHz的人员,请不要打开此频段,保留5G频段即可(例如后台开发人员)
5. 每一台路由器测试人员都应该有专人管理,实时把控连接数,不允许其他相关的人连接。

我们来解决其他人员的问题:

1
2
3
4
1. 在测试小组周围的,放置位置远离测试人员的路由器
2. 路由器只允许打开5GHz频段,有特殊需要时,再打开。
3. 可以的话,已部门为单位,使用一台路由器即可。
4. 不要连接弱网的Wi-Fi,对自己,对他人都是不好的体验。

以上方案纯属臆想,并未真正实践。

四、参考

Spoto Tsui

认识无线路由的运作原理,教你如何选择无线路由器

多个无线路由(AP)为何能提升家庭无线网络体验?