java Socket、SSLSocket模拟浏览器发送HTTP、HTTPS请求

说明

使用 Socket、SSLSocket 模拟浏览器发送HTTP、HTTPS请求

主类

package http;

import javax.net.ssl.*;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
//长连接,一个连接多次发多个请求
public class HttpClient {

    Map<String,String> headInfo = new HashMap<>();

    OutputStream outputStream;
    InputStream inputStream;
    String saveDir = "./download";
    private String fileName;

    public String getSaveDir() {
        return saveDir;
    }

    public void setSaveDir(String saveDir) {
        this.saveDir = saveDir;
    }

    /**
    使用socket连接
     */
    public void connect(String host, int port) throws IOException {

        Socket s = new Socket();
        SocketAddress sa = new InetSocketAddress(host,port);
        s.connect(sa);
        outputStream = s.getOutputStream();
        inputStream = s.getInputStream();
    }

    /**
     * 使用SSLSocket连接
     * @param host
     * @param port
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws KeyManagementException
     */
    public void connectHTTPS(String host, int port) throws IOException, NoSuchAlgorithmException, KeyManagementException {
//        X509TrustManager x509m = new X509TrustManagerImpl();
//        通过 SSLContext 可指定证书、管理器等,但主流网站的证书是ca等官方机构颁发,可执行信任,所以不指定也可行
//        SSLContext context = SSLContext.getInstance("SSL");
//        context.init(null,
//                new TrustManager[] { x509m },
//                new SecureRandom());
//        Socket s = context.getSocketFactory().createSocket();
        SSLSocket s = (SSLSocket)SSLSocketFactory.getDefault().createSocket(host,port);
//        Socket s = SSLSocketFactory.getDefault().createSocket();
//        SocketAddress sa = new InetSocketAddress(host,port);
//        s.connect(sa);
        outputStream = s.getOutputStream();
        inputStream = s.getInputStream();
    }

    /**
     * 定义管理器
     */
    class X509TrustManagerImpl implements X509TrustManager {

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain,
                String authType) throws CertificateException {
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain,
                String authType) throws CertificateException {
        }
    }

    /**
     * 请求url
     * @param url
     * @param host 访问csdn.com 等网站,必须指定 host,否则无响应
     * @param fileName 将访问的资源保存为该文件名
     * @throws IOException
     */
    public void request(String url,String host,String fileName) throws IOException {
        System.out.println("请求:"+url);
        this.fileName = fileName;
        headInfo.clear();
        sendRequestHead(url,host);
        handleResponseHead();
        save(url);
    }

    /**
     * 发送请求头信息
     * @param url
     * @param host
     */
    private void sendRequestHead(String url,String host){

        PrintWriter pw = new PrintWriter(outputStream);
        pw.write("GET "+url+" HTTP/1.1\r\n");
        pw.write("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\n");
        pw.write("Accept-Language: zh-CN,zh;q=0.9\r\n");
        pw.write("Host: "+host+"\r\n");
        pw.write("User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\r\n");
//        不要加下面头,否则返回的是zip压缩包,需要解压缩
        //        pw.write("Accept-Encoding: gzip, deflate, br\r\n");
        pw.write("Connection: keep-alive\r\n");
//        pw.write("\r\n");
        pw.write("\r\n");
        pw.flush();
    }
//    读取一行
    private String readLine(InputStream is) throws IOException {
        String line = null;
        char[] buff = new char[1024*8];

        int i = 0;
        while( true){
            int data = inputStream.read();
//            System.out.println("readLine::"+data);
            if(data == -1){
                break;
            }
            char c = (char)data;
            if(c == '\r'){

                data = inputStream.read();
                char c2 = (char)data;
                if( c2 == '\n') {
                    if( i == 0){
                        line = "";
                        break;
                    }else {
                        line = new String(buff, 0, i);
//                        System.out.println(line);
                        break;
                    }
                }
            }

            buff[i] = c;
            i++;
            if(i == buff.length){
                throw new IOException("缓冲区不足,读取一行报错!!");
            }
        }
        return line;
    }
