Fork me on GitHub

使用Embedded RedisServer写UT

当我们在进行开发的时候经常会用到Redis,但是在写junit的时候往往引用了Redis造成test case很难写,我们需要mock一个localhostRedis server来进行测试,因此我们可以借助embedded redisServer来实现,下面我们就看一下具体使用的示例

代码示例

@Before
public void setUp() throws IOException {
    initMocks(this);
    final Random random = new SecureRandom();
    redisServer = new RedisServer();
    redisServer.start();
 
    pool = new JedisPool();
    repository = new RedisKeyRepository(pool);
    manager = new RedisKeyManager(random, pool, repository);
    manager.setMaxActiveKeys(3);
 
    clearData();
    manager.initialiseNewRepository();
 
    resource = new ProtectedResource(repository, random);
}

这是一个非常简单的使用示例,我们还可以更改配置以及增加密码

@Before
public void setUpRedis() throws IOException, SchedulerConfigException {
    port = getPort();
    logger.debug("Attempting to start embedded Redis server on port " + port);
    redisServer = RedisServer.builder()
            .port(port)
            .build();
    redisServer.start();
    final short database = 1;
    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    jedisPoolConfig.setTestOnBorrow(true);
    jedisPool = new JedisPool(jedisPoolConfig, host, port, Protocol.DEFAULT_TIMEOUT, null, database);
 
    jobStore = new RedisJobStore();
    jobStore.setHost(host);
    jobStore.setLockTimeout(2000);
    jobStore.setPort(port);
    jobStore.setInstanceId("testJobStore1");
    jobStore.setDatabase(database);
    mockScheduleSignaler = mock(SchedulerSignaler.class);
    jobStore.initialize(null, mockScheduleSignaler);
    schema = new RedisJobStoreSchema();
 
    jedis = jedisPool.getResource();
    jedis.flushDB();
}

使用RedisServerBuilder构建Redis server,并且指定port

@Test
//Note the try/finally is to ensure that the server is shutdown so other tests do not have to
//provide auth information
public void testAuth() throws Exception {
 
  RedisServer server = RedisServer.builder().port(6381).setting("requirepass foobar").build();
  server.start();
  RedisOptions job = new RedisOptions()
    .setHost("localhost")
    .setPort(6381);
  RedisClient rdx = RedisClient.create(vertx, job);
 
  rdx.auth("barfoo", reply -> {
    assertFalse(reply.succeeded());
    rdx.auth("foobar", reply2 -> {
      assertTrue(reply2.succeeded());
      try {
        server.stop();
      } catch (Exception ignore) {
      }
      testComplete();
    });
  });
  await();
}

设置一个需要密码访问的Redis server,setting可以设置redis conf中的所有属性

更多用法

还有很多用法,具体查看下面的代码示例

@Test
public void testDebugSegfault() throws Exception {
 
  RedisServer server = RedisServer.builder().port(6381).build();
  server.start();
  RedisOptions job = new RedisOptions()
    .setHost("localhost")
    .setPort(6381);
  RedisClient rdx = RedisClient.create(vertx, job);
 
  rdx.debugSegfault(reply -> {
    // this should fail, since we crashed the server on purpose
    assertTrue(reply.failed());
    rdx.info(reply2 -> {
      assertFalse(reply2.succeeded());
      server.stop();
      testComplete();
    });
  });
  await();
}

public RedisServerResource(int port, String password) {
    this.port = port;
    try {
        RedisExecProvider redisExecProvider = RedisExecProvider.defaultProvider();
        this.redisServer = RedisServer
                .builder()
                .redisExecProvider(redisExecProvider)
                .port(port)
                .setting("requirepass " + password)
                .build();
    } catch (Throwable error) {
        String message = String.format("failed creating Redis server (port=%d)", port);
        throw new RuntimeException(message, error);
    }
}

@Before
public void before() throws Exception {
  mockTracer.reset();
 
  redisServer = RedisServer.builder().setting("bind 127.0.0.1").build();
  redisServer.start();
} 

private void startServer(TestContext testContext) {
    EmbeddedRedis embeddedRedis = AnnotationUtils.findAnnotation(testContext.getTestClass(), EmbeddedRedis.class);
    int port = embeddedRedis.port();
 
    try {
        server = new RedisServer(port);
        server.start();
    } catch (IOException e) {
        if (logger.isErrorEnabled()) {
            logger.error(e.getMessage(), e);
        }
    }
}

private RedisServer createRedisServer() {
    final RedisServerBuilder redisServerBuilder = RedisServer.builder()
            .port(redisPort)
            .setting("appendonly yes")
            .setting("appendfsync everysec");
    settings.stream().forEach(s -> redisServerBuilder.setting(s));
 
    final RedisServer redisServer = redisServerBuilder.build();
    return redisServer;
}

有可能我们启动会遇到下面的错误:

java.lang.RuntimeException: Can't start redis server. Check logs for details.
    at redis.embedded.AbstractRedisInstance.awaitRedisServerReady(AbstractRedisInstance.java:66)
    at redis.embedded.AbstractRedisInstance.start(AbstractRedisInstance.java:37)
    at redis.embedded.RedisServer.start(RedisServer.java:11)
    at com.bignibou.configuration.session.EmbeddedRedisConfiguration$RedisServerBean.afterPropertiesSet(EmbeddedRedisConfiguration.java:26)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1633)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)
    ... 15 more

这个是什么原因呢?我们进一步debug输出redis server的log看是什么问题,redis log如下:

