WebSocket通信
- WebSocket是一种基于TCP的网络通信协议,提供了浏览器和服务器之间的全双工通信(full-duplex)能力。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。这使得数据可以更快地从服务器传到浏览器,而且减少了数据传输的数据量,因为头信息比较小。在WebSocket API中,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
- HTTP协议和WebSocket协议的主要区别如下:
- 连接方式:HTTP协议是无短连接的。每次请求都需要建立新的连接,请求结束后连接就断开。而WebSocket协议是长连接的,客户端和服务器建立连接后,直到其中一方主动断开,连接才会断开。
- 数据传输:HTTP协议只能由客户端向服务器发起请求,服务器返回响应数据。而WebSocket协议是全双工通信,服务器和客户端都可以主动发送数据。
- 性能开销:由于HTTP协议每次请求都需要建立新的连接,所以开销较大。而WebSocket协议建立连接后,可以进行多次数据传输,开销较小。
- 数据格式:HTTP协议传输的数据格式比较复杂,包含了请求行、请求头、消息体等。而WebSocket协议传输的数据通过帧来传输,数据格式比较简单。
- 实时性:HTTP协议的实时性不强,需要客户端定时轮询服务器获取新的数据。而WebSocket协议可以实现服务器主动推送数据,实时性较强。
- WebSocket主要适用于以下几种场景:
- 实时应用:聊天应用、多人协作应用、在线游戏、实时购物等。
- 实时数据推送:股票、新闻、天气、设备状态等实时信息的推送。
- IOT物联网:实时获取设备状态,实时控制设备等。
- 实时分析:实时数据分析、实时监控系统等。
WebSocket入门案例
-
客户端:
- 创建WebSocket对象:在JavaScript中,我们可以创建一个WebSocket对象,指定要连接的服务器地址。
var ws = new WebSocket("ws://localhost:8080/websocket");
- 监听事件:WebSocket对象提供了四个事件:onopen、onmessage、onerror、onclose,我们可以通过监听这些事件来处理WebSocket的各种情况。
ws.onopen = function(event) { console.log("Connection open ..."); }; ws.onmessage = function(event) { console.log("Received Message: " + event.data); }; ws.onclose = function(event) { console.log("Connection closed ..."); }; ws.onerror = function(event) { console.log("Error: " + event.data); };
- 发送数据:WebSocket对象提供了一个send方法,我们可以通过这个方法向服务器发送数据。
ws.send("Hello Server!");
- 关闭连接:当我们不再需要WebSocket连接时,可以调用WebSocket对象的close方法来关闭连接。
ws.close();
可以直接使用js写个小页面
<!DOCTYPE HTML> <html> <head> <meta charset="UTF-8"> <title>WebSocket Demo</title> </head> <body> <input id="text" type="text" /> <button onclick="send()">Send Message</button> <button onclick="closeWebSocket()">Close</button> <div id="message"> </div> </body> <script type="text/javascript"> var websocket = null; var clientId = Math.random().toString(36).substr(2); //判断当前浏览器是否支持WebSocket if('WebSocket' in window){ //连接WebSocket节点 websocket = new WebSocket("ws://localhost:8080/ws/"+clientId); } else{ alert('Not support websocket') } //连接发生错误的回调方法 websocket.onerror = function(){ setMessageInnerHTML("error"); }; //连接成功建立的回调方法 websocket.onopen = function(){ setMessageInnerHTML("连接成功"); } //接收到消息的回调方法 websocket.onmessage = function(event){ setMessageInnerHTML(event.data); } //连接关闭的回调方法 websocket.onclose = function(){ setMessageInnerHTML("close"); } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function(){ websocket.close(); } //将消息显示在网页上 function setMessageInnerHTML(innerHTML){ document.getElementById('message').innerHTML += innerHTML + '<br/>'; } //发送消息 function send(){ var message = document.getElementById('text').value; websocket.send(message); } //关闭连接 function closeWebSocket() { websocket.close(); } </script> </html>
-
服务端:
-
导入WebSocket的maven坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
-
导入WebSocket服务端组件WebSocketServer,用于与客户端通信
package com.sky.websocket; import com.sky.handler.TurnoverReportVOEncoder; import org.springframework.stereotype.Component; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.Collection; import java.util.HashMap; import java.util.Map; /** * WebSocket服务 */ @Component @ServerEndpoint(value = "/ws/{sid}",encoders = {TurnoverReportVOEncoder.class}) // 为对象指定编码器(目前是转成json发送给客户端) public class WebSocketServer { //存放会话对象 private static Map<String, Session> sessionMap = new HashMap(); /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { System.out.println("客户端:" + sid + "建立连接"); sessionMap.put(sid, session); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, @PathParam("sid") String sid) { System.out.println("收到来自客户端:" + sid + "的信息:" + message); } /** * 连接关闭调用的方法 * * @param sid */ @OnClose public void onClose(@PathParam("sid") String sid) { System.out.println("连接断开:" + sid); sessionMap.remove(sid); } /** * 群发 * * @param message */ public void sendToAllClient(String message) { Collection<Session> sessions = sessionMap.values(); for (Session session : sessions) { try { //服务器向客户端发送消息 session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } public void sendObjToAllClient(Object object) { Collection<Session> sessions = sessionMap.values(); for (Session session : sessions) { try { //服务器向客户端发送对象--注意第4步骤,需要为该对象指定一个编码器 session.getBasicRemote().sendObject(object); } catch (Exception e) { e.printStackTrace(); } } } }
-
导入配置类WebSocketConfiguration,注册WebSocket的服务端组件ServerEndpointExporter
package com.sky.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * WebSocket配置类,用于注册WebSocket的Bean */ @Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
-
如果想向客户端推送封装好的对象,在WebSocket中,需要提供一个编码器来将这个对象转换为可以通过网络传输的格式,通常是字符串或者二进制数据。
package com.sky.handler; import com.fasterxml.jackson.databind.ObjectMapper; import com.sky.vo.TurnoverReportVO; import javax.websocket.EncodeException; import javax.websocket.Encoder; import javax.websocket.EndpointConfig; /** * @projectName: sky-take-out * @package: com.sky.handler * @className: TurnoverReportVOEncoder * @author: fangjiayueyuan * @description: TODO * @date: 2023/12/24 16:16 * @version: 1.0 */ public class TurnoverReportVOEncoder implements Encoder.Text<TurnoverReportVO>{ private static ObjectMapper objectMapper = new ObjectMapper(); @Override public String encode(TurnoverReportVO turnoverReportVO) throws EncodeException { try { // 使用Jackson库将对象转换为JSON字符串 return objectMapper.writeValueAsString(turnoverReportVO); } catch (Exception e) { throw new EncodeException(turnoverReportVO, "对象转换为JSON字符串时发生错误", e); } } @Override public void init(EndpointConfig endpointConfig) { // 这里可以进行编码器的初始化操作,但在这个例子中我们不需要进行任何操作 } @Override public void destroy() { // 这里可以进行编码器的清理操作,但在这个例子中我们不需要进行任何操作 } }
-
导入定时任务类WebSocketTask,定时向客户端推送数据
package com.sky.task; import com.sky.service.ReportService; import com.sky.vo.TurnoverReportVO; import com.sky.websocket.WebSocketServer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @Component public class WebSocketTask { @Autowired private WebSocketServer webSocketServer; @Autowired private ReportService reportService; /** * 通过WebSocket每隔5秒向客户端发送消息 */ @Scheduled(cron = "0/5 * * * * ?") public void sendMessageToClient() { webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now())); } /** * 通过WebSocket每隔5秒向客户端发送消息 */ @Scheduled(cron = "0/5 * * * * ?") public void sendObjMessageToClient() { TurnoverReportVO turnoverStatistics = reportService.getTurnoverStatistics(LocalDate.parse("2023-01-01"), LocalDate.now()); webSocketServer.sendToAllClient("传个对象过去"); webSocketServer.sendObjToAllClient(turnoverStatistics); } }
-
RPC通信
**RPC(Remote Procedure Call)**是一种通信协议,它允许运行在一台计算机上的程序调用另一台计算机上的程序中的函数或方法,就像调用本地函数一样,无需程序员显式处理底层的网络细节。
RPC的主要特征包括:
-
透明性:对于调用者来说,远程过程调用和本地过程调用是透明的,调用者无需关心过程调用的是本地过程还是远程过程。
-
语言无关性:RPC通常支持多种编程语言,只要两个通信的程序遵循同一RPC协议,它们就可以进行通信,无论它们是用什么编程语言编写的。
-
同步性:RPC通常是同步的,也就是说,当一个RPC调用发出后,调用者会停止执行,直到得到结果。然而,也有一些RPC系统支持异步调用。
为什么使用RPC:
-
简化分布式系统的开发:RPC隐藏了底层的网络通信和数据传输的复杂性,使得开发分布式应用更加简单。
-
提高代码的可重用性:通过RPC,可以将一些通用的功能实现为服务,然后在多个应用中重用这些服务。
-
提高系统的可扩展性:通过RPC,可以将一个大的系统分解为多个可以独立开发和部署的小的服务。
RPC的替代方案:
- RESTful API:RESTful API是一种基于HTTP协议的通信方式,它使用HTTP的方法(如GET、POST、PUT、DELETE等)来操作资源。RESTful API比RPC更简单,更易于使用,但它不如RPC灵活,因为它只能使用HTTP协议,而RPC可以使用任何传输协议。
- 消息队列:消息队列是一种异步的通信方式,它允许程序通过发送和接收消息来进行通信。消息队列可以解耦发送者和接收者,使得它们可以独立地扩展和失败。然而,消息队列的使用比RPC更复杂,因为它需要处理消息的发送、接收、存储和确认。
RPC入门案例
以Thrift为例:
-
定义数据类型和服务接口:使用Thrift的IDL(接口定义语言)定义数据类型和服务接口,然后通过Thrift的编译器生成对应语言的代码。
namespace java com.sankuai.mdp.thrift struct User{ 1:i32 id 2:string name 3:i32 age=0 } service UserService{ User getById(1:i32 id) bool isExist(1:string name) }
-
通过Thrift编译器生成Java代码:会生成两个对象:User、UserService
thrift --gen java HelloWorld.thrift
-
服务端代码,实现UserService.Iface接口;启动服务端.
package com.sankuai.mdp.thriftserversnapshot.service.impl; import com.sankuai.mdp.thriftapisnapshot.entity.User; import com.sankuai.mdp.thriftapisnapshot.entity.UserService; import org.apache.thrift.TException; /** * @projectName: thrift-api-snapshot * @package: com.sankuai.mdp.thriftserversnapshot.service.impl * @className: UserServiceImpl * @author: fangjiayueyuan * @description: TODO * @date: 2023/12/17 21:33 * @version: 1.0 */ public class UserServiceImpl implements UserService.Iface{ @Override public User getById(int id) throws TException { System.out.println("-----调用getById-----"); User user = new User(); user.setId(id); user.setName("dog"); user.setAge(18); return user; } @Override public boolean isExist(String name) throws TException { return false; } }
package com.sankuai.mdp.thriftserversnapshot.service.impl; import com.sankuai.mdp.thriftapisnapshot.entity.UserService; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.server.TServer; import org.apache.thrift.server.TSimpleServer; import org.apache.thrift.transport.TServerSocket; import org.apache.thrift.transport.TServerTransport; import org.apache.thrift.transport.TTransportException; /** * @projectName: thrift-api-snapshot * @package: com.sankuai.mdp.thriftserversnapshot.service.impl * @className: SimpleService * @author: fangjiayueyuan * @description: TODO * @date: 2023/12/17 21:59 * @version: 1.0 */ public class SimpleService { public static void main(String[] args) { try{ TServerTransport serverTransport = new TServerSocket(9090); UserService.Processor processor = new UserService.Processor(new UserServiceImpl()); TBinaryProtocol.Factory protocolFactory = new TBinaryProtocol.Factory(); TSimpleServer.Args targs = new TSimpleServer.Args(serverTransport); targs.processor(processor); targs.protocolFactory(protocolFactory); TServer server = new TSimpleServer(targs); server.serve(); } catch (TTransportException e) { throw new RuntimeException(e); } } }
-
客户端代码,调用服务端的方法,就像调用本地方法一样
package com.sankuai.mdp.thriftclientsnapshot.service.impl; import com.sankuai.mdp.thriftapisnapshot.entity.User; import com.sankuai.mdp.thriftapisnapshot.entity.UserService; import org.apache.thrift.TException; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.transport.TSocket; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TTransportException; /** * @projectName: thrift-api-snapshot * @package: com.sankuai.mdp.thriftclientsnapshot.service.impl * @className: SimpleClient * @author: fangjiayueyuan * @description: TODO * @date: 2023/12/17 21:58 * @version: 1.0 */ public class SimpleClient { public static void main(String[] args) { TTransport transport = null; try { transport = new TSocket("localhost", 9090); TBinaryProtocol protocol = new TBinaryProtocol(transport); UserService.Client client = new UserService.Client(protocol); transport.open(); User result = client.getById(1); System.out.println("Result:" + result); } catch (TTransportException e) { e.printStackTrace(); } catch (TException e) { throw new RuntimeException(e); } finally { if (transport != null) { transport.close(); } } } }
-
先后启动运行服务端SimpleService.java、客户端代码SimpleClient.java
Git
Http通信
http通信也可以实现微服务远程调用,但是是短连接的通信方式
-
短连接:短连接是指建立连接后,客户端与服务器进行一次请求和响应后,立即断开连接。短连接的优点是管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段。但缺点是每次请求都需要重新建立连接,会增加额外的开销。
-
长连接:长连接是指建立连接后,客户端与服务器可以进行多次请求和响应,直到其中一方主动断开连接。长连接的优点是可以省去频繁建立连接的开销,提高数据传输的效率。但缺点是需要额外的控制手段来管理和维护连接,如果连接数过多,可能会消耗较多的系统资源。
常用的Http微服务调用有使用RestTemplate类,RestTemplate和Thrift是两种不同的远程调用方式,它们的主要区别如下:
- 协议不同:RestTemplate是基于HTTP/HTTPS协议的,数据格式通常为JSON,适用于Web服务。而Thrift是Facebook开发的一种高效的、支持多种编程语言的远程服务调用框架,它使用了自定义的二进制协议,效率更高。
- 数据格式不同:RestTemplate通常使用JSON格式传输数据,而Thrift使用的是二进制格式,因此在网络传输效率上,Thrift会更高。
- 使用场景不同:RestTemplate更适合构建微服务架构,因为它基于HTTP/HTTPS协议,更符合互联网的开放、无状态、可连接性特性。而Thrift则更适合在内网中使用,因为它的二进制协议可以提供更高的性能。
- 跨语言支持:RestTemplate基于HTTP协议,可以被任何支持HTTP的语言调用。而Thrift提供了多种语言的库,可以方便的在不同语言之间进行调用。
- 服务发现:RestTemplate需要自己管理服务的URL,而Thrift则可以配合使用服务注册与发现的工具,如Zookeeper,进行服务的自动发现和负载均衡。