1 安装入口PackageInstallerActivity,这个类只是在安装前做准备。通过各种校验,然后弹出被安装应用的权限框,等待用户安装。具体的流程如下

1.1  求mSessionId 如果是已经存在的则判断对应的SessinInfo是否存在,否则默认一个-1

  

 if (PackageInstaller.ACTION_CONFIRM_PERMISSIONS.equals(intent.getAction())) {
			//可能是系统级别的应用安装时,需要授权走这个流程
            final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
            final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo(sessionId);
            if (info == null || !info.sealed || info.resolvedBaseCodePath == null) {
                Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
                finish();
                return;
            }
			//如果有SessInfo则证明传过来的sessionId是有效的,并且获取packageUri
            mSessionId = sessionId;
            packageUri = Uri.fromFile(new File(info.resolvedBaseCodePath));
            mOriginatingURI = null;
            mReferrerURI = null;
        } else {
			//如果是用户自己拉起来的安装,则默认sessionId为-1 病且获取 packageUri
            mSessionId = -1;
            packageUri = intent.getData();
            mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
            mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
        }

1.2 开始做校验

	// 返回URI解析错误 -3
        if (packageUri == null) {
            Log.w(TAG, "Unspecified source");
            setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
            finish();
            return;
        }
        //如果是手表就不支持
        if (DeviceUtils.isWear(this)) {
            showDialogInner(DLG_NOT_SUPPORTED_ON_WEAR);
            return;
        }

1. 3 如果是系统应用拉起安装则直接进入包分析阶段

 final boolean requestFromUnknownSource = isInstallRequestFromUnknownSource(intent);
        if (!requestFromUnknownSource) {
			//进入packageUri处理阶段
            processPackageUri(packageUri);
            return;
        }
		 //安装请求是否来自于一个未知的源。
		private boolean isInstallRequestFromUnknownSource(Intent intent) {
			String callerPackage = getCallingPackage();
			if (callerPackage != null && intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) {
				try {
					mSourceInfo = mPm.getApplicationInfo(callerPackage, 0);
					if (mSourceInfo != null) {
						if ((mSourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
							// Privileged apps are not considered an unknown source.
							//如果安装请求是来自于一个系统应用,则可以明确源是已知的
							return false;
						}
					}
				} catch (NameNotFoundException e) { }
			}
			return true;//否则源是未知
		}

1. 4 针对管理员账户做不同的处理

	// If the admin prohibits it, or we're running in a managed profile, just show error
        // and exit. Otherwise show an option to take the user to Settings to change the setting.
        // 是否是管理员用户
        final boolean isManagedProfile = mUserManager.isManagedProfile();
        if (isUnknownSourcesDisallowed()) {
            //如果有用户限制了未知来源应用的安装
            if ((mUserManager.getUserRestrictionSource(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
                    Process.myUserHandle()) & UserManager.RESTRICTION_SOURCE_SYSTEM) != 0) {
                //如果不是system用户限制当前用户安装未知来源app,启动设置,使用户在设置里面修改
                showDialogInner(DLG_UNKNOWN_SOURCES);
            } else {
				//如果是system用户限制的,则直接退出
                startActivity(new Intent(Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS));
                clearCachedApkIfNeededAndFinish();
            }
        } else if (!isUnknownSourcesEnabled() && isManagedProfile) {
            //如果不允许安装未知市场的应用,并且当前是管理员用户,则弹出"您的管理员不允许安装来源不明的应用"的对话框
            showDialogInner(DLG_ADMIN_RESTRICTS_UNKNOWN_SOURCES);
        } else if (!isUnknownSourcesEnabled()) {
            // Ask user to enable setting first
            //如果不允许安装未知市场的应用,则弹出这个对话框修改设置
            showDialogInner(DLG_UNKNOWN_SOURCES);
        } else {
			//进入packageUri处理阶段
            processPackageUri(packageUri);
        }

1. 5 处理文件的uri

	    //处理包的uri
		private void processPackageUri(final Uri packageUri) {
			mPackageURI = packageUri;
			final String scheme = packageUri.getScheme();
			final PackageUtil.AppSnippet as;
			switch (scheme) {
				case SCHEME_PACKAGE:
					try {
						mPkgInfo = mPm.getPackageInfo(packageUri.getSchemeSpecificPart(), PackageManager.GET_PERMISSIONS | PackageManager.GET_UNINSTALLED_PACKAGES);
					} catch (NameNotFoundException e) { }
					if (mPkgInfo == null) {
						Log.w(TAG, "Requested package " + packageUri.getScheme() + " not available. Discontinuing installation");
						showDialogInner(DLG_PACKAGE_ERROR);
						setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
						return;
					}
					as = new PackageUtil.AppSnippet(mPm.getApplicationLabel(mPkgInfo.applicationInfo), mPm.getApplicationIcon(mPkgInfo.applicationInfo));
					break;
				case SCHEME_FILE:
					File sourceFile = new File(packageUri.getPath());
					PackageParser.Package parsed = PackageUtil.getPackageInfo(sourceFile);
					// Check for parse errors
					if (parsed == null) {
						Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation");
						showDialogInner(DLG_PACKAGE_ERROR);
						setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
						return;
					}
					mPkgInfo = PackageParser.generatePackageInfo(parsed, null,  PackageManager.GET_PERMISSIONS, 0, 0, null, new PackageUserState());
					as = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile);
					break;
				case SCHEME_CONTENT:
					//重新复制一个安装包在解析,返回一个SCHEME_FILE类型的uri重新解析包uri
					mStagingAsynTask = new StagingAsyncTask();
					mStagingAsynTask.execute(packageUri);
					return;
				default:
					Log.w(TAG, "Unsupported scheme " + scheme);
					setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
					clearCachedApkIfNeededAndFinish();
					return;
			}
			PackageUtil.initSnippetForNewApp(this, as, R.id.app_snippet);
			//启动安装
			initiateInstall();
		}

