Spring Boot 在Netty上开发WebSocket和HTTP应用之三 -- HTTP(S)和WEBSOCKET/WSS单元测试篇

首先介绍HTTPS(含HTTP)的测试工具类,共有4个,代码如下:

public class HttpsRequestRunnable implements Runnable { 
    private static Logger logger = LoggerFactory.getLogger(HttpsRequestRunnable.class); 
    private static final LongAdder counter = new LongAdder();
    private String server_url ;
    private String strReqJson ;
    private String userId ;
    private String token ;
    public HttpsRequestRunnable(String server_url, String userId, String token,String strReqJson) {
        this.server_url = server_url ;
        this.strReqJson = strReqJson ;
        this.userId = userId ;
        this.token = token ;
    }
    public void run() {
        String respStr = HttpsRequestUtil.getFromHttps(server_url, userId, token, strReqJson);
        logger.info("respStr:{}",respStr);
        counter.increment();
        if(counter.intValue()%10==1) {
            logger.error("{} resContent {} ", counter.intValue(),respStr);
        }
    }   
}


public class HttpsRequestUtil { 
    private static Logger logger = LoggerFactory.getLogger(HttpsRequestUtil.class); 
    private static HttpURLConnection getConn(String server_url) throws Exception {
      URL url = new URL(server_url);
      if(server_url.toLowerCase().startsWith("https")) {
          HttpsURLConnection.setDefaultHostnameVerifier(new NullHostNameVerifier());
          HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
          TrustManager[] tm = { new MyX509TrustManager() };
          SSLContext sslContext = SSLContext.getInstance("TLS");
          sslContext.init(null, tm, /*new java.security.SecureRandom()*/null);
          SSLSocketFactory ssf = sslContext.getSocketFactory();          
          conn.setSSLSocketFactory(ssf);
          return conn ;
      }
      return  (HttpURLConnection) url.openConnection();
    }

    public static String getFromHttps(String server_url,String userId,String token,String strReqJson) {
        String CONTENT_TYPE = "application/json";
        StringBuilder sb2 = new StringBuilder();
        try {           
          HttpURLConnection conn = getConn(server_url);
            conn.setConnectTimeout(45 * 1000);
            conn.setReadTimeout(45 * 1000);
            conn.setDoInput(true);// 允许输入
            conn.setDoOutput(true);// 允许输出
            conn.setUseCaches(false); // 不允许使用缓存
            conn.setRequestMethod("POST");
            conn.setRequestProperty("connection", "keep-alive");
            conn.setRequestProperty("charset", "UTF-8");
            conn.setRequestProperty("Content-Type", CONTENT_TYPE);
            conn.setRequestProperty("userId", userId);
            conn.setRequestProperty("token", token);            
            DataOutputStream outStream = new DataOutputStream(conn.getOutputStream());          
            if(StringUtil.isNotEmpty(strReqJson)) {
                outStream.write(strReqJson.getBytes());
            }
            outStream.flush();
            outStream.close();
            conn.connect();         
            InputStream in = null;
            int resCode = conn.getResponseCode();
            logger.debug("resCode {} ", resCode);
            if (resCode == 200) {
                in = conn.getInputStream();
                int ch;
                while ((ch = in.read()) != -1) {
                    sb2.append((char) ch);
                }
            }
            conn.disconnect();      
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sb2.toString();
    }
}


public class MyX509TrustManager implements X509TrustManager {   
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {} 
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} 
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }   
}

class NullHostNameVerifier implements HostnameVerifier {
    @Override
    public boolean verify(String arg0, SSLSession arg1) {
        return true;
    }
}

然后是HTTP/HTTPS单元测试类(其实完全可以将testLocalHttpLogin的内容放入一个普通类的main方法中运行):

@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class HttpLocalFunctionTest {
  private static Logger logger = LoggerFactory.getLogger(HttpLocalFunctionTest.class);
  @Test
  public void testLocalHttpLogin() throws Exception {
    logger.info("begin2test_testLogin");
    //可以支持http和https
    String SERVER_URL = "http://127.0.0.1:8888/login" ; 
    String SERVER_URL = "https://127.0.0.1:8888/login" ; 
    String reqJson = "{\"name\":\"张三\",\"id\":0}";
    String userId = StringUtil.getRandomCode(5);
    String token  = StringUtil.getRandomCode(15);
    Thread t = new Thread(new HttpsRequestRunnable(SERVER_URL, userId,token, reqJson));
    t.start();
    t.join();
  }
}

