本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html

进程间通讯篇系列文章目录:

在上一篇博文中介绍了一种轻量级的跨进程通讯方案-Messenger,Messenger实现起来非常简单,其底层原理也是AIDL,更像是一个简易版的AIDL,但简单的东西往往也有其局限性,Messenger的主要作用是传递消息,它无法实现RPC功能也就是无法让我们在客户端本地就能调用远程的方法,而且Messenger是以串行的方式处理,无法同时处理多个请求,只能一个一个的处理。而AIDL就可以很好弥补Messenger的不足,虽然实现起来相对复杂一些,但它功能强大,无疑是跨进程通讯的首选方案。接下来我们先看看AIDL是什么,都可以传递哪些数据,并且本文会用一个小例子来直观的体会AIDL的实现过程。

读完本文你将深入掌握以下几个知识点:

  • AIDL是什么?
  • AIDL传递的类型。
  • 怎么创建AIDL。
  • AIDL文件中的定向tag:in、out、inout的区别。
  • 如何在AIDL中添加权限校验。

一、AIDL是什么?

AIDL全称Android Interface Definition Language,即Android接口定义语言。AIDL是Android中可以实现跨进程通讯的一种方案,通过AIDL可以实现RPC方式,所谓RPC是指远程过程调用(Remote Procedure Call),可以简单的理解为就像在本地一样方便的调动远程的方法。在Android的跨进程通讯的方案中,只有AIDL可以实现RPC方式。

二、AIDL文件支持哪些数据类型:

  • 基本数据类型:int、long、char、boolean、double等
  • String
  • CharSequence
  • ArrayList:里面每个元素也需要被AIDL支持
  • HashMap:里面的每个Key和Value也都需要被AIDL支持
  • Parcelable:所有实现了此接口的对象
  • AIDL:所有的AIDL接口本身也可以在AIDL文件中使用

三、创建AIDL

接下类用一个小例子来说明AIDL的创建过程及用法,尽管在同一个APP内依然可以指定两个进程,但为了更能凸显“跨进程”这一点,还是决定将此示例借助于两个APP来实现,毕竟在开发中真实的需求也是发生在两个APP中。

在实现AIDL的过程中服务端APP和客户端APP中要包含结构完全相同的AIDL接口文件,包括AIDL接口所在的包名及包路径要完全一样,否则就会报错,这是因为客户端需要反序列化服务端中所有和AIDL相关的类,如果类的完整路径不一致就无法反序列化成功。

小技巧:为了更加方便的创建AIDL文件,我们可以新建一个lib工程,让客户端APP和服务端APP同时依赖这个lib,这样只需要在这个lib工程中添加AIDL文件就可以了!

简要说明一下将要实现的小例子的需求:是一个通讯录,在服务端维护一个List用来存放联系人信息,客户端可以通过RPC方式来添加联系人、获取联系列表等功能。

1、新建一个承载AIDL文件的lib(在本示例中姑且叫做libaidl)

  • 创建一个Android Library类型的Module,为了与普通的java代码作区分,在main文件夹下为AIDL文件新建一个专门的文件夹,新建工程的结构如下:

  • 然后添加AIDL接口文件:

首先新建一个Contact类,通过上面的介绍我们知道,普通的java类是不能在AIDL中使用的,必须要实现Parcelable接口,并在AIDL文件中声明:

Contact.java

/**
* Created by liuwei on 18/2/8.
*/
public class Contact implements Parcelable {
private int phoneNumber;
private String name;
private String address; public Contact(int phoneNumber, String name, String address) {
this.phoneNumber = phoneNumber;
this.name = name;
this.address = address;
} @Override
public int describeContents() {
return 0;
} @Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(phoneNumber);
dest.writeString(name);
dest.writeString(address);
} private final static Creator<Contact> CREATOR = new Creator<Contact>() {
@Override
public Contact createFromParcel(Parcel source) {
return new Contact(source);
} @Override
public Contact[] newArray(int size) {
return new Contact[size];
}
}; public Contact(Parcel parcel) {
phoneNumber = parcel.readInt();
name = parcel.readString();
address = parcel.readString();
} @Override
public String toString() {
return "Contact{" +
"phoneNumber=" + phoneNumber +
", name='" + name + '\'' +
", address='" + address + '\'' +
'}';
}
}

