RPC通信协议远程服务调用(25)Java全栈
【RPC】
主要內容
學習目標
| 項目架構變化 | 掌握 |
| RPC簡介 | 掌握 |
| RMI實現RPC | 掌握 |
| HttpClient實現RPC | 了解 |
| Zookeeper安裝 | 掌握 |
| Zookeeper客戶端常用命令 | 掌握 |
| 向Zookeeper中注冊內容 | 掌握 |
| 從zookeeper中發現內容 | 掌握 |
| 手寫RPC框架 | 掌握 |
1 項目架構變化
??現在學習RPC。因為后期學習的Dubbo是一個RPC框架,學習好現在的內容,學習Dubbo將會變得容易一些。
1.1 單體架構
??1. 架構圖
????單體架構就是一個項目里面包含這個項目中全部代碼。一個應用搞定全部功能。
????DNS 服務器可以是單映射,也可以配置多個映射。
??2. 軟件代碼結構
????在單體架構項目中,團隊都是通過包(package)進行區分每個模塊。
????總體包結構:com.uestc.*.分層包。
項目名:-- com--uestc-- common-- utils--user-- controller-- service-- mapper-- sys-- controller-- service-- mapper??3. 優缺點
????優點:(1)部署簡單;(2)維護方便;(3)成本低。
????缺點:當項目規模大、用戶訪問頻率高、并發量大、數據量大時,會大大降低程序執行效率,甚至出現服務器宕機等情況。
??4. 適用項目:傳統管理項目,小型互聯網項目。
1.2 分布式架構
??1. 架構圖(簡易版)
??分布式架構會把一個項目按照特定要求(多按照模塊或功能)拆分成多個項目,每個項目分別部署到不同的服務器上。
??2. 軟件代碼結構
項目1:--com.uestc.xxx-- controller-- service-- mapper 項目2--com.uestc.mmm-- controller-- service-- mapper??3. 優缺點
????優點:(1)增大了系統可用性。減少單點故障,導致整個應用不可用;(2)增加重用性。因為模塊化,所以重用性更高;(3)增加可擴展性。有新的模塊增加新的項目即可;(4)增加每個模塊的負載能力。因為每個模塊都是一個項目,所以每個模塊的負載能力更強。
????缺點:(1)成本更高;(2)架構更加復雜(3)整體響應之間變長,一些業務需要多項目通信后給出結果;(4)吞吐量更大。吞吐量= 請求數/秒。
??4. 待解決問題:分布式架構中各個模塊如何進行通信?
????可以使用Http協議,也可以使用RPC協議通信,也可以使用其他的通信方式。我們本階段使用的是RPC協議,因為它比HTTP更適合項目內部通信。
2 RPC簡介
2.1 RFC
??RFC(Request For Comments) 是由互聯網工程任務組(IETF)發布的文件集。文件集中每個文件都有自己唯一編號,例如:rfc1831。目前RFC文件由互聯網協會(Internet Society,ISOC)贊助發型。
??RPC就收集到了rfc 1831中。可以通過下面網址查看:
??https://datatracker.ietf.org/doc/rfc1831/
2.2 RPC
??RPC在rfc 1831中收錄 ,RPC(Remote Procedure Call) 遠程過程調用協議
??RPC協議規定允許互聯網中一臺主機程序調用另一臺主機程序,而程序員無需對這個交互過程進行編程。在RPC協議中強調當A程序調用B程序中功能或方法時,A是不知道B中方法具體實現的。
??RPC是上層應用層協議,底層可以基于TCP協議,也可以基于HTTP協議。一般我們說RPC都是基于RPC的具體實現,如:Dubbo框架。從廣義上講只要是滿足網絡中進行通訊調用都統稱為RPC,甚至HTTP協議都可以說是RPC的具體實現,但是具體分析看來RPC協議要比HTTP協議更加高效,基于RPC的框架功能更多。
??RPC協議是基于分布式架構而出現的,所以RPC在分布式項目中有著得天獨厚的優勢。
2.3 RPC和HTTP對比
(1)具體實現
??RPC:可以基于TCP協議,也可以基于HTTP協議。
??HTTP:基于HTTP協議。
(2)效率
??RPC:自定義具體實現可以減少很多無用的報文內容,使得報文體積更小。
??HTTP:如果是HTTP 1.1 報文中很多內容都是無用的。如果是HTTP2.0以后和RPC相差不大,比RPC少的可能就是一些服務治理等功能。
(3)連接方式
??RPC:長連接支持。
??HTTP:每次連接都是3次握手。
(4)性能
??RPC可以基于很多序列化方式。如:thrift
??HTTP 主要是通過JSON,序列化和反序列效率更低。
(5)注冊中心
??RPC :一般RPC框架都帶有注冊中心。
??HTTP:都是直連。
(6)負載均衡
??RPC:絕大多數RPC框架都帶有負載均衡測量。
??HTTP:一般都需要借助第三方工具。如:nginx
(7)綜合結論
??RPC框架一般都帶有豐富的服務治理等功能,更適合企業內部接口調用。而HTTP更適合多平臺之間相互調用。
3 HttpClient實現RPC
3.1 HttpClient簡介
??在JDK中java.net包下提供了用戶HTTP訪問的基本功能,但是它缺少靈活性或許多應用所需要的功能。
??HttpClient起初是Apache Jakarta Common 的子項目。用來提供高效的、最新的、功能豐富的支持 HTTP 協議的客戶端編程工具包,并且它支持 HTTP 協議最新的版本。2007年成為頂級項目。
??通俗解釋:HttpClient可以實現使用Java代碼完成標準HTTP請求及響應。
3.2 代碼實現
(1)服務端
??新建項目HttpClientServer
(2)新建控制器
??com.uestc.controller.DemoController
(3)新建啟動器
??新建啟動器:com.msb.HttpClientServerApplication
@SpringBootApplication public class HttpClientServerApplication {public static void main(String[] args) {SpringApplication.run(HttpClientServerApplication.class,args);} }(4)客戶端
??新建HttpClientDemo項目
(5)添加依賴
<dependencies><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.10</version></dependency> </dependencies>(6)新建類
??新建com.uestc.HttpClientDemo,編寫主方法。
(7)使用GET方法訪問
public static void main(String[] args) {try {//創建http工具(理解成:瀏覽器) 發起請求,解析響應CloseableHttpClient httpClient = HttpClients.createDefault();//請求路徑URIBuilder uriBuilder = new URIBuilder("http://localhost:8080/demo");uriBuilder.addParameter("param", "get123");//創建HttpGet請求對象HttpGet get = new HttpGet(uriBuilder.build());//創建響應對象CloseableHttpResponse response = httpClient.execute(get);//由于響應體是字符串,因此把HttpEntity類型轉換為字符串類型,并設置字符編碼String result = EntityUtils.toString(response.getEntity(), "utf-8");//輸出結果System.out.println(result);//釋放資源response.close();httpClient.close();} catch (URISyntaxException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} }(8)使用POST方式訪問
public class HttpClientDemo {public static void main(String[] args) {try {//創建http工具(理解成:瀏覽器) 發起請求,解析響應CloseableHttpClient httpClient = HttpClients.createDefault();//創建HttpPOST請求對象HttpPost post = new HttpPost("http://localhost:8080/demo");//所有請求參數List<NameValuePair> params = new ArrayList<>();params.add(new BasicNameValuePair("param","123"));//創建HttpEntity接口的文本實現類的對象,放入參數并設置編碼HttpEntity httpEntity = new UrlEncodedFormEntity(params,"utf-8");//放入到HttpPost對象中post.setEntity(httpEntity); //創建響應對象CloseableHttpResponse response = httpClient.execute(post);//由于響應體是字符串,因此把HttpEntity類型轉換為字符串類型String result = EntityUtils.toString(response.getEntity());//輸出結果System.out.println(result);//釋放資源response.close();httpClient.close();} catch (IOException e) {e.printStackTrace();}} }3.3 Jackson用法
??簡單來說,從服務器上寫好的對象或者對象集合想要發送給瀏覽器,就要轉化為json字符串,而瀏覽器中的數據發送給服務器也要從json字符串解析為對象或者對象集合以供后續使用。
??1. 把對象轉換為json字符串
ObjectMapper objectMapper = new ObjectMapper(); People peo = new People(); objectMapper.writeValueAsString(peo);??2. 把json字符串轉換為對象
ObjectMapper objectMapper = new ObjectMapper(); People peo = objectMapper.readValue(content, People.class);??3. 把json字符串轉換為List集合
ObjectMapper objectMapper = new ObjectMapper(); JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, People.class); List<People> list = objectMapper.readValue(content, javaType);3.4 HttpClient請求包含JSON
public class HttpClientDemo {public static void main(String[] args) {try {CloseableHttpClient httpClient = HttpClients.createDefault();HttpPost post = new HttpPost("http://localhost:8080/demo");HttpEntity httpEntity= null; String json = "{}";StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);post.setEntity(entity);CloseableHttpResponse response = httpClient.execute(post);String result = EntityUtils.toString(response.getEntity());System.out.println(result);response.close();httpClient.close();} catch (IOException e) {e.printStackTrace();}} }3.5 控制器接口參數
??@RequestBody把請求體中流數據轉換為指定的對象。多用在請求參數是json數據且請求的Content-Type=”application/json”(注意:流數據是指,除了文本類型之外的數據都統稱為流數據)
@RequestMapping("/demo4") @ResponseBody public String demo4(@RequestBody List<People> list) {System.out.println(list);return list.toString(); }3.6 Ajax發送json參數寫法
var json = '[{"id":123,"name":"msb"},{"id":123,"name":"mashibing"}]';$.ajax({url:'/demo5',type:'post',success:function(data){alert(data);for(var i = 0 ;i<data.length;i++){alert(data[i].id +" "+data[i].name);}},contentType:'application/json',//請求體中內容類型dataType:'json',//響應內容類型。data:json});3.7 跨域
??跨域:協議、ip、端口中只要有一個不同就是跨域請求。
??同源策略:瀏覽器默認只允許ajax訪問同源(協議、ip、端口都相同)內容。
??解決同源策略:在控制器接口上添加@CrossOrigin。表示允許跨域。本質在響應頭中添加Access-Control-Allow-Origin: *
var json = '[{"id":123,"name":"msb"},{"id":456,"name":"mashibing"}]';$.ajax({url:'/demo5',type:'post',success:function(data){alert(data);for(var i = 0 ;i<data.length;i++){alert(data[i].id +" "+data[i].name);}},contentType:'application/json',//請求體中內容類型dataType:'json',//響應內容類型。data:json});4 RMI實現RPC
4.1 RMI簡介
??RMI(Remote Method Invocation) 遠程方法調用。
??RMI是從JDK1.2推出的功能,它可以實現在一個Java應用中可以像調用本地方法一樣調用另一個服務器中Java應用(JVM)中的內容。
??RMI 是Java語言的遠程調用,無法實現跨語言。
4.2 執行流程
??Registry(注冊表)是放置所有服務器對象的命名空間。 每次服務端創建一個對象時,它都會使用bind()或rebind()方法注冊該對象。 這些是使用稱為綁定名稱的唯一名稱注冊的。
??要調用遠程對象,客戶端需要該對象的引用。即通過服務端綁定的名稱從注冊表中獲取對象(lookup()方法)。
4.3 API介紹
(1)Remote
??java.rmi.Remote 定義了此接口為遠程調用接口。如果接口被外部調用,需要繼承此接口。
public interface Remote{}(2)RemoteException
??java.rmi.RemoteException
??繼承了Remote接口的接口中,如果方法是允許被遠程調用的,需要拋出此異常。
(3)UnicastRemoteObject
??java.rmi.server.UnicastRemoteObject
??此類實現了Remote接口和Serializable接口。
??自定義接口實現類除了實現自定義接口還需要繼承此類。
(4)LocateRegistry
??java.rmi.registry.LocateRegistry
??可以通過LocateRegistry在本機上創建Registry,通過特定的端口就可以訪問這個Registry。
(5)Naming
??java.rmi.Naming
??Naming定義了發布內容可訪問RMI名稱。也是通過Naming獲取到指定的遠程方法。
4.4 代碼實現
4.4.1 服務器端
(1)服務端創建
??創建RmiServer項目
(2)編寫接口
??com.uestc.service.DemoService 編寫
public interface DemoService extends Remote {String demo(String demo) throws RemoteException; }(3)編寫實現類
??com.uestc.service.impl.DemoServiceImpl 編寫。
??注意:構造方法是public的。默認生成protected
public class DemoServiceImpl extends UnicastRemoteObject implements DemoService {public DemoServiceImpl() throws RemoteException {}@Overridepublic String demo(String demo) throws RemoteException {return demo+"123";} }(4)編寫主方法
??編寫com.uestc.DemoServer類,生成主方法
public class DemoServer {public static void main(String[] args) {try{//創建接口實例DemoService demoService = new DemoServiceImpl();//創建注冊表LocateRegistry.createRegistry(8989);//綁定服務Naming.bind("rmi://localhost:8989/demoService", demoService);System.out.println("服務器啟動成功");}catch(Exception e ){e.printStackTrace();}} }(5)運行項目
??運行后項目,項目一直處于啟動狀態,表示可以遠程訪問此項目中的遠程方法。
4.4.2 客戶端
(1)創建客戶端代碼
??創建項目RmiClient
(2)復制服務端接口
??把服務端com.uestc.service.DemoService粘貼到項目中
(3)創建主方法類
??新建com.uestc.DemoClient
public class ClientDemo {public static void main(String[] args) {try {DemoService demoService =(DemoService)Naming.lookup("rmi://localhost:8989/demoService");String result = demoService.demo("jayden ");System.out.println(result);} catch (NotBoundException e) {e.printStackTrace();} catch (MalformedURLException e) {e.printStackTrace();} catch (RemoteException e) {e.printStackTrace();}} }(4)運行項目然后用瀏覽器訪問查看結果。
5 Zookeeper安裝
5.1 Zookeeper簡介
??zookeeper分布式管理軟件。常用它做注冊中心(依賴zookeeper的發布/訂閱功能)、配置文件中心、分布式鎖配置、集群管理等。
??zookeeper一共就有兩個版本。主要使用的是java語言寫的。
5.2 安裝
(1)使用你虛擬機或者云服務器的linux服務器上傳壓縮文件
??到官網https://zookeeper.apache.org/找到download下載zookeeper壓縮包apache-zookeeper-3.5.5-bin.tar.gz,然后上傳到 /usr/local/tmp中。
(2)解壓
tar zxf apache-zookeeper-3.5.5-bin.tar.gz cp -r apache-zookeeper-3.5.5-bin ../zookeeper(3)新建data目錄
??進入到zookeeper中
cd /usr/local/zookeeper mkdir data(4)修改配置文件
??進入conf中
cd conf cp zoo_sample.cfg zoo.cfg vim zoo.cfg??修改dataDir為data文件夾路徑
dataDir=/usr/local/zookeeper/data(5)啟動zookeeper
??進入bin文件夾
cd /usr/local/zookeeper/bin ./zkServer.sh start??通過status查看啟動狀態。稍微有個等待時間
./zkServer.sh status5.3 Zookeeper客戶端常用命令
??進入到./zkCli.sh命令行工具后,可以使用下面常用命令
1 ls
ls [-s][-R] /path??-s 詳細信息,替代老版的ls2
??-R 當前目錄和子目錄中內容都羅列出來
??例如:ls -R / 顯示根目錄下所有內容
2 create
create /path [data]??[data] 包含內容
??創建指定路徑信息
??例如:create /demo 創建/demo
3 get
get [-s] /path??[-s] 詳細信息
??查看指定路徑下內容。
??例如: get -s /demo
??null:存放的數據
??cZxid:創建時zxid(znode每次改變時遞增的事務id)
??ctime:創建時間戳
??mZxid:最近一次更新的zxid
??mtime:最近一次更新的時間戳
??pZxid:子節點的zxid
??cversion:子節點更新次數
??dataversion:節點數據更新次數
??aclVersion:節點ACL(授權信息)的更新次數
??ephemeralOwner:如果該節點為ephemeral節點(臨時,生命周期與session一樣), ephemeralOwner值表示與該節點綁定的session id. 如果該節點不是ephemeral節點, ephemeralOwner值為0.
??dataLength:節點數據字節數
??numChildren:子節點數量
4 set
set /path data??設置節點內容
5 delete
delete /path??刪除節點
5.4 向Zookeeper中注冊內容
??新建項目ZookeeperClient
(1)創建/demo
??使用zookeeper的客戶端命令工具創建/demo
./zkCli.sh create /demos(2)添加依賴
<dependencies><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.5.5</version></dependency> </dependencies>(3)編寫代碼
??創建類com.uestc.MyApp。
??ZooDefs.Ids.OPEN_ACL_UNSAFE 表示權限,具體完全開放的ACL,任何連接的客戶端都可以操作該屬性znode。
??CreateMode.PERSISTENT_SEQUENTIAL 永久存儲,文件內容編號遞增。
public static void main(String [] args){try {ZooKeeper zookeeper = new ZooKeeper("192.168.32.128:2181", 10000, new Watcher() {@Overridepublic void process(WatchedEvent watchedEvent) {System.out.println("獲取連接");}});String content = zookeeper.create("/demo/nn", "content".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);System.out.println("content"+content);} catch (IOException e) {e.printStackTrace();} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();} }(4)查看上傳數據
? ls -R / :查看列表? get /demo/nn0000000002 :查看內容5.5 從zookeeper中發現內容
??在原有項目中新建一個類,類中編寫主方法。
public static void main(String[] args) {try {ZooKeeper zookeeper = new ZooKeeper("192.168.32.128:2181", 10000, new Watcher() {@Overridepublic void process(WatchedEvent watchedEvent) {System.out.println("獲取連接");}});//獲取列表List<String> list = zookeeper.getChildren("/demo", false);for (String child : list) {byte[] result = zookeeper.getData("/demo/" + child, false, null);System.out.println(new String(result));}} catch (IOException e) {e.printStackTrace();} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();} }6 手寫RPC框架
??使用Zookeeper作為注冊中心,RMI作為連接技術,手寫RPC框架。
6.1 創建項目ParentDemo
??創建父項目ParentDemo。
??包含4個聚合子項目。
??pojo: service和consumer中需要的實體類
??service:包含被serviceimpl和consumer依賴的接口。
??provider:provider中寫兩個方法,一個是實現service的接口,第二個是作為服務器向zookeeper注冊內容,并且用Naming綁定zookeeper內容和具體實現的實現類。
??consumer:消費者,調用服務內容。
6.2 創建pojo項目
??略,就是寫一個可能用到的實力類,記得這個類要實現序列化Serializable。
6.2 創建service項目
??主要就是編寫服務器provider需要實現的接口集合,之后服務器只需要針對service實現各個接口即可,方便管理。這里要用到pojo,所以要在依賴中依賴pojo項目。
<dependencies><dependency><groupId>com.uestc</groupId><artifactId>pojo</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>??創建com.uestc.DemoService
public interface MyPersonService extends Remote {// 找到所有的Personpublic List<Person> findAll() throws RemoteException; }6.3 創建provider項目
??此項目編寫service項目中接口的具體實現,RMI服務發布和把信息發送到Zookeeper中。
??項目結構如下:
(1)在pom.xml中添加對service項目的依賴
<dependencies><dependencies><dependency><groupId>com.uestc</groupId><artifactId>service</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.5.7</version></dependency></dependencies> </dependencies>(2)創建接口的實現類DemoServiceImpl,實現service的接口方法
??創建com.uestc.service.impl.DemoServiceImpl
public class MyPersonServiceImpl extends UnicastRemoteObject implements MyPersonService {public MyPersonServiceImpl() throws RemoteException {}public List<Person> findAll() throws RemoteException {List<Person> personList = new ArrayList<Person>();personList.add(new Person(1, "jayden"));personList.add(new Person(2, "kim"));return personList;} }(3)創建RmiRun
??創建com.uestc.ProviderRun。實現RMI服務的發布和Zookeeper消息的發布。
6.4 創建consumer項目
??新建consumer項目,此項目需要從zookeeper中獲取rmi信息,并調用rmi服務
(1)在pom.xml中添加對service項目的依賴,因為service已經依賴過pojo項目了,所以根據繼承Consumer項目也依賴了pojo項目
<dependencies><dependency><groupId>com.uestc</groupId><artifactId>service</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.6.6</version></dependency><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.5.7</version></dependency></dependencies>(2) 創建接口和實現類
??創建com.uestc.service.PersonService接口
??創建com.uestc.service.impl.PersonServiceImpl實現類
public interface PersonService {public List<Person> show(); } @Service public class PersonServiceImpl implements PersonService {public List<Person> show() {try {ZooKeeper zooKeeper = new ZooKeeper("82.156.182.26:2181", 100000, new Watcher() {public void process(WatchedEvent watchedEvent) {System.out.println("鏈接成功");}});byte[] result = zooKeeper.getData("/rpc/provider", false, null);MyPersonService myPersonService = (MyPersonService) Naming.lookup(new String(result));return myPersonService.findAll();} catch (Exception e) {e.printStackTrace();}return null;} }(3)創建控制器
??創建com.uestc.controller.PersonController控制器
@Controller public class PersonController {@Autowiredprivate PersonService personService;@RequestMapping("/show")@ResponseBodypublic List<Person> show(){return personService.show();} }(4)創建啟動器
??創建com.uestc.ConsumerApplication
@SpringBootApplication public class ConsumerApplication {public static void main(String[] args) {SpringApplication.run(ConsumerApplication.class,args);} }6.5 測試
??在瀏覽器輸入:http://localhost:8080/show。
??觀察是否返回相應的json對象List
??注意provider如果第二次啟動可能會出錯,顯示KeeperErrorCode = NodeExists for /rpc/provider,這個時候需要在zookeeper將這個/rpc/provider路徑刪除了。
總結
以上是生活随笔為你收集整理的RPC通信协议远程服务调用(25)Java全栈的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: html5 postMessage解决跨
- 下一篇: Jess学习基础(二)