FunTester Socket 接口开发和测试实践

FunTester · 2020年11月17日 · 694 次阅读

由于接下来有一个聊天室需求的项目,所以在确定完技术方案后,就要开始着手socket协议的接口测试准备了。

在简单查阅一些Java实现websocket的案例资料,决定采用org.java_websocket.client.WebSocketClient;这个实现类进行封装,而非采用直接封装Socket这个,原因比较简单,我相信第一眼的感觉。看了好几个不同的socket client实现,就这个最简单。

大概抄了一下Demo的代码,但是问题来了,手头没有Websocket接口可提供测试的,着实有点尴尬。既然都抄了一个client的实现代码,再抄一个server实现的代码也不会太浪费时间。

  • 这里分享一个 VIP 群友的问题:学习之初,抄代码的意义何在?
  • 我觉得本次socket协议接口测试的学习这两天,抄代码对我的意义主要两点:1、能够迅速掌握一种解决问题的方案。2、能够迅速掌握该框架的基本功能的使用。
  • 从零开始学习少不了抄代码的过程,抄完之后,再去魔改,不断验证各种API函数的使用,看看源码和注释,逐步掌握该技能。

server 代码

我用的SpringBoot框架写的,下面是socket server实现代码:

@Component
@ServerEndpoint("/ws/{username}")
public class SocketS {

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

    private static Map<String, SocketS> clients = new ConcurrentHashMap<String, SocketS>();

    private Session session;

    private String username;

    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session) throws IOException {
        this.username = username;
        this.session = session;
        clients.put(username, this);
        logger.info("用户:{}已连接", username);
        sendMessageAll("用户:" + username + "已经上线了!");
    }

    @OnClose
    public void onClose() throws IOException {
        clients.remove(username);
        sendMessageAll("用户:" + username + "已经离线了!");
    }

    @OnMessage
    public void onMessage(String message) throws IOException {
        logger.info(message);
        sendMessageAll(this.username + ":" + message);
    }

    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    public void sendMessageTo(String message, String To) throws IOException {
        // session.getBasicRemote().sendText(message);
        //session.getAsyncRemote().sendText(message);
        for (SocketS item : clients.values()) {
            if (item.username.equals(To))
                synchronized (item.session) {
                    item.session.getBasicRemote().sendText(message);
                }
        }
    }

    public void sendMessageAll(String message) throws IOException {
        for (SocketS item : clients.values()) {
            synchronized (item.session) {
                item.session.getBasicRemote().sendText("世界喊话器     " + message + "   ");
            }
        }
    }


}

注解不兼容的坑

由于要开启WebSocket支持,所以一些教程上直接在启动类加上了注解@EnableWebSocket,然后启动的时候就会报错:

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
ERROR org.springframework.boot.SpringApplication:837  Application run failed
org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'defaultSockJsTaskScheduler' is expected to be of type 'org.springframework.scheduling.TaskScheduler' but was actually of type 'org.springframework.beans.factory.support.NullBean'
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:395)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:227)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1174)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1141)
    at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.resolveSchedulerBean(ScheduledAnnotationBeanPostProcessor.java:315)
    at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:256)
    at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:233)
    at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:105)
    at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
    at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
    at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
    at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:403)
    at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:360)
    at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:897)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:553)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
    at com.okay.family.FamilyApplication.main(FamilyApplication.java:25)

经过查询资料,发现这是开启定时任务注解@EnableScheduling和开启WebSocket注解@EnableWebSocket不能同时在启动类使用的缘故。

修改方案如下,取消两个注解,然后通过Bean注入的方式完成配置,下面是两个配置项的类代码:

  • 定时任务配置
package com.okay.family.common;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration
public class ScheduledConfig {

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduling = new ThreadPoolTaskScheduler();
        scheduling.setPoolSize(10);
        scheduling.initialize();
        return scheduling;
    }


}

  • WebSocket 配置
package com.okay.family.common;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
@EnableWebSocket
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpoint() {
        return new ServerEndpointExporter();
    }
}

client 代码和测试脚本

client 实现

这里的比较粗糙,仅限于交流使用,等我再学习学习之后,完善一下这个封装类。

测试脚本我用了三个人在某一个聊天室中里面从进入,发言,到退出聊天室的场景。

package com.fun.ztest;

import com.fun.frame.SourceCode;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.enums.ReadyState;
import org.java_websocket.handshake.ServerHandshake;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;


public class SocketTest extends WebSocketClient {

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

    public SocketTest(String url) throws URISyntaxException {
        super(new URI(url));
    }

    static SocketTest getInstance(String url) {
        try {
            return new SocketTest(url);
        } catch (URISyntaxException e) {
            logger.error("获取socketclient失败!", e);
            return null;
        }
    }