声明Contact类

Contact.aidl

package cn.codingblock.libaidl.contacts;
parcelable Contact;

创建AIDL接口文件,声明需要暴露给客户端的方法。

IContactsManager.aidl

package cn.codingblock.libaidl.contacts;

import cn.codingblock.libaidl.contacts.Contact;

interface IContactsManager {
int getPhoneNumber(in String name);
String getName(int phoneNumeber);
Contact getContact(int phoneNumber);
List<Contact> getContactList();
boolean addContact(in Contact contact);
}

注:在AIDL接口文件中如果引用到了某个类,即使与这个类的AIDL声明在同一个包中也使用import导入此类。

aidl文件最终的结构如下:

  • 在本次的示例中我们的客户端APP是ipcclient工程,服务端APP是ipc工程,记得在两个工程中添加libaidl的依赖(添加依赖的方法比较简单,就不多说了),服务端工程、客户端工程、lib工程的结构如下:

小问题:AIDl文件中in、out、inout的区别?

  • in、out、inout称为AIDL接口方法参数的定向tag,代表着数据的流向。
  • in:服务端收到对象后对此对象做任何修改都不会同步给客户端。
  • out:无论客户端传过去的对象有没有提前设置值,在Binder传输过程中都会new一个空对象传递给服务端,服务端接收到的对象后对此对象所做的修改都会同步给客户端。
  • inout:服务端接受对象后,无论是客户端还是服务端对此对象所做的修改都会两端同步。
  • 基本类型的参数只能是in。

对此问题感兴趣的同学可以查看AIDL所生成的Stub源码。

2、服务端实现(在ipc工程中)

  • 创建一个Service,用于响应客户端的绑定请求,我们将这个Service名为为ContactManagerService。
  • 接着创建一个类,让这个类继承AIDL接口中的Stub类,并实现其抽象方法。在Service中返回这个新建这个类的对象。

详细实现如下:ContactManagerService.java

/**
* Created by liuwei on 18/2/8.
*/
public class ContactManagerService extends Service { private final static String TAG = ContactManagerService.class.getSimpleName(); private CopyOnWriteArrayList<Contact> contacts = new CopyOnWriteArrayList<>(); @Override
public void onCreate() {
super.onCreate();
contacts.add(new Contact(110, "报警电话", "派出所"));
contacts.add(new Contact(119, "火警电话", "消防局"));
contacts.add(new Contact(112, "故障电话", "保障局"));
} @Nullable
@Override
public IBinder onBind(Intent intent) {
return new ContactManagerBinder();
} private class ContactManagerBinder extends IContactsManager.Stub{ /**
* 根据号码返回手机号
* @param name
* @return
* @throws RemoteException
*/
@Override
public int getPhoneNumber(String name) throws RemoteException {
if (!TextUtils.isEmpty(name)) {
for (Contact contact:contacts) {
if (contact.name.equals(name)){
return contact.phoneNumber;
}
}
}
return 0;
} /**
* 根据号码返回名称
* @param phoneNumber
* @return
* @throws RemoteException
*/
@Override
public String getName(int phoneNumber) throws RemoteException {
for (Contact contact:contacts) {
if (contact.phoneNumber == phoneNumber){
return contact.name;
}
}
return null;
} /**
* 根据号码返回联系人对象
* @param phoneNumber
* @return
* @throws RemoteException
*/
@Override
public Contact getContact(int phoneNumber) throws RemoteException {
for (Contact contact:contacts) {
if (contact.phoneNumber == phoneNumber) {
return contact;
}
}
return null;
} /**
* 获取联系人集合
* @return
* @throws RemoteException
*/
@Override
public List<Contact> getContactList() throws RemoteException {
return contacts;
} /**
* 添加联系人
* @param contact
* @return
* @throws RemoteException
*/
@Override
public boolean addContact(Contact contact) throws RemoteException {
if (contact != null) {
return contacts.add(contact);
}
return false;
}
}
}
  • 最后在清单文件中将此Service添加配置,并将export属性设为true以供外界调用:
<service android:name=".aidl.contact.ContactManagerService"
android:exported="true"/>

上面代码很简单,值得一提的是AIDL的方法都是在服务端的Binder线程池中执行的,如果有多个客户端同时请求,就会有多个线程来操作这些方法,本次示例将存放联系人的集合采用了CopyOnWriteArrayList实现,由于CopyOnWriteArrayList本身是线程安全的,所以在此我们不需要做额外的同步处理。

从上文我们知道,在List中AIDL只支持ArrayList的传输,那么在此处为什么可以使用CopyOnWriteArrayList呢?

这是因为AIDL支持的是List,之所以说AIDL只支持传递ArrayList

,是因为它在传递其他List类型时就会自动将其他类型在传递之前转换成ArrayList然后再返回给服务端,也就是说无论你在服务端使用其他的任何list的子类型,在客户端接收到的类型都是ArrayList。

所以本次示例中虽然服务端返回的事CopyOnWriteArrayList,但是在Binder中会按照List的规范去读取它并最终形成一个新的ArrayList返回给客户端,类似的还有ConcurrentHashMap对应于HashMap。(其实不光CopyOnWriteArrayList,还有LinkedList等其他的List子类型也都是可以的。)

3、客户端实现(在ipcclient工程中)

  • 在客户中绑定服务端的Service,绑定成功后就可以在ServiceConnection中的onServiceConnected方法中将返回的Binder对象转换成AIDL接口所属的类型。

首先向Intent指定Component,需要传入两个参数,一个是远程Service所在工程包名,另一个是远程Service的全量限定名,然后使用bindService绑定远程Service:

Intent intent = new Intent();
intent.setComponent(new ComponentName("cn.codingblock.ipc", "cn.codingblock.ipc.aidl.contact.ContactManagerService"));
bindService(intent, serviceConnection, BIND_AUTO_CREATE);

在serviceConnection中获取返回的Binder并使用IContactsManager.Stub.asInterface()方法将Binder对象转换成IContactsManager类型。

private ServiceConnection serviceConnection = new ServiceConnection(){
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mIContactsManager = IContactsManager.Stub.asInterface(service);
Log.i(TAG, "onServiceConnected: mIContactsManager=" + mIContactsManager);
} @Override
public void onServiceDisconnected(ComponentName name) {
mIContactsManager = null;
Log.i(TAG, "onServiceDisconnected: ");
}
};
  • 拿到Binder对象后就可以调用在AIDL文件中声明的方法了,来看一下完整的代码:
/**
* Created by liuwei on 18/2/8.
*/
public class ContactMangerActivity extends AppCompatActivity {
private static final String TAG = ContactMangerActivity.class.getSimpleName(); private IContactsManager mIContactsManager;
private EditText et_contact_name;
private EditText et_contact_phone_number;
private EditText et_contact_address; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contact_manger); ViewUtils.findAndOnClick(this, R.id.btn_add_contact, mOnClickListener);
ViewUtils.findAndOnClick(this, R.id.btn_get_phone_number, mOnClickListener);
ViewUtils.findAndOnClick(this, R.id.btn_get_name, mOnClickListener);
ViewUtils.findAndOnClick(this, R.id.btn_get_contact, mOnClickListener);
ViewUtils.findAndOnClick(this, R.id.btn_get_list, mOnClickListener); et_contact_name = ViewUtils.find(this, R.id.et_contact_name);
et_contact_phone_number = ViewUtils.find(this, R.id.et_contact_phone_number);
et_contact_address = ViewUtils.find(this, R.id.et_contact_address); Intent intent = new Intent();
intent.setComponent(new ComponentName("cn.codingblock.ipc", "cn.codingblock.ipc.aidl.contact.ContactManagerService"));
bindService(intent, serviceConnection, BIND_AUTO_CREATE); } private ServiceConnection serviceConnection = new ServiceConnection(){
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mIContactsManager = IContactsManager.Stub.asInterface(service);
Log.i(TAG, "onServiceConnected: mIContactsManager=" + mIContactsManager);
} @Override
public void onServiceDisconnected(ComponentName name) {
mIContactsManager = null;
Log.i(TAG, "onServiceDisconnected: ");
}
}; private View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_add_contact: Contact contact = new Contact(getEtContactPhoneNumber(), getEtContactName(), getEtContactAddress());
try {
mIContactsManager.addContact(contact);
} catch (RemoteException e) {
e.printStackTrace();
} break; case R.id.btn_get_phone_number:
String name = getEtContactName(); try {
Log.i(TAG, "onClick: " + name + "的电话:" + mIContactsManager.getPhoneNumber(name));
} catch (RemoteException e) {
e.printStackTrace();
} break; case R.id.btn_get_name: int number = getEtContactPhoneNumber(); try {
Log.i(TAG, "onClick: " + number + " 对应的名称:" + mIContactsManager.getName(number));
} catch (RemoteException e) {
e.printStackTrace();
} break; case R.id.btn_get_contact: int number1 = getEtContactPhoneNumber(); try {
Contact contact1 = mIContactsManager.getContact(number1); System.out.println(contact1); } catch (RemoteException e) {
e.printStackTrace();
} break; case R.id.btn_get_list: try {
List<Contact> contacts = mIContactsManager.getContactList(); System.out.println(contacts); } catch (RemoteException e) {
e.printStackTrace();
} break;
}
}
}; private String getEtContactName() {
String str = et_contact_name.getText().toString();
if (TextUtils.isEmpty(str)) {
Toast.makeText(this, "请输入联系人名称", Toast.LENGTH_SHORT).show();
return null;
}
return str;
} private int getEtContactPhoneNumber() {
String str = et_contact_phone_number.getText().toString();
if (TextUtils.isEmpty(str)) {
Toast.makeText(this, "请输入联系人电话", Toast.LENGTH_SHORT).show();
return 0;
}
return Integer.valueOf(str);
} private String getEtContactAddress() {
String str = et_contact_address.getText().toString();
if (TextUtils.isEmpty(str)) {
Toast.makeText(this, "请输入联系人地址", Toast.LENGTH_SHORT).show();
return null;
}
return str;
} @Override
protected void onDestroy() {
super.onDestroy();
unbindService(serviceConnection);
}
}

布局文件也就几个EditText和Button比较简单,这里就不贴出来了,接下来运行测试一下。

四、运行测试

两端都运行后,客户端界面如下图:

查看ipcclient工程的log如下,发现已经成功绑定了远程的Service:

.../cn.codingblock.ipcclient I/ContactMangerActivity: onServiceConnected: mIContactsManager=cn.codingblock.libaidl.contacts.IContactsManager$Stub$Proxy@6b60cb6

此时查看ipc工程的log如下:

.../cn.codingblock.ipc I/ContactManagerService: onCreate: ContactManagerService started...
.../cn.codingblock.ipc I/System.out: 现有的联系人:[Contact{phoneNumber=110, name='报警电话', address='派出所'}, Contact{phoneNumber=119, name='火警电话', address='消防局'}, Contact{phoneNumber=112, name='故障电话', address='保障局'}]

通过上面两个log说明客户端和服务端已经链接成功了,接下类测试一下各按钮远程方法,在号码输入框中输入110,依次点击获取联系人名称按钮和获取联系人信息按钮,log如下:

.../cn.codingblock.ipcclient I/ContactMangerActivity: onClick: 110 对应的名称:报警电话
.../cn.codingblock.ipcclient I/System.out: Contact{phoneNumber=110, name='报警电话', address='派出所'}

接着在三个输入框里面分别输入David,111,david`s home,然后点击添加联系人信息将联系人添加到远程列表里面,在点击获取联系人列表,log如下:

.../cn.codingblock.ipcclient I/System.out: [Contact{phoneNumber=110, name='报警电话', address='派出所'}, Contact{phoneNumber=119, name='火警电话', address='消防局'}, Contact{phoneNumber=112, name='故障电话', address='保障局'}, Contact{phoneNumber=111, name='David', address='david`s home'}]

可以看到david的信息已经成功添加进来了。

五、如何为AIDL添加权限验证