1.6 启动安装

//启动安装
		private void initiateInstall() {
			String pkgName = mPkgInfo.packageName;
			// Check if there is already a package on the device with this name
			// but it has been renamed to something else. 是否有同名应用已经安装上去了。在此安装则被认为是替换安装
			String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName });
			if (oldName != null && oldName.length > 0 && oldName[0] != null) {
				pkgName = oldName[0];
				mPkgInfo.packageName = pkgName;
				mPkgInfo.applicationInfo.packageName = pkgName;
			}
			// Check if package is already installed. display confirmation dialog if replacing pkg
			// 检查这个包是否真的被安装,如果要替换,则显示替换对话框
			try {
				// This is a little convoluted because we want to get all uninstalled
				// apps, but this may include apps with just data, and if it is just
				// data we still want to count it as "installed".
				// 获取设备上有残存数据,并且标记为“installed”的,实际上已经被卸载的应用。
				mAppInfo = mPm.getApplicationInfo(pkgName, PackageManager.GET_UNINSTALLED_PACKAGES);
				if ((mAppInfo.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
					////如果应用是被卸载的,但是又是被标识成安装过的,则认为是新安装
					mAppInfo = null;
				}
			} catch (NameNotFoundException e) {
				mAppInfo = null;
			}
			//列出权限列表,等待用户确认安装
			startInstallConfirm();
		}

1.7 确认安装权限

