zoukankan      html  css  js  c++  java
  • Socket接口开发和测试实践

    由于接下来有一个聊天室需求的项目,所以在确定完技术方案后,就要开始着手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热文精选

  • 相关阅读:
    PKU JudgeOnline 题目分类
    调试时拼凑带端口的完整网址/域名
    智能电脑监控器,完美解决想监控别人在自己电脑上的一切操作。
    如何清理LDF文件
    使用母版页后FindConttol需要注意
    【外刊IT评论】代码覆盖率:80%,不能少
    推荐2本普通人参悟的书
    处理在母版页加AJAX环境下处理滚动条回发保持不动的问题
    虚拟目录中的web.config不被上级目录的web.config影响的处理
    C++中^符号的意思
  • 原文地址:https://www.cnblogs.com/FunTester/p/14367323.html
Copyright © 2011-2022 走看看