其实在正式的开发工作中,我们不希望任何客户端都能绑定我们的服务端,因为这会存在极大安全隐患,所以当客户端想我们发来绑定请求是我们需要做权限校验,符合我们权限要求的客户端才可以与我们的服务端建立链接。

添加权限校验可能会有很多方法,没有对错之分,在实际开发中适合就好,接下来我们介绍一种相对来说比较方便的权限验证的方案:

  • 还是用上面的示例来说明,首先在服务端工程也就是ipc工程的清单文件中声明所需的权限,如下:
<!--声明权限-->
<uses-permission android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER"/>
<!--定义权限-->
<permission
android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER"
android:protectionLevel="normal"/>
  • 然后在ContactManagerService的onBinde方法中进行权限验证,验证不通过就直接返回null。
@Nullable
@Override
public IBinder onBind(Intent intent) {
if (checkCallingOrSelfPermission("cn.codingblock.permission.ACCESS_CONTACT_MANAGER") == PackageManager.PERMISSION_DENIED) {
Log.i(TAG, "onBind: 权限校验失败,拒绝绑定...");
return null;
}
Log.i(TAG, "onBind: 权限校验成功!");
return new ContactManagerBinder();
}

客户端先不做修改,运行测试一下,此时在客户端已经无法获取服务端的Binder对象,在客户端点击按钮操作时可以看到报空指针异常了:

/cn.codingblock.ipcclient E/AndroidRuntime: FATAL EXCEPTION: main
Process: cn.codingblock.ipcclient, PID: 4726
java.lang.NullPointerException: Attempt to invoke interface method 'java.util.List cn.codingblock.libaidl.contacts.IContactsManager.getContactList()' on a null object reference
at cn.codingblock.ipcclient.aidl.ContactMangerActivity$2.onClick(ContactMangerActivity.java:127)
at android.view.View.performClick(View.java:6256)
at android.view.View$PerformClick.run(View.java:24701)
at android.os.Handler.handleCallback(Handler.java:789)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6541)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
  • 接下来我们在客户端上加上链接服务端所需的权限:
<!--声明权限-->
<uses-permission android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER"/>
<!--定义权限-->
<permission
android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER"
android:protectionLevel="normal"/>

注意:要在客户端和服务端两个工程中都加入以上声明权限和定义权限的代码。

经反复测试发现:服务端工程中声明权限和定义权限的代码缺一不可,而客户端工程中如果只加入声明权限的代码,那么如果在安装时,客户端APP先于服务端APP安装,客户端就会由于找不到定义权限而无法成功获取权限!

所以为了保险起见,将两端都同时加入定义权限的代码和声明权限的代码,当然本示例中最好的方法是直接统一加入libaidl工程中,一次加入,两端可用!

六、小结

虽然AIDL在创建的时候步骤比较繁琐,但其功能十分强大。最后概括一下AIDL的创建步骤:

在服务端:

  • 创建一个AIDL接口文件(如果用到了其他的类,要将类序列化,并在AIDL文件中声明)
  • 再创建Service用于响应客户端的绑定请求。
  • 接着创建一个类,让这个类继承AIDL接口中的Stub类,并实现其抽象方法。在Service的onBind方法中返回这个新建这个类的对象。

接着在客户端:

  • 在客户中绑定服务端的Service,绑定成功后就可以在ServiceConnection中的onServiceConnected方法中将返回的Binder对象转换成AIDL接口所属的类型。
  • 拿到Binder对象后就可以调用在AIDL文件中声明的方法了。

最后想说的是,本系列文章为博主对Android知识进行再次梳理,查缺补漏的学习过程,一方面是对自己遗忘的东西加以复习重新掌握,另一方面相信在重新学习的过程中定会有巨大的新收获,如果你也有跟我同样的想法,不妨关注我一起学习,互相探讨,共同进步!

参考文献:

  • 《Android开发艺术探索》
  • 《socket_百度百科》

源码地址:本系列文章所对应的全部源码已同步至github,感兴趣的同学可以下载查看,结合代码看文章会更好。源码传送门

本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html

