0%

http://www.jianshu.com/p/47a4a7b99364

标题隐含了两个层面的意思,一个是主流,另一个是UI。主流既通用,一些常规的按钮、Switch、进度条等控件都是通用控件,因此本文将其囊括,但一些很炫酷但不通用的控件除外。UI不仅包括控件,还包括效果、动画甚至是一些UI辅助类库等。

网上有不少对开源项目进行整理的文章,以下两个最为知名:

  1. https://github.com/Trinea/android-open-project
    囊括了非常多的、各式各样的开源项目,包括但不限于UI、工具类库等。

  2. https://github.com/wasabeef/awesome-android-ui
    囊括了非常多的、各种类型的UI开源库。

本文的取材大部分来自这两篇文章,在此感谢文章作者的辛勤付出。更要感谢的是那些无私奉献代码的Opener。


既然是整理,那先得有一个分类,分类如下:

  1. 按钮

  2. Switch(开关)

  3. 编辑框(类EditText)

  4. 文本控件(类TextView)

  5. 进度条(圈)等展示进度相关

  6. 拖动条(类SeekBar)

  7. RatingBar

  8. 列表、网格、瀑布流控件

  9. 对话框(Dialog、BottomSheet等)

  10. 日历、时间选择

  11. 分类筛选

  12. 角标、徽章

  13. 图表

  14. 富文本编辑、代码高亮等

  15. 图片展示(类ImageView)

  16. 标签组控件(类FlowLayout)

  17. App新手引导、高亮

  18. 侧边栏索引、固定Header

  19. 菜单(类Menu,但不包括侧滑菜单)

  20. 通用下拉刷新、加载更多

  21. 广告轮播、垂直公告

  22. 滑动返回(类SwipeBack)

  23. 指示器、Tab

  24. 动画相关库

  25. 侧滑菜单(类SlidingMenu)

  26. 辅助类库

目前想到的通用控件种类就这么多,如有遗漏,欢迎指正哈。


以下是对上述分类的一一展开。我会列出每个分类下的热门开源项目,并适当的给出介绍。这些项目按照我所认为的通用性降序排列,排在越前面的说明通用性、实用性越高,但并不代表star就越多。有些项目有几千个star,效果看起来也很酷,但通用性很差,显得有些鸡肋,就靠后排列了,甚至介绍都免了。

按钮

Switch(开关)

编辑框(类EditText)

文本控件(类TextView)

进度条(圈)等展示进度相关

拖动条(类SeekBar)

RatingBar

列表、网格、瀑布流控件

对话框(Dialog、BottomSheet等)

日历、时间选择

分类筛选

角标、徽章

图表

富文本编辑、代码高亮等

图片展示(类ImageView)

标签组控件(类FlowLayout)

App新手引导、高亮

侧边栏索引、固定Header

菜单(类Menu,但不包括侧滑菜单)

通用下拉刷新、加载更多

广告轮播、垂直公告

滑动返回(类SwipeBack)

指示器、Tab

动画相关库

侧滑菜单(类SlidingMenu)

辅助类库

文/hackware(简书作者)
原文链接:http://www.jianshu.com/p/47a4a7b99364
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

首先,进行一下科普:

1.BLE(Bluetooth Low Energy),蓝牙4.0核心profile,主要特点是快速搜索,快速连接,超低功耗保持连接和数据传输,缺点:数据传输速率低,由于其具有低功耗特点,故而经常用在可穿戴设备之中。Android4.3才开始支持BLE api。

2.关于BLE数据传输:

a.profile可以理解为一种规范,一个标准的通信协议,其存在于手机中,蓝牙组织规定了一些标准的profile:HID OVER GATT ,防丢器等,每个profile中包含了多个service。

b.service 可以理解为一个服务,在BLE从机中有多个服务,电量信息,系统服务信息等,每一个service中包含了多个characteristic特征值,每一个具体的characteristic特征值才是BLE通信的主题。

举个例子吧:从机当前的电量是80%,从机会借由电量的characteristic特征值将这个信息储存于从机的profile中,主机可以通过该characteristic来读取80%这个数据。

c.characteristic特征值:BLE主机从机通信均是通过characteristic进行,可以将其理解为一个标签,通过该标签可以读取或写入相关信息。

d. UUID(统一标识码):service和characteristic均需要这个唯一的UUID进行标识

/****************************************************************************************/

科普结束,首先上效果图:

 这个就是实现后的效果,我用的BLE模块是:MicrodunioBT4.0

/*********************************************************分隔符********************************************************************************/

BLE设备和Android应用进行通信,首先要做的就是让Android应用找到BLE设备(代码分析部分,只对关键点进行注释)

复制代码

package com.TANK.temperature.BT; import java.util.List; import com.TANK.temperature.R; import com.TANK.temperature.BT.BluetoothLeClass.OnDataAvailableListener; import com.TANK.temperature.BT.BluetoothLeClass.OnServiceDiscoverListener; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Typeface; import android.net.wifi.WifiConfiguration.Status; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.widget.TextView; import android.widget.Toast; public class DeviceScanActivity extends Activity { private final static String TAG = DeviceScanActivity.class.getSimpleName(); private final static String UUID_KEY_DATA = “0000fff6-0000-1000-8000-00805f9b34fb”; private Handler mHandler; private static final long SCAN_PERIOD = 10000; /** 搜索BLE终端 */
private BluetoothAdapter mBluetoothAdapter; private BluetoothLeClass mBLE; private boolean mScanning; private BluetoothDevice BTtoLink = null; private String BTtoFind = “Microduino”; private TextView BT_find, BT_info, BT_link;//三个textview分别表示:设备是否找到,BLE模块传输的信息,连接状态
private String S_BT_info = “null”, info = “111”;

@Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub
    super.onCreate(savedInstanceState);

    setContentView(R.layout.temperature);
    mHandler \= new Handler();

    BT\_find \= (TextView) findViewById(R.id.BT\_find);
    BT\_info \= (TextView) findViewById(R.id.BT\_info);
    BT\_link \= (TextView) findViewById(R.id.BT\_link);

          
Typeface typeface = Typeface.createFromAsset(getAssets(), “fonts/maozedong.ttf”);//设置显示字体
BT_find.setTypeface(typeface);
BT_info.setTypeface(typeface);
BT_link.setTypeface(typeface);

    BT\_find.setText("查找中");
    BT\_info.setText("null");
    BT\_link.setText("未连接"); if (!getPackageManager().hasSystemFeature(   //判断主机是否支BLE牙设备

PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, “BLE is not supported”, Toast.LENGTH_SHORT)
.show();
finish();

    } final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH\_SERVICE);//获得Android设备中的bluetoothmanager
    mBluetoothAdapter = bluetoothManager.getAdapter();//获得bluetoothadapter
    if (mBluetoothAdapter == null) {
        Toast.makeText(this, "Bluetooth not supported", Toast.LENGTH\_SHORT)
                .show();
        finish(); return;
    }
    mBluetoothAdapter.enable(); //强制使能Bluetoothadapter,打开Android设备蓝牙
    mBLE = new BluetoothLeClass(this); //BLuetoothLeClass类
    if (!mBLE.initialize()) {
        Log.e(TAG, "Unable to initialize Bluetooth");
        finish();

    } // 发现BLE终端的Service时回调

mBLE.setOnServiceDiscoverListener(mOnServiceDiscover); // 收到BLE终端数据交互的事件
mBLE.setOnDataAvailableListener(mOnDataAvailable);

}

@Override protected void onResume() { // TODO Auto-generated method stub
    super.onResume();
    scanLeDevice(true);//搜索BLE设备

}

@Override protected void onPause() { // TODO Auto-generated method stub
    super.onPause();
    scanLeDevice(false);
    mBLE.disconnect();
}

@Override protected void onStop() { // TODO Auto-generated method stub
    super.onStop();
    mBLE.close();
} private void scanLeDevice(final boolean enable) { // TODO Auto-generated method stub
    if (enable) {

        mHandler.postDelayed(new Runnable() {

            @Override public void run() { // TODO Auto-generated method stub
                mScanning = false;
                mBluetoothAdapter.stopLeScan(mLeScanCallback);

            }
        }, SCAN\_PERIOD);//在搜索时间内,关闭搜索标志,不对搜索回调函数进行响应
        mScanning = true;
        mBluetoothAdapter.startLeScan(mLeScanCallback);

    } else {

        mScanning \= false;
        mBluetoothAdapter.stopLeScan(mLeScanCallback);

    }

} /\*\* \* 搜索到BLE终端服务的事件 \*/
private BluetoothLeClass.OnServiceDiscoverListener mOnServiceDiscover = new OnServiceDiscoverListener() {

    @Override public void onServiceDiscover(BluetoothGatt gatt) {

        displayGattServices(mBLE.getSupportedGattService());
    }

}; /\*\* \* 收到BLE终端数据交互的事件 \*/
private BluetoothLeClass.OnDataAvailableListener mOnDataAvailable = new OnDataAvailableListener() { /\*\* \* BLE终端数据写入的事件 \*/ @Override public void onCharacteristicRead(BluetoothGatt gatt,
            BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT\_SUCCESS)
            Log.e(TAG, "onCharRead "
                            + gatt.getDevice().getName() \+ " read "
                            + characteristic.getUuid().toString() \+ " -> "
                            + Utils.bytesToHexString(characteristic
                                    .getValue()));

    } /\*\* \* 对BLE终端读取数据 \*/ @Override public void onCharacteristicWrite(BluetoothGatt gatt,
            BluetoothGattCharacteristic characteristic) {
        info \= new String(characteristic.getValue());//对得到的byte数组进行解码,构造新的string
        Log.e(TAG, "onCharWrite " + gatt.getDevice().getName() + " write "
                + characteristic.getUuid().toString() + " -> " + info); if (!S\_BT\_info.equals(info)) {//判断读取的数据是否发生变化,如果变化,更新UI
            DeviceScanActivity.this.runOnUiThread(new Runnable() {

                @Override public void run() { // TODO Auto-generated method stub
                    BT\_link.setText("已连接");
                    StringBuilder sb \= new StringBuilder();//详情参见:http://blog.csdn.net/rmn190/article/details/1492013

sb.append(info);
sb.append(“度”);
BT_info.setText(sb.toString());
sb = null;

                }
            });

        }

    }
}; private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {//搜索回调函数:
    String BT\_name = null;

    @Override public void onLeScan(final BluetoothDevice device, int rssi, byte\[\] scanRecord) { // TODO Auto-generated method stub
        runOnUiThread(new Runnable() {

            @Override public void run() { // TODO Auto-generated method stub
                BT\_name = device.getName(); if (BT\_name.equals(BTtoFind)) { //如果是要找的设备,更新UI上信息,设置搜索标志,停止响应搜索回调函数,连接BLE设备
                    /\*\* 连接事件 \*/ BT\_find.setText("已找到设备!");
                    mScanning \= false;
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                    mBLE.connect(device.getAddress());

                }

            }
        });

    }
}; private void displayGattServices(BluetoothGattService bluetoothGattService) { if (bluetoothGattService == null) return; // -----Service的字段信息-----//
    int type = bluetoothGattService.getType();
    Log.e(TAG, "-->service type:" + Utils.getServiceType(type));
    Log.e(TAG, "-->includedServices size:"
            + bluetoothGattService.getIncludedServices().size());
    Log.e(TAG, "-->service uuid:" + bluetoothGattService.getUuid()); // -----Characteristics的字段信息-----//
    List<BluetoothGattCharacteristic> gattCharacteristics = bluetoothGattService
            .getCharacteristics(); for (final BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
        Log.e(TAG, "---->char uuid:" + gattCharacteristic.getUuid()); int permission = gattCharacteristic.getPermissions();
        Log.e(TAG, "---->char permission:"
                        + Utils.getCharPermission(permission)); int property = gattCharacteristic.getProperties();
        Log.e(TAG, "---->char property:" + Utils.getCharPropertie(property)); byte\[\] data = gattCharacteristic.getValue(); if (data != null && data.length > 0) {
            Log.e(TAG, "---->char value:" + new String(data));
        } // UUID\_KEY\_DATA是可以跟蓝牙模块串口通信的Characteristic
        if (gattCharacteristic.getUuid().toString().equals(UUID\_KEY\_DATA)) { // 测试读取当前Characteristic数据,会触发mOnDataAvailable.onCharacteristicRead()
            mHandler.postDelayed(new Runnable() {
                @Override public void run() {
                    mBLE.readCharacteristic(gattCharacteristic);
                }
            }, 500); // 接受Characteristic被写的通知,收到蓝牙模块的数据后会触发mOnDataAvailable.onCharacteristicWrite()
            mBLE.setCharacteristicNotification(gattCharacteristic, true); // 设置数据内容
            gattCharacteristic.setValue("send data->"); // 往蓝牙模块写入数据

mBLE.writeCharacteristic(gattCharacteristic);
} // —–Descriptors的字段信息—–//
List gattDescriptors = gattCharacteristic
.getDescriptors(); for (BluetoothGattDescriptor gattDescriptor : gattDescriptors) {
Log.e(TAG, “——–>desc uuid:” + gattDescriptor.getUuid()); int descPermission = gattDescriptor.getPermissions();
Log.e(TAG, “——–>desc permission:”
+ Utils.getDescPermission(descPermission)); byte[] desData = gattDescriptor.getValue(); if (desData != null && desData.length > 0) {
Log.e(TAG, “——–>desc value:” + new String(desData));
}
}

    }

}

}

