berry64
本帖最后由 berry64 于 2021-3-12 03:52 编辑

回到目录




第12章: 获取HTTP信息&多线程


什么是HTTP?
HTTP (Hyper Text Transfer Protocol) 就是我们平时上网时浏览器与网站服务器交流的协议,因为本教程再怎么说也是教插件的所以如果想要仔细了解http的具体内容请自行搜索.


为什么插件会要用到http?
在网络上有很多方便的API, 比如spiget, 或者是开发者自己编写的其他RESTAPI, 甚至有些微软Azure, 谷歌Tensor以及github 的人工智能的应用有不少都可以使用HTTP 的方式来调用,所以如果你的插件会要用到这些功能的话就需要使用HTTP。

目前在网上很多java的http客户端,比如Apache的http-client, MC使用的netty,还有(好像是?)java9里新加入的httpapi,不过为了方便学习&兼容性&我懒&一般插件其实用不到那么复杂的http,本教程将会使用java里面自带的旧的(url connection) API. 并且





那么赶快开始吧!
首先我们要了解一下HTTP 的几种常见类型以及一般插件会使用到的其他功能:
1. GET 请求  -  这大概是网络上最常见的请求了,顾名思义就是get(获得)一些资料,这个请求不可以附带body(等会会讲)
2. POST - 理论上是用于创建资源的请求,这个请求可以附带body,也就是附加资料(插件大多会是以JSON形式)
3. PUT - 与post同样,不过是用来更新资源的
4. DELETE - 用于删除资源
还有很多不同请求,这个网上都有的

虽然是这样定义的。。。但是众所周知软性规矩基本上是没有人遵守的,所以一般理解为GET就是用于不需要客户端/插件提供数据的,post/put/delete就是需要插件提供数据的,如果你用的api没有遵循具体的这些请求类型的规定就...蛮正常的。

还有返回值一些常用的:
- 200 正确
- 400 客户端错误
- 喜闻乐见的404是不存在的
- 500 服务器错误
- 302 重定向
- 418 划重点 这个要考

最后一个就是HTTP的Header, 不管是GET还是任何其他请求都可以提供Header, 这个header一般用于提供关于这个请求的资料比如:
- User-Agent 我们在用什么东西发送这个请求?
- Content-Type 如果是有内容的请求,我们的请求内容是什么格式的?
- Accepts 我们希望什么样的回复?
还有很多很多,甚至很多时候登陆/注册账号的时候都是把资料放在这个header里面,不过具体要看你服务器的要求了




所以Java代码呢?
先放一大段
  1. HttpURLConnection con = (HttpURLConnection) new URL("http://jsonplaceholder.typicode.com/todos/1").openConnection(); //先创建一个http连接
  2. con.setRequestMethod("GET"); //设置类型为GET
  3. con.setConnectTimeout(6000); //设置连接超时限制为6*1000毫秒
  4. con.setRequestProperty("Accept", "application/json"); //告诉服务器我们需要json类型的回复
  5. con.setRequestProperty("User-Agent", "PaintingWithPressurePlate"); //告诉服务器我们是用什么东西在看
  6. con.connect(); //连接!
  7. </font>
复制代码
然后我们再读取!

  1. if(con.getStatus() == 200) //只有200的值才说明我们的请求正确完成了,否则服务器那边可能除了一些问题
  2.     result = con.getInputStream();
  3. else
  4.    result = con.getErrorStream(); //问题内容(根据服务器返回这个有可能是空的)
  5. </font>
复制代码
这时候细心又口无遮拦的你可能发现了两个问题:
1. 如果是https怎么办
2. 读取回来个stream要老子怎么搞mmp

1. 如果我们查阅Java内部代码可以发现java的HttpsURLConnection 实际上是HttpUrlConnection的子类,也就是说Java自动会帮我们处理HTTPS的TLS以及其他加密步骤。
2. Stream 是为了增加灵活性,因为不是所有时候http的返回值都是一次完成的,有时候可能分开成好几段(跟你下载盗版游戏压缩包好几个一个意思),有时候可能是实时的(直播的时候)不过一般mc不太会用到,所以在这里提供一个简易的把stream读取成字符串string的方法,这个就不解释了,如果想学习可以自己慢慢品:


这个时候如果你运行 StreamUtils.getStringFromStream(con.getInputStream()); 你获得的就会是服务器的返回内容啦!
如果需要json... 自行搜索GSON, 阿里巴巴的fastjson, apache也有json,自己去玩吧反正现在有string了[br]

那么如何像服务器发送post这种带内容的请求呢?
  1. con.setRequestMethod("POST"); //把原来的GET改掉就好了
  2. con.setDoOutput(true); //然后这里要这样设置一下告诉程序我们需要输出(发送给服务器)一些东西
复制代码
那么内容就是这样发送:
  1. String data = "{ "Hello": "From the other side"}"
  2. con.getOutputStream().write(data.toBytes(StandardCharsets.UTF_8)); //把string转换成utf8的字节直接发送就ok
复制代码
到这里聪明又粗鲁的你可能又发现了一个问题:nm这个http我要是无法连接老子服务器就会卡6秒有个p用

秘技:多线程左右横跳
众所周知mc服务器是一个对多线程非常友好的程序,比如基本上所有东西都是同一个线程运行的,也就是说如果java再等待你的http请求回复的时候是不会进行服务器的计算的,就像你盯着寒假作业就算有时间也不会写一样,服务器会空空等着http请求结束才会继续运算。一般http请求都很快,几十毫秒就结束了所以服务器上并不会感觉到太大的卡顿,可是当网络掉链子的时候,服务器就会一直等6秒(我们刚刚设置的)之后才会意识到这个请求发不出去,然后丢出异常。


这个问题很容易解决,就像如果你学会影分身之后就可以让影分身替你去玩游戏,然后不太聪明的你自己写寒假作业一样,如果我们再搞一个线程(也就是程序运行的线路)让它去等着http结果,然后当等到了结果之后再回来运行。

到这里,你可能想起来我之前教程讲过的BukkitRunnable. 当然这里完全可以用BukkitRunnable来实现不过我们今天要讲的是BukkitRunnable基于的Java自带的Runnable类。在Java里实现多线程有很多办法,不过本教程将只会解释利用Runnable与new Thread的方法。先上一个模板:


  1. class MyClass implements Runnable {
  2.     private Thread t = null;

  3.     @Override
  4.     public void run(){
  5.           // 这里就是运行的代码
  6.     }

  7.     public void start() {
  8.         if (t == null) {
  9.            t = new Thread(this, "Thread-Pro-Plus");
  10.            t.start();
  11.         }
  12.      }
  13. }
复制代码

拆开解释一下:在第一行我们创建了一个继承Runnable接口的类,这个类指明了一个方法叫run(); 没错就和bukkitrunnable里面的是一模一样的。
然后为了我们自己方便我们创建了一个start()方法,在这个方法里我们:
1. 检测了我们没用尝试多线程一个多线程(是不是不合理,那就对了所以不能这么搞)
2. 如果没有,那么创建一个新的线程
3. 开始运行这个线程
这样操作之后就就可以使用new MyClass().start()运行了,这时候服务器就像这样了:

可以看到现在虽然说加了一个步骤,但是可以肯定new MyClass().start()的运行时间绝对比发送并等待http快得多,而且所有等待都会同步进行。所以我们就把刚刚那个HTTP 的代码放到这个run里面来再给他搞一个构造函数,这个样子
  1. class MyClass implements Runnable {
  2.     private Thread t = null;
  3.     private String url;
  4.     public MyClass(String URL) {
  5.         this.url = URL;
  6.     }

  7.     @Override
  8.     public void run(){
  9.           HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); //先创建一个http连接
  10.           con.setRequestMethod("GET"); //设置类型为GET
  11.           con.setConnectTimeout(6000); //设置连接超时限制为6*1000毫秒
  12.           con.setRequestProperty("Accept", "application/json"); //告诉服务器我们需要json类型的回复
  13.           con.setRequestProperty("User-Agent", "PaintingWithPressurePlate"); //告诉服务器我们是用什么东西在看
  14.           con.connect(); //连接!
  15.     }

  16.     public void start() {
  17.         if (t == null) {
  18.            t = new Thread(this, "Thread-Pro-Plus");
  19.            t.start();
  20.         }
  21.      }
  22. }