The windows version of redis allocates a large memory mapped file for sharing the heap with the forked process used in persistence operations. This file will be created in the current working directory or the directory specified by the 'heapdir' directive in the
.conf file. Windows is reporting that there is insufficient disk space available for this file (Windows error 0x70).
You may fix this probilem by either reducing the size of the Redis heap with the --maxheap flag, or by moving the heap file to a local drive with sufficient space.
Please see the documentation included with the binary distributions for more details on the --maxheap and --heapdir flags.
Redis can not continue, Exiting.

这里的原因是我们启动的时候heap不够,redis server默认的maxheap:1024000000,创建.conf文件时硬盘不够,那如何解决这个错误呢?

@Test
public void testAuth() throws Exception {
  RedisServer server = RedisServer.builder().port(6381).setting("maxheap 51200000").build();
  server.start();
}

关于redis maxheap的详细描述如下:

# The Redis heap must be larger than the value specified by the maxmemory
# flag, as the heap allocator has its own memory requirements and
# fragmentation of the heap is inevitable. If only the maxmemory flag is
# specified, maxheap will be set at 1.5*maxmemory. If the maxheap flag is
# specified along with maxmemory, the maxheap flag will be automatically
# increased if it is smaller than 1.5*maxmemory.
# 
# maxheap <bytes>
maxheap 51200000

注意:修改时需要考虑可用量,常规情况都无需修改这个参数

更多查看官方文档

通过对Maven的依赖分析剔除无用的jar引用

Published on:

当项目开发维护了一段时间时,经常会有项目打包速度慢,jar依赖多,依赖关系错综复杂,这种问题是项目维护最常见的问题,由于开发人员在bugfix或者feature开发时往往只是往项目中添加jar依赖,那我们如何分析出项目中哪些依赖是用到的,哪些依赖是不用的?

使用Maven analyze来进行分析

使用如下命令:

mvn dependency:analyze

会输出如下的日志:

[INFO] --- maven-dependency-plugin:2.8:analyze (default-cli) @ xxxproject ---
[WARNING] Used undeclared dependencies found:
[WARNING]    org.springframework:spring-beans:jar:4.0.0.RELEASE:compile
[WARNING]    org.springframework:spring-context:jar:4.0.0.RELEASE:compile
[WARNING] Unused declared dependencies found:
[WARNING]    com.alibaba:dubbo:jar:2.5.3:compile
[WARNING]    com.baidu.disconf:disconf-client:jar:2.6.32:compile
[WARNING]    org.mybatis:mybatis:jar:3.2.7:compile
[WARNING]    org.mybatis:mybatis-spring:jar:1.2.2:compile
[WARNING]    mysql:mysql-connector-java:jar:5.1.41:compile
[WARNING]    com.alibaba:druid:jar:1.0.9:compile
[WARNING]    com.github.sgroschupf:zkclient:jar:0.1:compile
[WARNING]    org.apache.zookeeper:zookeeper:jar:3.4.6:compile
[WARNING]    org.springframework:spring-jdbc:jar:4.0.0.RELEASE:compile
[WARNING]    org.slf4j:log4j-over-slf4j:jar:1.7.5:compile
[WARNING]    org.slf4j:jcl-over-slf4j:jar:1.7.5:runtime
[WARNING]    ch.qos.logback:logback-classic:jar:1.0.13:compile                         

我们就来说一下日志中的Used undeclared dependencies foundUnused declared dependencies found

Used undeclared dependencies found

这个是指某些依赖的包在代码中有用到它的代码,但是它并不是直接的依赖(就是说没有在pom中直接声明),是通过引入传递下来的包。

举个例子:

projectpom中声明了A.jar的依赖(没有声明B.jar的依赖) A.jar的依赖关系:A.jar -> B.jar 通过mvn dependency:analyze出现 [WARNING] Used undeclared dependencies found: B.jar 就说明project中的代码用到了B.jar的代码 这个时候你就可以把B.jar直接声明在pom中

Unused declared dependencies found

这个是指我们在pom中声明了依赖,但是在实际代码中并没有用到这个包!也就是多余的包。 这个时候我们就可以把这个依赖从pom中剔除。

但是这里我们需要注意: 这里说的实际代码没有用到,指的是在main/java和test里没有用的,但是并不是意味着真的没有用到这些包,有可能配置文件中引用或者其他扩展点自动加载这些包,所以我们在删除依赖的时候一定要小心,做好备份,因为这类引用maven是分析不出来的。

跨域踩坑经验总结(内涵:跨域知识科普)

Published on:

跨域问题是我们非常常见的问题,尤其在跨系统页面间的调用经常会遇到,解决的方式在网上一搜一大把,这里整理出我遇到跨域问题解决的方式以及思路,如何安全的解决跨域调用请继续往下看。

什么是跨域?

什么是Cross-origin_resource_sharing? 跨域请求存在的原因:由于浏览器的同源策略,即属于不同域的页面之间不能相互访问各自的页面内容。

跨域使用的场景?

  1. 域名不同
    • www.jiuyescm.comwww.jiuye.com即为不同的域名
  2. 二级域名相同,子域名不同
    • a.jiuyescm.comb.jiuyescm.com为子域不同
  3. 端口不同,协议不同
    • http://www.jiuyescm.comhttps://www.jiuyescm.com
    • www.jiuyescm.com:8888www.jiuyescm.com:8080

解决跨域的方式?

  1. jsonp
    • 安全性差,已经不推荐
  2. CORS(W3C标准,跨域资源共享 - Cross-origin resource sharing)
    • 服务端设置,安全性高,推荐使用
  3. websocke
    • 特殊场景时使用,不属于常规跨域操作
  4. 代理服务(nginx)
    • 可作为服务端cors配置的一种方式,推荐使用