private void startInstallConfirm() {
			TabHost tabHost = (TabHost)findViewById(android.R.id.tabhost);
			tabHost.setup();
			tabHost.setVisibility(View.VISIBLE);
			ViewPager viewPager = (ViewPager)findViewById(R.id.pager);
			TabsAdapter adapter = new TabsAdapter(this, tabHost, viewPager);
			// If the app supports runtime permissions the new permissions will
			// be requested at runtime, hence we do not show them at install.
			// 如果app支持运行时权限,这里会显示新的运行时权限
			// 根据版本判断app是否有可能有运行时权限
			boolean supportsRuntimePermissions = mPkgInfo.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M;
			boolean permVisible = false;
			mScrollView = null;
			mOkCanInstall = false;
			int msg = 0;
			//perms这个对象包括了该应用的用户的uid以及相应的一些权限,以及权限组信息。
			AppSecurityPermissions perms = new AppSecurityPermissions(this, mPkgInfo);
			//所有的权限数量
			final int N = perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL);
			if (mAppInfo != null) {
				//如果是替换应用
				msg = (mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ? R.string.install_confirm_question_update_system : R.string.install_confirm_question_update;
				mScrollView = new CaffeinatedScrollView(this);
				mScrollView.setFillViewport(true);
				boolean newPermissionsFound = false;
				if (!supportsRuntimePermissions) {
					newPermissionsFound = (perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0);
					if (newPermissionsFound) {
						permVisible = true;
						//显示新添加的权限项(这个view竟然是系统的view)
						mScrollView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_NEW));
					}
				}
				if (!supportsRuntimePermissions && !newPermissionsFound) {
					//如果既不支持可运行权限项也没有新权限发现,则提示没有新权限
					LayoutInflater inflater = (LayoutInflater)getSystemService( Context.LAYOUT_INFLATER_SERVICE);
					TextView label = (TextView)inflater.inflate(R.layout.label, null);
					label.setText(R.string.no_new_perms);
					mScrollView.addView(label);
				}
				adapter.addTab(tabHost.newTabSpec(TAB_ID_NEW).setIndicator(getText(R.string.newPerms)), mScrollView);
			} else  {
				findViewById(R.id.tabscontainer).setVisibility(View.GONE);
				findViewById(R.id.spacer).setVisibility(View.VISIBLE);
			}
			if (!supportsRuntimePermissions && N > 0) {
				permVisible = true;
				LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
				View root = inflater.inflate(R.layout.permissions_list, null);
				if (mScrollView == null) {
					mScrollView = (CaffeinatedScrollView)root.findViewById(R.id.scrollview);
				}
				//如果没有运行时权限并且有权限,则列出所有权限
				((ViewGroup)root.findViewById(R.id.permission_list)).addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));
				adapter.addTab(tabHost.newTabSpec(TAB_ID_ALL).setIndicator(getText(R.string.allPerms)), root);
			}
			if (!permVisible) {
				//如果不需要任何权限。更新的不需要新的权限以及运行时权限
				if (mAppInfo != null) {
					// This is an update to an application, but there are no
					// permissions at all.
					msg = (mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ? R.string.install_confirm_question_update_system_no_perms: R.string.install_confirm_question_update_no_perms;
					findViewById(R.id.spacer).setVisibility(View.VISIBLE);
				} else {
					// This is a new application with no permissions.
					msg = R.string.install_confirm_question_no_perms;
				}
				tabHost.setVisibility(View.INVISIBLE);
				mScrollView = null;
			}
			if (msg != 0) {
				((TextView)findViewById(R.id.install_confirm_question)).setText(msg);
			}
			mInstallConfirm.setVisibility(View.VISIBLE);
			mOk.setEnabled(true);
			if (mScrollView == null) {
				// There is nothing to scroll view, so the ok button is immediately
				// set to install.
				mOk.setText(R.string.install);
				mOkCanInstall = true;
			} else {
				mScrollView.setFullScrollAction(new Runnable() {
                @Override
                public void run() {
                    mOk.setText(R.string.install);
                    mOkCanInstall = true;
                }
            });
        }
    }

1. 8 点击安装

public void onClick(View v) {
			if (v == mOk) {
				if (mOkCanInstall || mScrollView == null) {
					if (mSessionId != -1) {
						//如果原来是确认权限请求则赋予安装权限退出
						mInstaller.setPermissionsResult(mSessionId, true);
						clearCachedApkIfNeededAndFinish();
					} else {
						//开始安装
						startInstall();
					}
				} else {mScrollView.pageScroll(View.FOCUS_DOWN);}
			} else if (v == mCancel) {
				// Cancel and finish 取消安装
				setResult(RESULT_CANCELED);
				if (mSessionId != -1) {
					mInstaller.setPermissionsResult(mSessionId, false);
				}
				clearCachedApkIfNeededAndFinish();
			}
		}

1.9 进入安装

private void startInstall() {
			// Start subactivity to actually install the application
			Intent newIntent = new Intent();
			//带走安装包的applicationInfo
			newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, mPkgInfo.applicationInfo);
			newIntent.setData(mPackageURI); //带走安装包的applicationInfo
			newIntent.setClass(this, InstallAppProgress.class);
			String installerPackageName = getIntent().getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME);
			if (mOriginatingURI != null) { //带走安装包的mOriginatingURI
				newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI);
			}
			if (mReferrerURI != null) {//带走安装包的mReferrerURI
				newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI);
			}
			if (mOriginatingUid != VerificationParams.NO_UID) {//带走安装包的mOriginatingUid,这个uid如果不是拉安装的应用的uid
				newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);
			}
			if (installerPackageName != null) {//带走安装包的installerPackageName
				newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, installerPackageName);
			}
			if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
				newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
				newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
			}
			if(localLOGV) Log.i(TAG, "downloaded app uri="+mPackageURI);
			startActivity(newIntent);
			finish();
		}

2  安装过程InstallAppProgress

2.1 注册安装监听

IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(BROADCAST_ACTION);
    registerReceiver(mBroadcastReceiver, intentFilter, BROADCAST_SENDER_PERMISSION, null /*scheduler*/);

2.2  正式安装

替换现存的包标示
    final int installFlags = getInstallFlags(mAppInfo.packageName);
if ("package".equals(mPackageURI.getScheme())) {
            try {
                //安装与该应用同名的应用,应该比较快,否则会抛出异常
                pm.installExistingPackage(mAppInfo.packageName);
                onPackageInstalled(PackageInstaller.STATUS_SUCCESS);
            } catch (PackageManager.NameNotFoundException e) {
                onPackageInstalled(PackageInstaller.STATUS_FAILURE_INVALID);
            }
        } else {
            final PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                    PackageInstaller.SessionParams.MODE_FULL_INSTALL);
            params.referrerUri = getIntent().getParcelableExtra(Intent.EXTRA_REFERRER);
            params.originatingUri = getIntent().getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
            params.originatingUid = getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID, UID_UNKNOWN);
            File file = new File(mPackageURI.getPath());
            try {
				//解析安装包,设置安装位置。这个安装位置是从AndroidManifest文件获取的,至于怎么获取,最后指向native 层。没有继续跟踪
                params.setInstallLocation(PackageParser.parsePackageLite(file, 0).installLocation);
            } catch (PackageParser.PackageParserException e) {
                Log.e(TAG, "Cannot parse package " + file + ". Assuming defaults.");
            }

            mInstallHandler.post(new Runnable() {
                @Override
                public void run() {
                    doPackageStage(pm, params);
                }
            });
        }

2.3 后台安装

private void doPackageStage(PackageManager pm, PackageInstaller.SessionParams params) {
        //初始化安装器
        final PackageInstaller packageInstaller = pm.getPackageInstaller();
        PackageInstaller.Session session = null;
        try {
            final String packageLocation = mPackageURI.getPath();
            final File file = new File(packageLocation);
            //获取sessionId
            final int sessionId = packageInstaller.createSession(params);
            final byte[] buffer = new byte[65536];
            //获取session
            session = packageInstaller.openSession(sessionId);
            final InputStream in = new FileInputStream(file);
            final long sizeBytes = file.length();
            final OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes);
            try {
                int c;
                //安装中..............
                while ((c = in.read(buffer)) != -1) {
                    out.write(buffer, 0, c);
                    if (sizeBytes > 0) {
                        final float fraction = ((float) c / (float) sizeBytes);
                        session.addProgress(fraction);
                    }
                }
                session.fsync(out);
            } finally {
                IoUtils.closeQuietly(in);
                IoUtils.closeQuietly(out);
            }
            // Create a PendingIntent and use it to generate the IntentSender
            //发起安装完成提交通知
            Intent broadcastIntent = new Intent(BROADCAST_ACTION);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(
                    InstallAppProgress.this /*context*/,
                    sessionId,
                    broadcastIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            session.commit(pendingIntent.getIntentSender());
        } catch (IOException e) {
            onPackageInstalled(PackageInstaller.STATUS_FAILURE);
        } finally {
            IoUtils.closeQuietly(session);
        }
    }

2.4  接受安装结果

  private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final int statusCode = intent.getIntExtra(
                    PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
            //等待安装
            if (statusCode == PackageInstaller.STATUS_PENDING_USER_ACTION) {
                context.startActivity((Intent)intent.getParcelableExtra(Intent.EXTRA_INTENT));
            } else {
				//返回安装结果
                onPackageInstalled(statusCode);
            }
        }
    };