接下来是WebSocket的模拟客户端。
需要引入 java_websocket.jar,pom.xml 如下:

    <dependency>
        <groupId>org.java-websocketgroupId>
        <artifactId>Java-WebSocketartifactId>
        <version>1.3.9version>
    dependency>

普通ws测试类对应的Java代码如下:

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft;
import org.java_websocket.handshake.ServerHandshake;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 此测试类模拟 WebSocket 的客户端对服务器做压力测试
 * 注意:此类无法在创建连接的同时发送userId和token信息,导致校验无法通过。
 */
public class CommonWsClientTest extends WebSocketClient {

    private static Logger logger = LoggerFactory.getLogger(CommonWsClientTest.class);

    public CommonWsClientTest( URI serverUri , Draft draft ) {
        super( serverUri, draft );
    }

    public CommonWsClientTest( URI serverURI ) {
        super( serverURI );
    }

    public CommonWsClientTest( URI serverUri, Map httpHeaders ) {
        super(serverUri, httpHeaders);
    }

    @Override
    public void onOpen( ServerHandshake handshakedata ) {
        String msg = "{\"msgType\":0,\"msgId\":0}" ;
        logger.info("onOpen:"+msg);
        send(msg);
    }

    @Override
    public void onMessage( String message ) {
      logger.info("receiveMsg:{}",message);
    }

    @Override
    public void onClose( int code, String reason, boolean remote ) {
    }

    @Override
    public void onError( Exception ex ) {
        ex.printStackTrace();
    }

    public static void process() throws URISyntaxException {
      //请特别注意,此种方式如果连接本地,请使用127.0.0.1 而不要使用localhost 否则有时无法连接。
        String url = "ws://127.0.0.1:9999/yourappname/push" ;
        CommonWsClientTest c = new CommonWsClientTest(new URI(url)); 
        c.connect();
    }