前端、后端如何配合处理跨域?

ps. 我们这里只介绍:CORS处理方式。

跨域常见错误

首先让我们看一下前端报出的跨域错误信息

第一种:No 'Access-Control-Allow-Origin' header is present on the requested resource,并且The response had HTTP status code 404

XMLHttpRequest cannot load http://b.domain.com, Response to preflinght request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://a.domain.com' is therefore not allowed access. The Response had HTTP status code 404.

ps.并且The response had HTTP status code 404

问题原因:服务器端后台没有允许OPTIONS请求

第二种:No 'Access-Control-Allow-Origin' header is present on the requested resource,并且The response had HTTP status code 405

XMLHttpRequest cannot load http://b.domain.com, Response to preflinght request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://a.domain.com' is therefore not allowed access. The Response had HTTP status code 405.

ps.并且The response had HTTP status code 405

问题原因:服务器端后台允许了OPTIONS请求,但是某些安全配置阻止了OPTIONS请求

第三种:No 'Access-Control-Allow-Origin' header is present on the requested resource,并且The response had HTTP status code 200

XMLHttpRequest cannot load http://b.domain.com, Response to preflinght request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://a.domain.com' is therefore not allowed access.

ps.并且The response had HTTP status code 200

问题原因:服务器端后台允许了OPTIONS请求,并且OPTIONS请求没有被阻止,但是头部不匹配。

第四种:heade contains multiple values '*,*',并且The response had HTTP status code 200

XMLHttpRequestcannot load http://b.domain.com. The 'Access-Control-Allow-Origin' header contains multiple values'*, *', but only one is allowed. Origin 'http://a.domain.com' is therefore notallowed access.

ps.并且The response had HTTP status code 200

问题原因:设置多次Access-Control-Allow-Origin=*,可能是配置的人对CORS实现原理和机制不了解导致。

突如其来的OPTIONS请求?

有时你会发现明明请求的是POST、GET、PUT、DELETE,但是浏览器中看到的确实OPTION,,为什么会变成OPTION?

原因:因为本次Ajax请求是“非简单请求”,所以请求前会发送一次预检请求(OPTIONS),这个操作由浏览器自己进行。如果服务器端后台接口没有允许OPTIONS请求,将会导致无法找到对应接口地址,因此需要服务端提供相应的信息到response header中,继续往下看。

后端需要返回的Header有哪些?

# 服务端允许访问的域名
Access-Control-Allow-Origin=https://idss-uat.jiuyescm.com
# 服务端允许访问Http Method
Access-Control-Allow-Methods=GET, POST, PUT, DELETE, PATCH, OPTIONS
# 服务端接受跨域带过来的Cookie,当为true时,origin必须是明确的域名不能使用*
Access-Control-Allow-Credentials=true
# Access-Control-Allow-Headers 表明它允许跨域请求包含content-type头,我们这里不设置,有需要的可以设置
#Access-Control-Allow-Headers=Content-Type,Accept
# 跨域请求中预检请求(Http Method为Option)的有效期,20天,单位秒
Access-Control-Max-Age=1728000

ps. 如果跨域需要携带cookie去请求,Access-Control-Allow-Credentials必须为true,但是需要注意当Access-Control-Allow-Credentials=true时,Access-Control-Allow-Origin就不能为” * “ ,必须是明确的域名,当然可以多个域名使用 “,” 分割

前端如何配合发起请求?

如果是浏览器直接访问跨域请求url,只要服务端返回 “Access-Control-Allow-X” 系列header在response中即可成功访问。

如果是ajax发起的请求该如何处理?

第一种:请求不需要携带cookie

$.ajax({
    url : 'url',
    data : data,
    dataType: 'json',
    type : 'POST',
    crossDomain: true,
    contentType: "application/json",
    success: function (data) {
        var a=JSON.stringify(data);
        if(data.result==true){
          ...........
       }else{
       ...........
     }
    },
    error:function (data) {
        var a=JSON.stringify(data);
        alert(a);
    }
});

ps. 增加crossDomain=true

第二种:请求需要携带cookie

$.ajax({
	url : 'url',
	data : data,
	dataType: 'json',
	type : 'POST',
	xhrFields: {
	    withCredentials: true
	},
	crossDomain: true,
	contentType: "application/json",
	success: function (data) {
        var a=JSON.stringify(data);
        if(data.result==true){
          ...........
       }else{
       ...........
     }
    },
    error:function (data) {
        var a=JSON.stringify(data);
        alert(a);
    }
});

ps. 增加crossDomain与xhr.withCredentials,发送Ajax时,Request header中便会带上 Cookie 信息。

到这里你以为跨域的相关都介绍完毕了?太天真

最后还有一个终极boss问题,是什么问题呢?

上面的第二种携带cookie的跨域请求调用方式在IOS下可以正常工作,但是在Android下无法正常工作并且还报错,额。。。。。

Ajax跨域请求跨平台兼容性问题

问题原因:因为Android下的webview不兼容这个写法,使用标准的 beforeSend(XHR) 替换

xhrFields: {
	withCredentials: true
}

ps. webview不兼容的写法,firefox下也不兼容

标准的写法:

$.ajax({
    type: "POST",
    url: "url",
    data:datatosend,
    dataType:"json",
    beforeSend: function(xhr) {
        xhr.withCredentials = true;
    }
    crossDomain:true,
    success: function (data) {
        var a=JSON.stringify(data);
        if(data.result==true){
          ...........
       }else{
       ...........
     }
    },
    error:function (data) {
        var a=JSON.stringify(data);
        alert(a);
    }
});