复制代码

复制代码

package com.TANK.temperature.BT; import java.util.UUID; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.util.Log; public class BluetoothLeClass{ private final static String TAG = BluetoothLeClass.class.getSimpleName(); private BluetoothManager mBluetoothManager; private BluetoothAdapter mBluetoothAdapter; private String mBluetoothDeviceAddress; private BluetoothGatt mBluetoothGatt; public interface OnConnectListener { public void onConnect(BluetoothGatt gatt);
} public interface OnDisconnectListener { public void onDisconnect(BluetoothGatt gatt);
} public interface OnServiceDiscoverListener { public void onServiceDiscover(BluetoothGatt gatt);
} public interface OnDataAvailableListener { public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status); public void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic);
} private OnConnectListener mOnConnectListener; private OnDisconnectListener mOnDisconnectListener; private OnServiceDiscoverListener mOnServiceDiscoverListener; private OnDataAvailableListener mOnDataAvailableListener; private Context mContext; public void setOnConnectListener(OnConnectListener l){
mOnConnectListener = l;
} public void setOnDisconnectListener(OnDisconnectListener l){
mOnDisconnectListener = l;
} public void setOnServiceDiscoverListener(OnServiceDiscoverListener l){
mOnServiceDiscoverListener = l;
} public void setOnDataAvailableListener(OnDataAvailableListener l){
mOnDataAvailableListener = l;
} public BluetoothLeClass(Context c){
mContext = c;
} // Implements callback methods for GATT events that the app cares about. For example, // connection change and services discovered.
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState == BluetoothProfile.STATE_CONNECTED) { if(mOnConnectListener!=null)
mOnConnectListener.onConnect(gatt);
Log.i(TAG, “Connected to GATT server.”); // Attempts to discover services after successful connection.
Log.i(TAG, “Attempting to start service discovery:” + mBluetoothGatt.discoverServices());

        } else if (newState == BluetoothProfile.STATE\_DISCONNECTED) { if(mOnDisconnectListener!=null)
                mOnDisconnectListener.onDisconnect(gatt);
            Log.i(TAG, "Disconnected from GATT server.");
        }
    }

    @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT\_SUCCESS && mOnServiceDiscoverListener!=null) {
                mOnServiceDiscoverListener.onServiceDiscover(gatt);
        } else {
            Log.w(TAG, "onServicesDiscovered received: " + status);
        }
    }

    @Override public void onCharacteristicRead(BluetoothGatt gatt,
                                     BluetoothGattCharacteristic characteristic, int status) { if (mOnDataAvailableListener!=null)
            mOnDataAvailableListener.onCharacteristicRead(gatt, characteristic, status);
    }

    @Override public void onCharacteristicChanged(BluetoothGatt gatt,
                                        BluetoothGattCharacteristic characteristic) { if (mOnDataAvailableListener!=null)
            mOnDataAvailableListener.onCharacteristicWrite(gatt, characteristic);
    }
}; /\*\* \* Initializes a reference to the local Bluetooth adapter.
 \*
 \* @return Return true if the initialization is successful. \*/