    @Override
    public void onOpen(ServerHandshake shake) {
        logger.info("开始建立socket连接...");
        for (Iterator<String> it = shake.iterateHttpFields(); it.hasNext(); ) {
            String key = it.next();
            logger.debug(key + ":" + shake.getFieldValue(key));
        }
    }

    @Override
    public void onMessage(String paramString) {
        logger.warn( paramString);
    }

    @Override
    public void onClose(int paramInt, String paramString, boolean paramBoolean) {
        logger.info("socket关闭...");
    }

    @Override
    public void onError(Exception e) {
        logger.error("socket异常!", e);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Message());
        Thread thread1 = new Thread(new Message());
        Thread thread2 = new Thread(new Message());
        thread.start();
        thread1.start();
        thread2.start();
        thread.join();
        thread1.join();
        thread2.join();
    }

    static class Message extends SourceCode implements Runnable {

        String name;

        String url;

        static AtomicInteger users = new AtomicInteger(1);

        SocketTest socketTest;

        Message() {
            name = DEFAULT_STRING + users.getAndIncrement();
            url = "ws://127.0.0.1:8080/ws/" + name;
            socketTest = SocketTest.getInstance(url);
            output(name);
        }

        @Override
        public void run() {
            try {
                socketTest.connect();
                sleep(2000);
                while (!socketTest.getReadyState().equals(ReadyState.OPEN)) {
                    SourceCode.sleep(2000);
                    logger.warn("还没有打开");
                    socketTest.reconnect();
                }
                logger.info("建立websocket连接");
                socketTest.send("我是" + name);
                sleep(getRandomInt(5));
                socketTest.send("我有事先走了!!!");
                sleep(getRandomInt(5));
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                socketTest.close();
            }
        }


    }


}

控制台输出

INFO-> 当前用户:fv,IP:10.60.192.21,工作目录:/Users/fv/Documents/workspace/fun/,系统编码格式:UTF-8,系统Mac OS X版本:10.15.7
INFO-> FunTester1
INFO-> FunTester2
INFO-> FunTester3
INFO-> 开始建立socket连接...
INFO-> 开始建立socket连接...
INFO-> 开始建立socket连接...
WARN-> 世界喊话器     用户:FunTester2已经上线了!   
WARN-> 世界喊话器     用户:FunTester2已经上线了!   
WARN-> 世界喊话器     用户:FunTester2已经上线了!   
WARN-> 世界喊话器     用户:FunTester3已经上线了!   
WARN-> 世界喊话器     用户:FunTester1已经上线了!   
WARN-> 世界喊话器     用户:FunTester1已经上线了!   
WARN-> 世界喊话器     用户:FunTester3已经上线了!   
WARN-> 世界喊话器     用户:FunTester1已经上线了!   
WARN-> 世界喊话器     用户:FunTester3已经上线了!   
INFO-> 建立websocket连接
INFO-> 建立websocket连接
WARN-> 世界喊话器     FunTester2:我是FunTester2   
WARN-> 世界喊话器     FunTester2:我是FunTester2   
WARN-> 世界喊话器     FunTester2:我是FunTester2   
WARN-> 世界喊话器     FunTester1:我是FunTester1   
WARN-> 世界喊话器     FunTester1:我是FunTester1   
WARN-> 世界喊话器     FunTester1:我是FunTester1   
INFO-> 建立websocket连接
WARN-> 世界喊话器     FunTester3:我是FunTester3   
WARN-> 世界喊话器     FunTester3:我是FunTester3   
WARN-> 世界喊话器     FunTester3:我是FunTester3   
WARN-> 世界喊话器     FunTester2:我有事先走了!!!   
WARN-> 世界喊话器     FunTester2:我有事先走了!!!   
WARN-> 世界喊话器     FunTester2:我有事先走了!!!   
WARN-> 世界喊话器     FunTester3:我有事先走了!!!   
WARN-> 世界喊话器     FunTester3:我有事先走了!!!   
WARN-> 世界喊话器     FunTester3:我有事先走了!!!   
WARN-> 世界喊话器     用户:FunTester2已经离线了!   
INFO-> socket关闭...
WARN-> 世界喊话器     用户:FunTester2已经离线了!   
WARN-> 世界喊话器     FunTester1:我有事先走了!!!   
WARN-> 世界喊话器     FunTester1:我有事先走了!!!   
INFO-> socket关闭...
WARN-> 世界喊话器     用户:FunTester3已经离线了!   
INFO-> socket关闭...

Process finished with exit code 0

纯文字版可能不够直观,下面分享一下截图,我把发言的内容WARN日志里面了。


公众号FunTester,原创分享爱好者,腾讯云、开源中国和掘金社区首页推荐,知乎八级强者,欢迎关注、交流,禁止第三方擅自转载。

FunTester 热文精选

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册