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/