public boolean initialize() { // For API level 18 and above, get a reference to BluetoothAdapter through // BluetoothManager.
    if (mBluetoothManager == null) {
        mBluetoothManager \= (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH\_SERVICE); if (mBluetoothManager == null) {
            Log.e(TAG, "Unable to initialize BluetoothManager."); return false;
        }
    }

    mBluetoothAdapter \= mBluetoothManager.getAdapter(); if (mBluetoothAdapter == null) {
        Log.e(TAG, "Unable to obtain a BluetoothAdapter."); return false;
    } return true;
} /\*\* \* Connects to the GATT server hosted on the Bluetooth LE device.
 \*
 \* @param address The device address of the destination device.
 \*
 \* @return Return true if the connection is initiated successfully. The connection result
 \*         is reported asynchronously through the
 \*         {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
 \*         callback. \*/
public boolean connect(final String address) { if (mBluetoothAdapter == null || address == null) {
        Log.w(TAG, "BluetoothAdapter not initialized or unspecified address."); return false;
    } // Previously connected device.  Try to reconnect.
    if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress) && mBluetoothGatt != null) {
        Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection."); if (mBluetoothGatt.connect()) { return true;
        } else { return false;
        }
    } final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); if (device == null) {
        Log.w(TAG, "Device not found.  Unable to connect."); return false;
    } // We want to directly connect to the device, so we are setting the autoConnect // parameter to false.
    mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback);
    Log.d(TAG, "Trying to create a new connection.");
    mBluetoothDeviceAddress \= address; return true;
} /\*\* \* Disconnects an existing connection or cancel a pending connection. The disconnection result
 \* is reported asynchronously through the
 \* {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
 \* callback. \*/
public void disconnect() { if (mBluetoothAdapter == null || mBluetoothGatt == null) {
        Log.w(TAG, "BluetoothAdapter not initialized"); return;
    }
    mBluetoothGatt.disconnect();
} /\*\* \* After using a given BLE device, the app must call this method to ensure resources are
 \* released properly. \*/
public void close() { if (mBluetoothGatt == null) { return;
    }
    mBluetoothGatt.close();
    mBluetoothGatt \= null;
} /\*\* \* Request a read on a given {@code BluetoothGattCharacteristic}. The read result is reported
 \* asynchronously through the {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
 \* callback.
 \*
 \* @param characteristic The characteristic to read from. \*/
public void readCharacteristic(BluetoothGattCharacteristic characteristic) { if (mBluetoothAdapter == null || mBluetoothGatt == null) {
        Log.w(TAG, "BluetoothAdapter not initialized"); return;
    }
    mBluetoothGatt.readCharacteristic(characteristic);
} /\*\* \* Enables or disables notification on a give characteristic.
 \*
 \* @param characteristic Characteristic to act on.
 \* @param enabled If true, enable notification.  False otherwise. \*/
public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,//便于更新数据
                                          boolean enabled) { if (mBluetoothAdapter == null || mBluetoothGatt == null) {
        Log.w(TAG, "BluetoothAdapter not initialized"); return;
    }
    mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
} public void writeCharacteristic(BluetoothGattCharacteristic characteristic){
     mBluetoothGatt.writeCharacteristic(characteristic);
} /\*\* \* Retrieves a list of supported GATT services on the connected device. This should be
 \* invoked only after {@code BluetoothGatt#discoverServices()} completes successfully.
 \*
 \* @return A {@code List} of supported services. \*/
public BluetoothGattService getSupportedGattService() {//根据service的UUID来获取service
    if (mBluetoothGatt == null) return null; return mBluetoothGatt.getService(UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb"));
}

}

复制代码

复制代码

public class Utils { private static HashMap<Integer, String> serviceTypes = new HashMap(); static { // Sample Services.
serviceTypes.put(BluetoothGattService.SERVICE_TYPE_PRIMARY, “PRIMARY”);
serviceTypes.put(BluetoothGattService.SERVICE_TYPE_SECONDARY, “SECONDARY”);
} public static String getServiceType(int type){ return serviceTypes.get(type);
} //-——————————————
private static HashMap<Integer, String> charPermissions = new HashMap(); static {
charPermissions.put(0, “UNKNOW”);
charPermissions.put(BluetoothGattCharacteristic.PERMISSION_READ, “READ”);
charPermissions.put(BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED, “READ_ENCRYPTED”);
charPermissions.put(BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED_MITM, “READ_ENCRYPTED_MITM”);
charPermissions.put(BluetoothGattCharacteristic.PERMISSION_WRITE, “WRITE”);
charPermissions.put(BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED, “WRITE_ENCRYPTED”);
charPermissions.put(BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED_MITM, “WRITE_ENCRYPTED_MITM”);
charPermissions.put(BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED, “WRITE_SIGNED”);
charPermissions.put(BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED_MITM, “WRITE_SIGNED_MITM”);
} public static String getCharPermission(int permission){ return getHashMapValue(charPermissions,permission);
} //-——————————————
private static HashMap<Integer, String> charProperties = new HashMap(); static {

    charProperties.put(BluetoothGattCharacteristic.PROPERTY\_BROADCAST, "BROADCAST");
    charProperties.put(BluetoothGattCharacteristic.PROPERTY\_EXTENDED\_PROPS, "EXTENDED\_PROPS");
    charProperties.put(BluetoothGattCharacteristic.PROPERTY\_INDICATE, "INDICATE");
    charProperties.put(BluetoothGattCharacteristic.PROPERTY\_NOTIFY, "NOTIFY");
    charProperties.put(BluetoothGattCharacteristic.PROPERTY\_READ, "READ");
    charProperties.put(BluetoothGattCharacteristic.PROPERTY\_SIGNED\_WRITE, "SIGNED\_WRITE");
    charProperties.put(BluetoothGattCharacteristic.PROPERTY\_WRITE, "WRITE");
    charProperties.put(BluetoothGattCharacteristic.PROPERTY\_WRITE\_NO\_RESPONSE, "WRITE\_NO\_RESPONSE");
} public static String getCharPropertie(int property){ return getHashMapValue(charProperties,property);
} //\--------------------------------------------------------------------------
private static HashMap<Integer, String> descPermissions = new HashMap(); static {
    descPermissions.put(0, "UNKNOW");
    descPermissions.put(BluetoothGattDescriptor.PERMISSION\_READ, "READ");
    descPermissions.put(BluetoothGattDescriptor.PERMISSION\_READ\_ENCRYPTED, "READ\_ENCRYPTED");
    descPermissions.put(BluetoothGattDescriptor.PERMISSION\_READ\_ENCRYPTED\_MITM, "READ\_ENCRYPTED\_MITM");
    descPermissions.put(BluetoothGattDescriptor.PERMISSION\_WRITE, "WRITE");
    descPermissions.put(BluetoothGattDescriptor.PERMISSION\_WRITE\_ENCRYPTED, "WRITE\_ENCRYPTED");
    descPermissions.put(BluetoothGattDescriptor.PERMISSION\_WRITE\_ENCRYPTED\_MITM, "WRITE\_ENCRYPTED\_MITM");
    descPermissions.put(BluetoothGattDescriptor.PERMISSION\_WRITE\_SIGNED, "WRITE\_SIGNED");
    descPermissions.put(BluetoothGattDescriptor.PERMISSION\_WRITE\_SIGNED\_MITM, "WRITE\_SIGNED\_MITM");
} public static String getDescPermission(int property){ return getHashMapValue(descPermissions,property);
} //这段代码没看明白,欢迎大神指教
private static String getHashMapValue(HashMap<Integer, String> hashMap,int number){
    String result \=hashMap.get(number); if(TextUtils.isEmpty(result)){
        List<Integer> numbers = getElement(number);
        result\=""; for(int i=0;i<numbers.size();i++){
            result+=hashMap.get(numbers.get(i))+"|";
        }
    } return result;
} /\*\* \* 位运算结果的反推函数10 -> 2 | 8; \*/
static private List<Integer> getElement(int number){
    List<Integer> result = new ArrayList<Integer>(); for (int i = 0; i < 32; i++){ int b = 1 << i; if ((number & b) > 0) 
            result.add(b);
    } return result;
} public static String bytesToHexString(byte\[\] src){  
    StringBuilder stringBuilder \= new StringBuilder(""); if (src == null || src.length <= 0) { return null;  
    } for (int i = 0; i < src.length; i++) { int v = src\[i\] & 0xFF;  
        String hv \= Integer.toHexString(v); if (hv.length() < 2) {  
            stringBuilder.append(0);  
        }  
        stringBuilder.append(hv);  
    } return stringBuilder.toString();  
}  

}

复制代码

1.Android广播机制概述

Android广播分为两个方面:广播发送者和广播接收者,通常情况下,BroadcastReceiver指的就是广播接收者(广播接收器)。广播作为Android组件间的通信方式,可以使用的场景如下:
1.同一app内部的同一组件内的消息通信(单个或多个线程之间);

2.同一app内部的不同组件之间的消息通信(单个进程);

3.同一app具有多个进程的不同组件之间的消息通信;

4.不同app之间的组件之间消息通信;

5.Android系统在特定情况下与App之间的消息通信。

从实现原理看上,Android中的广播使用了观察者模式,基于消息的发布/订阅事件模型。因此,从实现的角度来看,Android中的广播将广播的发送者和接受者极大程度上解耦,使得系统能够方便集成,更易扩展。具体实现流程要点粗略概括如下:

1.广播接收者BroadcastReceiver通过Binder机制向AMS(Activity Manager Service)进行注册;

2.广播发送者通过binder机制向AMS发送广播;

3.AMS查找符合相应条件(IntentFilter/Permission等)的BroadcastReceiver,将广播发送到BroadcastReceiver(一般情况下是Activity)相应的消息循环队列中;

4.消息循环执行拿到此广播,回调BroadcastReceiver中的onReceive()方法。

对于不同的广播类型,以及不同的BroadcastReceiver注册方式,具体实现上会有不同。但总体流程大致如上。

由此看来,广播发送者和广播接收者分别属于观察者模式中的消息发布和订阅两端,AMS属于中间的处理中心。广播发送者和广播接收者的执行是异步的,发出去的广播不会关心有无接收者接收,也不确定接收者到底是何时才能接收到。显然,整体流程与EventBus非常类似。

在上文说列举的广播机制具体可以使用的场景中,现分析实际应用中的适用性

第一种情形:同一app内部的同一组件内的消息通信(单个或多个线程之间),实际应用中肯定是不会用到广播机制的(虽然可以用),无论是使用扩展变量作用域、基于接口的回调还是Handler-post/Handler-Message等方式,都可以直接处理此类问题,若适用广播机制,显然有些“杀鸡牛刀”的感觉,会显太“重”;

第二种情形:同一app内部的不同组件之间的消息通信(单个进程),对于此类需求,在有些教复杂的情况下单纯的依靠基于接口的回调等方式不好处理,此时可以直接使用EventBus等,相对而言,EventBus由于是针对统一进程,用于处理此类需求非常适合,且轻松解耦。可以参见文件《Android各组件/控件间通信利器之EventBus》。

第三、四、五情形:由于涉及不同进程间的消息通信,此时根据实际业务使用广播机制会显得非常适宜。下面主要针对Android广播中的具体知识点进行总结。

2.BroadcastReceiver

自定义BroadcastReceiver

自定义广播接收器需要继承基类BroadcastReceivre,并实现抽象方法onReceive(context, intent)方法。广播接收器接收到相应广播后,会自动回到onReceive(..)方法。默认情况下,广播接收器也是运行在UI线程,因此,onReceive方法中不能执行太耗时的操作。否则将因此ANR。一般情况下,根据实际业务需求,onReceive方法中都会涉及到与其他组件之间的交互,如发送Notification、启动service等。
下面代码片段是一个简单的广播接收器的自定义:

复制代码

1 public class MyBroadcastReceiver extends BroadcastReceiver { 2 public static final String TAG = “MyBroadcastReceiver”;
3 public static int m = 1;
4
5 @Override
6 public void onReceive(Context context, Intent intent) { 7 Log.w(TAG, “intent:” + intent); 8 String name = intent.getStringExtra(“name”);
9 Log.w(TAG, “name:” + name + “ m=” + m); 10 m++; 11
12 Bundle bundle = intent.getExtras(); 13
14 } 15 }

复制代码

BroadcastReceiver注册类型

BroadcastReceiver总体上可以分为两种注册类型:静态注册和动态注册。

1).静态注册:
直接在AndroidManifest.xml文件中进行注册。规则如下:

复制代码

<receiver android:enabled=[“true” | “false”]
android:exported=[“true” | “false”]
android:icon=“drawable resource” android:label=“string resource” android:name=“string” android:permission=“string” android:process=“string” > . . . </receiver>

复制代码

其中,需要注意的属性
android:exported  ——此broadcastReceiver能否接收其他App的发出的广播,这个属性默认值有点意思,其默认值是由receiver中有无intent-filter决定的,如果有intent-filter,默认值为true,否则为false。(同样的,activity/service中的此属性默认值一样遵循此规则)同时,需要注意的是,这个值的设定是以application或者application user id为界的,而非进程为界(一个应用中可能含有多个进程);
android:name  —— 此broadcastReceiver类名;
android:permission  ——如果设置,具有相应权限的广播发送方发送的广播才能被此broadcastReceiver所接收;
android:process  ——broadcastReceiver运行所处的进程。默认为app的进程。可以指定独立的进程(Android四大基本组件都可以通过此属性指定自己的独立进程)

常见的注册形式有:

复制代码

<receiver android:name=“.MyBroadcastReceiver” >
<intent-filter>
<action android:name=“android.net.conn.CONNECTIVITY_CHANGE” />
</intent-filter>
<intent-filter>
<action android:name=“android.intent.action.BOOT_COMPLETED” />
</intent-filter>
</receiver>

复制代码

其中,intent-filter由于指定此广播接收器将用于接收特定的广播类型。本示例中给出的是用于接收网络状态改变或开启启动时系统自身所发出的广播。当此App首次启动时,系统会自动实例化MyBroadcastReceiver,并注册到系统中。

之前常说:静态注册的广播接收器即使app已经退出,主要有相应的广播发出,依然可以接收到,但此种描述自Android 3.1开始有可能不再成立,具体分析详见本文后面部分。

2).动态注册:
动态注册时,无须在AndroidManifest中注册组件。直接在代码中通过调用Context的registerReceiver函数,可以在程序中动态注册BroadcastReceiver。registerReceiver的定义形式如下:

1 registerReceiver(BroadcastReceiver receiver, IntentFilter filter) 2 registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler)

典型的写法示例如下:

复制代码

1 public class MainActivity extends Activity { 2 public static final String BROADCAST_ACTION = “com.example.corn”;
3 private BroadcastReceiver mBroadcastReceiver; 4
5 @Override
6 protected void onCreate(Bundle savedInstanceState) { 7 super.onCreate(savedInstanceState);
8 setContentView(R.layout.activity_main);
9
10 mBroadcastReceiver = new MyBroadcastReceiver(); 11 IntentFilter intentFilter = new IntentFilter(); 12 intentFilter.addAction(BROADCAST_ACTION); 13 registerReceiver(mBroadcastReceiver, intentFilter); 14 } 15
16 @Override 17 protected void onDestroy() { 18 super.onDestroy(); 19 unregisterReceiver(mBroadcastReceiver); 20 } 21
22 }

复制代码

注:Android中所有与观察者模式有关的设计中,一旦涉及到register,必定在相应的时机需要unregister。因此,上例在onDestroy()回到中需要unregisterReceiver(mBroadcastReceiver)。

当此Activity实例化时,会动态将MyBroadcastReceiver注册到系统中。当此Activity销毁时,动态注册的MyBroadcastReceiver将不再接收到相应的广播。

3.广播发送及广播类型

经常说”发送广播“和”接收“,表面上看广播作为Android广播机制中的实体,实际上这一实体本身是并不是以所谓的”广播“对象存在的,而是以”意图“(Intent)去表示。定义广播的定义过程,实际就是相应广播”意图“的定义过程,然后通过广播发送者将此”意图“发送出去。被相应的BroadcastReceiver接收后将会回调onReceive()函数。

下段代码片段显示的是一个普通广播的定义过程,并发送出去。其中setAction(..)对应于BroadcastReceiver中的intentFilter中的action。

1 Intent intent = new Intent();
2 intent.setAction(BROADCAST_ACTION);
3 intent.putExtra(“name”, “qqyumidi”);
4 sendBroadcast(intent);

根据广播的发送方式,可以将其分为以下几种类型:
1.Normal Broadcast:普通广播

2.System Broadcast: 系统广播

3.Ordered broadcast:有序广播

4.Sticky Broadcast:粘性广播(在 android 5.0/api 21中deprecated,不再推荐使用,相应的还有粘性有序广播,同样已经deprecated)

5.Local Broadcast:App应用内广播

下面分别总结下各种类型的发送方式及其特点。

1).Normal Broadcast:普通广播