到这跨域的相关使用就介绍完毕,这次是真的结束了。Keep Real!

Docker启动的容器如何清理日志?看这里

Published on:
Tags: docker

Docker run起来的容器随着时间久了,容器内的服务输出的日志也在日积月累,需要定期的进行日志清理。

如果公司使用DevOps的话更加需要对容器内的日志进行定期清理,业务的镜像服务或许还好一些,因为开发同学每天都在用、每天都会upgrade服务,在upgrade的时候会删除老的容器,再重新run一个新容器去替换掉老的,但是有一些长期run的服务就很少有人关注了,比如说rancher、还有一些基础服务,可能很长时间也不会去做upgrade操作,因此容器内的日志就越来越多,如果不清理总有一天会撑爆服务器硬盘,到那个时候再去清理恢复服务的话,有可能会有磁盘文件损坏的风险。

因此我们需要定期的对Docker容器内的日志进行清理。

如何查看Docker内容器的日志?可以参考文章:《如何直接操作Docker容器?》

在清理容器日志前,我们首先要知道Docker将容器的日志放在那里?

Docker将容器的日志放在/var/lib/docker/containers/containerid/containerid-json.log

ps. containerid是容器id一般是82bbc....这个风格,64位字符

当然找不到的话也可以使用文件搜索的方式去查找Docker的容器日志放在那里,查找的时候按照上面的名称风格去查找,例如:

find / -type f -name "*-json.log"

容器的id怎么查看呢?

docker ps

通过ps找到容器id,也找到日志所在的位置后,接下来就是清理日志的操作了,日志文件不能直接删除,直接删除会影响正在运行的容器,可以通过清空文件内容的方式来处理。

清空文件的方式有很多种如下:

$ : > filename 
$ > filename 
$ echo "" > filename 
$ echo > filename 
$ cat /dev/null > filename

选一种即可

cat /dev/null >/var/lib/docker/containers/containerid/containerid-json.log

Zookeeper事务日志和snapshot清理方式

Published on:
Tags: zookeeper

Zookeeper运行过程会产生大量的事务日志和snapshot镜像文件,文件的目录是通过zoo.confdatadir参数指定的,下面我们就说一下如何清理事务日志和snapshot。

清理的方式有如下三种:

下面我们一一介绍每种清理方式是如何使用的。

zookeeper配置自动清理

zookeeper在3.4.0版本以后提供了自动清理snapshot和事务日志的功能通过配置 autopurge.snapRetainCount 和 autopurge.purgeInterval 这两个参数能够实现定时清理了。这两个参数都是在zoo.cfg中配置的:

我们使用的zk版本是:3.4.6,因此可以使用自带的清理功能

autopurge.purgeInterval 这个参数指定了清理频率,单位是小时,需要填写一个1或更大的整数,默认是0,表示不开启自己清理功能。

autopurge.snapRetainCount 这个参数和上面的参数搭配使用,这个参数指定了需要保留的文件数目。默认是保留3个。

示例:

autopurge.snapRetainCount=60 
autopurge.purgeInterval=48

保留48小时内的日志,并且保留60个文件

ps.但是修改conf需要重启服务,生产可能不会考虑重启服务因此使用其他方法。

使用自定义清理脚本

clean_zook_log.sh脚本内容如下

#!/bin/bash
            
#snapshot file dir
dataDir=/var/zookeeper/version-2
#tran log dir
dataLogDir=/var/zookeeper/version-2
logDir=/usr/local/zookeeper/logs
#Leave 60 files
count=60
count=$[$count+1]
ls -t $dataLogDir/log.* | tail -n +$count | xargs rm -f
ls -t $dataDir/snapshot.* | tail -n +$count | xargs rm -f
ls -t $logDir/zookeeper.log.* | tail -n +$count | xargs rm -f

这个脚本保留最新的60个文件,可以将他写到 将这个脚本添加到crontab中,设置为每天凌晨2点?或者其他时间执行即可。

crontab -e 2 2 * * * /bin/bash /usr/local/zookeeper/bin/clean_zook_log.sh > /dev/null 2>&1

ps.不用修改配置,不需要重启zk集群,推荐使用

使用zkCleanup.sh清理

这个脚本是使用的zookeeper.jar里的org.apache.zookeeper.server.PurgeTxnLog这个class的main函数清理的,因此需要启动一个java进程,比shell清理要重一些。

org.apache.zookeeper.server.PurgeTxnLog文档

sh /usr/local/zookeeper/bin/zkCleanup.sh 数据目录 -n 20

参数说明

数据目录: /var/zookeeper 20: 保留快照日志的数量

ps.因为zookeeper从3.4.0版本之后提供了对历史事务日志和快照文件的自动清理,所以这个脚本很少使用,另外在生产环境中我们一般采取自动脚本来定点定量清除指定日期的日志文件

到这里三种清理方式都介绍完毕了,根据自己的实际情况选择一种使用就可以了。祝大家周末愉快以及端午节快乐,ok 收工 回家。

如何扩展个人微信号来实现群组管理的功能?

Published on:
Tags: wechat

最近在思考一个问题,如何能在系统中集成微信群组管理的功能,比如说邀请好友进群、对群组进行管理、创建群组、删除群组之类的操作,说白了就是将微信的功能嵌入到自己的程序里面去。这样就可以有效的管理多个群组来扩展一些客服的功能。

于是查找关于微信API的资料,第一反应就是先去官方的开放文档中查看是否有类似API开放。

翻了一圈资料,看了微信服务号、订阅号、小程序、以及企业微信的开放文档后整理出来目前官方开放的API的功能现状如下:

官方是没有提供任何个人群管理接口API,只有一些类似外挂类的工具可以对个人群组进行管理。

但是外挂类的工具又怕有风险,说不定哪天就over了。

在资料翻阅的过程,发现了可以通过微友助手在群组里添加机器人,但是这个方式可能不是我们想要的。

官方开放的客服API可以与客服系统进行对接,它是将微信端作为客服的入口与客服系统对接,生成客诉工单或者是跟客服对话,这也不是我们想要的方式。

在没有官方API可用的情况下我们想使用这方面的功能该如何操作呢?

发现曙光

微信目前官方提供的终端,除了手机端以外还有电脑端和WEB网页端。

咦,有WEB网页端那不就有API可以操作么?只是可能我们要写类似于外挂一样的东西模拟官方的微信网页端操作。

于是开始搜索这方面的资料,很幸运找到了ItChat这个类库。

这个类库的实现方式就是我们刚才说到的模拟网页版本的rest请求去扩展的一些接口。

那已经有人做好了轮子那我们就可以直接使用了。

ItChat的实现方式

第一部分,分析微信协议,开始获取并模拟扩展个人微信号所需要的协议。 第二部分,利用上面分析出来的协议 第三部分,包装成扩展功能API

网页端微信协议分析思路可以查看:手把手教你扩展个人微信号(1)

接口的使用可以查看:手把手教你扩展个人微信号(2)

有兴趣的可以进去看一下。

Github链接:ItChat

总结

这个库的实现方式还是很有趣的,使用网页版微信调用的rest接口,跟常规外挂一样模拟网页微信的操作,只要网页版本微信不关闭应该都能用,只是可能需要紧跟着网页版本的微信rest接口持续升级

github上有1w多star,很明显说明了这个扩展功能还是很多人迫切想使用的,后面我会尝试一下然后把遇到的问题和使用经验会再分享出来。Keep Real!

如何使用抓包调试工具 —— Charles

Published on:

以下信息转自公司内网资料,觉得很实用就转载出来提供参考。

一、Charles是什么?

Charles是在 Mac或Windows下常用的http协议网络包截取工具,在平常的测试与调式过程中,掌握此工具就基本可以不用其他抓包工具了。

二、为什么是Charles?

为什么要用抓包工具?大家在平常移动App调试测试中是如何进行抓包的?

主要特点如下:

  1. 支持SSL代理,可以截取分析SSL的请求
  2. 支持流量控制。可以模拟慢速网络(2G,3G),以及等待时间较长的请求。
  3. 支持AJAX调试。可以自动把JSON或者XML数据格式化,方便查看。
  4. 支持重发网络请求,方便后端调试。
  5. 支持修改网络请求参数。
  6. 支持网络请求的截取和动态修改。
  7. 最重要的一个优点就是有不同平台的版本(Mac,Windows、Linux)即学一个打遍天下。

三、Charles基本工作原理

charles是通过网络代理来进行抓包的,下面先了解一下http代理的原理:

  • 普通http请求过程

一般情况下的HTTP请求与响应

  • 加入了Charles的HTTP代理的请求与响应过程

中间的代理服务器就是Charles

四、Charles的下载与安装过程

是一个安装包是一个dmg后缀的文件。打开后将Charles拖到Application目录下即完成安装。

在Mac下你打开Launchpad即可看到一个像花瓶一样的Charles程序图标

  • Windows下安装

下载后直接双击根据安装向导一步一步安装即可

五、Http抓包操作步骤

Step 1: 开启Charleshttp代理

  • 设置Charles代理

第一次启动默认会开启本机的系统代理,因为我们只是监控移动端的所以将此选去除(去掉选项前面的小钩)

  • 激活http代理功能

Step 2: 手机端Wifi添加代理

Android端

  • 在手机端打开你的Wifi设置然后长按已经连接的Wifi在弹出来的菜单中选择【修改网络】
  • 沟上[显示高级]选项–【手动】
  • 输入代理服务器的IP与端口,IP即安装了Charles的电脑IP地址,端口就是前面一步设置Charles时所设置的端口。

注意:手机所连接Wifi要与电脑在同一个LAN(局域网)

iOS端

  • 点击你所连接的wifi
  • 输入代理服务器的IP与端口,

IP即安装了Charles的电脑IP地址,端口就是前面一步设置Charles时所设置的端口。

注意:手机所连接Wifi要与电脑在同一个LAN(局域网)

Step 3:开启Charles录制功能

  1. 当手机连接上代理后Charles会弹出相应的提示框,点击Allow即可
  2. 点击工具栏上的开始录制按钮,即启动了Charles的抓包功能了。

Step 4:启动应用开始抓包

  1. 在手机上操作相应的App进行抓包。
  2. 在Charles的主界面上就可看到相应的请求内容。

Step 5:分析抓取的数据包

  1. Charles 主要提供两种查看封包的视图,分别名为 “Structure”和 “Sequence”:
    • Structure 视图将网络请求按访问的域名分类;
    • Sequence 视图将网络请求按访问的时间排序。
  2. 大家可以根据具体的需要在这两种视图之前来回切换。请求多了有些时候会看不过来,Charles提供了一个简单的Filter功能,可以输入关键字来快速筛选出URL 中带指定关键字的网络请求。
  3. 对于某一个具体的网络请求,你可以查看其详细的请求内容和响应内容。如果请求内容是POST 的表单,Charles 会自动帮你将表单进行分项显示。如果响应内容是 JSON 格式的,那么 Charles可以自动帮你将JSON 内容格式化,方便你查看。如果响应内容是图片,那么 Charles可以显示出图片的预览。

