说明
使用 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");
}
}