此处将普通广播界定为:开发者自己定义的intent,以context.sendBroadcast_“AsUser”(intent, …)形式。具体可以使用的方法有:
sendBroadcast(intent)/sendBroadcast(intent, receiverPermission)/sendBroadcastAsUser(intent, userHandler)/sendBroadcastAsUser(intent, userHandler,receiverPermission)。
普通广播会被注册了的相应的感兴趣(intent-filter匹配)接收,且顺序是无序的。如果发送广播时有相应的权限要求,BroadCastReceiver如果想要接收此广播,也需要有相应的权限。

2).System Broadcast: 系统广播

Android系统中内置了多个系统广播,只要涉及到手机的基本操作,基本上都会发出相应的系统广播。如:开启启动,网络状态改变,拍照,屏幕关闭与开启,点亮不足等等。每个系统广播都具有特定的intent-filter,其中主要包括具体的action,系统广播发出后,将被相应的BroadcastReceiver接收。系统广播在系统内部当特定事件发生时,有系统自动发出。

3)Ordered broadcast:有序广播

有序广播的有序广播中的“有序”是针对广播接收者而言的,指的是发送出去的广播被BroadcastReceiver按照先后循序接收。有序广播的定义过程与普通广播无异,只是其的主要发送方式变为:sendOrderedBroadcast(intent, receiverPermission, …)。

对于有序广播,其主要特点总结如下:

1>多个具当前已经注册且有效的BroadcastReceiver接收有序广播时,是按照先后顺序接收的,先后顺序判定标准遵循为:将当前系统中所有有效的动态注册和静态注册的BroadcastReceiver按照priority属性值从大到小排序,对于具有相同的priority的动态广播和静态广播,动态广播会排在前面。

2>先接收的BroadcastReceiver可以对此有序广播进行截断,使后面的BroadcastReceiver不再接收到此广播,也可以对广播进行修改,使后面的BroadcastReceiver接收到广播后解析得到错误的参数值。当然,一般情况下,不建议对有序广播进行此类操作,尤其是针对系统中的有序广播。

4)Sticky Broadcast:粘性广播(在 android 5.0/api 21中deprecated,不再推荐使用,相应的还有粘性有序广播,同样已经deprecated)。

既然已经deprecated,此处不再多做总结。

5)Local Broadcast:App应用内广播(此处的App应用以App应用进程为界)

由前文阐述可知,Android中的广播可以跨进程甚至跨App直接通信,且注册是exported对于有intent-filter的情况下默认值是true,由此将可能出现安全隐患如下:

1.其他App可能会针对性的发出与当前App intent-filter相匹配的广播,由此导致当前App不断接收到广播并处理;

2.其他App可以注册与当前App一致的intent-filter用于接收广播,获取广播具体信息。

无论哪种情形,这些安全隐患都确实是存在的。由此,最常见的增加安全性的方案是:

1.对于同一App内部发送和接收广播,将exported属性人为设置成false,使得非本App内部发出的此广播不被接收;

2.在广播发送和接收时,都增加上相应的permission,用于权限验证;

3.发送广播时,指定特定广播接收器所在的包名,具体是通过intent.setPackage(packageName)指定在,这样此广播将只会发送到此包中的App内与之相匹配的有效广播接收器中。

App应用内广播可以理解成一种局部广播的形式,广播的发送者和接收者都同属于一个App。实际的业务需求中,App应用内广播确实可能需要用到。同时,之所以使用应用内广播时,而不是使用全局广播的形式,更多的考虑到的是Android广播机制中的安全性问题。

相比于全局广播,App应用内广播优势体现在:

1.安全性更高;

2.更加高效。

为此,Android v4兼容包中给出了封装好的LocalBroadcastManager类,用于统一处理App应用内的广播问题,使用方式上与通常的全局广播几乎相同,只是注册/取消注册广播接收器和发送广播时将主调context变成了LocalBroadcastManager的单一实例。

代码片段如下:

复制代码

1 //registerReceiver(mBroadcastReceiver, intentFilter);
2 //注册应用内广播接收器
3 localBroadcastManager = LocalBroadcastManager.getInstance(this);
4 localBroadcastManager.registerReceiver(mBroadcastReceiver, intentFilter);
5
6 //unregisterReceiver(mBroadcastReceiver);
7 //取消注册应用内广播接收器
8 localBroadcastManager.unregisterReceiver(mBroadcastReceiver);
9
10 Intent intent = new Intent(); 11 intent.setAction(BROADCAST_ACTION); 12 intent.putExtra(“name”, “qqyumidi”); 13 //sendBroadcast(intent); 14 //发送应用内广播
15 localBroadcastManager.sendBroadcast(intent);

复制代码

4.不同注册方式的广播接收器回调onReceive(context, intent)中的context具体类型

1).对于静态注册的ContextReceiver,回调onReceive(context, intent)中的context具体指的是ReceiverRestrictedContext;

2).对于全局广播的动态注册的ContextReceiver,回调onReceive(context, intent)中的context具体指的是Activity Context;

3).对于通过LocalBroadcastManager动态注册的ContextReceiver,回调onReceive(context, intent)中的context具体指的是Application Context。

注:对于LocalBroadcastManager方式发送的应用内广播,只能通过LocalBroadcastManager动态注册的ContextReceiver才有可能接收到(静态注册或其他方式动态注册的ContextReceiver是接收不到的)。

5.不同Android API版本中广播机制相关API重要变迁

1).Android5.0/API level 21开始粘滞广播和有序粘滞广播过期,以后不再建议使用;

2).”静态注册的广播接收器即使app已经退出,主要有相应的广播发出,依然可以接收到,但此种描述自Android 3.1开始有可能不再成立“

Android 3.1开始系统在Intent与广播相关的flag增加了参数,分别是FLAG_INCLUDE_STOPPED_PACKAGES和FLAG_EXCLUDE_STOPPED_PACKAGES。

FLAG_INCLUDE_STOPPED_PACKAGES:包含已经停止的包(停止:即包所在的进程已经退出)

FLAG_EXCLUDE_STOPPED_PACKAGES:不包含已经停止的包

主要原因如下:

自Android3.1开始,系统本身则增加了对所有app当前是否处于运行状态的跟踪。在发送广播时,不管是什么广播类型,系统默认直接增加了值为FLAG_EXCLUDE_STOPPED_PACKAGES的flag,导致即使是静态注册的广播接收器,对于其所在进程已经退出的app,同样无法接收到广播。

详情参加Android官方文档:http://developer.android.com/about/versions/android-3.1.html#launchcontrols

由此,对于系统广播,由于是系统内部直接发出,无法更改此intent flag值,因此,3.1开始对于静态注册的接收系统广播的BroadcastReceiver,如果App进程已经退出,将不能接收到广播。

但是对于自定义的广播,可以通过复写此flag为FLAG_INCLUDE_STOPPED_PACKAGES,使得静态注册的BroadcastReceiver,即使所在App进程已经退出,也能能接收到广播,并会启动应用进程,但此时的BroadcastReceiver是重新新建的。

1 Intent intent = new Intent(); 2 intent.setAction(BROADCAST_ACTION); 3 intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); 4 intent.putExtra(“name”, “qqyumidi”); 5 sendBroadcast(intent);

注1:对于动态注册类型的BroadcastReceiver,由于此注册和取消注册实在其他组件(如Activity)中进行,因此,不受此改变影响。

注2:在3.1以前,相信不少app可能通过静态注册方式监听各种系统广播,以此进行一些业务上的处理(如即时app已经退出,仍然能接收到,可以启动service等..),3.1后,静态注册接受广播方式的改变,将直接导致此类方案不再可行。于是,通过将Service与App本身设置成不同的进程已经成为实现此类需求的可行替代方案。

-——————————————————————————–
笔者水平有限,若有错漏,欢迎指正,如果转载以及CV操作,请务必注明出处,谢谢!

ASP.NET Core中使用GraphQL


字段#

我们已经很好的理解了GraphQL中的字段。在之前HelloWorldQuery的例子中,我们添加了2个字段hellohowdy. 它们都是标量字段。正如GraphQL官网文档中声明的那样

“At its simplest, GraphQL is about asking for specific fields on objects”

简单来说,GraphQL就是询问对象中的一些特定字段

来源: graphql.org

下面我们来为我们的实例程序添加一些复杂的类型。比如,现在我们需要编写一个库存系统,我们首先添加一个货物类Item, 其代码如下:

1
2
3
4
5
6
7
8
public class Item  
{
public string Barcode { get; set; }

public string Title { get; set; }

public decimal SellingPrice { get; set; }
}

但是我们不希望直接针对这个对象创建查询,因为它不是一个GraphQL对象,它没有继承自ObjectGraphType, 为了创建一个GraphQL查询,我们需要创建一个新类ItemType, 它继承自ObjectGraphType类。

另外ObjectGraphType类是一个泛型类,所以这里我们需要指定它的泛型参数是Item

1
2
3
4
5
6
7
8
9
public class ItemType : ObjectGraphType<Item>  
{
public ItemType()
{
Field(i => i.Barcode);
Field(i => i.Title);
Field(i => i.SellingPrice);
}
}

这里有2点需要注意。首先我们不在针对字段进行类型声明了。GraphQL库将实体类属性字段类型映射成GraphQL的内置类型。例如这里Barcode的类型string会被映射成GraphQL的内置类型StringGraphType。其次这里我们使用了Lambda表达式设置了实体类属性和GraphQL字段之间的映射, 这有点类似于数据库模型和ViewModel之间的转换的映射。

下一步,我们需要在HelloWorldQuery中注册ItemType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public HelloWorldQuery()  
{
...
...

Field<ItemType>(
"item",
resolve: context =>
{
return new Item {
Barcode = "123",
Title = "Headphone",
SellingPrice = 12.99M
};
}
);
}

这里我们暂时设置了一个硬编码的返回值。所以当查询item对象的时候,这个硬编码的返回值会输出出来。

现在我们启动项目,进入GraphiQL界面

首先我们设置查询为

1
2
3
4
5
6
query {
item{
barcode
sellingPrice
}
}

运行查询之后,结果是

1
2
3
4
5
6
7
8
{
"data": {
"item": {
"barcode": "123",
"sellingPrice": 12.99
}
}
}

然后我们修改查询为

1
2
3
4
5
6
7
query {
item{
barcode
sellingPrice
title
}
}

运行查询之后,结果是

1
2
3
4
5
6
7
8
9
{
"data": {
"item": {
"barcode": "123",
"sellingPrice": 12.99,
"title": "Headphone"
}
}
}

这说明我们的GraphQL查询已经生效,api根据我们需要的字段返回了正确的返回值。

参数#

这里我们可以使用参数去除前面的硬编码。

为了说明如何使用参数,这里我们首先创建一个数据源类DataSource, 其代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DataSource  
{
public IList<Item> Items
{
get;
set;
}

public DataSource()
{
Items = new List<Item>(){
new Item { Barcode= "123", Title="Headphone", SellingPrice=50},
new Item { Barcode= "456", Title="Keyboard", SellingPrice= 40},
new Item { Barcode= "789", Title="Monitor", SellingPrice= 100}
};
}

public Item GetItemByBarcode(string barcode)
{
return Items.First(i => i.Barcode.Equals(barcode));
}
}

这里除了Items集合,我们还添加了一个方法GetItemByBarcode, 这个方法可以根据传递的barcode参数返回第一个匹配的Item

