Netty 之 Http+Websocket通信+文件上传
1.引入pom依赖
<dependencies> ...... <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.44.Final</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> </dependencies>
2.请求分发
a.创建启动类,添加 DispatchHandler、HttpHandler、WebSocketHandler、FileUploadHandler 四个自定义处理器到管道中
public class StartServer { //端口号 private int port; public StartServer(int port) { this.port = port; } //启动方法 public void start() throws Exception { //负责接收客户端的连接的线程。线程数设置为1即可,netty处理链接事件默认为单线程,过度设置反而浪费cpu资源 EventLoopGroup bossGroup = new NioEventLoopGroup(1); //负责处理数据传输的工作线程。线程数默认为CPU核心数乘以2 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup); bootstrap.channel(NioServerSocketChannel.class); //在ServerChannelInitializer中初始化ChannelPipeline责任链,并添加到serverBootstrap中 bootstrap.childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel channel) { //添加HTTP编解码 channel.pipeline().addLast("decoder", new HttpRequestDecoder()); channel.pipeline().addLast("encoder", new HttpResponseEncoder()); //消息聚合器,将消息聚合成FullHttpRequest channel.pipeline().addLast("aggregator", new HttpObjectAggregator(1024*1024*5)); //支持大文件传输 channel.pipeline().addLast("chunked", new ChunkedWriteHandler()); //自定义Handler channel.pipeline().addLast("dispatchHandler", new DispatchHandler()); channel.pipeline().addLast("httpHandler", new HttpHandler()); channel.pipeline().addLast("webSocketHandler", new WebSocketHandler()); channel.pipeline().addLast("fileUploadHandler", new FileUploadHandler()); } }); //标识当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最大长度 bootstrap.option(ChannelOption.SO_BACKLOG, 1024); //Netty4使用对象池,重用缓冲区 bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); //是否启用心跳保活机制 bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true); //禁止使用Nagle算法,便于小数据即时传输 bootstrap.childOption(ChannelOption.TCP_NODELAY, true); //绑定端口后,开启监听 ChannelFuture future = bootstrap.bind(port).sync(); future.addListener(f -> { if (f.isSuccess()) { System.out.println("服务启动成功"); } else { System.out.println("服务启动失败"); } }); //等待服务监听端口关闭 future.channel().closeFuture().sync(); } finally { //释放资源 workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } //测试代码 public static void main(String[] args) { try { int port = 8080; new StartServer(port).start(); } catch (Exception e) { e.printStackTrace(); } } }
b.创建请求分发处理器,负责分发Http请求和WebSocket请求
public class DispatchHandler extends SimpleChannelInboundHandler<Object> { @Override protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof FullHttpRequest) { FullHttpRequest request = (FullHttpRequest) msg; //判断是否为websocket握手请求 if(isWebSocketHandShake(request)) { ctx.fireChannelRead(new WebSocketRequestVo(request)); //文件上传 }else if(isFileUpload(request)){ ctx.fireChannelRead(new FileUploadRequestVo(request)); //Http请求 }else{ ctx.fireChannelRead(new HttpRequestVo(request)); } //websocket请求 } else if (msg instanceof WebSocketFrame) { WebSocketFrame frame = (WebSocketFrame) msg; ctx.fireChannelRead(new WebSocketRequestVo(frame)); } } //判断是否为websocket握手请求 private boolean isWebSocketHandShake(FullHttpRequest request){ //1、判断是否为get 2、判断Upgrade头是否包含websocket 3、Connection头是否包含upgrade return request.method().equals(HttpMethod.GET) && request.headers().contains(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET, true) && request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE, true); } //判断是否为文件上传 private boolean isFileUpload(FullHttpRequest request){ //1、判断是否为文件上传自定义URI 3、判断是否为POST方法 2、判断Content-Type头是否包含multipart/form-data String uri = ParamUtil.getUri(request); String contentType = request.headers().get(HttpHeaderNames.CONTENT_TYPE); if(contentType == null || contentType.isEmpty()){ return false; } return MyConfig.FILE_UPLOAD_URL.equals(uri) && request.method() == HttpMethod.POST && contentType.toLowerCase().contains(HttpHeaderValues.MULTIPART_FORM_DATA); } }
3.Websocket部分
a.创建Websocket请求Vo类
public class WebSocketRequestVo { //握手请求 private FullHttpRequest request; //websocket请求 private WebSocketFrame frame; public WebSocketRequestVo(FullHttpRequest request) { this.request = request; } public WebSocketRequestVo(WebSocketFrame frame) { this.frame = frame; } public FullHttpRequest getRequest() { return request; } public void setRequest(FullHttpRequest request) { this.request = request; } public WebSocketFrame getFrame() { return frame; } public void setFrame(WebSocketFrame frame) { this.frame = frame; } }
b.创建Websocket请求处理器
public class WebSocketHandler extends SimpleChannelInboundHandler<WebSocketRequestVo> { //属性名称:握手处理器 private static final AttributeKey<WebSocketServerHandshaker> HAND_SHAKE_ATTR = AttributeKey.valueOf("HAND_SHAKE"); //属性名称:websocket自定义id private static final AttributeKey<String> SOCKET_ID_ATTR = AttributeKey.valueOf("SOCKET_ID"); @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { //删除信道 String id = ctx.channel().attr(SOCKET_ID_ATTR).get(); if(id == null){ return; } WsClientManager.getInstance().removeChannel(id); //TODO System.out.println("[" + id + "]断开连接。。。"); } @Override protected void channelRead0(ChannelHandlerContext ctx, WebSocketRequestVo requestVo) throws Exception { //处理握手 if(requestVo.getRequest() != null){ this.handleShake(ctx, requestVo.getRequest()); } //处理websocket数据 if(requestVo.getFrame() != null){ this.handleFrame(ctx, requestVo.getFrame()); } } //处理握手 private void handleShake(ChannelHandlerContext ctx, FullHttpRequest request){ WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(null, null, false); WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(request); if (handshaker == null) { WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); } else { handshaker.handshake(ctx.channel(), request); //保存socket的自定义ID与信道的对应关系 Map<String, String> params = ParamUtil.getRequestParams(request); String id = params.get(MyConfig.SOCKET_ID); WsClientManager.getInstance().putChannel(id, ctx.channel()); //绑定属性到channel ctx.channel().attr(HAND_SHAKE_ATTR).set(handshaker); ctx.channel().attr(SOCKET_ID_ATTR).set(id); //TODO System.out.println("[" + id + "]正在握手。。。"); } } //处理websocket数据 private void handleFrame(ChannelHandlerContext ctx, WebSocketFrame frame){ // 判断是否关闭链路的指令 if (frame instanceof CloseWebSocketFrame) { WebSocketServerHandshaker handshaker = ctx.channel().attr(HAND_SHAKE_ATTR).get(); if(handshaker == null){ ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); return; } handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); return; } // 判断是否ping消息 if (frame instanceof PingWebSocketFrame) { ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain())); return; } // 暂仅支持文本消息,不支持二进制消息 if (! (frame instanceof TextWebSocketFrame)) { throw new UnsupportedOperationException("暂不支持该消息类型:" + frame.getClass().getName()); } // 处理消息 String msg = ((TextWebSocketFrame) frame).text(); WsClientManager.getInstance().handleMsg(msg); } }
c.创建消息Vo类
public class MsgVo implements Serializable { private int type; private String fromId; private String toId; private String body; public int getType() { return type; } public void setType(int type) { this.type = type; } public String getFromId() { return fromId; } public void setFromId(String fromId) { this.fromId = fromId; } public String getToId() { return toId; } public void setToId(String toId) { this.toId = toId; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } }
d.创建Websocket客户端管理类
public class WsClientManager { //单例 private static WsClientManager instance = new WsClientManager(); private WsClientManager(){} public static WsClientManager getInstance(){ return instance; } //socket自定义ID与信道的对应关系 private Map<String, Channel> channelMap = new ConcurrentHashMap<>(); //添加信道 public void putChannel(String id, Channel channel){ this.channelMap.put(id, channel); } //获取信道 public void getChannel(String id){ this.channelMap.get(id); } //删除信道 public void removeChannel(String id){ this.channelMap.remove(id); } //发送消息 public void sendMsg(String id, String msg){ TextWebSocketFrame frame = new TextWebSocketFrame(msg); Channel channel = channelMap.get(id); if(channel != null){ channel.writeAndFlush(frame); } } //群发消息 public void sendMsg2All(String msg){ for(Channel channel : channelMap.values()){ TextWebSocketFrame frame = new TextWebSocketFrame(msg); channel.writeAndFlush(frame); } } //处理消息 public void handleMsg(String msgJson){ MsgVo msgVo = JSON.parseObject(msgJson, MsgVo.class); if(msgVo.getType() == MsgTypeEnum.ONE2ONE.getCode()){ this.sendMsg(msgVo.getToId(), msgJson); this.sendMsg(msgVo.getFromId(), msgJson); }else if(msgVo.getType() == MsgTypeEnum.ONE2ALL.getCode()){ this.sendMsg2All(msgJson); } } }
e.创建消息类型枚举
public enum MsgTypeEnum { ONE2ONE(1,"私聊"), ONE2ALL(2,"公屏"); private int code; private String name; public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getName() { return name; } public void setName(String name) { this.name = name; } private MsgTypeEnum(int code, String name) { this.code = code; this.name = name; } public static MsgTypeEnum getMsgTypeEnum(int code) { for(MsgTypeEnum item : MsgTypeEnum.values()) { if (item.getCode() == code) { return item; } } return null; } }
4.Http部分
a.创建Http请求Vo
public class HttpRequestVo { private FullHttpRequest request; public HttpRequestVo(FullHttpRequest request) { this.request = request; } public FullHttpRequest getRequest() { return request; } public void setRequest(FullHttpRequest request) { this.request = request; } }
b.创建Http请求处理器
public class HttpHandler extends SimpleChannelInboundHandler<HttpRequestVo> { @Override protected void channelRead0(ChannelHandlerContext ctx, HttpRequestVo requestVo) throws Exception { FullHttpRequest request = requestVo.getRequest(); String uri = ParamUtil.getUri(request); HandleMappingVo mappingVo = ControllerContext.getInstance().getHandleMapping(uri); //没有对应映射关系,则返回400 if(mappingVo == null){ ResponseUtil.sendHttpResponse(ctx, request, ResponseUtil.get400Response()); return; } Map<String, String> params = ParamUtil.getRequestParams(request); //执行Controller中的方法 try { String content = (String) mappingVo.getMethod().invoke(mappingVo.getInstance(), request, params); ResponseUtil.sendHttpResponse(ctx, request, ResponseUtil.get200Response(content)); }catch (Exception e){ e.printStackTrace(); ResponseUtil.sendHttpResponse(ctx, request, ResponseUtil.get500Response()); } } }
c.创建请求路径映射Vo
public class HandleMappingVo { //类实例 private Object instance; //方法 private Method method; //请求路径 private String requestPath; public HandleMappingVo(Object instance, Method method, String requestPath) { this.instance = instance; this.method = method; this.requestPath = requestPath; } public Object getInstance() { return instance; } public void setInstance(Object instance) { this.instance = instance; } public Method getMethod() { return method; } public void setMethod(Method method) { this.method = method; } public String getRequestPath() { return requestPath; } public void setRequestPath(String requestPath) { this.requestPath = requestPath; } }
d.创建自定义 Controller 注解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyController { String value() default ""; }
e.创建自定义 RequestMapping 注解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyRequestMapping { String value(); }
f.创建全局上下文类
public class ControllerContext { //保存类路径的缓存 private List<String> classCache = Collections.synchronizedList(new ArrayList<String>()); //保存类实例的容器 private Map<String, Object> beanFactory = new ConcurrentHashMap<>(); //Controller方法与请求路径的映射关系 private Map<String, HandleMappingVo> handleMapping = new ConcurrentHashMap<>(); //单例 private static ControllerContext instance = new ControllerContext(); public static ControllerContext getInstance(){ return instance; } //初始化 private ControllerContext(){ String path = MyConfig.CONTROLLER_PATH; //扫描包 scanPackage(path); //注册Bean registerBean(); //MVC路径映射 mappingMVC(); } /** * 扫描包 */ private void scanPackage(final String path) { URL url = this.getClass().getClassLoader().getResource(path.replaceAll("\\.", "/")); try { File file = new File(url.toURI()); file.listFiles(new FileFilter(){ //遍历当前目录下的所有文件 @Override public boolean accept(File childFile) { //递归查找文件 if(childFile.isDirectory()){ scanPackage(path + "." + childFile.getName()); }else{ if(childFile.getName().endsWith(".class")){ String classPath = path + "." + childFile.getName().replace(".class",""); classCache.add(classPath); } } return true; } }); } catch (Exception e) { e.printStackTrace(); } } /** * 注册Bean */ private void registerBean(){ if(classCache.isEmpty()){ return; } for(String path:classCache){ try { //使用反射,通过类路径获取class 对象 Class<?> clazz = Class.forName(path); //找出需要被容器管理的类,@MyController if(clazz.isAnnotationPresent(MyController.class)){ //根据类对象,创建实例 Object instance = clazz.newInstance(); //首字母小写的类名作为默认名字 String aliasName = lowerClass(clazz.getSimpleName()); if(clazz.isAnnotationPresent(MyController.class)){ MyController controller = clazz.getAnnotation(MyController.class); if(! "".equals(controller.value())){ aliasName = controller.value(); } } if(beanFactory.get(aliasName) == null){ beanFactory.put(aliasName, instance); } } } catch (Exception e){ e.printStackTrace(); } } } /* * MVC路径映射 */ private void mappingMVC() { //遍历容器,保存Requestmapping、Contoller与请求路径的对应关系 for(Map.Entry<String,Object> entry : beanFactory.entrySet()){ Object instance = entry.getValue(); Class<?> clazz = instance.getClass(); if(clazz.isAnnotationPresent(MyController.class)){ //获取类上的路径 String classPath = ""; if(clazz.isAnnotationPresent(MyRequestMapping.class)){ MyRequestMapping clazzAnnotation = clazz.getAnnotation(MyRequestMapping.class); classPath = clazzAnnotation.value(); } //获取方法上的路径 Method[] methods = clazz.getMethods(); for(Method method : methods){ if(method.isAnnotationPresent(MyRequestMapping.class)){ MyRequestMapping methodAnnotation = method.getAnnotation(MyRequestMapping.class); if(methodAnnotation != null){ String methodPath = methodAnnotation.value(); String requestPath = classPath + methodPath; HandleMappingVo mappingVo= new HandleMappingVo(instance, method, requestPath); handleMapping.put(requestPath, mappingVo); } } } } } } /** * 首字母小写 */ private String lowerClass(String simpleName) { char[] chars = simpleName.toCharArray(); chars[0] += 32; String result = String.valueOf(chars); return result; } /** * 获取Controller方法与请求路径的映射关系 */ public HandleMappingVo getHandleMapping(String requestPath){ return handleMapping.get(requestPath); } }
g.创建测试Controller
@MyController("testController") public class TestController { @MyRequestMapping("/add") public String add(FullHttpRequest request, Map<String, String> params){ int param1 = Integer.valueOf(params.get("param1")); int param2 = Integer.valueOf(params.get("param2")); return String.valueOf(param1 + param2); } }
5.文件上传部分
a.创建文件上传请求Vo类
public class FileUploadRequestVo { private FullHttpRequest request; public FileUploadRequestVo(FullHttpRequest request) { this.request = request; } public FullHttpRequest getRequest() { return request; } public void setRequest(FullHttpRequest request) { this.request = request; } }
b.创建文件上传请求处理器
public class FileUploadHandler extends SimpleChannelInboundHandler<FileUploadRequestVo> { @Override protected void channelRead0(ChannelHandlerContext ctx, FileUploadRequestVo requestVo) throws Exception { FullHttpRequest request = requestVo.getRequest(); FileUpload fileUpload = ParamUtil.getFileUpload(request); if(fileUpload == null || ! fileUpload.isCompleted()){ ResponseUtil.sendHttpResponse(ctx, request, ResponseUtil.get400Response()); return; } String fileName = fileUpload.getFilename(); //毫秒数+.文件后缀 String newName = System.currentTimeMillis() + fileName.substring(fileName.lastIndexOf(".")); fileUpload.renameTo(new File(MyConfig.FILE_UPLOAD_ABSPATH_PREFIX + newName)); ResponseUtil.sendHttpResponse(ctx, request, ResponseUtil.get200Response(MyConfig.FILE_UPLOAD_MAPPING_URL_PREFIX + newName)); } }
6.其他
a.配置常量类
public class MyConfig { //websocket的自定义id public static final String SOCKET_ID = "userId"; //controller的路径 public static final String CONTROLLER_PATH = "com.wode.http.controller"; //文件上传请求uri public static final String FILE_UPLOAD_URL = "/upload"; //外部访问本地文件映射路径 public static final String FILE_UPLOAD_MAPPING_URL_PREFIX = "http://localhost:8090/file/"; //文件上传本地绝对路径前缀 public static final String FILE_UPLOAD_ABSPATH_PREFIX = "E:/tmp/"; }
b.请求参数工具类
public class ParamUtil { /** * 获取请求参数 */ public static Map<String, String> getRequestParams(HttpRequest request){ Map<String, String>requestParams=new HashMap<>(); // 处理get请求 if (request.method() == HttpMethod.GET) { QueryStringDecoder decoder = new QueryStringDecoder(request.uri()); Map<String, List<String>> params = decoder.parameters(); for(Map.Entry<String, List<String>> entry : params.entrySet()){ requestParams.put(entry.getKey(), entry.getValue().get(0)); } } // 处理POST请求 if (request.method() == HttpMethod.POST) { HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(new DefaultHttpDataFactory(false), request); List<InterfaceHttpData> postData = decoder.getBodyHttpDatas(); for(InterfaceHttpData data : postData){ if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) { MemoryAttribute attribute = (MemoryAttribute) data; requestParams.put(attribute.getName(), attribute.getValue()); } } } return requestParams; } /** * 获取文件上传参数 */ public static FileUpload getFileUpload(HttpRequest request){ // 处理POST请求 if (request.method() == HttpMethod.POST) { HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(new DefaultHttpDataFactory(false), request); List<InterfaceHttpData> postData = decoder.getBodyHttpDatas(); for(InterfaceHttpData data : postData){ if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.FileUpload) { FileUpload fileUpload = (FileUpload) data; return fileUpload; } } } return null; } /** * 获取请求Uri */ public static String getUri(HttpRequest request){ return new QueryStringDecoder(request.uri()).path(); } }
c.响应工具类
public class ResponseUtil { /** * 获取400响应 */ public static FullHttpResponse get400Response(){ return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); } /** * 获取200响应 */ public static FullHttpResponse get200Response(String content){ return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(content.getBytes())); } /** * 获取500响应 */ public static FullHttpResponse get500Response(){ return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR, Unpooled.wrappedBuffer("服务器异常".getBytes())); } /** * 发送HTTP响应 */ public static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) { // 返回应答给客户端 if (response.status().code() != 200) { ByteBufUtil.writeUtf8(response.content(), response.status().toString()); } //添加header描述length,避免客户端接收不到数据 response.headers().add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN); response.headers().add(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); //解决跨域的问题 response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN,"*"); response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS,"*");//允许headers自定义 response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS,"GET, POST, PUT,DELETE"); response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS,"true"); // 如果是非Keep-Alive,关闭连接 if (! HttpUtil.isKeepAlive(request) || response.status().code() != 200) { response.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); }else{ response.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); ctx.channel().writeAndFlush(response); } } }
7.测试
a.Websocket测试:前端页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Home</title> <style type="text/css"> h1{ text-align:center; } .row{ margin:10px 0px; } .col{ display: inline-block; margin:0px 5px; } .msg-container{ width: 40%; height: 500px; } .type-select-wrapper{ margin: 0 0 0 10%; } .msg-div{ width: 80%; margin:0 0 0 10%; height: 300px; border:solid 1px; overflow: scroll; } </style> </head> <body> <div> <div class="row"> <div class="col msg-container"> <div class="row"> <h1>消息窗口1</h1> </div> <div class="row"> <div class="col msg-div"></div> </div> <div class="row"> <div class="col type-select-wrapper"> <select class="type-select"> <option data-value="1">私聊</option> <option selected data-value="2">公屏</option> </select> </div> <div class="col msg-input-wrapper"> <input class="msg-input" type="text"/> </div> <div class="col"> <button id="send-btn-1">发送</button> </div> </div> </div> <div class="col msg-container"> <div class="row"> <h1>消息窗口2</h1> </div> <div class="row"> <div class="col msg-div"></div> </div> <div class="row"> <div class="col type-select-wrapper"> <select class="type-select"> <option data-value="1">私聊</option> <option selected data-value="2">公屏</option> </select> </div> <div class="col msg-input-wrapper"> <input class="msg-input" type="text"/> </div> <div class="col"> <button id="send-btn-2">发送</button> </div> </div> </div> </div> <div class="row"> <div class="col msg-container"> <div class="row"> <h1>消息窗口3</h1> </div> <div class="row"> <div class="col msg-div"></div> </div> <div class="row"> <div class="col type-select-wrapper"> <select class="type-select"> <option data-value="1">私聊</option> <option selected data-value="2">公屏</option> </select> </div> <div class="col msg-input-wrapper"> <input class="msg-input" type="text"/> </div> <div class="col"> <button id="send-btn-3">发送</button> </div> </div> </div> </div> </div> </body> <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script> <script> let userIdArray = ["张三", "李四", "王五"]; let wsArray = new Array(3); $(function(){ initWebSocketFunc(userIdArray[0], 0); initWebSocketFunc(userIdArray[1], 1); initWebSocketFunc(userIdArray[2], 2); $("#send-btn-1").on("click", {num: 0, fromId: userIdArray[0], toId: userIdArray[1]}, sendMsgFunc); $("#send-btn-2").on("click",{num: 1, fromId: userIdArray[1], toId: userIdArray[2]}, sendMsgFunc); $("#send-btn-3").on("click",{num: 2, fromId: userIdArray[2], toId: userIdArray[0]}, sendMsgFunc); }); let initWebSocketFunc = function(userId, num){ // 初始化一个 WebSocket 对象 let ws = new WebSocket("ws://localhost:8080/ws?userId=" + userId); // 建立 web socket 连接成功触发事件 ws.onopen = function () { console.log("正在建立连接..."); }; // 接收服务端数据时触发事件 ws.onmessage = function (evt) { let msg = JSON.parse(evt.data); let out; if(msg.type == 1){ out = "[私聊] " + (msg.fromId==userId?"你":msg.fromId) + " 对 " + (msg.toId==userId?"你":msg.toId) + " 说:" + msg.body; }else if(msg.type == 2){ out = "[公屏] " + (msg.fromId==userId?"你":msg.fromId) + " 说:" + msg.body; } $(".msg-div:eq(" + num + ")").append(out + "<br/>"); // $($(".msg-div")[num]).append(out + "<br/>"); }; // 断开 web socket 连接成功触发事件 ws.onclose = function () { console.log("连接已关闭..."); }; wsArray[num] = ws; }; let sendMsgFunc = function(e){ let num = e.data.num; let fromId = e.data.fromId; let toId = e.data.toId; let type = $(".type-select:eq(" + num + ")").find("option:selected").attr("data-value"); let msg = $(".msg-input:eq(" + num + ")").val(); // let type = $($(".type-select")[num]).find("option:selected").attr("data-value"); // let msg = $($(".msg-input")[num]).val(); let msgData = { type: type, fromId: fromId, body: msg }; if(type == 1){ msgData.toId = toId; } let msgStr = JSON.stringify(msgData); wsArray[num].send(msgStr); } </script> </html>
d.文件上传测试:前端页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Upload</title> <style type="text/css"> h1{ text-align:center; } .row{ margin:10px 0px; } .col{ display: inline-block; margin:0px 5px; } </style> </head> <body> <div> <div class="row"> <div class="col"> <div class="row"> <h1>文件上传</h1> </div> <div class="row"> <form id="upload" enctype="multipart/form-data" method="post"> <div class="col"> <input type="file" name="file"/> </div> <div class="col"> <button id="send-btn">发送</button> </div> </form> </div> </div> </div> </div> </body> <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script> <script> $(function(){ $("#send-btn").on("click", sendFunc); }); let sendFunc = function(){ let formData = new FormData($('#upload')[0]); $.ajax({ url:"http://localhost:8080/upload", type:"post", data:formData, async: false, cache: false, // 告诉jQuery不要去处理发送的数据 processData : false, // 告诉jQuery不要去设置Content-Type请求头 contentType : false, success:function(res){ debugger; if(res){ alert("上传成功!"); } console.log(res); }, error:function(jqXHR, textStatus, errorThrown){ debugger; alert("网络连接失败,稍后重试", errorThrown); } }); } </script> </html>
c.Http测试:访问 http://localhost:8080/add?param1=2¶m2=3,查看返回数据
8.参考文章
https://www.jianshu.com/p/56216d1052d7
https://www.bbsmax.com/A/xl56PV69zr/