六、Https抓包操作步骤

Step 1:了解一下https的基本原理;

HTTPS其实是有两部分组成:HTTP+ SSL / TLS,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。具体是如何进行加密,解密,验证的,且看图,下面这个图的解说 详细说明,请参考:http://blog.csdn.net/clh604/article/details/22179907

Step 2:在手机端安装SSL证书

  • 将证书文件从Charles导出
  • 然后通过adb或者其他工具将其复制到手机的SD卡中。

从Charles导出证书文件

  • 将证书文件导入Android手机

在手机的设置界面找到【安全】—》【从内部存储设备或SD卡安装】—-》选择SD卡上的证书—》弹出设置证书名对话框,输入一个易记的名字,然后根据提示进行导入即可

  • 将证书文件导入iOS手机

  1. 在iPhone手机上打开Safari浏览器,然后在地址栏中输入www.charlesproxy.com/getssl。
  2. 稍后会弹出安装描述文件提示,点击右上角的【安装】按钮进行证书安装即可。
  3. 在iOS 10.3之后,需要手动打开开关以信任证书,设置->通用->关于本机->证书信任设置-> 找到charles proxy custom root certificate然后信任该证书即可.

Step 3:激活Charles的SSL代理

  1. 选择【Proxy】—>【SSL Proxying Settings..】设置。
  2. 在弹出来的对话框中沟选【Enable SSL Proxying】。

Step 4:将指定的URL请求开启SSL代理功能

  1. 选择抓取的https链接,然后右键选择【Enable SSL Proxying】。
  2. 如果不激活SSL代理,所以https请求都是乱码无法查看。

再次请求这个Https时,其请求内容已经一目了然了。

七、Charles进阶—修改请求也响应的内容

Step 1:设置Charless断点

选择【Breakpoint Settings…】—>勾选【Enable Breakpoints】来激活断点功能

Step 2:对指定的URL开启断点功能

  1. 选择一个URL链接-à右键开启菜单—》选择【Breakpoints】即可开启此请求的断点。
  2. 这样Charles会遇到此请求时会弹出中断对话框。

Step 3:编辑请求与响应的内容。

  • 编辑请求内容,在中断对话框中,用户可以点击Edit Request来编辑请求的内容,编辑完成后然后点击【Execute】发出去这个请求给服务端

  • 编辑服务器响应的内容,在【Edit Request】对话中点击【Execute】发出请求后,服务端返回来数据后,用户点击【Edit Response】可对响应内容进行编辑完成后然后点击【Execute】发出去这个数据给客户端。

八、Charles进阶—弱网模拟

  1. 菜单中选择【Proxy】—>【Throttle Settings..】-à激活【Enable Throttling】。
  2. 在Throttle Configuration设置弱网的参数。
  3. 以下是各种网制式的速率参考文档:

移动网络制式与网速的参考文档

弱网模拟设置

Trouble Shooting —— Docker Pull Image : error pulling image configuration: unexpected EOF错误

问题现象

执行docker pull命令报错:

docker@rancher-192:~$ docker pull 192.168.0.34:5000/imageName:latest
latest: Pulling from imageName
75a822cd7888: Pulling fs layer
046e44ee6057: Download complete
8c47541cb10b: Waiting
e17edf9a1bd4: Waiting
error pulling image configuration: unexpected EOF

查看日志错误如下:

docker@rancher-192:~$ journalctl -u docker.service
-- Logs begin at Mon 2018-05-14 04:14:07 CST, end at Tue 2018-05-29 11:31:02 CST. --
May 29 11:28:22 rancher-192.168.0.83 docker[993]: time="2018-05-29T11:28:22.601383366+08:00" level=error msg="Not continuing with pull after error: error pulling image configuration: unexpected EOF"
May 29 11:30:36 rancher-192.168.0.83 docker[993]: time="2018-05-29T11:30:36.987345560+08:00" level=error msg="Not continuing with pull after error: error pulling image configuration: unexpected EOF"

随便找一台其他机器上进行pull操作,一样报错,但是pull其他镜像确实正常的,查看其他机器上日志如下:

docker@devserver1:~/messer/public$ journalctl -u docker.service
-- Logs begin at Fri 2018-05-25 23:29:13 CST, end at Tue 2018-05-29 11:34:24 CST. --
May 29 11:34:24 devserver1 docker[893]: time="2018-05-29T11:34:24.167053102+08:00" level=error msg="Not continuing with pull after error: error pulling image configuration: unexpected EOF"
May 29 11:34:24 devserver1 docker[893]: time="2018-05-29T11:34:24.212480193+08:00" level=info msg="Layer sha256:3fc67fe0621339e8f025cb429eecee5db64025673f3eafb02d12b512f07bbba5 cleaned up"

这个问题让我们想到了之前我写过一篇文章《Trouble Shooting —— Docker Pull Image : Filesystem layer verification failed for digest sha256错误》也是docker pull的时候报错:

8b7054...: Verifying Checksum
Filesystem layer verification failed for digest sha256: 8b7054.....

通过使用之前文章的解决方案依然可以解决这个问题。

这类问题可以使用绕过校验重新build后push刷新digest值后,恢复原始build参数再重新push恢复默认操作来进行解决。

如何免费的让你的网站变得更加安全 - HTTPS

Published on:

在这个数据不安全的世界里,很有可能你早上买了个东西下午就会有类似的推销电话打过来骚扰你,这些数据信息从哪里来的呢?当然很多时候是人为的贩卖信息造成的,但是数据来源很大一部分是来自于互联网。因此站点使用https已经是最基本的防护,当我去访问一个站点它如果不是https的我可能都不想访问它更别提输入一些个人信息了。那怎么才能让我们提供的网站安全的服务你的用户呢?当然是使用证书来保护网站来往的数据。

如果不差钱的话还是使用收费的证书去给你的网站开启https。当然国内也有很多免费的证书,去谷哥或者度娘能检索到一大把的免费证书信息,各大云服务商上面也有免费的证书可以申请使用,我下面就介绍一个免费的使用方式。

Let’s Encrypt

Let's Encrypt是一个于2015年三季度推出的数字证书认证机构,旨在以自动化流程消除手动创建和安装证书的复杂流程,并推广使万维网服务器的加密连接无所不在,为安全网站提供免费的SSL/TLS证书。

Let's Encrypt由互联网安全研究小组(缩写ISRG)提供服务。主要赞助商包括电子前哨基金会Mozilla基金会Akamai以及思科。2015年4月9日,ISRG与Linux基金会宣布合作。

通过官网我们能看到赞助商还是蛮多的,赞助商列表

上述来自于维基百科,查看原文

从介绍中能了解到它是为了解决,以自动化流程消除手动创建和安装证书的复杂流程,让证书使用更加简单。

我们通过Let's Encrypt官网的Getting Started中可以查看具体的使用说明

下面我们简单介绍一下使用步骤:

使用步骤

安装证书非常简单,只需要使用Certbot,就可以完成。

  • 打开Certbot,选择你的网站使用的应用服务器和操作系统。如下图:

  • 选择完后会生成安装教程,不用想太多Step by step就好了,如下图:

安装基础环境

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx 

安装证书

安装完之后直接运行sudo certbot --nginx即可

certbot 会自动修改nginx配置文件(nginx.conf)并且列出你的虚拟站点让你选择是否开启HTTPS,当然你只用选择是否开启即可,选择完后它会自动下载证书并且修改nginx配置文件

修改后的nginx.conf是什么样的?让我们看一下

listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/your.domain/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/your.domain/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot

还会很贴心的帮你生成http跳转到https的配置,如下:

# Redirect non-https traffic to https
if ($scheme != "https") {
  return 301 https://$host$request_uri;
} # managed by Certbot

到这里就完成了证书安装,是不是很简单。当然我们之前也说过证书是有有效期的,那过期了之后我们如何操作?再根据上面的操作执行一次?

当然不是了,我们可以使用自动检测的方式来进行自动的更新证书与nginx配置。具体看下面操作:

证书自动更新

下面是官方对于证书续订的说明:

Automating renewal
The Certbot packages on your system come with a cron job that will renew your certificates automatically before they expire. 
Since Let's Encrypt certificates last for 90 days, it's highly advisable to take advantage of this feature. 
You can test automatic renewal for your certificates by running this command:

$ sudo certbot renew --dry-run

首先Let’s Encrypt 的证书只有90天的有效期,所以我们可以使用crontab来进行定时自动更新。

crontab如何使用这里就不多做介绍了,可以查看crontab使用说明

使用下面的表达式让其在每个月的一号强制更新证书,但是证书的强制更新不能太频繁,太频繁会提前进入证书授权限制。

0 0 1 * * /usr/bin/certbot renew --force-renewal
10 0 1 * * /usr/sbin/service nginx restart

renew的使用说明

到这里证书安装以及自动更新就介绍完毕了,当然我们的站点中有很多静态资源或超链接的地方,在启用https后可能也要进行一轮的检查由http修改为https,主要就是那些hard code的地方需要找出来进行修改掉。

为了保护你服务的用户信息安全,我强烈建议开启HTTPS,只要是个站点服务就应该开启HTTPS这才是负责任的的体现,Keep Real。

Mysql数据库字符集utf8mb4使用问题

Published on:
Tags: mysql utf8mb4

问题发生在这个字上,首先先让我们看这个字的字符信息

utf8字符集信息

Utf-8 Character

Symbol information table

Name: Utf-8 Character
Unicode Subset: CJK Extension B
Unicode HEX: U+20046
ASCII value: 131142
HTML: 𠁆
CSS: \20046

它属于utf8的字符集,具体可参考:传送门

既然属于utf8的字符集那为什么数据库保存这个字会出现非法字符的错误呢?错误如下:

### Cause: java.sql.SQLException: Incorrect string value: '\xF0\xA5\x8A\x8D' for column 'DESCRIPTION' at row 1
; uncategorized SQLException for SQL []; SQL state [HY000]; error code [1366]; Incorrect string value: '\xF0\xA5\x8A\x8D' for column 'DESCRIPTION' at row 1; nested exception is java.sql.SQLException: Incorrect string value: '\xF0\xA5\x8A\x8D' for column 'DESCRIPTION' at row 1
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:84)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73)

让我们先了解一下utf8的编码,UTF-8编码是U+2528D,属于CJK Unified Ideographs Extension B(中日韩统一表意文字扩充B)字符集的字符,处于第二辅助平面(SIP,表意文字补充平面),不属于我们通常所见的基本多文种平面(BMP,即Unicode编码范围在0000-FFFF之内)的字符。保存一个字;相比之下,在BMP范围之内的字符只需要占用3 Bytes。仅仅就因为字符保存位数不同,就让程序开发出现了难题。

来自wikipedia的Unicode字符平面映射

目前的Unicode字符分为17组编排,每组称为平面(Plane),而每平面拥有65536(即216)个代码点。然而目前只用了少数平面。