然后现在我们来修改之前的item查询, 添加一个arguments参数, 其代码如下:

1
2
3
4
5
6
7
8
9
Field<ItemType>(
"item",
arguments: new QueryArguments(new QueryArgument<StringGraphType> { Name = "barcode" }),
resolve: context =>
{
var barcode = context.GetArgument<string>("barcode");
return new DataSource().GetItemByBarcode(barcode);
}
);

arguments是一个参数列表,里面可以包含必填参数和选填参数。针对每个参数,我们都需要指定它对应的类型,这里Name属性是设置了当前参数的名称。

resolve参数中, 你可以使用context.GetArgument()方法获取查询中传递的参数值。

现在我们重新启动项目,并在GraphiQL中添加如下查询

1
2
3
4
5
6
query {  
item (barcode: "123") {
title
sellingPrice
}
}

输出的查询结果

1
2
3
4
5
6
7
8
{
"data": {
"item": {
"title": "Headphone",
"sellingPrice": 50
}
}
}

这个结果与我们预想的一样。

但是这时候如果我们不传递barcode参数

1
2
3
4
5
6
query {  
item {
title
sellingPrice
}
}

程序就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"data": {
"item": null
},
"errors": [
{
"message": "Error trying to resolve item.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"item"
],
"extensions": {
"code": "INVALID_OPERATION"
}
}
]
}

原因是当前barcode是一个可空项,程序查询时, First方法会报错。所以这时候我们可以使用NonNullGraphType来设置barcode为一个必填项。

1
QueryArgument<NonNullGraphType<StringGraphType>> { Name = "barcode" }  

这样重新启动项目后,继续使用之前报错的查询,GraphiQL就会给出校验错误。

变量#

现在是时候将参数变成动态了。 我们不希望每次在查询中写死查询条件,我们希望这个查询参数是动态的,这时候我们就需要使用到变量。

首先,这里我们需要确保我们的GraphQL中间件可以接受参数,所以我们需要在GraphQLRequest类中添加一个参数变量

1
2
3
4
5
public class GraphQLRequest
{
public string Query { get; set; }
public JObject Variables { get; set; }
}

然后我们需要修改GraphQLMiddleware中间件的InvokeAsync方法, 在其中添加一行代码设置doc.Inputs

1
2
3
4
5
6
7
8
var result = await _executor.ExecuteAsync(doc =>
{
doc.Schema = _schema;
doc.Query = request.Query;

doc.Inputs = request.Variables.ToInputs();

}).ConfigureAwait(false);

现在我们的item查询已经支持动态参数了,我们可以运行程序,在GraphiQL中设置如下查询

1
2
3
4
5
6
query($barcode: String!){  
item(barcode: $barcode){
title
sellingPrice
}
}

查询中变量是以$开头的, 后面需要加上变量类型,因为之前我们这是了barcode参数为必填项,所以$barcode变量我们也要设置成必填。变量的必填设置是在变量类型后添加一个!号。

最后,在GraphiQL中,你可以使用QUERY VARIABLES面板中输入参数的值。如下图所示,最终结果正确的返回了。

本文源代码:https://github.com/lamondlu/GraphQL_Blogs/tree/master/Part%20V

ASP.NET Core中使用GraphQL


本篇中我将演示如何配置持久化仓储,这里原文中是使用的Postgres, 这里我改用了EF Core For SqlServer。本文的例子需要在上一篇的代码基础上修改。没有代码的同学,可以去https://github.com/lamondlu/GraphQL_Blogs/tree/master/Part%20V下载。

之前我们编写了一个DataStore类,里面硬编码了一个数据集合,这里我们希望改用依赖注入的方式进行解耦,所以首先我们需要创建一个抽象接口IDataStore

1
2
3
4
5
public interface IDataStore
{
IEnumerable<Item> GetItems();
Item GetItemByBarcode(string barcode);
}

由于接下来我们需要使用EF Core, 所以这里我们需要添加一个EF Core的上下文类ApplicationDbContext

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
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{

}

public DbSet<Item> Items { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Item>().ToTable("Items");
modelBuilder.Entity<Item>().HasKey(p => p.Barcode);

modelBuilder.Entity<Item>().HasData(new Item {
Barcode = "123",
Title = "Headphone",
SellingPrice = 50 });

modelBuilder.Entity<Item>().HasData(new Item {
Barcode = "456",
Title = "Keyboard",
SellingPrice = 40 });
modelBuilder.Entity<Item>().HasData(new Item {
Barcode = "789",
Title = "Monitor",
SellingPrice = 100 });

base.OnModelCreating(modelBuilder);
}
}

这里为了导入一些初始数据,我们在OnModelCreating方法中使用HasData方法添加了3个初始数据。

下面我们修改DataStore类, DataStore应该实现IDataStore接口, 其中的GetItemByBarcodeGetItems方法需要改为从数据库中读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DataStore : IDataStore
{
private ApplicationDbContext _applicationDbContext;

public DataStore(ApplicationDbContext applicationDbContext)
{
_applicationDbContext = applicationDbContext;
}

public Item GetItemByBarcode(string barcode)
{
return _applicationDbContext.Items.First(i => i.Barcode.Equals(barcode));
}

public IEnumerable<Item> GetItems()
{
return _applicationDbContext.Items;
}
}

接下来,我们要在Startup.cs类中的ConfigureServices添加Entity Framework配置

1
2
3
4
services.AddDbContext<ApplicationDbContext>(option =>
{
option.UseSqlServer(Configuration.GetConnectionString("SampleDB"));
});

TIPS: 这里注意不要忘记创建一个appsettings.json, 在其中添加数据库连接字符串

配置完成之后,我们需要使用以下命令添加Migration,并更新数据库

1
2
dotnet ef migrations add Initial
dotnet ef database update

现在针对数据库的修改都已经完成了。

另外我们还需要修改服务注册代码,将注册服务的生命周期从单例(Singleton)改为作用域(Scoped), 因为当注入服务的生命周期为单例时,需要处理多线程问题和潜在的内存泄漏问题。

1
2
3
services.AddScoped<IDataStore, DataStore>();
services.AddScoped<HelloWorldQuery>();
services.AddScoped<ISchema, HelloWorldSchema>();

修改完成后,Startup.cs最终代码如下:

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
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(option =>
{
option.UseSqlServer(Configuration.GetConnectionString("SampleDB"));
});

services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
services.AddSingleton<IDocumentWriter, DocumentWriter>();

services.AddScoped<IDataStore, DataStore>();
services.AddScoped<HelloWorldQuery>();
services.AddScoped<ISchema, HelloWorldSchema>();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseDefaultFiles();
app.UseStaticFiles();

app.UseMiddleware<GraphQLMiddleware>();
}
}

现在我们启动项目, 程序会抛出一个错误

System.InvalidOperationException: Cannot resolve scoped service ‘GraphQL.Types.ISchema’ from root provider

这个问题的原因是,中间件是单例的,如果在中间件的构造函数中使用作用域(Scoped)的依赖注入, 会导致这个问题(具体请参见https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1)。这里`ISchema`的生命周期是作用域,并且在`GraphQLMiddleware`类中是从构造函数注入的,所以这里我们需要修改`GraphQLMiddleware`类,`ISchema`需要改从`Invoke`方法注入。

中间件最终代码如下:

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
public class GraphQLMiddleware
{
private readonly RequestDelegate _next;
private readonly IDocumentWriter _writer;
private readonly IDocumentExecuter _executor;
public GraphQLMiddleware(RequestDelegate next, IDocumentWriter writer, IDocumentExecuter executor)
{
_next = next;
_writer = writer;
_executor = executor;
}

public async Task InvokeAsync(HttpContext httpContext, ISchema schema)
{
if (httpContext.Request.Path.StartsWithSegments("/api/graphql")
&& string.Equals(httpContext.Request.Method,
"POST",
StringComparison.OrdinalIgnoreCase))
{
string body;
using (var streamReader = new StreamReader(httpContext.Request.Body))
{
body = await streamReader.ReadToEndAsync();

var request = JsonConvert.DeserializeObject<GraphQLRequest>(body);

var result = await _executor.ExecuteAsync(doc =>
{
doc.Schema = schema;
doc.Query = request.Query;
doc.Inputs = request.Variables.ToInputs();
}).ConfigureAwait(false);

var json = await _writer.WriteToStringAsync(result);
await httpContext.Response.WriteAsync(json);
}
}
else
{
await _next(httpContext);
}
}
}

修改完成之后,我们重新启动项目,项目正常启动成功, GraphiQL界面出现。

现在我们还是使用上一章的查询代码,查询二维码是123的货物数据。

数据正常从数据库中读取成功。下一章我们将讲解在ASP.NET Core中如何使用GraphQL添加修改数据。

本文源代码: https://github.com/lamondlu/GraphQL_Blogs/tree/master/Part%20VI

ASP.NET Core中使用GraphQL - 目录


到目前为止我们一直在使用GraphQL操作单个实体。在本篇博文中,我们将使用GraphQL操作实体集合。

这里我们使用的场景是处理一个顾客的所有订单,顾客和订单之间的关系是一对多。一个顾客可以有多个订单,相应的一个订单只属于一个顾客。

数据库修改#

下面我们首先创建2个新的类CustomerOrder

Customer
1
2
3
4
5
6
7
public class Customer
{
public int CustomerId { get; set; }
public string Name { get; set; }
public string BillingAddress { get; set; }
public IEnumerable<Order> Orders { get; set; }
}
Order
1
2
3
4
5
6
7
8
9
public class Order
{
public int OrderId { get; set; }
public string Tag { get; set; }
public DateTime CreatedAt { get; set; }

public Customer Customer { get; set; }
public int CustomerId { get; set; }
}

然后我们修改ApplicationDbContext类,在OnModelCreating配置一下表的主外键。

1
2
3
4
5
6
7
modelBuilder.Entity<Customer>()
.HasKey(p => p.CustomerId);
modelBuilder.Entity<Customer>().HasMany(p => p.Orders)
.WithOne()
.HasForeignKey(p => p.CustomerId);

modelBuilder.Entity<Order>().HasKey(p => p.OrderId);

最后我们使用如下命令创建迁移并更新数据库

1
2
dotnet ef migrations add OneToManyRelationship  
dotnet ef database update

至此数据库修改完成。

添加GraphQL代码#

下面我们需要添加GraphQL针对CustomerOrder表的字段配置。

OrderType
1
2
3
4
5
6
7
8
9
10
11
public class OrderType: ObjectGraphType <Order> {  
public OrderType(IDataStore dataStore) {
Field(o => o.Tag);
Field(o => o.CreatedAt);
Field <CustomerType, Customer> ()
.Name("Customer")
.ResolveAsync(ctx => {
return dataStore.GetCustomerByIdAsync(ctx.Source.CustomerId);
});
}
}
CustomerType.cs
1
2
3
4
5
6
7
8
9
10
11
public class CustomerType: ObjectGraphType <Customer> {  
public CustomerType(IDataStore dataStore) {
Field(c => c.Name);
Field(c => c.BillingAddress);
Field <ListGraphType<OrderType> , IEnumerable<Order>> ()
.Name("Orders")
.ResolveAsync(ctx => {
return dataStore.GetOrdersByCustomerIdAsync(ctx.Source.CustomerId);
});
}
}