    public static void main( String[] args ) {
        process();      
        try {
            Thread.sleep(5000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

wss模拟客户端,支持压力测试:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

/**
 * 此测试类模拟 WebSecuritySocket 的客户端对服务器做压力测试
 * 注意:此类无法在创建连接的同时发送[ HTTP header] userId和token信息,导致校验无法通过。
 */
public class SSLWsClientStressTest {

  private static String url = "wss://www.yourdns.com:9999/yourappname/push";

  private static String KEYSTORE = "D:\\workspace\\netserver\\keysecurity\\hansmachine.jks";
  private static String STOREPASSWORD = "123456";
  private static String KEYPASSWORD = "123456";

  public static void main(String[] args) {
    String clientNumber = "1";
    if (args.length > 0) {
      clientNumber = args[0];
    }

    if (args.length > 1) {
      url = args[1];
    } 
    // ws and wss 的网址 不能以 / 结尾否则会报错。
    if (url.endsWith("/")) {
      url = url.substring(0, url.length() - 1);
    }
    System.out.println("-----SSLClientExample-------");
    System.out.println("clientNumber:" + clientNumber);
    System.out.println("KEYSTORE:" + KEYSTORE);
    System.out.println("url:" + url);
    for (int i = 0; i < Integer.parseInt(clientNumber); i++) {
      new Thread(new Runnable() {
        public void run() {
          try {
            process();
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
      }).start();
      if (i % 100 == 0) {
        try {
          Thread.sleep(20);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
    try {
      Thread.sleep(5000000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  private static  SSLContext sslContext = null ;

  public static void process() throws Exception {
    String STORETYPE = "JKS";
    WebSocketHeartBeatClient chatclient = new WebSocketHeartBeatClient(new URI(url));
    initSSLContext(STORETYPE);
    SSLSocketFactory factory = sslContext.getSocketFactory();
    chatclient.setSocket(factory.createSocket());
    chatclient.connectBlocking();
    String msg = "{\"msgType\":0,\"msgId\":0}";
    chatclient.send(msg);
    Thread.sleep(5000000);
  }

  private static void initSSLContext(String STORETYPE) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException,
          FileNotFoundException, UnrecoverableKeyException, KeyManagementException {
    if(sslContext!=null) {
      return  ;
    }
    KeyStore ks = KeyStore.getInstance(STORETYPE);
    File kf = new File(KEYSTORE);
    ks.load(new FileInputStream(kf), STOREPASSWORD.toCharArray());

    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init(ks, KEYPASSWORD.toCharArray());
    TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
    tmf.init(ks);

    SSLContext localsslContext = null;
    localsslContext = SSLContext.getInstance("TLS");
    localsslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
     sslContext = localsslContext;
  }
}
import java.net.URI;
import java.util.concurrent.atomic.LongAdder;

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;

public class WebSocketHeartBeatClient extends WebSocketClient {

    private static LongAdder count = new LongAdder();

    public WebSocketHeartBeatClient( URI serverUri ) {
        super( serverUri );
    }

    @Override
    public void onOpen( ServerHandshake handshakedata ) {
        //缺点:这里无法模拟发送http header信息?
        //handshakedata.
    }

    @Override
    public void onMessage( String message ) {
        //System.out.println( "got: " + message );
        count.increment();
        int c = count.intValue();
        if(c%50==0) {
            System.out.println("ReceiveMsgCount:"+c);
        }
    }

    @Override
    public void onClose( int code, String reason, boolean remote ) {
        //System.out.println( "Disconnected" );
    }

    @Override
    public void onError( Exception ex ) {
        ex.printStackTrace();
    }

}

以上两个方法都是使用的第三方客户端工具java_websocket,一般情况下够用了,但是如果考虑到安全性,
服务器端有时候会要求客户端在握手的时候发送[ HTTP header] userId和token信息进行校验,
如果校验失败服务器端拒绝连接。
此时需要Netty出马模拟客户端:

import java.net.URI;
import java.util.concurrent.atomic.LongAdder;

import javax.net.ssl.SSLException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.yjzx.server.common.util.ConfigConstants;
import com.yjzx.server.common.util.StringUtil;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;

/**
 * 目前已经实现在建立连接的时候同时发送http header 信息,
 * 然后发送一个心跳包信息。
 */
public final class WebSocketSecurityClient {
    private static Logger logger = LoggerFactory.getLogger(WebSocketSecurityClient.class);
    private static final String path = "/yourappname/push";
    private static LongAdder count = new LongAdder();
    public static void sendMessage(String json) throws Exception {
          String host = "www.yourdns.com";
          int port = 9999;
        String url = "wss://" + host + ":" + port  + path ;
        sendMessage(url,json);
    }

    /**
     * websocket 信息发送,支持 ws:// 方式和 wss:// 方式
     * 样例1:wss://www.abc.com:9999/yourdns/server
     * 样例2:ws://www.abc.com:9999/yourdns/server
     * @param url
     * @param json
     * @throws Exception
     */
    public static void sendMessage(String url, String json) throws Exception {
//      如果多一个斜线就会--没反应,也不报错! wss://www.abc.com:9999//yourappname/server
        URI uri = new URI(url);
        String host = uri.getHost();
        int port = uri.getPort();

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();

            //握手时发送头信息,供服务器校验
            DefaultHttpHeaders customHeaders = new DefaultHttpHeaders();
            String userId = "9999" ;
            customHeaders.add("userId", userId);
            customHeaders.add("token", "ABCD9876");
            final WebSocketClientHandler webSocketClientHandler =
                    new WebSocketClientHandler(
                            WebSocketClientHandshakerFactory.newHandshaker(
                                    uri, WebSocketVersion.V13, null, true, customHeaders));

            b.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    ChannelPipeline p = ch.pipeline();

                    if(url.toLowerCase().startsWith("wss")) {
                      p.addLast("ssl", getSslHandler(ch,host,port));
                      logger.info("initChannel_AddSSLHandlerFinished");
                    }

                    p.addLast(new HttpClientCodec());
                    p.addLast(new HttpObjectAggregator(8192));
                    p.addLast(WebSocketClientCompressionHandler.INSTANCE);
                    p.addLast(webSocketClientHandler);

                }
            });

            Channel clientSocketChannel = b.connect(uri.getHost(), port).sync().channel();
            webSocketClientHandler.handshakeFuture().sync();

            count.increment();
            int c = count.intValue();
            if(c%20==0) {
                logger.warn("count:{}",c);
            }

            //服务器端与手机(设备端)约定是5分钟(300s)通过websocket发送一次心跳包
            //故此处模拟此场景。
            for (int i = 1; i <= 5; i++) {
              WebSocketFrame frame2 = new TextWebSocketFrame(json);
              clientSocketChannel.writeAndFlush(frame2);
              logger.info("begin2sleep 300s");
              Thread.sleep(300*1000);
            }

            // 不要退出
            Thread.sleep(5000000);
        } finally {
            group.shutdownGracefully();
        }
    }

    private static SslHandler getSslHandler(SocketChannel ch, String host,int port) {
        SslContext sslCtx;
        try {
            sslCtx = SslContextBuilder.forClient()
            .trustManager(InsecureTrustManagerFactory.INSTANCE).build();
            return sslCtx.newHandler(ch.alloc(), host, port);
        } catch (SSLException e) {
            e.printStackTrace();
            return null ;
        }
    }

}
import java.util.concurrent.atomic.AtomicInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
import io.netty.util.CharsetUtil;

public class WebSocketClientHandler extends SimpleChannelInboundHandler {

    private static Logger logger = LoggerFactory.getLogger(WebSocketClientHandler.class);

    private final WebSocketClientHandshaker handshaker;
    private ChannelPromise handshakeFuture;

    private static AtomicInteger connSuccCounter = new AtomicInteger(0)  ;
    private static AtomicInteger connFailCounter = new AtomicInteger(0)  ;

    public WebSocketClientHandler(WebSocketClientHandshaker handshaker) {
        this.handshaker = handshaker;
    }

    public ChannelFuture handshakeFuture() {
        return handshakeFuture;
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        handshakeFuture = ctx.newPromise();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        handshaker.handshake(ctx.channel());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        logger.info("WebSocket Client disconnected!");
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        Channel ch = ctx.channel();
        if (!handshaker.isHandshakeComplete()) {
            try {
                handshaker.finishHandshake(ch, (FullHttpResponse) msg);
                handshakeFuture.setSuccess();

                int c = connSuccCounter.incrementAndGet();
                logger.info("CONNECTSUCC,INDEX:"+c);
            } catch (WebSocketHandshakeException e) {
                int c = connFailCounter.incrementAndGet();
                logger.info("CONNECTFAIL,INDEX:"+c);
//                logger.info("WebSocket Client failed to connect");
                handshakeFuture.setFailure(e);
            }
            return;
        }

        if (msg instanceof FullHttpResponse) {
            FullHttpResponse response = (FullHttpResponse) msg;
            throw new IllegalStateException(
                    "Unexpected FullHttpResponse (getStatus=" + response.status() +
                            ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
        }

        WebSocketFrame frame = (WebSocketFrame) msg;
        if (frame instanceof TextWebSocketFrame) {
            TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
            logger.info("WebSocket Client received message: " + textFrame.text());
        } else if (frame instanceof PongWebSocketFrame) {
            logger.info("WebSocket Client received pong");
        } else if (frame instanceof CloseWebSocketFrame) {
            logger.info("WebSocket Client received closing");
            ch.close();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        if (!handshakeFuture.isDone()) {
            handshakeFuture.setFailure(cause);
        }
        ctx.close();
    }
}

下面这个才是测试类,当然也可以将测试方法的内容放在普通类的main方法中执行,效果是一样的

package com.yjzx.client.websocket.netty;


import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * 因为本地netty作为客户端启动的时候也开了好几个线程,导致使用netty作为wss客户端的时候无法开更多线程连接服务器
 * 但是此方式可以作为功能测试。
 */
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING )
public class WssIntercomFunctionTest {

    private static Logger logger = LoggerFactory.getLogger(WssIntercomFunctionTest.class);

    @Test
    public void test200SendHeartBeatInWssOrWs() throws Exception {
        Thread.sleep(3000);
        String msg = "{\"msgType\":0,\"msgId\":0}" ;
        logger.info("msg:{}",msg);

        //支持 SSL 加密websocket       
        String url = "wss://www.yourdns.com:9999/yourappname/push" ;

        //也可以支持websocket方式
        String url = "ws://www.yourdns.com:9999/yourappname/push" ;

        WebSocketSecurityClient.sendMessage(url,msg);
    }

}

你可能感兴趣的:(Spring,Boot,Netty,WebSocket)