//    处理响应头信息
    private void handleResponseHead() throws IOException {
        String line = readLine(inputStream);
        while(!"".equals(line)){
            System.out.println(line);
            int pos = line.indexOf(":");
            if(pos>0) {
                String head = line.substring(0, pos);
                String value = line.substring(pos + 2);
                headInfo.put(head.toLowerCase(), value);
            }
            line = readLine(inputStream);
        }
    }

    /**
     * 将访问资源保存到本地
     * @param url
     * @throws IOException
     */
    private void save(String url) throws IOException {
        File saveDirFile = new File(saveDir);
        if(!saveDirFile.exists()){
            saveDirFile.mkdirs();
        }
        String resourseName = null;
        if("/".equals(url)){
            resourseName = "index.html";
        }else{
            int pos = url.lastIndexOf("/");
            resourseName = url.substring(pos+1);

        }
        String fileNameLower = resourseName.toLowerCase();
        if(fileNameLower.endsWith(".html") || fileNameLower.endsWith(".htm")
                || fileNameLower.endsWith(".jsp")
                || fileNameLower.endsWith(".asp")
                || fileNameLower.endsWith(".php")
                || resourseName.indexOf(".")==-1
        ){

            File saveFile = new File(saveDirFile,fileName);
            String contentLengthStr = headInfo.get("content-length");
            if(contentLengthStr!=null && !"".equals(contentLengthStr)) {
                saveFile(saveFile);
            }else {
                String transferEncoding = headInfo.get("Transfer-Encoding".toLowerCase());
                if("chunked".equals(transferEncoding)){
                    saveChunked(saveFile);
                }
            }
        }else{

            File saveFile = new File(saveDirFile,fileName);
            saveFile(saveFile);
        }
    }

    /**
     * 访问网页,一般是分成若干的块,即:Chunked,该方法处理这种情况
     * @param fileName
     * @throws IOException
     */
    private void saveChunked(File fileName) throws IOException {
        FileOutputStream fis = new FileOutputStream(fileName);
        while(true){
            String chunkedLenStr = readLine(inputStream);
            if("".equals(chunkedLenStr)){
                continue;
            }
            if(chunkedLenStr == null){
                break;
            }
            int chunkedLen = Integer.parseInt(chunkedLenStr,16);
            if(chunkedLen == 0){
                // 块大小是0时,还要读一个空行,注意,之后不要再读取,否则需要较长时间才会返回-1,而且无法使用当前连接继续发请求
                String line = readLine(inputStream);
                break;
            }
            byte[] chunkedData = readChunked(chunkedLen);
            fis.write(chunkedData);
            fis.flush();

        }
    }

    /**
     * 读取指定大小的 Chunked
     * @param contentLength
     * @return
     * @throws IOException
     */
    private byte[] readChunked(int contentLength) throws IOException {

        byte[] buff = new byte[contentLength];
        int off = 0;
        int len = contentLength;
        while (true){

            int lenRead = inputStream.read(buff,off,len);
//            System.out.println("readChunked:"+lenRead);
            if(lenRead == -1 || lenRead == contentLength){
                break;
            }
            off = lenRead;
            len = len - lenRead;
            if(len == 0){
                break;
            }
        }
//        System.out.println("readChunked--结束");
        return buff;
    }

    /**
     * 访问 .js、图片、.css 等静态资源,一般在响应头中包含 content-length,该方法处理这种情况
     * @param fileName
     * @throws IOException
     */
    private void saveFile(File fileName) throws IOException {
        String contentLengthStr = headInfo.get("content-length");
        int contentLength = Integer.parseInt(contentLengthStr);
        FileOutputStream fis = new FileOutputStream(fileName);

        byte[] buff = new byte[1024*8];
        int lenCount = 0;
        while (true){

            int len = inputStream.read(buff);
            if(len == -1){
                break;
            }
            fis.write(buff,0,len);
            fis.flush();
            lenCount = lenCount + len;
            if(lenCount == contentLength){
                break;
            }
        }
        fis.close();
    }

}

测试类

提示:访问百度不好使

public class HttpClient_test {

    public static void main(String[] args) throws Exception {
        HttpClient client = new HttpClient();
//        访问baidu不好使
//        client.connectHTTPS("baidu.com",443);
//        client.request("/","www.baidu.com","baidu.html");
//        client.connectHTTPS("online.zretc.net",443);
//        client.request("/login","online.zretc.net","智云枢.html");
//        client.connectHTTPS("blog.csdn.net",443);
//        client.request("/P19777/article/details/100580370","blog.csdn.net","100580370.html");
//        client.connect("malaoshi.top",80);
        client.connectHTTPS("malaoshi.top",443);
        client.request("/show_1IX5pF5QEiLf.html","www.malaoshi.top","show_1IX5pF5QEiLf--.html");
        client.request("/upload/0/0/1IX5pF4sjPOc.png","www.malaoshi.top","1IX5pF4sjPOc.png");
        client.request("/upload/0/0/1IX5Wz268OFt.jpg","www.malaoshi.top","1IX5Wz268OFt.jpg");
//        client.connectHTTPS("cc.58.com",443);
//        client.request("/ershouche/?PGTID=0d100000-0013-fd90-fff0-cc46418023e7&ClickID=4","cc.58.com","58.html");
//        client.request("/ghost/css/bootstrap.min.css");
//        request("localhost",8081,"/html2/img/1.png");
    }
}

原文出处:/show_1IX5puKCs1fR.html