Android 7.0 安装器安装过程分析 (com.android.packageinstaller)的更多相关文章

  1. .Net 转战 Android 4.4 日常笔记(5)--新软件Android Studio 0.5.8安装与配置及问题解决

    说真心话,Eclipse跟我们.net的VS比起来就是屌丝比高富帅,一切都是那么的难用,速度慢得我无法忍受 于是想试试Google钦点的Android Studio IDE工具,这跟ADT一样也是一套 ...

  2. 在Android Studio 0.5.2中使用ArcGIS Android SDK

    环境 操作系统:Mac OSX 10.8.5Android Studio: 0.5.2ArcGIS Android SDK: 10.2.3 操作步骤 在Android Studio中新建一个Modul ...

  3. Android 7.0 调取系统相机崩溃解决android.os.FileUriExposedException

    一.写在前面 最近由于廖子尧忙于自己公司的事情和OkGo(一款专注于让网络请求更简单的网络框架) ,故让LZ 接替维护ImagePicker(一款支持单.多选.旋转和裁剪的图片选择器),也是处理了诸多 ...

  4. 【适配整理】Android 7.0 调取系统相机崩溃解决android.os.FileUriExposedException

    一.写在前面 最近由于廖子尧忙于自己公司的事情和 OkGo (一款专注于让网络请求更简单的网络框架) ,故让LZ 接替维护 ImagePicker(一款支持单.多选.旋转和裁剪的图片选择器),也是处理 ...

  5. 【Android Studio安装部署系列】三十五、从Android studio3.0.1升级到Android studio3.1.4【以及创建android p模拟器的尝试(未成功)】

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 概述 因为想要使用Android P模拟器,所以需要将Android Studio升级到3.1版本以上. Android P模拟器的最低版 ...

  6. [php-pear]如何使用 PHP-PEAR安装器,以及使用 PEAR 安装扩展库

    我们都知道 PHP PEAR,就是 PHP Extension and Application Respository,也就是 PHP 扩展和应用代码库. PHP 也可以通过 PEAR 安装器来进行 ...

  7. 谷歌安装器扫描时提示“需要root权限”,不用root也可以的!

    能FQ的用户会用谷歌服务,一般的新手机没有安装谷歌框架,但是在用谷歌安装器安装谷歌市场时会提示"需要root权限",我用的是360手机,按照下面的教程搞好了: 安装完GSM包就可以 ...

  8. android 5.0新特性

    Android Lollipop 面向开发人员的主要功能 Material Design 设计 注重性能 通知 以大屏幕呈现 以文档为中心 连接性能再上一级 高性能图形 音频处理功能更强 摄像头和视频 ...

  9. Android 8.0 功能和 API

    Android 8.0 为用户和开发者引入多种新功能.本文重点介绍面向开发者的新功能. 用户体验 通知 在 Android 8.0 中,我们已重新设计通知,以便为管理通知行为和设置提供更轻松和更统一的 ...

随机推荐

  1. 大数据和BI商业智能有何区别?有何相关?

    大数据 ≠BI商业智能,大数据也不是传统商业智能的简单升级. 1.大数据和BI两者的区别 BI(BusinessIntelligence)即商业智能,它是企业数据化管理的一整套的方案,用来将企业中现有 ...

  2. ContentSize、Contentoffset以及ContentInset属性

    ContentSize UIScrollView可以滚动的区域.在我的理解中,我把UIScrollView看成是具有上下两层的一个复合视图,frame控制着上层的大小,我们看到的UIScrollVie ...

  3. UDP协议详解

    1.UDP协议的作用 IP协议无法区别同一个主机系统上的多个应用程序.UDP采用端口标识同一主机上的不同应用程序. 无法采取进程ID来标识不同应用程序的原因: 1)系统中应用程序的进程ID分配和销毁是 ...

  4. [大数据]-Elasticsearch5.3.1 IK分词,同义词/联想搜索设置

    --题外话:最近发现了一些问题,一些高搜索量的东西相当一部分没有价值.发现大部分是一些问题的错误日志.而我是个比较爱贴图的.搜索引擎的检索会将我们的博文文本分词.所以图片内容一般是检索不到的,也就是说 ...

  5. Mac iterm2 linux vim 语言问题

  6. PHP简单分页省略中间页码

    <?php /** * @desc created by sublime text3 * @author jxl <[57953279@qq.com>]> * @since 2 ...

  7. XInput和DirectInput

    原文链接:https://msdn.microsoft.com/en-us/library/windows/desktop/ee417014(v=vs.85).aspx XInput是一个允许应用从W ...

  8. win10下装mysql-5.7.18-winx64

    步骤1 官网下载地址:https://dev.mysql.com/downloads/mysql/ 选择手动安装版: 解压到D盘mysql文件夹下: 比以往的版本里缺少了两个.ini文件,直接copy ...

  9. Java-面向对象总结

    下面是学习面向对象的知识点和总结: 面向对象思想: 遇到需求,首先去找是否有现成的类来实现此功能,创建对象来调用,以此来组合成新的类实现自己的需求. 在java中是以类为单位,一个类包括成员变量.成员 ...

  10. 【Netty】WebSocket

    一.前言 前面学习了codec和ChannelHandler之间的关系,接着学习WebSocket. 二.WebSocket 2.1. WebSocket介绍 WebSocket协议允许客户端和服务器 ...