为了查询所有的顾客和订单,我们还需要暴露出2个新的节点。所以我们修改在InventoryQuery构造函数中添加如下代码:

InventoryQuery
1
2
3
4
5
6
7
8
9
10
11
12
13
Field<ListGraphType<OrderType>, IEnumerable<Order>>()  
.Name("Orders")
.ResolveAsync(ctx =>
{
return dataStore.GetOrdersAsync();
});

Field<ListGraphType<CustomerType>, IEnumerable<Customer>>()
.Name("Customers")
.ResolveAsync(ctx =>
{
return dataStore.GetCustomersAsync();
});

然后我们需要在IDataStore中定义6个新的方法,并在DataStore中实现它们。

IDataStore
1
2
3
4
5
6
7
8
9
10
11
Task<IEnumerable<Order>> GetOrdersAsync();

Task<IEnumerable<Customer>> GetCustomersAsync();

Task<Customer> GetCustomerByIdAsync(int customerId);

Task<IEnumerable<Order>> GetOrdersByCustomerIdAsync(int customerId);

Task<Order> AddOrderAsync(Order order);

Task<Customer> AddCustomerAsync(Customer customer);
DataStore
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
public async Task<IEnumerable<Order>> GetOrdersAsync()
{
return await _context.Orders
.AsNoTracking()
.ToListAsync();
}

public async Task<IEnumerable<Customer>> GetCustomersAsync()
{
return await _context.Customers
.AsNoTracking()
.ToListAsync();
}

public async Task<Customer> GetCustomerByIdAsync(int customerId)
{
return await _context.Customers
.FindAsync(customerId);
}

public async Task<IEnumerable<Order>> GetOrdersByCustomerIdAsync(int customerId)
{
return await _context.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync();
}

public async Task<Order> AddOrderAsync(Order order)
{
var addedOrder = await _context.Orders.AddAsync(order);
await _context.SaveChangesAsync();
return addedOrder.Entity;
}

public async Task<Customer> AddCustomerAsync(Customer customer)
{
var addedCustomer = await _context.Customers.AddAsync(customer);
await _context.SaveChangesAsync();
return addedCustomer.Entity;
}

添加完以上代码之后,我们就需要定义添加订单和顾客的输入类型了。还记得在上一章中我们如何添加货物的么?我们添加了一个ItemInputType类,定义了添加货物需要收集的字段,所以这里同理,我们也需要为订单和顾客定义对应的InputObjectGraphType

OrderInputType
1
2
3
4
5
6
7
8
9
public class OrderInputType : InputObjectGraphType {  
public OrderInputType()
{
Name = "OrderInput";
Field<NonNullGraphType<StringGraphType>>("tag");
Field<NonNullGraphType<DateGraphType>>("createdAt");
Field<NonNullGraphType<IntGraphType>>("customerId");
}
}
CustomerInputType
1
2
3
4
5
6
7
8
public class CustomerInputType : InputObjectGraphType {  
public CustomerInputType()
{
Name = "CustomerInput";
Field<NonNullGraphType<StringGraphType>>("name");
Field<NonNullGraphType<StringGraphType>>("billingAddress");
}
}

当前添加以上代码之后,我们还需要在Startup类中注册这几个新类型

1
2
3
4
5
6
7
8
9
public void ConfigureServices(IServiceCollection services)  
{
....
....
services.AddScoped<CustomerType>();
services.AddScoped<CustomerInputType>();
services.AddScoped<OrderType>();
services.AddScoped<OrderInputType>();
}

如果现在启动项目,你会得到以下错误

1
Failed to call Activator.CreateInstance. Type: chapter1.OrderType

这里的问题是在InventorySchema构造函数中的注入没起作用, 原因是GraphQL在解决依赖的时候,只能处理一层, 这里OrderTypeCustomerType是2层的关系。如果想解决这个问题,我们需要在Startup中再注册一个依赖解决器。

1
2
services.AddScoped<IDependencyResolver>(s => 
new FuncDependencyResolver(s.GetRequiredService));

修改完成之后我们还需要修改InventorySchema, 在构造函数中将依赖解决器注入。

1
2
3
4
5
6
public class InventorySchema: Schema {  
public InventorySchema(IDependencyResolver resolver): base(resolver) {
Query = resolver.Resolve<InventoryQuery>();
Mutation = resolver.Resolve<InventoryMutation>();
}
}

现在再次启动项目,程序不报错了。

最终效果#

下面我们首先创建一个Customer

然后我们继续创建2个Order


最后我们来查询一下刚才创建的数据是否存在

数据读取正确,这说明我们的数据添加成功了。

本文源代码: https://github.com/lamondlu/GraphQL_Blogs/tree/master/Part%20VIII

前言

前面我们聊了一下一个应用程序 应该监控的8个关键位置.

. 嗯..地址如下:

应用程序的8个关键性能指标以及测量方法

最后卖了个小关子,是关于如何监控ASP.NET Core的.

今天我们就来讲讲如何监控它,下面上效果图:

阅读本文需要了解的相关技术与内容:

InfluxDb(分布式时序数据库,开源)(注:分布式部分已商业化最新的分布式版本已不在开源,单例的继续开源)

Grafana(开源的,功能齐全的度量仪表盘和图形编辑器)

App Metrics(主角,开源的支持.NET Core的监控插件,采用管道注入的方式,对代码的入侵性极小)

本文测试环境为Windows64位,当然 这个方案全部都可以在linux上实现(甚至windows才应该是备用方案 - -,尴尬.)

所以本文分为3个步骤

1.安装InfluxDb,并创建数据库

2.安装Grafana,并添加相关配置

3.在ASP.NET Core中使用App Metrics

下面我们正式开始

正文

**1.安装InfluxDb,**并创建数据库

App.Metrics支持的库很多,有InfluxDBGraphitePrometheus.

今天我们主要已InfluxDb为例子.

首先下载,InfluxDb

知道你们懒得找..地址如下:https://portal.influxdata.com/downloads#influxdb

下载你需要的对应的系统的版本.这里我就直接下载Windows Binaries (64-bit)

linux的安装步骤我就不多说了,网上一大把..讲一下windows安装InfluxDb的注意事项(..这玩意对liunx的支持比windows好的多)..

解压后打开influxdb.conf,因为influxdb的默认配置全是针对linux配置的..所以我们要修改一下配置文件.

修改下面3个liunx的路径,改为winodws路径如下:

[meta]

Where the metadata/raft database is stored

dir = “D:/influxdb/meta”

[data]

The directory where the TSM storage engine stores TSM files.

dir = “D:/influxdb/data”

The directory where the TSM storage engine stores WAL files.

wal-dir = “D:/influxdb/wal”

然后修改网页图形化管理界面配置,端口如下:

复制代码

[admin]

Determines whether the admin service is enabled.

enabled = true # The default bind address used by the admin service.
bind-address = “:8083”

复制代码

最后cmd运行,进入到你的解压目录,执行命令:

influxd -config influxdb.conf

得到如图效果:

我们就安装完成了.

我们进入管理界面的监听端口:http://localhost:8083/,会看到如下界面:

然后我们输入SQL语句  CREATE DATABASE “你的库名”

看到下图,就是执行成功了.

我们查一下看是不是真的,输入SQL语句:SHOW DATABASES

如图:

除了默认的第一个库以外,还有你自己添加的库名,就说明是加成功了,好了,安装就到此结束

2.安装Grafana,并添加相关配置

然后我们安装Grafana,

下载地址:https://grafana.com/get

我们解压后进入bin目录,如图:

直接运行grafana-server.exe即可.

Grafana默认会监听3000的端口,所以我们进入http://localhost:3000,

会让你登陆,直接输入本地的管理员帐户即可,帐户:admin  密码:admin,进入后如图:

安装完成之后,我们下载相关的仪表模版.

地址如下:https://grafana.com/dashboards/2140   (是个json文件)

然后我们导入我们的仪表:如图操作即可:

然后,添加我们上面的数据源.

如图:

选择Add DataSource,然后操作如下:

这样,我们就完成了Grafana的安装配置和添加数据源

3.在ASP.NET Core中使用App Metrics

接下来就是我们的重头戏了,在ASP.NET Core中使用AppMetrics.

我们随便创建一个ASP.NET Core MVC项目,如图:

 用nuget包添加引用:

核心程序:

管道注入的扩展:

ASP.NET Core MVC的扩展:

其他相关要用到的库:(注:我们这里的数据源是influxDB所以用的这个库,其他的库请搜索其他库的后缀)

我们回到我们熟悉的C#代码,在Startup中我们编写注入代码如下:

首先修改ConfigureServices方法,如下:

复制代码