平面 始末字符值 中文名称 英文名称
0号平面 U+0000 - U+FFFF 基本多文种平面 Basic Multilingual Plane,简称BMP
1号平面 U+10000 - U+1FFFF 多文种补充平面 Supplementary Multilingual Plane,简称SMP
2号平面 U+20000 - U+2FFFF 表意文字补充平面 Supplementary Ideographic Plane,简称SIP
3号平面 U+30000 - U+3FFFF 表意文字第三平面 (未正式使用[1]) Tertiary Ideographic Plane,简称TIP
4号平面 至 13号平面 U+40000 - U+DFFFF (尚未使用)
14号平面 U+E0000 - U+EFFFF 特别用途补充平面 Supplementary Special-purpose Plane,简称SSP
15号平面 U+F0000 - U+FFFFF 保留作为私人使用区(A区)[2] Private Use Area-A,简称PUA-A
16号平面 U+100000 - U+10FFFF 保留作为私人使用区(B区)[2] Private Use Area-B,简称PUA-B

具体可查看:传送门

究其原因就是这个字保存需要占4 Bytes的字节,mysql在MySQL 5.6+版本之后支持4Bytes字节(utf8mb4)的存储,首先先看一下mysql编码如何设置?

数据库字符集设置

建库语句中需要指定字符集编码为:utf8mb4

建库语句中需要指定字符集校对规则为:utf8mb4_general_ci

建表语句中需要指定字符集编码为:utf8mb4

表字段需要指定字符集编码为:utf8mb4

表字段collation需要指定字符集校验规则:utf8mb4_general_ci

如下图:

CREATE TABLE `t_application` (
   `ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
   `APP_ID` bigint(20) NOT NULL COMMENT '应用编号',
   `NAME` varchar(100) NOT NULL COMMENT '应用名称',
   `DESCRIPTION` varchar(200) DEFAULT NULL COMMENT '应用描述',
   PRIMARY KEY (`ID`)
 ) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4 CHECKSUM=1 DELAY_KEY_WRITE=1 ROW_FORMAT=DYNAMIC

utf8mb4不生效问题分析

完成上面的字符集设置之后我们使用程序保存字符来进行测试,还是提示非法的字符集错误,那这又是为什么呢?

让我们来看回数据库charset设置,SHOW VARIABLES LIKE 'CHARACTER%',关注character_set_server这个字符设置

character_set_server在默认情况下为latin1

/*
character_set_server: 服务器安装时指定的默认字符集设定。
character_set_database: 数据库服务器中某个库使用的字符集设定,如果建库时没有指明,将使用服务器安装时指定的字符集设置。
  
建表时候,字段字符集的选取方式如下:
 
1. * if 字段指定的字符集
2. * else if 表指定的字符集
3. * else if @@character_set_database
4. * else @@character_set_server (如果没有设定,这个值为latin1)
  
*/

按照上面的说法,如果character_set_server和character_set_database变量的值不同,则新建数据库的字符集以character_set_server为准,而不是按照character_set_database。

我的理解是character_set_database指定了utf8mb4后目标库应该按照设置的字符集格式走才对,只有没有设置的库才会走默认的,但是现在测试下来,character_set_server必须修改为utf8mb4,否则保存这种字符依然提示非法字符集,不太清楚具体什么原因。

如果数据库按照上面设置了以后还是无法保存的话,应该就是mysql驱动的问题和数据库连接串字符集的问题。下面让我们看一下从mysql server到mysql驱动到数据源再到应用所有的utf8mb4设置。

完整的正确设置

Mysql服务端配置

建库语句中需要指定字符集编码为:utf8mb4

建库语句中需要指定字符集校对规则为:utf8mb4_general_ci

建表语句中需要指定字符集编码为:utf8mb4

表字段需要指定字符集编码为:utf8mb4

表字段collation需要指定字符集校验规则:utf8mb4_general_ci

mysql ini配置文件指定character_set_server

[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci

ps. character_set_server设置成utf8或使用默认latin1,在直接使用sql插入特殊字符和emoji表情时数据库显示?????乱码,使用程序(mybatis)插入特殊字符和emoji表情时报错:java.sql.SQLException: Incorrect string value: ‘\xF0\xA5\x8A\x8D’ for column ‘某某列’ at row 1

java连接mysql驱动版本

mysql-connector-java版本在5.1.13+才支持utf8mb4,因此在选用连接驱动时应注意这个问题,我们使用的驱动是mysql-connector-java-5.1.41。

数据源配置

我们使用的druid数据源配置上需要加上connectionInitSqls属性配置,具体如下:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
        destroy-method="close">
    ....此处省略其他属性.....
    <property name="connectionInitSqls" value="set names utf8mb4;" />
</bean>

ps.但是我实际测试下来,这个参数不加也没有什么问题,可以正常保存这种特殊字符和emoji表情,官方建议配置。

数据库连接串配置

我们的配置如下:

jdbc.url=jdbc:mysql://ip:port/databaseName?useUnicode=true&characterEncoding=UTF-8&noAccessToProcedureBodies=true&autoReconnect=true

建议去掉useUnicode和characterEncoding的配置,使用如下配置:

jdbc.url=jdbc:mysql://ip:port/databaseName?noAccessToProcedureBodies=true&autoReconnect=true

ps.但是我实际测试下来,连接串上增加useUnicode=true&characterEncoding=utf8也没有什么问题,可以正常保存这种特殊字符和emoji表情。

总结

最主要的是mysql服务端的character_set_server需要和character_set_database保持一致修改为utf8mb4,因此我们先不考虑数据库连接串和druid数据源配置修改,但是mysql-connector-java版本必须使用5.1.13+