Android查缺补漏(IPC篇)-- 进程间通讯之AIDL详解的更多相关文章

  1. 知识点查缺补漏贴01-进程间通讯之mmap文件共享

    引文: 个人名言:“同一条河里淹死两次的人,是傻子,淹死三次及三次以上的人是超人”.经历过上次悲催的面试,决定沉下心来,好好的补充一下基础知识点.本文是这一系列第一篇:进程间通讯之mmap. 一.概念 ...

  2. 知识点查缺补漏贴02:Linux环境fork()函数详解

    引言 先来看一段代码吧, #include <sys/types.h> #include <unistd.h> #include <stdio.h> #includ ...

  3. Android查缺补漏(IPC篇)-- 进程间通讯基础知识热身

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8479282.html 在Android中进程间通信是比较难的一部分,同时又非常 ...

  4. Android查缺补漏(IPC篇)-- Bundle、文件共享、ContentProvider、Messenger四种进程间通讯介绍

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8387752.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  5. Android查缺补漏(IPC篇)-- 进程间通讯之Socket简介及示例

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8425736.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  6. Android查缺补漏(IPC篇)-- 款进程通讯之AIDL详解

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  7. Android查缺补漏(线程篇)-- IntentService的源码浅析

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8975114.html 在Android中有两个比较容易弄混的概念,Servic ...

  8. Android查缺补漏(View篇)--自定义 View 的基本流程

    View是Android很重要的一部分,常用的View有Button.TextView.EditView.ListView.GridView.各种layout等等,开发者通过对这些View的各种组合以 ...

  9. Android查缺补漏(View篇)--事件分发机制源码分析

    在上一篇博文中分析了事件分发的流程及规则,本篇会从源码的角度更进一步理解事件分发机制的原理,如果对事件分发规则还不太清楚的童鞋,建议先看一下上一篇博文 <Android查缺补漏(View篇)-- ...

随机推荐

  1. (使用lua++)Lua脚本和C++交互(三)

    前两篇文章中介绍了C++调用lua.lua栈操作的一些相关知识. 下面说一下Lua的工具.我们下一步要用到其中的一个帮助我们的开发,其实,Lua里面有很多简化开发的工具,你可以去www.sourcef ...

  2. iOS CoreMotion 纪录步数

    - (void)startUpdateAccelerometer{    /* 设置采样的频率,单位是秒 */    NSTimeInterval updateInterval = 0.05; // ...

  3. android选择图片或拍照图片上传到服务器(包括上传参数)

    From:http://blog.csdn.net/springsky_/article/details/8213898具体上传代码: 1.选择图片和上传界面,包括上传完成和异常的回调监听 [java ...

  4. java如何重命名文件?

    /** * 修改文件名 * @param oldFilePath 原文件路径 * @param newFileName 新文件名称 * @param overriding 判断标志(如果存在相同名的文 ...

  5. OC开发_Storyboard——NaviationController简单例子

    一个简单的Navigation的例子,demo里面用到了上一个demo的MVC,可以参考下:http://www.cnblogs.com/daomul/p/4426063.html 建立一个Nav其实 ...

  6. 常用meta标签及说明

    1.charset   定义文档的字符编码 例如: <meta charset="UTF-8"> 2. name  把 content 属性关联到一个名称,其属性有   ...

  7. 牛客网多校赛第七场J--Sudoku Subrectangle

    链接:https://www.nowcoder.com/acm/contest/145/J 来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 32768K,其他语言6553 ...

  8. Eclipse Tomcat插件的配置, 及 Tomcat 的配置

    Eclipse Tomcat插件的配置, 及 Tomcat 的配置   首先下载 对应 eclipse 版本的 tomcat 插件版本,(这里要注意: Tomcat 插件是Tomcat 插件,Tomc ...

  9. DNS named. bind linux (ACL/View)---dnsmasq-with docker,hosts in docker.

    [bind--named.conf] https://blog.csdn.net/z_yttt/article/details/53020814 [Docker搭建dnsmasq] https://b ...

  10. kafka集群与zookeeper集群 配置过程

    Kafka的集群配置一般有三种方法,即 (1)Single node – single broker集群: (2)Single node – multiple broker集群:    (3)Mult ...