public void ConfigureServices(IServiceCollection services)
{ var database = “TestData”; var uri = new Uri(“http://127.0.0.1:8086“);

        services.AddMetrics(options \=> {
            options.GlobalTags.Add("app", "sample app");
            options.GlobalTags.Add("env", "stage");
        })
           .AddHealthChecks()
           .AddJsonSerialization()
           .AddReporting(
              factory \=> {
                  factory.AddInfluxDb( new InfluxDBReporterSettings
                    {
                        InfluxDbSettings \= new InfluxDBSettings(database, uri),
                        ReportInterval \= TimeSpan.FromSeconds(5)
                    });
              })
           .AddMetricsMiddleware(options \=> options.IgnoredHttpStatusCodes = new\[\] { 404 }); // Add framework services.

services.AddMvc();
}

复制代码

加入我们influxDB相关配置,其中database就是你自己输入的数据库名. uri就是你数据库的地址.

然后我们需要在程序启动的时候就注入我们的Metrics监控,所以修改Configure代码如下:

复制代码

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime lifetime)
{
loggerFactory.AddConsole(Configuration.GetSection(“Logging”));
app.UseMetrics();
app.UseMetricsReporting(lifetime); //loggerFactory.AddDebug();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        } else {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();

        app.UseMvc(routes \=> {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }

复制代码

其中app.UseMetrics();  app.UseMetricsReporting(lifetime) ,是我们的注入代码..

其他的地方.不用动一丝一毫..怎么样 是不是几乎没有什么入侵性~.

 最后,我们跑起来.

随便在home中点击几下页面,或者调用一下..

然后进入到Grafana的监控页面中,就可以看到如下信息:

写在最后

这篇只是简单的讲了如何做到初级的监控,其实还有心跳检测等比较多的功能,后面会慢慢来讲..

.Net Core也就今年社区才慢慢起步,所这个监控也是刚刚完成,作者一直在积极的更新中.

比较遗憾的是本来这个是支持 .NET4.5.2以上的,但是由于关注的人比较少.所以就只出了RC的预览版.对最新版支持不完善.

我也在GitHub上给作者提了Issues,https://github.com/alhardy/AppMetrics/issues/177

希望感兴趣的基友们一起加入,这里也吐槽一下,..我就一句话..社区需要我们共通创建,而不是等着摘现成的桃子..

作者:顾振印 出处:http://www.cnblogs.com/GuZhenYin/ 如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面

前言

前面我们聊了一下一个应用程序 应该监控的8个关键位置.

. 嗯..地址如下:

应用程序的8个关键性能指标以及测量方法

最后卖了个小关子,是关于如何监控ASP.NET Core的.

今天我们就来讲讲如何监控它,下面上效果图:

阅读本文需要了解的相关技术与内容:

InfluxDb(分布式时序数据库,开源)(注:分布式部分已商业化最新的分布式版本已不在开源,单例的继续开源)

Grafana(开源的,功能齐全的度量仪表盘和图形编辑器)

App Metrics(主角,开源的支持.NET Core的监控插件,采用管道注入的方式,对代码的入侵性极小)

本文测试环境为Windows64位,当然 这个方案全部都可以在linux上实现(甚至windows才应该是备用方案 - -,尴尬.)

所以本文分为3个步骤

1.安装InfluxDb,并创建数据库

2.安装Grafana,并添加相关配置

3.在ASP.NET Core中使用App Metrics

下面我们正式开始

正文

**1.安装InfluxDb,**并创建数据库

App.Metrics支持的库很多,有InfluxDBGraphitePrometheus.

今天我们主要已InfluxDb为例子.

首先下载,InfluxDb

知道你们懒得找..地址如下:https://portal.influxdata.com/downloads#influxdb

下载你需要的对应的系统的版本.这里我就直接下载Windows Binaries (64-bit)

linux的安装步骤我就不多说了,网上一大把..讲一下windows安装InfluxDb的注意事项(..这玩意对liunx的支持比windows好的多)..

解压后打开influxdb.conf,因为influxdb的默认配置全是针对linux配置的..所以我们要修改一下配置文件.

修改下面3个liunx的路径,改为winodws路径如下:

[meta]

Where the metadata/raft database is stored

dir = “D:/influxdb/meta”

[data]

The directory where the TSM storage engine stores TSM files.

dir = “D:/influxdb/data”

The directory where the TSM storage engine stores WAL files.

wal-dir = “D:/influxdb/wal”

然后修改网页图形化管理界面配置,端口如下:

复制代码

[admin]

Determines whether the admin service is enabled.

enabled = true # The default bind address used by the admin service.
bind-address = “:8083”

复制代码

最后cmd运行,进入到你的解压目录,执行命令:

influxd -config influxdb.conf

得到如图效果:

我们就安装完成了.

我们进入管理界面的监听端口:http://localhost:8083/,会看到如下界面:

然后我们输入SQL语句  CREATE DATABASE “你的库名”

看到下图,就是执行成功了.

我们查一下看是不是真的,输入SQL语句:SHOW DATABASES

如图:

除了默认的第一个库以外,还有你自己添加的库名,就说明是加成功了,好了,安装就到此结束

2.安装Grafana,并添加相关配置

然后我们安装Grafana,

下载地址:https://grafana.com/get

我们解压后进入bin目录,如图:

直接运行grafana-server.exe即可.

Grafana默认会监听3000的端口,所以我们进入http://localhost:3000,

会让你登陆,直接输入本地的管理员帐户即可,帐户:admin  密码:admin,进入后如图:

安装完成之后,我们下载相关的仪表模版.

地址如下:https://grafana.com/dashboards/2140   (是个json文件)

然后我们导入我们的仪表:如图操作即可:

然后,添加我们上面的数据源.

如图:

选择Add DataSource,然后操作如下:

这样,我们就完成了Grafana的安装配置和添加数据源

3.在ASP.NET Core中使用App Metrics

接下来就是我们的重头戏了,在ASP.NET Core中使用AppMetrics.

我们随便创建一个ASP.NET Core MVC项目,如图:

 用nuget包添加引用:

核心程序:

管道注入的扩展:

ASP.NET Core MVC的扩展:

其他相关要用到的库:(注:我们这里的数据源是influxDB所以用的这个库,其他的库请搜索其他库的后缀)

我们回到我们熟悉的C#代码,在Startup中我们编写注入代码如下:

首先修改ConfigureServices方法,如下:

复制代码

public void ConfigureServices(IServiceCollection services)
{ var database = “TestData”; var uri = new Uri(“http://127.0.0.1:8086“);

        services.AddMetrics(options \=> {
            options.GlobalTags.Add("app", "sample app");
            options.GlobalTags.Add("env", "stage");
        })
           .AddHealthChecks()
           .AddJsonSerialization()
           .AddReporting(
              factory \=> {
                  factory.AddInfluxDb( new InfluxDBReporterSettings
                    {
                        InfluxDbSettings \= new InfluxDBSettings(database, uri),
                        ReportInterval \= TimeSpan.FromSeconds(5)
                    });
              })
           .AddMetricsMiddleware(options \=> options.IgnoredHttpStatusCodes = new\[\] { 404 }); // Add framework services.

services.AddMvc();
}

复制代码

加入我们influxDB相关配置,其中database就是你自己输入的数据库名. uri就是你数据库的地址.

然后我们需要在程序启动的时候就注入我们的Metrics监控,所以修改Configure代码如下:

复制代码

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime lifetime)
{
loggerFactory.AddConsole(Configuration.GetSection(“Logging”));
app.UseMetrics();
app.UseMetricsReporting(lifetime); //loggerFactory.AddDebug();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        } else {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();

        app.UseMvc(routes \=> {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }

复制代码

其中app.UseMetrics();  app.UseMetricsReporting(lifetime) ,是我们的注入代码..

其他的地方.不用动一丝一毫..怎么样 是不是几乎没有什么入侵性~.

 最后,我们跑起来.

随便在home中点击几下页面,或者调用一下..

然后进入到Grafana的监控页面中,就可以看到如下信息:

写在最后

这篇只是简单的讲了如何做到初级的监控,其实还有心跳检测等比较多的功能,后面会慢慢来讲..

.Net Core也就今年社区才慢慢起步,所这个监控也是刚刚完成,作者一直在积极的更新中.

比较遗憾的是本来这个是支持 .NET4.5.2以上的,但是由于关注的人比较少.所以就只出了RC的预览版.对最新版支持不完善.

我也在GitHub上给作者提了Issues,https://github.com/alhardy/AppMetrics/issues/177

希望感兴趣的基友们一起加入,这里也吐槽一下,..我就一句话..社区需要我们共通创建,而不是等着摘现成的桃子..

作者:顾振印 出处:http://www.cnblogs.com/GuZhenYin/ 如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面

本系列会分为2-3篇文章.

本文的内容:

SignalR是一个.NET Core/.NET Framework的开源实时框架. SignalR的可使用Web Socket, Server Sent Events 和 Long Polling作为底层传输方式.

SignalR基于这三种技术构建, 抽象于它们之上, 它让你更好的关注业务问题而不是底层传输技术问题.

SignalR这个框架分服务器端和客户端, 服务器端支持ASP.NET Core 和 ASP.NET; 而客户端除了支持浏览器里的javascript以外, 也支持其它类型的客户端, 例如桌面应用.

回落机制

SignalR使用的三种底层传输技术分别是Web Socket, Server Sent Events 和 Long Polling.

其中Web Socket仅支持比较现代的浏览器, Web服务器也不能太老.

而Server Sent Events 情况可能好一点, 但是也存在同样的问题.

所以SignalR采用了回落机制, SignalR有能力去协商支持的传输类型.

Web Socket是最好的最有效的传输方式, 如果浏览器或Web服务器不支持它的话, 就会降级使用SSE, 实在不行就用Long Polling.

一旦建立连接, SignalR就会开始发送keep alive消息, 来检查连接是否还正常. 如果有问题, 就会抛出异常.

因为SignalR是抽象于三种传输方式的上层, 所以无论底层采用的哪种方式, SignalR的用法都是一样的.

SignalR默认采用这种回落机制来进行传输和连接.

但是也可以禁用回落机制, 只采用其中一种传输方式.

RPC

RPC (Remote Procedure Call). 它的优点就是可以像调用本地方法一样调用远程服务.

SignalR采用RPC范式来进行客户端与服务器端之间的通信.

SignalR利用底层传输来让服务器可以调用客户端的方法, 反之亦然, 这些方法可以带参数, 参数也可以是复杂对象, SignalR负责序列化和反序列化.

Hub

Hub是SignalR的一个组件, 它运行在ASP.NET Core应用里. 所以它是服务器端的一个类.

Hub使用RPC接受从客户端发来的消息, 也能把消息发送给客户端. 所以它就是一个通信用的Hub.

在ASP.NET Core里, 自己创建的Hub类需要继承于基类Hub.

在Hub类里面, 我们就可以调用所有客户端上的方法了. 同样客户端也可以调用Hub类里的方法.

这种Hub+RPC的方式还是非常适合实时场景的.

之前说过方法调用的时候可以传递复杂参数, SignalR可以将参数序列化和反序列化. 这些参数被序列化的格式叫做Hub 协议, 所以Hub协议就是一种用来序列化和反序列化的格式.

Hub协议的默认协议是JSON, 还支持另外一个协议是MessagePack. MessagePack是二进制格式的, 它比JSON更紧凑, 而且处理起来更简单快速, 因为它是二进制的.

此外, SignalR也可以扩展使用其它协议..

横向扩展

随着系统的运行, 有时您可能需要进行横向扩展. 就是应用运行在多个服务器上.

这时负载均衡器会保证每个进来的请求按照一定的逻辑分配到可能是不同的服务器上.

在使用Web Socket的时候, 没什么问题, 因为一旦Web Socket的连接建立, 就像在浏览器和那个服务器之间打开了隧道一样, 服务器是不会切换的.

但是如果使用Long Polling, 就可能有问题了, 因为使用Long Polling的情况下, 每次发送消息都是不同的请求, 而每次请求可能会到达不同的服务器. 不同的服务器可能不知道前一个服务器通信的内容, 这就会造成问题.

针对这个问题, 我们需要使用Sticky Sessions (粘性会话).

Sticky Sessions 貌似有很多中实现方式, 但是主要是下面要介绍的这种方式.

作为第一次请求的响应的一部分, 负载均衡器会在浏览器里面设置一个Cookie, 来表示使用过这个服务器. 在后续的请求里, 负载均衡器读取Cookie, 然后把请求分配给同一个服务器. 

建立项目

使用空模板建立ASP.NET Core项目.

建立一个CountService:

建立一个CountHub, 继承于Hub:

配置SignalR

在Startup里注册SignalR:

如果需要的话可以在**AddSignalR()**这个方法里使用lambda表达式进行一些配置.

然后在管道里使用SignalR, 使用app**.UseSignalR()**:

这里我已经建立了一个Hub, 叫做CountHub.

该方法的参数类型是Action, 然后在这里配置hub的路由.

使用Hub

首先建立一个Controller, 并注入IHubContext:

接下来我们就可以使用IHubContext这个对象与客户端进行实时通信了.

下面建立一个POST Action, 客户端点击按钮之后来到这个Action, 在这里我们使用hub为所有的客户端发送一个消息:

这里, 我调用了所有客户端上的someFunc这个方法, 参数是一个对象.

但是使用这种IHubContext的注入方式, 我们无法在它那取得Caller(调用该方法的客户端)这个属性.

Context

从Hub的Context属性, 我们可以获得用户的信息.

我们在CountHub里override父类的一个方法OnConnectedAsync():

如果有新的连接建立了, 这个方法就会被执行.

在Hub类里, 我们可以访问到Context属性. 从Context属性那, 我们可以获得一个常用的属性叫做ConnectionId. 这个ConnectionId就是连接到Hub的这个客户端的唯一标识.

使用ConnectionId, 我们就可以取得这个客户端, 并调用其方法, 如图中的Clients.Client(connectionId).xxx.

Hub的Clients属性表示客户端, 它有若干个方法可以选择客户端, 刚才的Client(connectionId)就是使用connectionId找到这一个客户端. 而AllExcept(connectionId)就是除了这个connectionId的客户端之外的所有客户端. 更多方法请查看文档.

SignalR还有Group分组的概念, 而且操作简单, 这里用到的是Hub的Groups属性. 向一个Group名添加第一个connectionId的时候, 分组就被建立. 移除分组内最后一个客户端的时候, 分组就被删除了. 使用Clients.Group(“组名”)可以调用组内客户端的方法.

授权和验证

SignalR会采用ASP.NET Core配置好的授权和验证体系.

用法和Controller差不多:

想要取得User对象, 需要使用Context.User, 它的类型是ClaimsPrinciple:

客户端

客户端需要安装signalr这个库. 可以使用npm安装 @aspnet/signalr

但是实际上只需要signalr.js一个文件即可.

客户端代码如下:

 

点击按钮后先执行Controller的POST方法, POST返回的是Accepted(1), 所以id是1.

使用singalR对象的HubConnectionBuilder来构建connection. 使用返回的connection对象, 我们可以用它的on方法来处理服务器端方法调用的响应. 响应方法的参数可以是简单类型也可以是复杂的对象.

使用connection.**start()来打开连接, 使用catch()**来捕获异常, 使用connection.stop() 关闭连接.

先运行一下看看效果:

可以看到使用Clients.All, 所有的客户端的方法都会被调用.

刚打开页面的时候, 我们就尝试建立连接, 从F12可以看到一个叫做negotiate的请求被发送了:

这个请求的body如下:

可以看到客户端选择了一个connectionId,  里面还有浏览器支持的传输方式.

服务器的响应:

响应也包含着connectionId, 以及服务器支持的传输方式. 这里三种都支持. 由于我没有指定传输方式, 所以SignalR选择了最好的方式: websocket.

而在我点击按钮后, Web Socket连接才被初始化:

如果需要手动指定传输方式, 请在**withUrl()**方法的第二个参数指定传输方式: 

其它类型的客户端

.NET 客户端可以安装 Microsoft.AspNetCore.SignalR.Client 这个包来支持SignalR.

具体用法请查看官方文档, 语法和js的差不多.

MessagePack协议

需要安装 Microsoft.AspNetCore.SignalR.Protocols.MessagePack.

然后在Startup里面使用**AddMessagePackProtocol()**这个方法即可:

这样的话, 服务器端既支持JSON, 也支持MessagePack了.

另外.NET客户端也需要安装这个MessagePack包.

而js客户端需要安装 @aspnet/signalr-protocol-msgpack.

横向扩展 Scale-out

可以采用Redis, 需要安装 Microsoft.AspNetCore.SignalR.Redis. 这个包.

然后在Startup里面配置:

这个没试过, 请看官方文档.

SignalR就介绍这些….

本系列会分为2-3篇文章.

本文的内容:

SignalR是一个.NET Core/.NET Framework的开源实时框架. SignalR的可使用Web Socket, Server Sent Events 和 Long Polling作为底层传输方式.

SignalR基于这三种技术构建, 抽象于它们之上, 它让你更好的关注业务问题而不是底层传输技术问题.

SignalR这个框架分服务器端和客户端, 服务器端支持ASP.NET Core 和 ASP.NET; 而客户端除了支持浏览器里的javascript以外, 也支持其它类型的客户端, 例如桌面应用.

回落机制

SignalR使用的三种底层传输技术分别是Web Socket, Server Sent Events 和 Long Polling.

其中Web Socket仅支持比较现代的浏览器, Web服务器也不能太老.

而Server Sent Events 情况可能好一点, 但是也存在同样的问题.

所以SignalR采用了回落机制, SignalR有能力去协商支持的传输类型.

Web Socket是最好的最有效的传输方式, 如果浏览器或Web服务器不支持它的话, 就会降级使用SSE, 实在不行就用Long Polling.

一旦建立连接, SignalR就会开始发送keep alive消息, 来检查连接是否还正常. 如果有问题, 就会抛出异常.

因为SignalR是抽象于三种传输方式的上层, 所以无论底层采用的哪种方式, SignalR的用法都是一样的.

SignalR默认采用这种回落机制来进行传输和连接.

但是也可以禁用回落机制, 只采用其中一种传输方式.

RPC

RPC (Remote Procedure Call). 它的优点就是可以像调用本地方法一样调用远程服务.

SignalR采用RPC范式来进行客户端与服务器端之间的通信.

SignalR利用底层传输来让服务器可以调用客户端的方法, 反之亦然, 这些方法可以带参数, 参数也可以是复杂对象, SignalR负责序列化和反序列化.

Hub

Hub是SignalR的一个组件, 它运行在ASP.NET Core应用里. 所以它是服务器端的一个类.

Hub使用RPC接受从客户端发来的消息, 也能把消息发送给客户端. 所以它就是一个通信用的Hub.

在ASP.NET Core里, 自己创建的Hub类需要继承于基类Hub.

在Hub类里面, 我们就可以调用所有客户端上的方法了. 同样客户端也可以调用Hub类里的方法.

这种Hub+RPC的方式还是非常适合实时场景的.

之前说过方法调用的时候可以传递复杂参数, SignalR可以将参数序列化和反序列化. 这些参数被序列化的格式叫做Hub 协议, 所以Hub协议就是一种用来序列化和反序列化的格式.

Hub协议的默认协议是JSON, 还支持另外一个协议是MessagePack. MessagePack是二进制格式的, 它比JSON更紧凑, 而且处理起来更简单快速, 因为它是二进制的.

此外, SignalR也可以扩展使用其它协议..

横向扩展

随着系统的运行, 有时您可能需要进行横向扩展. 就是应用运行在多个服务器上.

这时负载均衡器会保证每个进来的请求按照一定的逻辑分配到可能是不同的服务器上.

在使用Web Socket的时候, 没什么问题, 因为一旦Web Socket的连接建立, 就像在浏览器和那个服务器之间打开了隧道一样, 服务器是不会切换的.

但是如果使用Long Polling, 就可能有问题了, 因为使用Long Polling的情况下, 每次发送消息都是不同的请求, 而每次请求可能会到达不同的服务器. 不同的服务器可能不知道前一个服务器通信的内容, 这就会造成问题.

针对这个问题, 我们需要使用Sticky Sessions (粘性会话).

Sticky Sessions 貌似有很多中实现方式, 但是主要是下面要介绍的这种方式.

作为第一次请求的响应的一部分, 负载均衡器会在浏览器里面设置一个Cookie, 来表示使用过这个服务器. 在后续的请求里, 负载均衡器读取Cookie, 然后把请求分配给同一个服务器. 

建立项目

使用空模板建立ASP.NET Core项目.

建立一个CountService:

建立一个CountHub, 继承于Hub:

配置SignalR

在Startup里注册SignalR:

如果需要的话可以在**AddSignalR()**这个方法里使用lambda表达式进行一些配置.

然后在管道里使用SignalR, 使用app**.UseSignalR()**:

这里我已经建立了一个Hub, 叫做CountHub.

该方法的参数类型是Action, 然后在这里配置hub的路由.

使用Hub

首先建立一个Controller, 并注入IHubContext:

接下来我们就可以使用IHubContext这个对象与客户端进行实时通信了.

下面建立一个POST Action, 客户端点击按钮之后来到这个Action, 在这里我们使用hub为所有的客户端发送一个消息:

这里, 我调用了所有客户端上的someFunc这个方法, 参数是一个对象.

但是使用这种IHubContext的注入方式, 我们无法在它那取得Caller(调用该方法的客户端)这个属性.

Context

从Hub的Context属性, 我们可以获得用户的信息.

我们在CountHub里override父类的一个方法OnConnectedAsync():

如果有新的连接建立了, 这个方法就会被执行.

在Hub类里, 我们可以访问到Context属性. 从Context属性那, 我们可以获得一个常用的属性叫做ConnectionId. 这个ConnectionId就是连接到Hub的这个客户端的唯一标识.

使用ConnectionId, 我们就可以取得这个客户端, 并调用其方法, 如图中的Clients.Client(connectionId).xxx.

Hub的Clients属性表示客户端, 它有若干个方法可以选择客户端, 刚才的Client(connectionId)就是使用connectionId找到这一个客户端. 而AllExcept(connectionId)就是除了这个connectionId的客户端之外的所有客户端. 更多方法请查看文档.

SignalR还有Group分组的概念, 而且操作简单, 这里用到的是Hub的Groups属性. 向一个Group名添加第一个connectionId的时候, 分组就被建立. 移除分组内最后一个客户端的时候, 分组就被删除了. 使用Clients.Group(“组名”)可以调用组内客户端的方法.

授权和验证

SignalR会采用ASP.NET Core配置好的授权和验证体系.

用法和Controller差不多:

想要取得User对象, 需要使用Context.User, 它的类型是ClaimsPrinciple:

客户端

客户端需要安装signalr这个库. 可以使用npm安装 @aspnet/signalr

但是实际上只需要signalr.js一个文件即可.

客户端代码如下:

 

点击按钮后先执行Controller的POST方法, POST返回的是Accepted(1), 所以id是1.

使用singalR对象的HubConnectionBuilder来构建connection. 使用返回的connection对象, 我们可以用它的on方法来处理服务器端方法调用的响应. 响应方法的参数可以是简单类型也可以是复杂的对象.

使用connection.**start()来打开连接, 使用catch()**来捕获异常, 使用connection.stop() 关闭连接.

先运行一下看看效果:

可以看到使用Clients.All, 所有的客户端的方法都会被调用.

刚打开页面的时候, 我们就尝试建立连接, 从F12可以看到一个叫做negotiate的请求被发送了:

这个请求的body如下:

可以看到客户端选择了一个connectionId,  里面还有浏览器支持的传输方式.

服务器的响应:

响应也包含着connectionId, 以及服务器支持的传输方式. 这里三种都支持. 由于我没有指定传输方式, 所以SignalR选择了最好的方式: websocket.

而在我点击按钮后, Web Socket连接才被初始化:

如果需要手动指定传输方式, 请在**withUrl()**方法的第二个参数指定传输方式: 

其它类型的客户端

.NET 客户端可以安装 Microsoft.AspNetCore.SignalR.Client 这个包来支持SignalR.

具体用法请查看官方文档, 语法和js的差不多.

MessagePack协议

需要安装 Microsoft.AspNetCore.SignalR.Protocols.MessagePack.

然后在Startup里面使用**AddMessagePackProtocol()**这个方法即可:

这样的话, 服务器端既支持JSON, 也支持MessagePack了.

另外.NET客户端也需要安装这个MessagePack包.

而js客户端需要安装 @aspnet/signalr-protocol-msgpack.

横向扩展 Scale-out

可以采用Redis, 需要安装 Microsoft.AspNetCore.SignalR.Redis. 这个包.

然后在Startup里面配置:

这个没试过, 请看官方文档.

SignalR就介绍这些….