由于接下来有一个聊天室需求的项目,所以在确定完技术方案后,就要开始着手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,原创分享爱好者,腾讯云、开源中国和掘金社区首页推荐,知乎八级强者,欢迎关注、交流,禁止第三方擅自转载。