复制代码
这样我们就可以用new MyClass("http://test.com/api").start()来新建一个线程访问这个网址了





那要怎么获取这个请求的返回值?
异步发送http做到了,那么异步读取呢?这里我们将会使用到Java8的Completable Future
因为如果我们要读取http的信息,那么我们的http请求就肯定需要结束,而且反正我们为了发送http请求新开了一个线程,所以我们就可以直接使用这个线程来读取信息。那么我们可以这样写代码

  1. class MyClass implements Runnable {
  2.     private Thread t = null;
  3.     private String url;
  4.     private CompletableFuture<String> task = null; //因为我们要读取http请求的字符串,所以这里用string类型
  5.     public MyClass(String URL) {
  6.         this.url = URL;
  7.     }

  8.     @Override
  9.     public void run(){
  10.        try{
  11.           HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
  12.           con.setRequestMethod("GET");
  13.           con.setConnectTimeout(6000);
  14.           con.setRequestProperty("Accept", "application/json");
  15.           con.setRequestProperty("User-Agent", "PaintingWithPressurePlate");
  16.           con.connect(); //连接!
  17.           task.complete(StringUtils.getStringFromStream(con.getInputStream)); //运行到这里就说明我们的请求结束了并且可以处理返回值了
  18.        } catch (IOException e){
  19.          task.cancel(true); //如果请求出错了会到这里
  20.        }
  21.     }

  22.     public CompletableFuture<String> start() {  //这里让他返回一个CompletableFuture
  23.         if (t == null) {
  24.            task = new CompletableFuture<>(); //新建
  25.            t = new Thread(this, "Thread-Pro-Plus");
  26.            t.start();
  27.         }
  28.         return task;
  29.      }
  30. }
复制代码
这样一番操作之后我们就可以这样使用这段代码了
  1. (new MyClass("http://test.com/api").start()).thenAccept(data => {
  2.     //这里的data就是http返回来的字符串了
  3. });
复制代码

这里你可能会很疑惑,这个thenaccpet到底干了什么。
要知道Java本身并不会把所有东西异步处理,所以当我们运行到这一行的时候虽然我们运行了start方法,但是这个方法仍然是在祝线程运行的,也就是说当我们从左到右运行到thenaccept的时候,实际上虽然线程已经创建了,但是http请求估计还没有完成,这里我们使用thenAccept就相当于向CompletableFuture里面存了一段代码,而这段代码将会在我们调用task.complete()的时候被调用,并且这段代码会有一个string函数,也就是我们处理出来的http返回值,这样我们就可以保证是http请求完成之后再运行的这段代码。





完成!
至此我们已经差不多讲解了java里面使用http的基础以及如果用多线程来实现异步获取http资源,但是我们这里要注意一个点:因为MC卓越的多线程兼容性,如果你试着在thenaccept里面(记得我们会在新的线程里运行这个方法,也就是不是跟服务器同一个线程的)实行一些mc游戏的操作,比如更改世界的一个方块那么轻则报错重则死机,所以这里我们可以用Bukkit提供的一个方法 new BukkitRunnable(代码).runTask(this); 这个将会把你的代码在下一个tick同步运行,也就是说我们可以从其他线程里向主线程添加任务了。

在本教程里我们并没有讲解POST请求的实行方式,这里留给你自己摸索,欢迎回复本帖放上你自己的答案哦~ (注意post请求的话你还可以自己提供数据向用http请求发送)



回到目录

seadshine
哇,你是真的厉害,受教了

seadshine
少熬夜,真的辛苦你了,太谢谢了,我感觉学费省了

白给一帆
感谢你的教程!

kirito=-=
这真的是零基础的吗

奥力給
跳到这有点蒙,希望把之前过期的帖子解锁一下...

天涯646
一脸蒙什么的看不懂

Anschluss_zeit
终于更新了啊

Anschluss_zeit
很喜欢你的教程,希望继续更新qwq

1144812512
感谢教程                           

空寂king

感谢教程      

SBBCMKCUF

太谢谢了,我感觉学费省了

JJDaXian
学习了11111111111111111

Yanyan_kagou
可以先去熟悉熟悉一些插件,会改插件之类的再来学习这个就很简单了

小罗过的开心
真的好有用